email. */ function currentUserDisplayName(): string { $user = $_SESSION['user'] ?? []; $name = trim((string) (($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? ''))); if ($name !== '') { return $name; } return trim((string) ($user['email'] ?? '')); } /** * Absolute logo URL (used in e-mails and metadata). */ function appLogoUrlAbsolute(int $size = 128): string { $url = appLogoUrl($size); return appUrl($url); } /** * Read one cached app setting from config/settings.php. */ function appSetting(string $key): ?string { if (!class_exists('MintyPHP\\Service\\Settings\\SettingServicesFactory')) { return null; } static $settingsFactory = null; if (!$settingsFactory instanceof \MintyPHP\Service\Settings\SettingServicesFactory) { $settingsFactory = new \MintyPHP\Service\Settings\SettingServicesFactory(); } return $settingsFactory->createSettingCacheService()->get($key); } /** * Shared per-request factory for tenant/department/role domain services. */ function directoryServicesFactory(): \MintyPHP\Service\Directory\DirectoryServicesFactory { static $factory = null; if (!$factory instanceof \MintyPHP\Service\Directory\DirectoryServicesFactory) { $factory = new \MintyPHP\Service\Directory\DirectoryServicesFactory(); } return $factory; } /** * Shared per-request factory for access/permission services. */ function accessServicesFactory(): \MintyPHP\Service\Access\AccessServicesFactory { static $factory = null; if (!$factory instanceof \MintyPHP\Service\Access\AccessServicesFactory) { $factory = new \MintyPHP\Service\Access\AccessServicesFactory(); } return $factory; } /** * Shared per-request permission gateway wrapper. */ function permissionGateway(): \MintyPHP\Service\Access\PermissionGateway { return accessServicesFactory()->createPermissionGateway(); } /** * Shared per-request factory for audit services. */ function auditServicesFactory(): \MintyPHP\Service\Audit\AuditServicesFactory { static $factory = null; if (!$factory instanceof \MintyPHP\Service\Audit\AuditServicesFactory) { $factory = new \MintyPHP\Service\Audit\AuditServicesFactory(); } return $factory; } /** * Shared per-request factory for branding services. */ function brandingServicesFactory(): \MintyPHP\Service\Branding\BrandingServicesFactory { static $factory = null; if (!$factory instanceof \MintyPHP\Service\Branding\BrandingServicesFactory) { $factory = new \MintyPHP\Service\Branding\BrandingServicesFactory(); } return $factory; } /** * Load configured themes with a safe fallback list. */ function appThemes(): array { if (!class_exists('MintyPHP\\Service\\Settings\\SettingServicesFactory')) { return [ 'light' => 'Light', 'dark' => 'Dark', ]; } static $settingsFactory = null; if (!$settingsFactory instanceof \MintyPHP\Service\Settings\SettingServicesFactory) { $settingsFactory = new \MintyPHP\Service\Settings\SettingServicesFactory(); } return $settingsFactory->createThemeConfigService()->all(); } /** * Resolve default locale only if it exists in APP_LOCALES. */ function appDefaultLocale(): ?string { $locale = appSetting('app_locale'); if ($locale === null) { return null; } $available = defined('APP_LOCALES') ? APP_LOCALES : []; if ($available && !in_array($locale, $available, true)) { return null; } return $locale; } /** * Resolve default theme from settings, then APP_THEME, then light. */ function appDefaultTheme(): string { $themes = appThemes(); $tenantTheme = strtolower(trim((string) ($_SESSION['current_tenant']['default_theme'] ?? ''))); if ($tenantTheme !== '' && isset($themes[$tenantTheme])) { return $tenantTheme; } $setting = appSetting('app_theme'); if ($setting !== null && isset($themes[$setting])) { return $setting; } $envTheme = getenv('APP_THEME') ?: 'light'; $envTheme = strtolower(trim((string) $envTheme)); return isset($themes[$envTheme]) ? $envTheme : 'light'; } /** * Resolve effective theme with optional per-user override. */ function currentTheme(): string { $themes = appThemes(); $theme = appDefaultTheme(); if (!allowUserTheme()) { return $theme; } $user = $_SESSION['user'] ?? []; $userTheme = strtolower(trim((string) ($user['theme'] ?? ''))); if ($userTheme !== '' && isset($themes[$userTheme])) { return $userTheme; } return $theme; } /** * Feature flag: allow users to choose their own theme. */ function allowUserTheme(): bool { $tenantValue = $_SESSION['current_tenant']['allow_user_theme'] ?? null; if ($tenantValue !== null && $tenantValue !== '') { $tenantValue = strtolower(trim((string) $tenantValue)); if (in_array($tenantValue, ['1', 'true', 'yes', 'on'], true)) { return true; } if (in_array($tenantValue, ['0', 'false', 'no', 'off'], true)) { return false; } } $value = appSetting('app_theme_user'); if ($value === null || $value === '') { return true; } return in_array(strtolower($value), ['1', 'true', 'yes', 'on'], true); } /** * Feature flag: allow public self-registration. */ function allowRegistration(): bool { $value = appSetting('app_registration'); if ($value === null || $value === '') { return true; } return in_array(strtolower($value), ['1', 'true', 'yes', 'on'], true); } /** * Resolve active primary color (tenant override -> app setting). */ function appPrimaryColor(): ?string { // Tenant-scoped branding has precedence over global settings. $tenantColor = $_SESSION['current_tenant']['primary_color'] ?? null; if ($tenantColor !== null) { $tenantColor = strtolower(trim((string) $tenantColor)); if (preg_match('/^#([0-9a-f]{3}|[0-9a-f]{6})$/i', $tenantColor)) { return $tenantColor; } } $value = appSetting('app_primary_color'); if ($value === null) { return null; } $value = strtolower(trim($value)); if (!preg_match('/^#([0-9a-f]{3}|[0-9a-f]{6})$/i', $value)) { return null; } return $value; } /** * Convert current hex color to CSS HSL custom properties. */ function appPrimaryCssVars(): string { $hex = appPrimaryColor(); if ($hex === null) { return ''; } $hex = ltrim($hex, '#'); if (strlen($hex) === 3) { $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2]; } $r = hexdec(substr($hex, 0, 2)) / 255; $g = hexdec(substr($hex, 2, 2)) / 255; $b = hexdec(substr($hex, 4, 2)) / 255; // Short-circuit grayscale values to avoid unstable hue math. $monoThreshold = 0.000001; if (abs($r - $g) < $monoThreshold && abs($g - $b) < $monoThreshold) { $l = round($r * 100, 2) . '%'; return "--app-primary-h-base: 0; --app-primary-s-base: 0%; --app-primary-l-base: {$l}; --app-primary-h-light: 0; --app-primary-s-light: 0%; --app-primary-l-light: {$l};"; } $max = max($r, $g, $b); $min = min($r, $g, $b); $delta = $max - $min; if ($delta === 0.0) { $l = round((($max + $min) / 2) * 100, 2) . '%'; return "--app-primary-h-base: 0; --app-primary-s-base: 0%; --app-primary-l-base: {$l}; --app-primary-h-light: 0; --app-primary-s-light: 0%; --app-primary-l-light: {$l};"; } $h = 0.0; if ($max === $r) { $h = 60 * fmod((($g - $b) / $delta), 6); } elseif ($max === $g) { $h = 60 * ((($b - $r) / $delta) + 2); } else { $h = 60 * ((($r - $g) / $delta) + 4); } if ($h < 0) { $h += 360; } $l = ($max + $min) / 2; $denominator = 1 - abs(2 * $l - 1); if ($delta === 0.0 || $denominator === 0.0) { $s = 0.0; } else { $s = $delta / $denominator; } $h = round($h, 2); $s = round($s * 100, 2) . '%'; $l = round($l * 100, 2) . '%'; return "--app-primary-h-base: {$h}; --app-primary-s-base: {$s}; --app-primary-l-base: {$l}; --app-primary-h-light: {$h}; --app-primary-s-light: {$s}; --app-primary-l-light: {$l};"; } /** * Resolve app logo path (custom upload with fallback asset). */ function appLogoUrl(?int $size = null): string { if (class_exists('MintyPHP\\Service\\Branding\\BrandingServicesFactory')) { $logoService = brandingServicesFactory()->createBrandingLogoService(); if ($logoService->hasLogo()) { $query = $size ? '?size=' . (int) $size : ''; return lurl('branding/logo' . $query); } } return asset('brand/logo.svg'); } /** * Resolve favicon path (tenant favicon with global fallback). */ function appFaviconUrl(string $file): string { $tenantUuid = $_SESSION['current_tenant']['uuid'] ?? ''; if ($tenantUuid !== '' && class_exists('MintyPHP\\Service\\Tenant\\TenantServicesFactory')) { static $tenantServicesFactory = null; if (!$tenantServicesFactory instanceof \MintyPHP\Service\Tenant\TenantServicesFactory) { $tenantServicesFactory = new \MintyPHP\Service\Tenant\TenantServicesFactory(); } if ($tenantServicesFactory->createTenantFaviconService()->hasFavicon($tenantUuid)) { return asset('favicon/tenants/' . $tenantUuid . '/favicon/' . ltrim($file, '/')); } } return asset('favicon/' . ltrim($file, '/')); } /** * Sort array items by 'description' case-insensitively. */ function sortByDescription(array &$items): void { usort($items, static fn($a, $b) => strcasecmp((string) ($a['description'] ?? ''), (string) ($b['description'] ?? '')) ); }