Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/components/screens/admin/adminEnumLabels.ts
T

110 lines
4.4 KiB
TypeScript

/**
* Display labels for admin values that are stored as identifiers.
*
* The admin audit from 2026-07-26 found the same value rendered two ways
* depending on the screen: `/support` listed `open` / `normal` / `seed` while the
* ticket detail showed "Offen" / "Normal", and `/accounts/billing` printed
* `business` where `/accounts` printed "Business". These helpers are the single
* place that decides how such a value reads, so a list and a detail view can no
* longer drift apart.
*
* Two kinds of values are handled differently:
*
* - Closed sets (ticket status, priority) resolve to an i18n key.
* - Open sets (operator-defined plan codes, provider keys) are identifiers we
* cannot translate exhaustively, so they are humanized instead — never shown
* raw and never invented as a missing translation key.
*/
/** Ticket statuses that have a translation under `admin.support.status.*`. */
export const SUPPORT_TICKET_STATUSES = ["open", "pending", "answered", "closed"] as const;
/** Ticket priorities that have a translation under `admin.support.priority.*`. */
export const SUPPORT_TICKET_PRIORITIES = ["low", "normal", "high", "urgent"] as const;
/** Subscription states that have a translation under `admin.billing.status.*`. */
export const SUBSCRIPTION_STATUSES = ["trial", "active", "past_due", "cancelled", "frozen"] as const;
/**
* Provider keys whose casing is a brand name rather than a word, so title-casing
* would spell them wrong ("Hubspot" instead of "HubSpot").
*/
const BRAND_NAMES: Readonly<Record<string, string>> = {
aircall: "Aircall",
brevo: "Brevo",
discord: "Discord",
github: "GitHub",
gitlab: "GitLab",
hubspot: "HubSpot",
kwp: "KWP",
lago: "Lago",
mailchimp: "Mailchimp",
mailjet: "Mailjet",
meilisearch: "Meilisearch",
mollie: "Mollie",
pandadoc: "PandaDoc",
sendgrid: "SendGrid",
slack: "Slack",
webhook: "Webhook",
};
/**
* Turns a stored identifier into readable text: `past_due` → "Past due",
* `admin-sales` → "Admin sales", `business` → "Business". Only the first word is
* capitalized, because German sentence case reads better than Title Case for
* multi-word values.
*/
export function humanizeIdentifier(value: string): string {
const words = value
.trim()
.split(/[\s_.-]+/u)
.filter(Boolean);
if (words.length === 0) return value;
const [first, ...rest] = words;
return [first.charAt(0).toUpperCase() + first.slice(1).toLowerCase(), ...rest.map((word) => word.toLowerCase())].join(" ");
}
/** i18n key for a ticket status, or `null` when the value is not a known status. */
export function supportStatusLabelKey(status: string): string | null {
return (SUPPORT_TICKET_STATUSES as readonly string[]).includes(status) ? `admin.support.status.${status}` : null;
}
/** i18n key for a ticket priority, or `null` when the value is unknown. */
export function supportPriorityLabelKey(priority: string): string | null {
return (SUPPORT_TICKET_PRIORITIES as readonly string[]).includes(priority) ? `admin.support.priority.${priority}` : null;
}
/** i18n key for a subscription state, or `null` when the value is unknown. */
export function subscriptionStatusLabelKey(status: string): string | null {
return (SUBSCRIPTION_STATUSES as readonly string[]).includes(status) ? `admin.billing.status.${status}` : null;
}
/**
* Ticket intake channel. The set grows with every new intake path (`email`,
* `inline`, seeded demo data), so it is humanized rather than translated.
*/
export function supportSourceLabel(source: string): string {
return humanizeIdentifier(source);
}
/** Operator-defined subscription plan code, e.g. `business` → "Business". */
export function planCodeLabel(planCode: string | null | undefined): string | null {
if (!planCode?.trim()) return null;
return humanizeIdentifier(planCode);
}
/** Integration or provider key, with brand casing preserved. */
export function providerLabel(provider: string): string {
const normalized = provider.trim().toLowerCase();
return BRAND_NAMES[normalized] ?? humanizeIdentifier(provider);
}
/**
* Renders a closed-set value through i18n and falls back to a humanized form for
* values the catalog does not know yet. Keeps an unexpected value readable
* instead of showing either a raw enum or a missing translation key.
*/
export function translateOrHumanize(translate: (key: string) => string, labelKey: string | null, rawValue: string): string {
return labelKey ? translate(labelKey) : humanizeIdentifier(rawValue);
}