diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c5a500e..04e7a24b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Format based on [Keep a Changelog](https://keepachangelog.com/). - **Product types as Bill-of-Materials components** *(admin)* — a BOM line can now be a manufactured **product type** (a sub-assembly), not only a material. In the BOM editor a Material / Product type switch picks the component kind; product-type lines carry the same quantity-per-unit, step, scrap %, consumption timing and notes as materials. A product type can't be a component of itself, and each appears once per template. Lines are captured in the work-order snapshot as sub-assembly references; they're a simple component reference (they don't explode into their own BOM) and are skipped by the material stock/consumption engine. Additive — existing material BOMs are unaffected. ### Fixed +- **Saving system settings crashed on PostgreSQL** *(admin)* — the plant-timezone save wrote the raw identifier (e.g. `Europe/Warsaw`) into the JSON `system_settings.value` column, which PostgreSQL rejects (`invalid input syntax for type json`), 500-ing the whole Settings → System save; SQLite tolerated it, so tests missed it. The value is now JSON-encoded (and decoded on read, tolerating legacy raw values). - **Header clock ignored the configured timezone** *(all users)* — the live clock top-right was hardcoded to `Europe/Warsaw`, so on any install with a different timezone it was the one timestamp in the UI that disagreed with all the others. It now goes through the same `formatDate`/`formatTime` helpers as the rest of the app. ## [0.21.0] - 2026-08-21 diff --git a/backend/app/Support/TimezoneRegistry.php b/backend/app/Support/TimezoneRegistry.php index 8642a2e6..e2f52137 100644 --- a/backend/app/Support/TimezoneRegistry.php +++ b/backend/app/Support/TimezoneRegistry.php @@ -59,11 +59,21 @@ public static function stored(): ?string return null; } - if (! $row || ! self::isValid((string) $row->value)) { + if (! $row) { return null; } - return self::$cached = (string) $row->value; + // `system_settings.value` is a JSON column, so the stored value is + // JSON-encoded (e.g. "Europe/Warsaw"). Decode it; tolerate a legacy raw + // string too. + $decoded = json_decode((string) $row->value, true); + $value = is_string($decoded) ? $decoded : (string) $row->value; + + if (! self::isValid($value)) { + return null; + } + + return self::$cached = $value; } /** @@ -95,7 +105,9 @@ public static function save(string $timezone): void DB::table('system_settings')->updateOrInsert( ['key' => self::SETTING_KEY], - ['value' => $timezone, 'updated_at' => now()], + // JSON-encode: `value` is a JSON column, so a bare string is rejected + // by Postgres (works on SQLite, which is why tests missed it). + ['value' => json_encode($timezone), 'updated_at' => now()], ); self::$cached = $timezone; diff --git a/backend/tests/Feature/Web/SystemSettingsTimezoneTest.php b/backend/tests/Feature/Web/SystemSettingsTimezoneTest.php index a1bab99a..468c6647 100644 --- a/backend/tests/Feature/Web/SystemSettingsTimezoneTest.php +++ b/backend/tests/Feature/Web/SystemSettingsTimezoneTest.php @@ -66,12 +66,14 @@ public function test_admin_can_change_the_plant_timezone(): void ->post('/settings/system', $this->payload(['app_timezone' => 'America/Argentina/Buenos_Aires'])) ->assertSessionHasNoErrors(); - // Stored raw, not JSON-encoded — TimezoneRegistry reads it as a plain - // identifier, so an accidental json_encode() here would break `stored()`. + // `system_settings.value` is a JSON column, so the identifier must be + // JSON-encoded — a bare string is rejected by Postgres (SQLite tolerates + // it, which is how the bug shipped). stored() decodes it back. $this->assertDatabaseHas('system_settings', [ 'key' => TimezoneRegistry::SETTING_KEY, - 'value' => 'America/Argentina/Buenos_Aires', + 'value' => json_encode('America/Argentina/Buenos_Aires'), ]); + TimezoneRegistry::flush(); $this->assertSame('America/Argentina/Buenos_Aires', TimezoneRegistry::stored()); }