Files
CRM_Montzar/packages/shared/src/person-name.ts
T
MahdiandCursor d7b5085096 feat(leads): keep gender on leads and form submissions
A lead can carry the same honorific as a person, and a form that asks gender writes it onto the lead.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-31 12:49:42 +03:30

135 lines
4.6 KiB
TypeScript

/** مرد، زن، یا هیچکدام (مثلاً فروشگاه) */
export type PersonGender = "male" | "female" | "none";
export const PERSON_GENDERS: PersonGender[] = ["male", "female", "none"];
export const PERSON_GENDER_LABELS: Record<PersonGender, string> = {
male: "مرد",
female: "زن",
none: "هیچکدام",
};
const MALE_TITLE = "جناب آقای";
const FEMALE_TITLE = "سرکار خانم";
function normalizeFa(value: string) {
return value.replace(/ي/g, "ی").replace(/ك/g, "ک").replace(/\s+/g, " ").trim();
}
function splitWords(value: string) {
const parts = normalizeFa(value).split(" ").filter(Boolean);
return {
firstName: parts[0] ?? "",
lastName: parts.slice(1).join(" "),
};
}
/** عنوان ابتدای نام را برمی‌دارد تا فیلدهای نام، عنوان را دوباره ذخیره نکنند. */
export function splitPersonHonorific(firstName: string, lastName: string): {
firstName: string;
lastName: string;
gender: Exclude<PersonGender, "none"> | null;
} {
let full = normalizeFa(`${firstName} ${lastName}`);
let gender: Exclude<PersonGender, "none"> | null = null;
// فاصله اختیاری است: «سرکارخانم» و «جناب آقاي» هر دو عنوان‌اند.
for (;;) {
const male = full.match(/^جناب\s*آقای(?=\s|$)/);
const female = full.match(/^سرکار\s*خانم(?=\s|$)/);
const hit = male?.[0] ?? female?.[0];
if (!hit) break;
gender = male ? "male" : "female";
full = normalizeFa(full.slice(hit.length));
}
const plain = splitWords(full);
return { ...plain, gender };
}
/**
* نام ذخیره‌شده را با عنوان می‌سازد.
* اگر جنسیت یا تیک نفرستاده شود و خود نام عنوان داشته باشد، همان برداشت می‌شود.
*/
export function applyPersonGenderTitle(input: {
firstName: string;
lastName: string;
gender?: PersonGender | null;
showGenderInName?: boolean | null;
}): {
firstName: string;
lastName: string;
gender: PersonGender;
showGenderInName: boolean;
} {
const bare = splitPersonHonorific(input.firstName, input.lastName);
const gender: PersonGender =
input.gender === "male" || input.gender === "female" || input.gender === "none"
? input.gender
: (bare.gender ?? "none");
let show = input.showGenderInName;
if (typeof show !== "boolean") show = bare.gender != null && gender !== "none";
if (gender === "none") show = false;
const body = [bare.firstName, bare.lastName].filter(Boolean).join(" ");
const titled =
show && gender === "male"
? `${MALE_TITLE} ${body}`.trim()
: show && gender === "female"
? `${FEMALE_TITLE} ${body}`.trim()
: body;
const parts = splitWords(titled);
return {
firstName: parts.firstName || "—",
lastName: parts.lastName || "—",
gender,
showGenderInName: show,
};
}
/** پاسخ فیلد جنسیت فرم: مرد، زن، یا هیچکدام. مقدار ناشناس null است. */
export function parsePersonGenderAnswer(raw: string): PersonGender | null {
const text = normalizeFa(raw).toLowerCase();
if (!text) return null;
const has = (words: string[]) => words.some((word) => text === word || text.includes(word));
if (has(["هیچکدام", "none", "نامشخص"])) return "none";
if (has(["female", "woman", "خانم", "زن"])) return "female";
if (has(["male", "man", "آقا", "مرد"])) return "male";
return null;
}
export function isGenderQuestionLabel(label: string): boolean {
return normalizeFa(label).replace(/\s+/g, "").includes("جنسیت");
}
/**
* عنوان روی نام مخاطب لید می‌نشیند.
* اگر نام مخاطب خالی باشد، عنوان لید همان نام شخص است.
*/
export function applyLeadGenderName(input: {
title: string;
contactName?: string | null;
gender?: PersonGender | null;
showGenderInName?: boolean | null;
}): {
title: string;
contactName: string | null;
gender: PersonGender;
showGenderInName: boolean;
} {
const contact = (input.contactName ?? "").trim();
const title = input.title.trim();
const onContact = contact.length > 0;
const named = applyPersonGenderTitle({
firstName: onContact ? contact : title,
lastName: "",
gender: input.gender,
showGenderInName: input.showGenderInName,
});
const full = [named.firstName, named.lastName].filter((part) => part && part !== "—").join(" ");
return {
title: onContact ? title : full || title,
contactName: onContact ? full || null : null,
gender: named.gender,
showGenderInName: named.showGenderInName,
};
}