Files
edr-platform/apps/edr-freight-web/backoffice/src/user-management/TemplateSample/utils/letterLabelsConstants.ts
natib21 e6e44e773b fix ui
2026-07-10 11:25:59 +00:00

244 lines
7.4 KiB
TypeScript

/**
* Letter Labels constants & helpers
*
* Customizable, bilingual (English / Amharic) labels used by letter templates
* (To, From, Subject, CC, ...). Each label may carry an optional CSS style
* object (e.g. underline, bold). Stored under the setting code
* `letter-labels-text` as a JSON object.
*/
export type LetterLabelKey =
| "to"
| "from"
| "subject"
| "urgent"
| "cc"
| "date"
| "record"
| "ref_no"
| "delegate"
| "for_your_reference";
/** CSS style map for a single label (kebab-case CSS properties). */
export type LetterLabelStyle = Record<string, string>;
export interface LetterLabelEntry {
en: string;
am: string;
style?: LetterLabelStyle;
}
export type LetterLabelsText = Record<LetterLabelKey, LetterLabelEntry>;
export const LETTER_LABELS_SETTING_CODE = "letter-labels-text";
export const LETTER_LABEL_KEYS: LetterLabelKey[] = [
"to",
"from",
"subject",
"urgent",
"cc",
"date",
"record",
"ref_no",
"delegate",
"for_your_reference",
];
/**
* Default labels for every key in English & Amharic. Some stored values include
* trailing punctuation (Subject:, CC:, Ref No:), others don't (To, From).
*/
export const DEFAULT_LETTER_LABELS: LetterLabelsText = {
to: { en: "To", am: "ለ" },
from: { en: "From", am: "ከ" },
subject: { en: "Subject:", am: "ጉዳዩ:" },
urgent: { en: "Urgent", am: "አስቸኳይ" },
cc: { en: "CC:", am: "ግልባጭ:", style: { "text-decoration": "underline" } },
date: { en: "Date", am: "ቀን" },
record: { en: "Record", am: "መዝገብ" },
ref_no: { en: "Ref No:", am: "ቁጥር:" },
delegate: { en: "Delegate", am: "ተወካይ" },
for_your_reference: { en: "For your reference", am: "ለመረጃ ያህል" },
};
/** Human-friendly field names for the editor (not the printed label text). */
export const LETTER_LABEL_DISPLAY_NAMES: Record<
LetterLabelKey,
{ en: string; am: string }
> = {
to: { en: "To", am: "ለ" },
from: { en: "From", am: "ከ" },
subject: { en: "Subject label", am: "የጉዳይ መለያ" },
urgent: { en: "Urgent", am: "አስቸኳይ" },
cc: { en: "CC", am: "ግልባጭ" },
date: { en: "Date", am: "ቀን" },
record: { en: "Record title", am: "የደብዳቤ ርዕስ" },
ref_no: { en: "Reference number", am: "የማጣቀሻ ቁጥር" },
delegate: { en: "Delegate / Regarding", am: "ስለ" },
for_your_reference: { en: "For your reference", am: "እንዲያውቁት" },
};
export const isLetterLabelsSetting = (code: string): boolean =>
code === LETTER_LABELS_SETTING_CODE;
/** Sidebar section code each label field belongs to. */
export const LETTER_LABEL_CATEGORY_MAP: Record<LetterLabelKey, string> = {
record: "reference-number",
ref_no: "reference-number",
date: "reference-number",
subject: "subject",
urgent: "subject",
to: "receiver",
from: "receiver",
delegate: "body",
cc: "cc",
for_your_reference: "cc",
};
export type LetterLabelSectionCode =
| "reference-number"
| "subject"
| "receiver"
| "body"
| "cc";
/** Normalize API / sidebar section codes to the canonical label section. */
export function normalizeLabelSectionCode(
sectionCode: string,
): LetterLabelSectionCode | null {
const lower = sectionCode.trim().toLowerCase();
if (!lower) return null;
if (
lower.includes("reference-number") ||
lower.includes("reference_number") ||
lower.includes("referencenumber") ||
lower === "reference" ||
(lower.includes("reference") && lower.includes("number")) ||
lower.includes("record-number") ||
lower.includes("record_number")
) {
return "reference-number";
}
if (lower.includes("subject")) return "subject";
if (
lower.includes("receiver") ||
lower.includes("recipient") ||
lower === "receivers"
) {
return "receiver";
}
if (lower.includes("body") || lower.includes("content")) return "body";
if (lower.includes("cc")) return "cc";
return null;
}
const labelMatchesSettingCode = (
labelKey: LetterLabelKey,
settingCode: string,
): boolean => {
const lower = settingCode.trim().toLowerCase();
if (!lower) return false;
switch (labelKey) {
case "to":
return (
(/receiver|recipient|^letter-to\b/i.test(lower) ||
lower.includes("receiver-list")) &&
!/from|collaborator|sender|cc/i.test(lower)
);
case "from":
return /from|collaborator|sender/i.test(lower);
case "subject":
return /subject/i.test(lower) && !/urgent/i.test(lower);
case "urgent":
return /urgent/i.test(lower);
case "record":
return /record/i.test(lower) && !/ref/i.test(lower);
case "ref_no":
return /ref/i.test(lower) && /no|number/i.test(lower);
case "date":
return /date/i.test(lower);
case "delegate":
return /delegate|regarding/i.test(lower) || lower.includes("body");
case "cc":
return (
/letter-cc/i.test(lower) &&
!/list|prefix|suffix|receiver|for-your-reference|foryourreference/i.test(
lower,
)
);
case "for_your_reference":
return /for-your-reference|foryourreference/i.test(lower);
default:
return false;
}
};
export function getLetterLabelKeysForCategory(
categoryCode: string,
settingCodes: string[] = [],
): LetterLabelKey[] {
const canonical = normalizeLabelSectionCode(categoryCode);
const matched = new Set<LetterLabelKey>();
if (canonical) {
for (const key of LETTER_LABEL_KEYS) {
if (LETTER_LABEL_CATEGORY_MAP[key] === canonical) {
matched.add(key);
}
}
}
for (const settingCode of settingCodes) {
for (const key of LETTER_LABEL_KEYS) {
if (labelMatchesSettingCode(key, settingCode)) {
matched.add(key);
}
}
}
return LETTER_LABEL_KEYS.filter((key) => matched.has(key));
}
const isPlainObject = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
/**
* Validates a CSS style object, keeping only non-empty string values.
* Returns undefined when nothing valid remains.
*/
export const normalizeLabelStyle = (
value: unknown,
): LetterLabelStyle | undefined => {
if (!isPlainObject(value)) return undefined;
const out: LetterLabelStyle = {};
for (const [key, raw] of Object.entries(value)) {
if (key === "font-size") continue;
if (typeof raw === "string" && raw.trim() !== "") out[key] = raw;
}
return Object.keys(out).length > 0 ? out : undefined;
};
/**
* Merges an incoming (possibly partial / invalid) value with the defaults so the
* result always has all keys with valid en/am strings. Preserves style objects.
*/
export const normalizeLetterLabelsValue = (value: unknown): LetterLabelsText => {
const input = isPlainObject(value) ? value : {};
const result = {} as LetterLabelsText;
for (const key of LETTER_LABEL_KEYS) {
const def = DEFAULT_LETTER_LABELS[key];
const raw = isPlainObject(input[key]) ? (input[key] as Record<string, unknown>) : {};
const en = typeof raw.en === "string" ? raw.en : def.en;
const am = typeof raw.am === "string" ? raw.am : def.am;
const style = normalizeLabelStyle(raw.style) ?? def.style;
result[key] = style ? { en, am, style } : { en, am };
}
return result;
};