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

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;
}