cachedUserTz = $this->detectUserTimezone($request); } /** * Get the resolved user timezone (detect lazily if needed). */ public function userTimezone(?RequestInterface $request = null): string { if ($this->cachedUserTz !== null) { return $this->cachedUserTz; } $this->cachedUserTz = $this->detectUserTimezone($request); return $this->cachedUserTz; } /** * Core detection logic (headers > user pref > settings > school config > default). */ public function detectUserTimezone(?RequestInterface $request = null): string { $request = $request ?: service('request'); // 1) Explicit client header try { $tzHeader = $request->getHeaderLine('X-Timezone') ?: $request->getHeaderLine('Timezone') ?: $request->getHeaderLine('Accept-Timezone') ?: null; if ($tzHeader && in_array($tzHeader, timezone_identifiers_list(), true)) { return $tzHeader; } } catch (\Throwable $e) { // ignore } // 2) User preference (if logged-in) try { $userId = (int) (session('user_id') ?: 0); if ($userId > 0) { // Do not select a specific column; some DBs may not have it yet $pref = (new PreferencesModel()) ->where('user_id', $userId) ->first(); $tz = $pref['timezone'] ?? null; if ($tz && in_array($tz, timezone_identifiers_list(), true)) { return $tz; } } } catch (\Throwable $e) { // ignore } // 3) Global settings timezone (if available) try { $settings = (new SettingsModel())->getSettings(); $tz = $settings['timezone'] ?? null; if ($tz && in_array($tz, timezone_identifiers_list(), true)) { return $tz; } } catch (\Throwable $e) { // ignore } // 4) School attendance timezone (if defined) try { $tz = (string) (config('School')->attendance['timezone'] ?? ''); if ($tz && in_array($tz, timezone_identifiers_list(), true)) { return $tz; } } catch (\Throwable $e) { // ignore } // 5) Default return $this->defaultUserTimezone; } /** * Server timezone (for storage/UTC operations). */ public function serverTimezone(): string { return $this->serverTimezone; } /** * Current time in user timezone as CI Time. */ public function nowLocal(?string $tz = null): Time { $tz = $tz ?: $this->userTimezone(); return Time::now($tz); } /** * Current time in UTC as CI Time. */ public function nowUTC(): Time { return Time::now($this->serverTimezone); } /** * Convert any supported input to UTC string (Y-m-d H:i:s by default). * * @param string|Time|\DateTimeInterface|null $value */ public function toUTC($value, ?string $fromTz = null, string $format = 'Y-m-d H:i:s'): ?string { if ($value === null || $value === '') { return null; } $fromTz = $fromTz ?: $this->userTimezone(); try { if ($value instanceof Time) { return $value->setTimezone($this->serverTimezone)->toDateTimeString(); } if ($value instanceof \DateTimeInterface) { return Time::createFromInstance($value) ->setTimezone($this->serverTimezone) ->format($format); } // assume string return Time::parse((string)$value, $fromTz) ->setTimezone($this->serverTimezone) ->format($format); } catch (\Throwable $e) { return null; } } /** * Convert UTC (or provided timezone) to user-local string. * * @param string|Time|\DateTimeInterface|null $value */ public function toLocal($value, ?string $sourceTz = null, ?string $targetTz = null, string $format = 'Y-m-d H:i:s'): ?string { if ($value === null || $value === '') { return null; } $targetTz = $targetTz ?: $this->userTimezone(); if ($sourceTz === null && $this->isDateOnlyString($value)) { // Date-only strings should not shift across timezones. $sourceTz = $targetTz; } else { $sourceTz = $sourceTz ?: $this->serverTimezone; } try { if ($value instanceof Time) { return $value->setTimezone($targetTz)->format($format); } if ($value instanceof \DateTimeInterface) { return Time::createFromInstance($value) ->setTimezone($targetTz) ->format($format); } // assume string return Time::parse((string)$value, $sourceTz) ->setTimezone($targetTz) ->format($format); } catch (\Throwable $e) { return null; } } /** * Convenience formatter for user-local. */ public function formatLocal($value, string $format = 'Y-m-d H:i', ?string $sourceTz = null): string { return (string) ($this->toLocal($value, $sourceTz, null, $format) ?? ''); } /** * Convenience formatter for UTC. */ public function formatUTC($value, string $format = 'Y-m-d H:i:s', ?string $fromTz = null): string { return (string) ($this->toUTC($value, $fromTz, $format) ?? ''); } private function isDateOnlyString($value): bool { if (!is_string($value)) { return false; } $value = trim($value); return (bool) preg_match('/^\d{4}-\d{2}-\d{2}$/', $value); } }