You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
619 lines
17 KiB
619 lines
17 KiB
import type {
|
|
MarriageField,
|
|
MarriageFieldValue,
|
|
MarriagePhoneFieldValue,
|
|
MarriageBirthplaceFieldValue,
|
|
} from "@/hooks/marriage/types";
|
|
import { dictionaries } from "@/translations/dictionaries";
|
|
import { resolveCountryName } from "@/data/countries";
|
|
|
|
const persianToEnglishMap: Record<string, string> = {};
|
|
for (const [enKey, faVal] of Object.entries(dictionaries.fa)) {
|
|
if (typeof faVal === "string") {
|
|
persianToEnglishMap[faVal.trim()] = enKey;
|
|
}
|
|
}
|
|
|
|
const normalizedEnglishKeys: Record<string, string> = {};
|
|
for (const enKey of Object.keys(dictionaries.en)) {
|
|
const norm = enKey.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
if (norm) {
|
|
normalizedEnglishKeys[norm] = enKey;
|
|
}
|
|
}
|
|
|
|
export function isMarriagePhoneFieldValue(
|
|
value: unknown,
|
|
): value is MarriagePhoneFieldValue {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
return false;
|
|
}
|
|
|
|
const phoneValue = value as Partial<MarriagePhoneFieldValue>;
|
|
|
|
return (
|
|
typeof phoneValue.countryCode === "string" &&
|
|
typeof phoneValue.phoneNumber === "string"
|
|
);
|
|
}
|
|
|
|
export function isMarriageBirthplaceFieldValue(
|
|
value: unknown,
|
|
): value is MarriageBirthplaceFieldValue {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
return false;
|
|
}
|
|
|
|
const bpValue = value as Partial<MarriageBirthplaceFieldValue>;
|
|
|
|
return (
|
|
typeof bpValue.country === "string" && typeof bpValue.city === "string"
|
|
);
|
|
}
|
|
|
|
export function formatFieldValue(
|
|
value: MarriageFieldValue,
|
|
dictionary?: Record<string, string>,
|
|
): string | null {
|
|
if (value === null || value === undefined || value === "") {
|
|
return null;
|
|
}
|
|
|
|
if (isMarriagePhoneFieldValue(value)) {
|
|
return `+${value.countryCode}${value.phoneNumber}`;
|
|
}
|
|
|
|
if (typeof value === "boolean") {
|
|
return value
|
|
? dictionary?.["Yes"] || "Yes"
|
|
: dictionary?.["No"] || "No";
|
|
}
|
|
|
|
if (typeof value === "object") {
|
|
if ("country" in value || "city" in value || "state" in value) {
|
|
const v = value as { country?: string; state?: string; city?: string };
|
|
const rawCountry = typeof v.country === "string" ? v.country.trim() : "";
|
|
const rawCity = typeof v.city === "string" ? v.city.trim() : "";
|
|
const rawState = typeof v.state === "string" ? v.state.trim() : "";
|
|
|
|
const localizedCountry = rawCountry
|
|
? dictionary?.[rawCountry] || resolveCountryName(rawCountry) || rawCountry
|
|
: "";
|
|
const localizedCity = rawCity
|
|
? dictionary?.[rawCity] || rawCity
|
|
: "";
|
|
const localizedState = rawState
|
|
? dictionary?.[rawState] || rawState
|
|
: "";
|
|
|
|
const parts = [localizedCity, localizedState, localizedCountry].filter(Boolean);
|
|
return parts.join(", ");
|
|
}
|
|
}
|
|
|
|
return String(value);
|
|
}
|
|
|
|
export function titleFromKey(key: string): string {
|
|
const leafKey = key.includes(".") ? key.split(".").pop()! : key;
|
|
return leafKey
|
|
.replace(/^q\d+[_-]?/i, "")
|
|
.replace(/[_-]+/g, " ")
|
|
.replace(/\s+/g, " ")
|
|
.trim()
|
|
.replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
}
|
|
|
|
function isEnglishDictionary(dictionary?: Record<string, string>): boolean {
|
|
if (!dictionary) return false;
|
|
return (
|
|
dictionary === (dictionaries.en as Record<string, string>) ||
|
|
(dictionary["Yes"] === "Yes" &&
|
|
dictionary["No"] === "No" &&
|
|
dictionary["Cancel"] === "Cancel")
|
|
);
|
|
}
|
|
|
|
export const FIELD_KEY_TO_CANONICAL_EN: Record<string, string[]> = {
|
|
"personal_identity.date_of_birth": ["Date of Birth"],
|
|
date_of_birth: ["Date of Birth"],
|
|
"personal_identity.place_of_birth": [
|
|
"Place of Birth (Country and City)",
|
|
"Place of Birth",
|
|
"Birthplace",
|
|
],
|
|
place_of_birth: [
|
|
"Place of Birth (Country and City)",
|
|
"Place of Birth",
|
|
"Birthplace",
|
|
],
|
|
birthplace: ["Birthplace", "Place of Birth"],
|
|
"personal_identity.place_of_upbringing": [
|
|
"Place of Upbringing / Where You Grew Up (Country and City)",
|
|
"Place of Upbringing",
|
|
"City of Upbringing / Growing Up",
|
|
],
|
|
place_of_upbringing: [
|
|
"Place of Upbringing / Where You Grew Up (Country and City)",
|
|
"Place of Upbringing",
|
|
"City of Upbringing / Growing Up",
|
|
],
|
|
"personal_identity.current_nationality_citizenship": [
|
|
"Current Nationality / Citizenship",
|
|
"Current Nationality / Citizenship (or Countries of Citizenship)",
|
|
"Nationality",
|
|
"Citizenship",
|
|
],
|
|
current_nationality_citizenship: [
|
|
"Current Nationality / Citizenship",
|
|
"Current Nationality / Citizenship (or Countries of Citizenship)",
|
|
"Nationality",
|
|
"Citizenship",
|
|
],
|
|
"personal_identity.ethnicity_family_origin_race": [
|
|
"Ethnicity / Family Origin / Race",
|
|
"Ethnicity",
|
|
],
|
|
ethnicity_family_origin_race: [
|
|
"Ethnicity / Family Origin / Race",
|
|
"Ethnicity",
|
|
],
|
|
"personal_identity.mother_tongue": [
|
|
"Mother Tongue",
|
|
"Native Language",
|
|
"Language",
|
|
],
|
|
mother_tongue: [
|
|
"Mother Tongue",
|
|
"Native Language",
|
|
"Language",
|
|
],
|
|
"personal_identity.religion": [
|
|
"What is your religion?",
|
|
"Religion",
|
|
],
|
|
religion: [
|
|
"What is your religion?",
|
|
"Religion",
|
|
],
|
|
"personal_identity.islamic_madhhab": [
|
|
"What is your branch / school of thought in Islam?",
|
|
"Islamic Madhhab",
|
|
"School of Thought",
|
|
],
|
|
islamic_madhhab: [
|
|
"What is your branch / school of thought in Islam?",
|
|
"Islamic Madhhab",
|
|
"School of Thought",
|
|
],
|
|
"contact_residence.current_residence_location": [
|
|
"Current Place of Residence (Country, City / State)",
|
|
"Current Place of Residence",
|
|
"Current Residence",
|
|
],
|
|
current_residence_location: [
|
|
"Current Place of Residence (Country, City / State)",
|
|
"Current Place of Residence",
|
|
"Current Residence",
|
|
],
|
|
};
|
|
|
|
export function formatFieldLabel(
|
|
field: MarriageField,
|
|
dictionary?: Record<string, string>,
|
|
): string {
|
|
const englishTitle = titleFromKey(field.key);
|
|
const rawLabel = (field.label || "").trim();
|
|
const isEnglish = isEnglishDictionary(dictionary);
|
|
|
|
// If rawLabel is provided from the backend (the dynamic source of truth)
|
|
if (rawLabel) {
|
|
// 1. Direct dictionary match for field.label (e.g. static UI overrides)
|
|
if (dictionary?.[rawLabel]) {
|
|
return dictionary[rawLabel];
|
|
}
|
|
|
|
// 2. If rawLabel is in Persian reverse map, lookup translation in dictionary
|
|
const enKeyFromFa = persianToEnglishMap[rawLabel];
|
|
if (enKeyFromFa) {
|
|
if (dictionary?.[enKeyFromFa]) {
|
|
return dictionary[enKeyFromFa];
|
|
}
|
|
if (isEnglish) {
|
|
return enKeyFromFa;
|
|
}
|
|
}
|
|
|
|
// 3. If active dictionary is English and rawLabel contains non-Latin (Persian/Arabic) characters, fallback to englishTitle
|
|
const hasPersianOrArabic = /[-ۿ]/.test(rawLabel);
|
|
if (hasPersianOrArabic && isEnglish) {
|
|
return englishTitle || rawLabel;
|
|
}
|
|
|
|
// 4. In all non-English locales (Arabic, Persian, etc.), trust the backend's dynamic label directly!
|
|
return rawLabel;
|
|
}
|
|
|
|
// If rawLabel is missing, synthesize from canonical keys or key name
|
|
const canonicalCandidates =
|
|
FIELD_KEY_TO_CANONICAL_EN[field.key] ||
|
|
FIELD_KEY_TO_CANONICAL_EN[field.key.split(".").pop() || ""] ||
|
|
[];
|
|
for (const candidate of canonicalCandidates) {
|
|
if (dictionary?.[candidate]) {
|
|
return dictionary[candidate];
|
|
}
|
|
}
|
|
|
|
if (dictionary?.[englishTitle]) {
|
|
return dictionary[englishTitle];
|
|
}
|
|
|
|
const normKey =
|
|
(field.key || "")
|
|
.split(".")
|
|
.pop()
|
|
?.toLowerCase()
|
|
.replace(/[^a-z0-9]/g, "") || "";
|
|
const matchedCanonicalKey = normalizedEnglishKeys[normKey];
|
|
if (matchedCanonicalKey) {
|
|
if (dictionary?.[matchedCanonicalKey]) {
|
|
return dictionary[matchedCanonicalKey];
|
|
}
|
|
if (isEnglish) {
|
|
return matchedCanonicalKey;
|
|
}
|
|
}
|
|
|
|
return englishTitle || field.key;
|
|
}
|
|
|
|
export function formatOptionValue(
|
|
value: MarriageFieldValue,
|
|
dictionary?: Record<string, string>,
|
|
): string | null {
|
|
if (Array.isArray(value)) {
|
|
const formattedItems = value
|
|
.map((item) => formatOptionValue(item as MarriageFieldValue, dictionary))
|
|
.filter(Boolean);
|
|
return formattedItems.length ? formattedItems.join(", ") : null;
|
|
}
|
|
|
|
const base = formatFieldValue(value, dictionary);
|
|
if (!base) return null;
|
|
|
|
const trimmed = base.trim();
|
|
|
|
// 1. Direct dictionary match
|
|
if (dictionary?.[trimmed]) return dictionary[trimmed];
|
|
|
|
// 2. Reverse map if base is in Persian
|
|
const enKeyFromFa = persianToEnglishMap[trimmed];
|
|
if (enKeyFromFa) {
|
|
if (dictionary?.[enKeyFromFa]) {
|
|
return dictionary[enKeyFromFa];
|
|
}
|
|
return enKeyFromFa;
|
|
}
|
|
|
|
// 3. Try replacing underscores with spaces: "single;_never_married" -> "single; never married"
|
|
const withSpaces = trimmed.replace(/_/g, " ").trim();
|
|
if (dictionary?.[withSpaces]) return dictionary[withSpaces];
|
|
|
|
// 4. Try capitalized first letter
|
|
const capitalized = withSpaces.charAt(0).toUpperCase() + withSpaces.slice(1);
|
|
if (dictionary?.[capitalized]) return dictionary[capitalized];
|
|
|
|
// 5. Normalized match for option value
|
|
const normVal = withSpaces.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
const matchedCanonicalKey = normalizedEnglishKeys[normVal];
|
|
if (matchedCanonicalKey) {
|
|
if (dictionary?.[matchedCanonicalKey]) {
|
|
return dictionary[matchedCanonicalKey];
|
|
}
|
|
return matchedCanonicalKey;
|
|
}
|
|
|
|
return withSpaces;
|
|
}
|
|
|
|
export type SummaryFieldItem = {
|
|
id: string;
|
|
key: string;
|
|
label: string;
|
|
value: string;
|
|
iconName: "location" | "ruler" | "graduationCap" | "bookOpen" | "briefcase";
|
|
};
|
|
|
|
export const SUMMARY_FIELD_MATCHERS = {
|
|
placeOfBirth: [
|
|
"place_of_birth",
|
|
"personal_identity.place_of_birth",
|
|
"birthplace",
|
|
"personal_identity.birthplace",
|
|
"personal_info.birthplace",
|
|
"birth_place",
|
|
"country_city_of_birth",
|
|
"birth_country_city",
|
|
],
|
|
birthCountry: ["country_of_birth", "birth_country"],
|
|
birthCity: ["city_of_birth", "birth_city"],
|
|
currentResidence: [
|
|
"current_residence_location",
|
|
"contact_residence.current_residence_location",
|
|
"current_residence",
|
|
"contact_residence.current_residence",
|
|
"contact_residence_family_communication.current_residence",
|
|
"residence_location",
|
|
"current_place_of_residence",
|
|
"residence",
|
|
],
|
|
currentCountry: [
|
|
"country_of_current_residence",
|
|
"current_country",
|
|
"residence_country",
|
|
"country",
|
|
"current_residence_country",
|
|
],
|
|
currentCity: [
|
|
"city_state_of_current_residence",
|
|
"city_of_current_residence",
|
|
"current_city",
|
|
"residence_city",
|
|
"city",
|
|
"city_state",
|
|
"state_city",
|
|
"state",
|
|
"province",
|
|
],
|
|
height: [
|
|
"height_in_centimeters",
|
|
"appearance_health.height_in_centimeters",
|
|
"appearance_health_activity.height_in_centimeters",
|
|
"appearance_health.height",
|
|
"height",
|
|
"height_cm",
|
|
],
|
|
highestEducation: [
|
|
"highest_completed_education_degree",
|
|
"education_career.highest_completed_education_degree",
|
|
"highest_level_of_education",
|
|
"highest_education_level",
|
|
"education_career_economic_status.highest_level_of_education",
|
|
"highest_completed_formal_educational_degree",
|
|
"education_level",
|
|
"education",
|
|
],
|
|
fieldOfStudy: [
|
|
"field_of_study",
|
|
"education_career.field_of_study",
|
|
"education_career_economic_status.field_of_study",
|
|
"study_field",
|
|
"study",
|
|
"major",
|
|
],
|
|
employmentStatus: [
|
|
"current_employment_status",
|
|
"education_career.current_employment_status",
|
|
"employment_status",
|
|
"education_career_economic_status.employment_status",
|
|
],
|
|
jobTitle: [
|
|
"job_title_and_field_of_activity",
|
|
"job_title",
|
|
"education_career.job_title",
|
|
"education_career_economic_status.job_title",
|
|
"job_title_and_description",
|
|
"job_position",
|
|
"occupation",
|
|
],
|
|
} as const;
|
|
|
|
export function matchesSummaryCandidate(
|
|
field: MarriageField,
|
|
candidates: readonly string[],
|
|
): boolean {
|
|
const rawKey = (field.key || "").toLowerCase();
|
|
const keyParts = rawKey.split(".");
|
|
const suffix = keyParts[keyParts.length - 1];
|
|
const normalizedKey = rawKey.replace(/[^a-z0-9]/g, "");
|
|
const normalizedSuffix = suffix.replace(/[^a-z0-9]/g, "");
|
|
|
|
for (const c of candidates) {
|
|
const normC = c.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
if (
|
|
normalizedSuffix === normC ||
|
|
normalizedKey.endsWith(normC) ||
|
|
rawKey === c.toLowerCase() ||
|
|
suffix === c.toLowerCase()
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
export function getOrderedSummaryFields(
|
|
publicInfo: MarriageField[] | null | undefined,
|
|
dictionary?: Record<string, string>,
|
|
): SummaryFieldItem[] {
|
|
const fields = publicInfo ?? [];
|
|
const usedIndexes = new Set<number>();
|
|
|
|
function pickSummaryField(
|
|
candidates: readonly string[],
|
|
fallbackLabelKey?: string,
|
|
): { key: string; label: string; value: string } | null {
|
|
for (const [idx, field] of fields.entries()) {
|
|
if (usedIndexes.has(idx)) continue;
|
|
if (matchesSummaryCandidate(field, candidates)) {
|
|
const val = formatOptionValue(field.value, dictionary);
|
|
if (val) {
|
|
usedIndexes.add(idx);
|
|
const label =
|
|
formatFieldLabel(field, dictionary) ||
|
|
(fallbackLabelKey && dictionary?.[fallbackLabelKey]) ||
|
|
fallbackLabelKey ||
|
|
field.key;
|
|
return {
|
|
key: field.key,
|
|
label,
|
|
value: val,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// 1. Place of Birth (Country and City)
|
|
let placeOfBirth = pickSummaryField(
|
|
SUMMARY_FIELD_MATCHERS.placeOfBirth,
|
|
"Place of Birth (Country and City)",
|
|
);
|
|
if (!placeOfBirth) {
|
|
const bCountry = pickSummaryField(SUMMARY_FIELD_MATCHERS.birthCountry);
|
|
const bCity = pickSummaryField(SUMMARY_FIELD_MATCHERS.birthCity);
|
|
if (bCountry || bCity) {
|
|
const combined = [bCity?.value, bCountry?.value].filter(Boolean).join(", ");
|
|
if (combined) {
|
|
placeOfBirth = {
|
|
key: "place_of_birth",
|
|
label:
|
|
dictionary?.["Place of Birth (Country and City)"] ||
|
|
dictionary?.["Place of Birth"] ||
|
|
"Place of Birth (Country and City)",
|
|
value: combined,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. Current Place of Residence (Country, City / State)
|
|
let currentResidence = pickSummaryField(
|
|
SUMMARY_FIELD_MATCHERS.currentResidence,
|
|
"Current Place of Residence (Country, City / State)",
|
|
);
|
|
if (!currentResidence) {
|
|
const cCity = pickSummaryField(SUMMARY_FIELD_MATCHERS.currentCity);
|
|
const cCountry = pickSummaryField(SUMMARY_FIELD_MATCHERS.currentCountry);
|
|
if (cCountry || cCity) {
|
|
const combined = [cCity?.value, cCountry?.value].filter(Boolean).join(", ");
|
|
if (combined) {
|
|
currentResidence = {
|
|
key: "current_residence",
|
|
label:
|
|
dictionary?.["Current Place of Residence (Country, City / State)"] ||
|
|
dictionary?.["Current Place of Residence"] ||
|
|
dictionary?.["Current Residence"] ||
|
|
"Current Place of Residence (Country, City / State)",
|
|
value: combined,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. Height in Centimeters
|
|
const height = pickSummaryField(
|
|
SUMMARY_FIELD_MATCHERS.height,
|
|
"Height in Centimeters",
|
|
);
|
|
|
|
// 4. What is your highest completed formal educational degree?
|
|
const highestEducation = pickSummaryField(
|
|
SUMMARY_FIELD_MATCHERS.highestEducation,
|
|
"What is your highest completed formal educational degree?",
|
|
);
|
|
|
|
// 5. Field of Study
|
|
const fieldOfStudy = pickSummaryField(
|
|
SUMMARY_FIELD_MATCHERS.fieldOfStudy,
|
|
"Field of Study",
|
|
);
|
|
|
|
// 6. What is your current employment status?
|
|
const employmentStatus = pickSummaryField(
|
|
SUMMARY_FIELD_MATCHERS.employmentStatus,
|
|
"What is your current employment status?",
|
|
);
|
|
|
|
// 7. Job Title and Field of Activity
|
|
const jobTitle = pickSummaryField(
|
|
SUMMARY_FIELD_MATCHERS.jobTitle,
|
|
"Job Title and Field of Activity",
|
|
);
|
|
|
|
const items: SummaryFieldItem[] = [];
|
|
|
|
if (placeOfBirth) {
|
|
items.push({
|
|
id: placeOfBirth.key,
|
|
key: placeOfBirth.key,
|
|
label: placeOfBirth.label,
|
|
value: placeOfBirth.value,
|
|
iconName: "location",
|
|
});
|
|
}
|
|
|
|
if (currentResidence) {
|
|
items.push({
|
|
id: currentResidence.key,
|
|
key: currentResidence.key,
|
|
label: currentResidence.label,
|
|
value: currentResidence.value,
|
|
iconName: "location",
|
|
});
|
|
}
|
|
|
|
if (height) {
|
|
items.push({
|
|
id: height.key,
|
|
key: height.key,
|
|
label: height.label,
|
|
value: height.value,
|
|
iconName: "ruler",
|
|
});
|
|
}
|
|
|
|
if (highestEducation) {
|
|
items.push({
|
|
id: highestEducation.key,
|
|
key: highestEducation.key,
|
|
label: highestEducation.label,
|
|
value: highestEducation.value,
|
|
iconName: "graduationCap",
|
|
});
|
|
}
|
|
|
|
if (fieldOfStudy) {
|
|
items.push({
|
|
id: fieldOfStudy.key,
|
|
key: fieldOfStudy.key,
|
|
label: fieldOfStudy.label,
|
|
value: fieldOfStudy.value,
|
|
iconName: "bookOpen",
|
|
});
|
|
}
|
|
|
|
if (employmentStatus) {
|
|
items.push({
|
|
id: employmentStatus.key,
|
|
key: employmentStatus.key,
|
|
label: employmentStatus.label,
|
|
value: employmentStatus.value,
|
|
iconName: "briefcase",
|
|
});
|
|
}
|
|
|
|
if (jobTitle) {
|
|
items.push({
|
|
id: jobTitle.key,
|
|
key: jobTitle.key,
|
|
label: jobTitle.label,
|
|
value: jobTitle.value,
|
|
iconName: "briefcase",
|
|
});
|
|
}
|
|
|
|
return items;
|
|
}
|