*/ function familyHubUsTimezoneIdentifiers(): array { $ids = DateTimeZone::listIdentifiers(DateTimeZone::PER_COUNTRY, 'US'); if (is_array($ids) && $ids !== []) { sort($ids); return array_values($ids); } return [ 'Pacific/Honolulu', 'America/Anchorage', 'America/Los_Angeles', 'America/Phoenix', 'America/Denver', 'America/Chicago', 'America/New_York', ]; } /** * @return array */ function familySettingsDefaultsRaw(): array { return [ 'currency_symbol' => '★', 'currency_name' => 'Stars', 'currency_permanence' => 'permanent', 'timezone' => 'America/New_York', 'week_starts_on' => 0, 'calendar_two_way_google' => false, 'calendar_bill_days' => [], 'nfc_base_url' => '', 'nfc_show_confirmation' => true, 'nfc_scan_cooldown_seconds' => 0, ]; } /** * @param mixed $raw * @return list */ function normalizeCalendarBillDaysRaw($raw): array { if (!is_array($raw)) { return []; } $out = []; foreach ($raw as $row) { if (!is_array($row)) { continue; } $d = isset($row['dayOfMonth']) ? (int) $row['dayOfMonth'] : 0; $title = trim((string) ($row['title'] ?? '')); if ($d < 1 || $d > 31 || $title === '') { continue; } $out[] = ['dayOfMonth' => $d, 'title' => $title]; } return $out; } function normalizeTimezoneToUs(string $tz, array $allowed): string { $tz = trim($tz); if ($tz !== '' && in_array($tz, $allowed, true)) { return $tz; } return 'America/New_York'; } /** * Apply defaults and normalize timezone / calendar fields after merge with stored JSON. * * @param array $s * @return array */ function normalizeLoadedFamilySettings(array $s): array { $allowed = familyHubUsTimezoneIdentifiers(); $s['timezone'] = normalizeTimezoneToUs((string) ($s['timezone'] ?? ''), $allowed); $v = $s['calendar_two_way_google'] ?? false; $s['calendar_two_way_google'] = $v === true || $v === 1 || $v === '1' || $v === 'true'; $s['calendar_bill_days'] = normalizeCalendarBillDaysRaw($s['calendar_bill_days'] ?? []); $s['nfc_base_url'] = trim((string) ($s['nfc_base_url'] ?? '')); $showConfirmation = $s['nfc_show_confirmation'] ?? true; $s['nfc_show_confirmation'] = $showConfirmation === true || $showConfirmation === 1 || $showConfirmation === '1' || $showConfirmation === 'true'; $cooldown = (int) ($s['nfc_scan_cooldown_seconds'] ?? 0); $s['nfc_scan_cooldown_seconds'] = max(0, min(600, $cooldown)); return $s; } function defaultFamilySettings(): array { return normalizeLoadedFamilySettings(familySettingsDefaultsRaw()); } function loadFamilySettings(): array { $raw = readJsonFile('family_settings.json'); $merged = familySettingsDefaultsRaw(); if (is_array($raw)) { $merged = array_merge($merged, $raw); } return normalizeLoadedFamilySettings($merged); } /** * Base app URL (without trailing slash) used for externally shared links. */ function familyHubAppUrl(array $familySettings): string { $base = trim((string) ($familySettings['nfc_base_url'] ?? '')); if ($base !== '') { return rtrim($base, '/'); } $https = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'; $scheme = $https ? 'https' : 'http'; $host = trim((string) ($_SERVER['HTTP_HOST'] ?? 'localhost')); $script = trim((string) ($_SERVER['SCRIPT_NAME'] ?? '/index.php')); $dir = rtrim(str_replace('\\', '/', dirname($script)), '/'); if ($dir === '' || $dir === '.') { $dir = ''; } if (substr($dir, -4) === '/api') { $dir = substr($dir, 0, -4); } return $scheme . '://' . $host . $dir; } /** * Tab label: symbol + name (e.g. "★ Stars"). */ function currencyTabLabel(array $familySettings): string { $sym = trim((string) ($familySettings['currency_symbol'] ?? '')); $name = trim((string) ($familySettings['currency_name'] ?? '')); $label = trim($sym . ' ' . $name); return $label !== '' ? $label : 'Currency'; }