From a02f718a41ec499d448e635d39a1799ee94bba26 Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Thu, 12 Feb 2026 22:10:37 +0100 Subject: [PATCH] refactor languages module to domain/controller and release 0.254 update package --- DATABASE_STRUCTURE.md | 26 + PROJECT_STRUCTURE.md | 11 + REFACTORING_PLAN.md | 16 + TESTING.md | 20 + admin/templates/components/table-list.php | 10 +- admin/templates/languages/language-edit.php | 101 +--- admin/templates/languages/languages-list.php | 58 +- .../templates/languages/translation-edit.php | 60 +- .../templates/languages/translations-list.php | 44 +- .../Domain/Languages/LanguagesRepository.php | 330 +++++++++++ .../admin/Controllers/LanguagesController.php | 560 ++++++++++++++++++ autoload/admin/class.Site.php | 7 + autoload/admin/controls/class.Languages.php | 82 --- autoload/admin/factory/class.Languages.php | 130 +--- autoload/admin/view/class.Languages.php | 34 -- .../Languages/LanguagesRepositoryTest.php | 116 ++++ .../Controllers/LanguagesControllerTest.php | 63 ++ updates/0.20/ver_0.254.zip | Bin 0 -> 14334 bytes updates/0.20/ver_0.254_files.txt | 2 + updates/changelog.php | 8 + updates/shopPRO.zip | Bin 15623 -> 14334 bytes updates/versions.php | 2 +- 22 files changed, 1190 insertions(+), 490 deletions(-) create mode 100644 autoload/Domain/Languages/LanguagesRepository.php create mode 100644 autoload/admin/Controllers/LanguagesController.php delete mode 100644 autoload/admin/controls/class.Languages.php delete mode 100644 autoload/admin/view/class.Languages.php create mode 100644 tests/Unit/Domain/Languages/LanguagesRepositoryTest.php create mode 100644 tests/Unit/admin/Controllers/LanguagesControllerTest.php create mode 100644 updates/0.20/ver_0.254.zip create mode 100644 updates/0.20/ver_0.254_files.txt diff --git a/DATABASE_STRUCTURE.md b/DATABASE_STRUCTURE.md index 0253566..99353fe 100644 --- a/DATABASE_STRUCTURE.md +++ b/DATABASE_STRUCTURE.md @@ -183,3 +183,29 @@ Uzytkownicy panelu administratora. **Uzywane w:** `Domain\User\UserRepository`, `admin\Controllers\UsersController`, `admin\factory\Users` **Aktualizacja 2026-02-12:** uzycia `pp_users` sa prowadzone przez `Domain\\User\\UserRepository` (legacy `admin\\factory\\Users` usunieto). + +## pp_langs +Jezyki panelu i frontendu. + +| Kolumna | Opis | +|---------|------| +| id | PK (2-literowe ID jezyka, np. pl, en) | +| name | Nazwa jezyka | +| status | 1 = aktywny, 0 = nieaktywny | +| start | 1 = domyslny jezyk | +| o | Kolejnosc | + +**Uzywane w:** `Domain\\Languages\\LanguagesRepository`, `admin\\Controllers\\LanguagesController`, `admin\\factory\\Languages`, `front\\factory\\Languages` + +## pp_langs_translations +Slownik tlumaczen panelu/frontendu. + +| Kolumna | Opis | +|---------|------| +| id | PK | +| text | Klucz/tekst bazowy | +| | Kolumny dynamiczne per jezyk (np. pl, en) | + +**Uzywane w:** `Domain\\Languages\\LanguagesRepository`, `admin\\Controllers\\LanguagesController`, `front\\factory\\Languages` + +**Aktualizacja 2026-02-12:** modul jezykow i tlumaczen (`pp_langs`, `pp_langs_translations`) obslugiwany przez `Domain\\Languages\\LanguagesRepository`. diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md index b6b27f7..aa1961c 100644 --- a/PROJECT_STRUCTURE.md +++ b/PROJECT_STRUCTURE.md @@ -391,3 +391,14 @@ Aktualnie w suite sÄ… też testy modułów `Dictionaries`, `Articles` i `Users` - Usuniêto legacy klasy: `autoload/admin/controls/class.Users.php`, `autoload/admin/factory/class.Users.php`, `autoload/admin/view/class.Users.php`. - Walidacja: przy w³¹czonym 2FA pole `twofa_email` jest wymagane. - Widoki users przeniesione na `components/table-list` i `components/form-edit`. +- **NOWE:** `Domain\\Languages\\LanguagesRepository` - repozytorium jezykow i tlumaczen (lista, zapis, usuwanie, max_order) +- **NOWE:** `admin\\Controllers\\LanguagesController` - kontroler DI (`list/view_list`, `language_*`, `translation_*`) +- **UPDATE:** modul Languages przepiety z `grid/gridEdit` na `components/table-list` i `components/form-edit` +- **CLEANUP:** usuniete legacy klasy `autoload/admin/controls/class.Languages.php`, `autoload/admin/view/class.Languages.php` +- Testy: 130 tests, 301 assertions + +## Aktualizacja 2026-02-12 (Languages final) +- Dodano `Domain\\Languages\\LanguagesRepository` oraz `admin\\Controllers\\LanguagesController`. +- Modul `/admin/languages/` (jezyki + tlumaczenia) dziala na nowym routingu DI. +- Widoki jezykow przepiete na `components/table-list` i `components/form-edit`. +- Usunieto legacy: `autoload/admin/controls/class.Languages.php`, `autoload/admin/view/class.Languages.php`. diff --git a/REFACTORING_PLAN.md b/REFACTORING_PLAN.md index 63377ff..d1b0a81 100644 --- a/REFACTORING_PLAN.md +++ b/REFACTORING_PLAN.md @@ -574,3 +574,19 @@ Gdy `persist = true`: - `UsersController` obsluguje: `list/view_list`, `user_edit`, `user_save`, `user_delete`, `login_form`, `twofa`. - Dodano walidacje warunkowa: `twofa_email` wymagany gdy `twofa_enabled = 1`. - Widoki users migrowane z `grid/gridEdit` na `table-list` i `form-edit`. + +## Aktualizacja 2026-02-12 - Languages +- **NOWE:** `Domain\\Languages\\LanguagesRepository` (languages + translations CRUD/list) +- **NOWE:** `admin\\Controllers\\LanguagesController` (DI) +- **UPDATE:** `admin\\Site` - nowy kontroler DI dla modulu `Languages` +- **UPDATE:** `admin\\factory\\Languages` jako fasada delegujaca do repozytorium +- **UPDATE:** widoki `languages/*` migrowane na `components/table-list` i `components/form-edit` +- **CLEANUP:** usunieto legacy `admin\\controls\\Languages` i `admin\\view\\Languages` +- Testy po zmianie: 130 tests, 301 assertions + +## Aktualizacja 2026-02-12 (Languages final) +- **NOWE:** `Domain\\Languages\\LanguagesRepository` (list/save/delete dla jezykow i tlumaczen) +- **NOWE:** `admin\\Controllers\\LanguagesController` (DI) dla akcji `view_list/list`, `language_*`, `translation_*` +- **UPDATE:** `admin\\factory\\Languages` jako fasada delegujaca do repozytorium +- **CLEANUP:** usunieto legacy `admin\\controls\\Languages` oraz `admin\\view\\Languages` +- **UPDATE:** poprawki globalne `components/table-list` dla krotkich kolumn/filtrów diff --git a/TESTING.md b/TESTING.md index 0a94d8b..7028dfe 100644 --- a/TESTING.md +++ b/TESTING.md @@ -164,3 +164,23 @@ Ostatnio zweryfikowano: 2026-02-12 ```text OK (120 tests, 262 assertions) ``` + +Aktualizacja po migracji Languages (2026-02-12): +```text +OK (130 tests, 301 assertions) +``` + +Nowe testy dodane 2026-02-12: +- `tests/Unit/Domain/Languages/LanguagesRepositoryTest.php` +- `tests/Unit/admin/Controllers/LanguagesControllerTest.php` + +## Aktualizacja suite (release 0.254) +Ostatnio zweryfikowano: 2026-02-12 + +```text +OK (130 tests, 301 assertions) +``` + +Nowe testy dodane 2026-02-12: +- `tests/Unit/Domain/Languages/LanguagesRepositoryTest.php` +- `tests/Unit/admin/Controllers/LanguagesControllerTest.php` diff --git a/admin/templates/components/table-list.php b/admin/templates/components/table-list.php index ddb168b..c6a91d4 100644 --- a/admin/templates/components/table-list.php +++ b/admin/templates/components/table-list.php @@ -23,11 +23,11 @@ $isCompactColumn = function(array $column): bool { $key = strtolower(trim((string)($column['key'] ?? ''))); $label = strtolower(trim((string)($column['label'] ?? ''))); - if (in_array($key, ['status', 'active', 'enabled', 'is_active'], true)) { + if (in_array($key, ['status', 'active', 'enabled', 'is_active', 'start', 'default'], true)) { return true; } - if (in_array($label, ['status', 'aktywny', 'aktywnosc', 'active'], true)) { + if (in_array($label, ['status', 'aktywny', 'aktywnosc', 'active', 'domyslny', 'domyÅ›lny', 'default'], true)) { return true; } @@ -277,9 +277,9 @@ $isCompactColumn = function(array $column): bool { } .js-table-filters-form .js-filter-compact-select { - width: auto; - min-width: 110px; - max-width: 140px; + width: 100%; + min-width: 0; + max-width: none; } .table-list-table th.table-col-compact, diff --git a/admin/templates/languages/language-edit.php b/admin/templates/languages/language-edit.php index af603a8..be2362b 100644 --- a/admin/templates/languages/language-edit.php +++ b/admin/templates/languages/language-edit.php @@ -1,101 +1,2 @@ - $this->form]); ?> -ob_start(); -?> - language['id'] ) - { - echo \Html::input( - array( - 'type' => 'hidden', - 'name' => 'id', - 'value' => $this -> language['id'] - ) - ); - echo \Html::input( - array( - 'type' => 'hidden', - 'name' => 'o', - 'value' => $this -> language['o'] - ) - ); - echo \Html::input( - array( - 'label' => 'JÄ™zyk', - 'type' => 'text', - 'class' => 'require', - 'name' => 'name', - 'value' => $this -> language['name'] - ) - ); - } - else - { - echo \Html::input( - array( - 'type' => 'hidden', - 'name' => 'o', - 'value' => $this -> order + 1 - ) - ); - echo \Html::input( - array( - 'label' => 'JÄ™zyk', - 'type' => 'text', - 'class' => 'require', - 'name' => 'name' - ) - ); - echo \Html::input( - array( - 'label' => 'ID (2 znaki)', - 'class' => 'require', - 'type' => 'text', - 'name' => 'id' - ) - ); - } - ?> - 'Aktywny', - 'name' => 'status', - 'checked' => $this -> language['status'] == 1 ? true : false - ) - );?> - 'DomyÅ›lny', - 'name' => 'start', - 'checked' => $this -> language['start'] == 1 ? true : false - ) - );?> - id = 'language-edit'; -$grid -> gdb_opt = $gdb; -$grid -> include_plugins = true; -$grid -> title = 'Edycja jÄ™zyka'; -$grid -> external_code = $out; -$grid -> actions = [ - 'save' => [ 'url' => '/admin/languages/language_save/', 'back_url' => '/admin/languages/view_list/' ], - 'cancel' => [ 'url' => '/admin/languages/view_list/' ] - ]; -$grid -> persist_edit = true; -$grid -> id_param = 'id'; -echo $grid -> draw(); -?> - \ No newline at end of file diff --git a/admin/templates/languages/languages-list.php b/admin/templates/languages/languages-list.php index de4d85f..b336124 100644 --- a/admin/templates/languages/languages-list.php +++ b/admin/templates/languages/languages-list.php @@ -1,58 +1,2 @@ - $this->viewModel]); ?> -$grid = new \grid( 'pp_langs' ); -$grid -> gdb_opt = $gdb; -$grid -> order = [ 'column' => 'o', 'type' => 'ASC' ]; -$grid -> search = [ - [ 'name' => 'JÄ™zyk', 'db' => 'name', 'type' => 'text' ], - [ 'name' => 'Aktywny', 'db' => 'status', 'type' => 'select', 'replace' => [ 'array' => [ 0 => 'nie', 1 => 'tak' ] ] ], - [ 'name' => 'DomyÅ›lny', 'db' => 'start', 'type' => 'select', 'replace' => [ 'array' => [ 0 => 'nie', 1 => 'tak' ] ] ] - ]; -$grid -> columns_view = [ - [ - 'name' => 'Lp.', - 'th' => [ 'class' => 'g-lp' ], - 'td' => [ 'class' => 'g-center' ], - 'autoincrement' => true - ], - [ - 'name' => 'DomyÅ›lny', - 'db' => 'start', - 'th' => [ 'class' => 'g-center', 'style' => 'width: 150px;' ], - 'td' => [ 'class' => 'g-center' ], - 'php' => 'if ( [start] ) echo "tak"; else echo "nie";' - ], - [ - 'name' => 'Aktywny', - 'db' => 'status', - 'replace' => [ 'array' => [ 0 => 'nie', 1 => 'tak' ] ], - 'th' => [ 'class' => 'g-center', 'style' => 'width: 150px;' ], - 'td' => [ 'class' => 'g-center' ] - ], - [ - 'name' => 'JÄ™zyk', - 'db' => 'name' - ], - [ - 'name' => 'Edytuj', - 'action' => [ 'type' => 'edit', 'url' => '/admin/languages/language_edit/id=[id]' ], - 'th' => [ 'class' => 'g-center', 'style' => 'width: 70px;' ], - 'td' => [ 'class' => 'g-center' ] - ], - [ - 'name' => 'UsuÅ„', - 'action' => [ 'type' => 'delete', 'url' => '/admin/languages/language_delete/id=[id]' ], - 'th' => [ 'class' => 'g-center', 'style' => 'width: 70px;' ], - 'td' => [ 'class' => 'g-center' ] - ] - ]; -$grid -> buttons = [ - [ - 'label' => 'Dodaj jÄ™zyk', - 'url' => '/admin/languages/language_edit/', - 'icon' => 'fa-plus-circle', - 'class' => 'btn-success' - ] - ]; -echo $grid -> draw(); \ No newline at end of file diff --git a/admin/templates/languages/translation-edit.php b/admin/templates/languages/translation-edit.php index 64895ed..be2362b 100644 --- a/admin/templates/languages/translation-edit.php +++ b/admin/templates/languages/translation-edit.php @@ -1,60 +1,2 @@ - $this->form]); ?> -ob_start(); -?> - 'text', - 'label' => 'Tekst', - 'name' => 'text', - 'class' => 'require', - 'value' => $this -> translation['text'] - ) -); - -if ( is_array( $this -> languages ) ): foreach ( $this -> languages as $language ): - echo \Html::input( - array( - 'type' => 'text', - 'label' => $language['name'], - 'name' => $language['id'], - 'value' => $this -> translation[$language['id']] - ) ); - endforeach; -endif; -$out = ob_get_clean(); - -$grid = new \gridEdit; -$grid -> id = 'translation-edit'; -$grid -> gdb_opt = $gdb; -$grid -> include_plugins = true; -$grid -> title = 'Edycja tÅ‚umaczenia'; -$grid -> fields = [ - [ - 'db' => 'id', - 'type' => 'hidden', - 'value' => $this -> translation['id'] - ] -]; -$grid -> external_code = $out; -$grid -> actions = [ - 'save' => [ 'url' => '/admin/languages/translation_save/', 'back_url' => '/admin/languages/translation_list/' ], - 'cancel' => [ 'url' => '/admin/languages/translation_list/' ] -]; -$grid -> persist_edit = true; -$grid -> id_param = 'id'; -echo $grid -> draw(); -?> - \ No newline at end of file diff --git a/admin/templates/languages/translations-list.php b/admin/templates/languages/translations-list.php index a7e2d6d..b336124 100644 --- a/admin/templates/languages/translations-list.php +++ b/admin/templates/languages/translations-list.php @@ -1,44 +1,2 @@ - $this->viewModel]); ?> -$grid = new \grid( 'pp_langs_translations' ); -$grid -> gdb_opt = $gdb; -$grid -> order = [ 'column' => 'text', 'type' => 'ASC' ]; -$grid -> search = [ - [ 'name' => 'Tekst', 'db' => 'text', 'type' => 'text' ] - ]; -$grid -> columns_view = [ - [ - 'name' => 'Lp.', - 'th' => [ 'class' => 'g-lp' ], - 'td' => [ 'class' => 'g-center' ], - 'autoincrement' => true - ], - [ - 'name' => 'Tekst', - 'db' => 'text', - 'php' => 'echo "[text]";', - 'sort' => true - ], - [ - 'name' => 'Edytuj', - 'action' => [ 'type' => 'edit', 'url' => '/admin/languages/translation_edit/id=[id]' ], - 'th' => [ 'class' => 'g-center', 'style' => 'width: 70px;' ], - 'td' => [ 'class' => 'g-center' ] - ], - [ - 'name' => 'UsuÅ„', - 'action' => [ 'type' => 'delete', 'url' => '/admin/languages/translation_delete/id=[id]' ], - 'th' => [ 'class' => 'g-center', 'style' => 'width: 70px;' ], - 'td' => [ 'class' => 'g-center' ] - ] - ]; -$grid -> buttons = [ - [ - 'label' => 'Dodaj tÅ‚umaczenie', - 'url' => '/admin/languages/translation_edit/', - 'icon' => 'fa-plus-circle', - 'class' => 'btn-success' - ] - ]; -echo $grid -> draw(); \ No newline at end of file diff --git a/autoload/Domain/Languages/LanguagesRepository.php b/autoload/Domain/Languages/LanguagesRepository.php new file mode 100644 index 0000000..7957acc --- /dev/null +++ b/autoload/Domain/Languages/LanguagesRepository.php @@ -0,0 +1,330 @@ +db = $db; + } + + /** + * @return array{items: array>, total: int} + */ + public function listForAdmin( + array $filters, + string $sortColumn = 'o', + string $sortDir = 'ASC', + int $page = 1, + int $perPage = 15 + ): array { + $allowedSortColumns = [ + 'id' => 'pl.id', + 'name' => 'pl.name', + 'status' => 'pl.status', + 'start' => 'pl.start', + 'o' => 'pl.o', + ]; + + $sortSql = $allowedSortColumns[$sortColumn] ?? 'pl.o'; + $sortDir = strtoupper(trim($sortDir)) === 'DESC' ? 'DESC' : 'ASC'; + $page = max(1, $page); + $perPage = min(self::MAX_PER_PAGE, max(1, $perPage)); + $offset = ($page - 1) * $perPage; + + $where = ['1 = 1']; + $params = []; + + $name = trim((string)($filters['name'] ?? '')); + if ($name !== '') { + if (strlen($name) > 255) { + $name = substr($name, 0, 255); + } + $where[] = 'pl.name LIKE :name'; + $params[':name'] = '%' . $name . '%'; + } + + $status = trim((string)($filters['status'] ?? '')); + if ($status === '0' || $status === '1') { + $where[] = 'pl.status = :status'; + $params[':status'] = (int)$status; + } + + $start = trim((string)($filters['start'] ?? '')); + if ($start === '0' || $start === '1') { + $where[] = 'pl.start = :start'; + $params[':start'] = (int)$start; + } + + $whereSql = implode(' AND ', $where); + + $sqlCount = " + SELECT COUNT(0) + FROM pp_langs AS pl + WHERE {$whereSql} + "; + + $stmtCount = $this->db->query($sqlCount, $params); + $countRows = $stmtCount ? $stmtCount->fetchAll() : []; + $total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0; + + $sql = " + SELECT + pl.id, + pl.name, + pl.status, + pl.start, + pl.o + FROM pp_langs AS pl + WHERE {$whereSql} + ORDER BY {$sortSql} {$sortDir}, pl.o ASC, pl.id ASC + LIMIT {$perPage} OFFSET {$offset} + "; + + $stmt = $this->db->query($sql, $params); + $items = $stmt ? $stmt->fetchAll() : []; + + return [ + 'items' => is_array($items) ? $items : [], + 'total' => $total, + ]; + } + + /** + * @return array{items: array>, total: int} + */ + public function listTranslationsForAdmin( + array $filters, + string $sortColumn = 'text', + string $sortDir = 'ASC', + int $page = 1, + int $perPage = 15 + ): array { + $allowedSortColumns = [ + 'id' => 'plt.id', + 'text' => 'plt.text', + ]; + + $sortSql = $allowedSortColumns[$sortColumn] ?? 'plt.text'; + $sortDir = strtoupper(trim($sortDir)) === 'DESC' ? 'DESC' : 'ASC'; + $page = max(1, $page); + $perPage = min(self::MAX_PER_PAGE, max(1, $perPage)); + $offset = ($page - 1) * $perPage; + + $where = ['1 = 1']; + $params = []; + + $text = trim((string)($filters['text'] ?? '')); + if ($text !== '') { + if (strlen($text) > 255) { + $text = substr($text, 0, 255); + } + $where[] = 'plt.text LIKE :text'; + $params[':text'] = '%' . $text . '%'; + } + + $whereSql = implode(' AND ', $where); + + $sqlCount = " + SELECT COUNT(0) + FROM pp_langs_translations AS plt + WHERE {$whereSql} + "; + + $stmtCount = $this->db->query($sqlCount, $params); + $countRows = $stmtCount ? $stmtCount->fetchAll() : []; + $total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0; + + $sql = " + SELECT + plt.id, + plt.text + FROM pp_langs_translations AS plt + WHERE {$whereSql} + ORDER BY {$sortSql} {$sortDir}, plt.id ASC + LIMIT {$perPage} OFFSET {$offset} + "; + + $stmt = $this->db->query($sql, $params); + $items = $stmt ? $stmt->fetchAll() : []; + + return [ + 'items' => is_array($items) ? $items : [], + 'total' => $total, + ]; + } + + public function languageDetails(string $languageId): ?array + { + $language = $this->db->get('pp_langs', '*', ['id' => $languageId]); + return $language ?: null; + } + + public function translationDetails(int $translationId): ?array + { + $translation = $this->db->get('pp_langs_translations', '*', ['id' => $translationId]); + return $translation ?: null; + } + + public function maxOrder(): int + { + $max = $this->db->max('pp_langs', 'o'); + return $max ? (int)$max : 0; + } + + public function languagesList(bool $onlyActive = false): array + { + $where = []; + if ($onlyActive) { + $where['status'] = 1; + } + + $rows = $this->db->select('pp_langs', '*', array_merge(['ORDER' => ['o' => 'ASC']], $where)); + return is_array($rows) ? $rows : []; + } + + public function deleteLanguage(string $languageId): bool + { + $languageId = $this->sanitizeLanguageId($languageId); + if ($languageId === null) { + return false; + } + + if ((int)$this->db->count('pp_langs') <= 1) { + return false; + } + + if (!$this->db->count('pp_langs', ['id' => $languageId])) { + return false; + } + + $dropResult = $this->db->query('ALTER TABLE pp_langs_translations DROP COLUMN `' . $languageId . '`'); + if (!$dropResult) { + return false; + } + + $deleteResult = $this->db->delete('pp_langs', ['id' => $languageId]); + if (!$deleteResult) { + return false; + } + + \S::htacces(); + \S::delete_dir('../temp/'); + return true; + } + + public function saveLanguage(string $languageId, string $name, $status, $start, int $order): ?string + { + $languageId = $this->sanitizeLanguageId($languageId); + if ($languageId === null) { + return null; + } + + $statusVal = $this->toSwitchValue($status); + $startVal = $this->toSwitchValue($start); + + $exists = (bool)$this->db->count('pp_langs', ['id' => $languageId]); + + if ($startVal === 1) { + $this->db->update('pp_langs', ['start' => 0], ['id[!]' => $languageId]); + } + + if ($exists) { + $this->db->update('pp_langs', [ + 'status' => $statusVal, + 'start' => $startVal, + 'name' => $name, + 'o' => $order, + ], [ + 'id' => $languageId, + ]); + } else { + $addResult = $this->db->query('ALTER TABLE pp_langs_translations ADD COLUMN `' . $languageId . '` TEXT NULL DEFAULT NULL'); + if (!$addResult) { + return null; + } + + $insertResult = $this->db->insert('pp_langs', [ + 'id' => $languageId, + 'name' => $name, + 'status' => $statusVal, + 'start' => $startVal, + 'o' => $order, + ]); + if (!$insertResult) { + return null; + } + } + + if (!(int)$this->db->count('pp_langs', ['start' => 1])) { + $idTmp = (string)$this->db->get('pp_langs', 'id', ['ORDER' => ['o' => 'ASC']]); + if ($idTmp !== '') { + $this->db->update('pp_langs', ['start' => 1], ['id' => $idTmp]); + } + } + + \S::htacces(); + \S::delete_dir('../temp/'); + return $languageId; + } + + public function deleteTranslation(int $translationId): bool + { + $result = $this->db->delete('pp_langs_translations', ['id' => $translationId]); + return (bool)$result; + } + + public function saveTranslation(int $translationId, string $text, array $translations): ?int + { + if ($translationId > 0) { + $this->db->update('pp_langs_translations', ['text' => $text], ['id' => $translationId]); + } else { + $insertResult = $this->db->insert('pp_langs_translations', ['text' => $text]); + if (!$insertResult) { + return null; + } + $translationId = (int)$this->db->id(); + } + + if ($translationId <= 0) { + return null; + } + + foreach ($translations as $languageId => $value) { + $safeLanguageId = $this->sanitizeLanguageId((string)$languageId); + if ($safeLanguageId === null) { + continue; + } + + $this->db->update('pp_langs_translations', [ + $safeLanguageId => (string)$value, + ], [ + 'id' => $translationId, + ]); + } + + \S::htacces(); + \S::delete_dir('../temp/'); + return $translationId; + } + + private function sanitizeLanguageId(string $languageId): ?string + { + $languageId = strtolower(trim($languageId)); + if (!preg_match('/^[a-z]{2}$/', $languageId)) { + return null; + } + + return $languageId; + } + + private function toSwitchValue($value): int + { + return ($value === 'on' || $value === 1 || $value === '1' || $value === true) ? 1 : 0; + } +} + diff --git a/autoload/admin/Controllers/LanguagesController.php b/autoload/admin/Controllers/LanguagesController.php new file mode 100644 index 0000000..2a13763 --- /dev/null +++ b/autoload/admin/Controllers/LanguagesController.php @@ -0,0 +1,560 @@ +repository = $repository; + $this->formHandler = new FormRequestHandler(); + } + + public function list(): string + { + $sortableColumns = ['o', 'name', 'status', 'start']; + + $filterDefinitions = [ + [ + 'key' => 'name', + 'label' => 'Jezyk', + 'type' => 'text', + ], + [ + 'key' => 'status', + 'label' => 'Aktywny', + 'type' => 'select', + 'options' => [ + '' => '- aktywny -', + '1' => 'tak', + '0' => 'nie', + ], + ], + [ + 'key' => 'start', + 'label' => 'Domyslny', + 'type' => 'select', + 'options' => [ + '' => '- domyslny -', + '1' => 'tak', + '0' => 'nie', + ], + ], + ]; + + $listRequest = \admin\Support\TableListRequestFactory::fromRequest( + $filterDefinitions, + $sortableColumns, + 'o' + ); + + $sortDir = $listRequest['sortDir']; + if (trim((string)\S::get('sort')) === '') { + $sortDir = 'ASC'; + } + + $result = $this->repository->listForAdmin( + $listRequest['filters'], + $listRequest['sortColumn'], + $sortDir, + $listRequest['page'], + $listRequest['perPage'] + ); + + $rows = []; + $lp = ($listRequest['page'] - 1) * $listRequest['perPage'] + 1; + foreach ($result['items'] as $item) { + $id = (string)($item['id'] ?? ''); + $name = trim((string)($item['name'] ?? '')); + + $rows[] = [ + 'lp' => $lp++ . '.', + 'start' => ((int)($item['start'] ?? 0) === 1) ? 'tak' : 'nie', + 'status' => ((int)($item['status'] ?? 0) === 1) ? 'tak' : 'nie', + 'name' => '' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . '', + '_actions' => [ + [ + 'label' => 'Edytuj', + 'url' => '/admin/languages/language_edit/id=' . $id, + 'class' => 'btn btn-xs btn-primary', + ], + [ + 'label' => 'Usun', + 'url' => '/admin/languages/language_delete/id=' . $id, + 'class' => 'btn btn-xs btn-danger', + 'confirm' => 'Na pewno chcesz usunac wybrany jezyk?', + ], + ], + ]; + } + + $total = (int)$result['total']; + $totalPages = max(1, (int)ceil($total / $listRequest['perPage'])); + + $viewModel = new \admin\ViewModels\Common\PaginatedTableViewModel( + [ + ['key' => 'lp', 'label' => 'Lp.', 'class' => 'text-center', 'sortable' => false], + ['key' => 'start', 'sort_key' => 'start', 'label' => 'Domyslny', 'class' => 'text-center', 'sortable' => true, 'raw' => true], + ['key' => 'status', 'sort_key' => 'status', 'label' => 'Aktywny', 'class' => 'text-center', 'sortable' => true, 'raw' => true], + ['key' => 'name', 'sort_key' => 'name', 'label' => 'Jezyk', 'sortable' => true, 'raw' => true], + ], + $rows, + $listRequest['viewFilters'], + [ + 'column' => $listRequest['sortColumn'], + 'dir' => $sortDir, + ], + [ + 'page' => $listRequest['page'], + 'per_page' => $listRequest['perPage'], + 'total' => $total, + 'total_pages' => $totalPages, + ], + array_merge($listRequest['queryFilters'], [ + 'sort' => $listRequest['sortColumn'], + 'dir' => $sortDir, + 'per_page' => $listRequest['perPage'], + ]), + $listRequest['perPageOptions'], + $sortableColumns, + '/admin/languages/view_list/', + 'Brak danych w tabeli.', + '/admin/languages/language_edit/', + 'Dodaj jezyk' + ); + + return \Tpl::view('languages/languages-list', [ + 'viewModel' => $viewModel, + ]); + } + + public function view_list(): string + { + return $this->list(); + } + + public function language_edit(): string + { + $languageId = trim((string)\S::get('id')); + $language = $this->repository->languageDetails($languageId) ?? []; + $validationErrors = $_SESSION['form_errors'][$this->getLanguageFormId()] ?? null; + if ($validationErrors) { + unset($_SESSION['form_errors'][$this->getLanguageFormId()]); + } + + return \Tpl::view('languages/language-edit', [ + 'form' => $this->buildLanguageFormViewModel($language, $this->repository->maxOrder(), $validationErrors), + ]); + } + + public function language_save(): void + { + $legacyValues = \S::get('values'); + if ($legacyValues) { + $values = json_decode((string)$legacyValues, true); + $response = ['status' => 'error', 'msg' => 'Podczas zapisywania jezyka wystapil blad.']; + + if (is_array($values)) { + $savedId = $this->repository->saveLanguage( + (string)($values['id'] ?? ''), + (string)($values['name'] ?? ''), + $values['status'] ?? 0, + $values['start'] ?? 0, + (int)($values['o'] ?? 0) + ); + if ($savedId) { + $response = ['status' => 'ok', 'msg' => 'Jezyk zostal zapisany.', 'id' => $savedId]; + } + } + + echo json_encode($response); + exit; + } + + $languageId = trim((string)\S::get('id')); + $language = $this->repository->languageDetails($languageId) ?? []; + $viewModel = $this->buildLanguageFormViewModel($language, $this->repository->maxOrder()); + + $result = $this->formHandler->handleSubmit($viewModel, $_POST); + if (!$result['success']) { + $_SESSION['form_errors'][$this->getLanguageFormId()] = $result['errors']; + echo json_encode(['success' => false, 'errors' => $result['errors']]); + exit; + } + + $data = $result['data']; + $requestId = strtolower(trim((string)\S::get('id'))); + $idFromData = strtolower(trim((string)($data['id'] ?? ''))); + $id = $idFromData !== '' ? $idFromData : $requestId; + if (!preg_match('/^[a-z]{2}$/', $id)) { + echo json_encode([ + 'success' => false, + 'errors' => ['id' => 'ID jezyka musi miec 2 litery (np. pl, en).'], + ]); + exit; + } + + $savedId = $this->repository->saveLanguage( + $id, + trim((string)($data['name'] ?? '')), + $data['status'] ?? 0, + $data['start'] ?? 0, + (int)($data['o'] ?? 0) + ); + + if ($savedId) { + echo json_encode([ + 'success' => true, + 'id' => $savedId, + 'message' => 'Jezyk zostal zapisany.', + ]); + exit; + } + + echo json_encode([ + 'success' => false, + 'errors' => ['general' => 'Podczas zapisywania jezyka wystapil blad.'], + ]); + exit; + } + + public function language_delete(): void + { + if ($this->repository->deleteLanguage((string)\S::get('id'))) { + \S::alert('Jezyk zostal usuniety.'); + } + + header('Location: /admin/languages/view_list/'); + exit; + } + + public function translation_list(): string + { + $sortableColumns = ['text', 'id']; + $filterDefinitions = [ + [ + 'key' => 'text', + 'label' => 'Tekst', + 'type' => 'text', + ], + ]; + + $listRequest = \admin\Support\TableListRequestFactory::fromRequest( + $filterDefinitions, + $sortableColumns, + 'text' + ); + + $sortDir = $listRequest['sortDir']; + if (trim((string)\S::get('sort')) === '') { + $sortDir = 'ASC'; + } + + $result = $this->repository->listTranslationsForAdmin( + $listRequest['filters'], + $listRequest['sortColumn'], + $sortDir, + $listRequest['page'], + $listRequest['perPage'] + ); + + $rows = []; + $lp = ($listRequest['page'] - 1) * $listRequest['perPage'] + 1; + foreach ($result['items'] as $item) { + $id = (int)($item['id'] ?? 0); + $text = trim((string)($item['text'] ?? '')); + + $rows[] = [ + 'lp' => $lp++ . '.', + 'text' => '' . htmlspecialchars($text, ENT_QUOTES, 'UTF-8') . '', + '_actions' => [ + [ + 'label' => 'Edytuj', + 'url' => '/admin/languages/translation_edit/id=' . $id, + 'class' => 'btn btn-xs btn-primary', + ], + [ + 'label' => 'Usun', + 'url' => '/admin/languages/translation_delete/id=' . $id, + 'class' => 'btn btn-xs btn-danger', + 'confirm' => 'Na pewno chcesz usunac wybrane tlumaczenie?', + ], + ], + ]; + } + + $total = (int)$result['total']; + $totalPages = max(1, (int)ceil($total / $listRequest['perPage'])); + + $viewModel = new \admin\ViewModels\Common\PaginatedTableViewModel( + [ + ['key' => 'lp', 'label' => 'Lp.', 'class' => 'text-center', 'sortable' => false], + ['key' => 'text', 'sort_key' => 'text', 'label' => 'Tekst', 'sortable' => true, 'raw' => true], + ], + $rows, + $listRequest['viewFilters'], + [ + 'column' => $listRequest['sortColumn'], + 'dir' => $sortDir, + ], + [ + 'page' => $listRequest['page'], + 'per_page' => $listRequest['perPage'], + 'total' => $total, + 'total_pages' => $totalPages, + ], + array_merge($listRequest['queryFilters'], [ + 'sort' => $listRequest['sortColumn'], + 'dir' => $sortDir, + 'per_page' => $listRequest['perPage'], + ]), + $listRequest['perPageOptions'], + $sortableColumns, + '/admin/languages/translation_list/', + 'Brak danych w tabeli.', + '/admin/languages/translation_edit/', + 'Dodaj tlumaczenie' + ); + + return \Tpl::view('languages/translations-list', [ + 'viewModel' => $viewModel, + ]); + } + + public function translation_edit(): string + { + $translationId = (int)\S::get('id'); + $translation = $this->repository->translationDetails($translationId) ?? []; + $languages = $this->repository->languagesList(); + $validationErrors = $_SESSION['form_errors'][$this->getTranslationFormId()] ?? null; + if ($validationErrors) { + unset($_SESSION['form_errors'][$this->getTranslationFormId()]); + } + + return \Tpl::view('languages/translation-edit', [ + 'form' => $this->buildTranslationFormViewModel($translation, $languages, $validationErrors), + ]); + } + + public function translation_save(): void + { + $legacyValues = \S::get('values'); + if ($legacyValues) { + $values = json_decode((string)$legacyValues, true); + $response = ['status' => 'error', 'msg' => 'Podczas zapisywania tlumaczenia wystapil blad.']; + + if (is_array($values)) { + $languagesMap = $this->extractLegacyTranslations($values, $this->repository->languagesList()); + $savedId = $this->repository->saveTranslation( + (int)($values['id'] ?? 0), + (string)($values['text'] ?? ''), + $languagesMap + ); + if ($savedId) { + $this->clearLanguageSessions($this->repository->languagesList()); + $response = ['status' => 'ok', 'msg' => 'Tlumaczenie zostalo zapisane.', 'id' => $savedId]; + } + } + + echo json_encode($response); + exit; + } + + $translationId = (int)\S::get('id'); + $translation = $this->repository->translationDetails($translationId) ?? []; + $languages = $this->repository->languagesList(); + $viewModel = $this->buildTranslationFormViewModel($translation, $languages); + + $result = $this->formHandler->handleSubmit($viewModel, $_POST); + if (!$result['success']) { + $_SESSION['form_errors'][$this->getTranslationFormId()] = $result['errors']; + echo json_encode(['success' => false, 'errors' => $result['errors']]); + exit; + } + + $data = $result['data']; + $languagesMap = []; + foreach ($languages as $language) { + $langId = (string)($language['id'] ?? ''); + $key = 'lang_' . $langId; + $languagesMap[$langId] = (string)($data[$key] ?? ''); + } + + $savedId = $this->repository->saveTranslation( + $translationId, + (string)($data['text'] ?? ''), + $languagesMap + ); + + if ($savedId) { + $this->clearLanguageSessions($languages); + echo json_encode([ + 'success' => true, + 'id' => $savedId, + 'message' => 'Tlumaczenie zostalo zapisane.', + ]); + exit; + } + + echo json_encode([ + 'success' => false, + 'errors' => ['general' => 'Podczas zapisywania tlumaczenia wystapil blad.'], + ]); + exit; + } + + public function translation_delete(): void + { + if ($this->repository->deleteTranslation((int)\S::get('id'))) { + \S::alert('Tlumaczenie zostalo usuniete.'); + } + + header('Location: /admin/languages/translation_list/'); + exit; + } + + private function buildLanguageFormViewModel(array $language, int $maxOrder, ?array $errors = null): FormEditViewModel + { + $languageId = strtolower(trim((string)($language['id'] ?? ''))); + $isNew = $languageId === ''; + + $data = [ + 'id' => $languageId, + 'name' => (string)($language['name'] ?? ''), + 'status' => (int)($language['status'] ?? 0), + 'start' => (int)($language['start'] ?? 0), + 'o' => (int)($language['o'] ?? ($maxOrder + 1)), + ]; + + $fields = []; + if ($isNew) { + $fields[] = FormField::text('id', [ + 'label' => 'ID (2 znaki)', + 'required' => true, + 'attributes' => ['maxlength' => 2], + ]); + } + + $fields[] = FormField::text('name', [ + 'label' => 'Jezyk', + 'required' => true, + ]); + $fields[] = FormField::switch('status', [ + 'label' => 'Aktywny', + ]); + $fields[] = FormField::switch('start', [ + 'label' => 'Domyslny', + ]); + $fields[] = FormField::hidden('o', $data['o']); + + $actionUrl = '/admin/languages/language_save/' . ($isNew ? '' : ('id=' . $languageId)); + $actions = [ + FormAction::save($actionUrl, '/admin/languages/view_list/'), + FormAction::cancel('/admin/languages/view_list/'), + ]; + + return new FormEditViewModel( + $this->getLanguageFormId(), + $isNew ? 'Nowy jezyk' : 'Edycja jezyka', + $data, + $fields, + [], + $actions, + 'POST', + $actionUrl, + '/admin/languages/view_list/', + true, + $isNew ? [] : ['id' => $languageId], + null, + $errors + ); + } + + private function buildTranslationFormViewModel(array $translation, array $languages, ?array $errors = null): FormEditViewModel + { + $translationId = (int)($translation['id'] ?? 0); + $isNew = $translationId <= 0; + + $data = [ + 'id' => $translationId, + 'text' => (string)($translation['text'] ?? ''), + ]; + + $fields = [ + FormField::text('text', [ + 'label' => 'Tekst', + 'required' => true, + ]), + ]; + + foreach ($languages as $language) { + $langId = (string)($language['id'] ?? ''); + $fieldName = 'lang_' . $langId; + $data[$fieldName] = (string)($translation[$langId] ?? ''); + $fields[] = FormField::text($fieldName, [ + 'label' => (string)($language['name'] ?? $langId), + ]); + } + + $actionUrl = '/admin/languages/translation_save/' . ($isNew ? '' : ('id=' . $translationId)); + $actions = [ + FormAction::save($actionUrl, '/admin/languages/translation_list/'), + FormAction::cancel('/admin/languages/translation_list/'), + ]; + + return new FormEditViewModel( + $this->getTranslationFormId(), + $isNew ? 'Nowe tlumaczenie' : 'Edycja tlumaczenia', + $data, + $fields, + [], + $actions, + 'POST', + $actionUrl, + '/admin/languages/translation_list/', + true, + $isNew ? [] : ['id' => $translationId], + null, + $errors + ); + } + + private function extractLegacyTranslations(array $values, array $languages): array + { + $result = []; + foreach ($languages as $language) { + $langId = (string)($language['id'] ?? ''); + $result[$langId] = (string)($values[$langId] ?? ''); + } + + return $result; + } + + private function clearLanguageSessions(array $languages): void + { + foreach ($languages as $language) { + if (!isset($language['id'])) { + continue; + } + \S::delete_session('lang-' . (string)$language['id']); + } + } + + private function getLanguageFormId(): string + { + return 'languages-language-edit'; + } + + private function getTranslationFormId(): string + { + return 'languages-translation-edit'; + } +} diff --git a/autoload/admin/class.Site.php b/autoload/admin/class.Site.php index 30daac6..015134a 100644 --- a/autoload/admin/class.Site.php +++ b/autoload/admin/class.Site.php @@ -255,6 +255,13 @@ class Site new \Domain\User\UserRepository( $mdb ) ); }, + 'Languages' => function() { + global $mdb; + + return new \admin\Controllers\LanguagesController( + new \Domain\Languages\LanguagesRepository( $mdb ) + ); + }, ]; return self::$newControllers; diff --git a/autoload/admin/controls/class.Languages.php b/autoload/admin/controls/class.Languages.php deleted file mode 100644 index e282212..0000000 --- a/autoload/admin/controls/class.Languages.php +++ /dev/null @@ -1,82 +0,0 @@ - 'error', 'msg' => 'Podczas zapisywania jÄ™zyka wystÄ…piÅ‚ błąd. ProszÄ™ spróbować ponownie.' ]; - $values = json_decode( \S::get( 'values' ), true ); - - if ( \admin\factory\Languages::language_save( - $values['id'], $values['name'], $values['status'], - $values['start'], $values['o'] ) ) - $response = [ 'status' => 'ok', 'msg' => 'JÄ™zyk zostaÅ‚ zapisany.', 'id' => $id ]; - - echo json_encode( $response ); - exit; - } - - public static function language_edit() - { - return \admin\view\Languages::language_edit( - \admin\factory\Languages::language_details( - \S::get( 'id' ) - ), \admin\factory\Languages::max_order() - ); - } - - public static function view_list() - { - return \admin\view\Languages::languages_list(); - } - - public static function translation_delete() - { - if ( \admin\factory\Languages::translation_delete( \S::get( 'id' ) ) ) - \S::alert( 'TÅ‚umaczenie zostaÅ‚o usuniÄ™te.' ); - header( 'Location: /admin/languages/translation_list/' ); - exit; - } - - public static function translation_save() - { - $response = [ 'status' => 'error', 'msg' => 'Podczas zapisywania tÅ‚umaczenia wystÄ…piÅ‚ błąd. ProszÄ™ spróbować ponownie.' ]; - $values = json_decode( \S::get( 'values' ), true ); - - $languages_list = \admin\factory\Languages::languages_list(); - if ( is_array( $languages_list ) and !empty( $languages_list ) ) foreach ( $languages_list as $language ) - { - \S::delete_session( 'lang-' . $language['id'] ); - $languages[ $language['id'] ] = $values[ $language['id'] ]; - } - - if ( $id = \admin\factory\Languages::translation_save( $values['id'], $values['text'], $languages ) ) - $response = [ 'status' => 'ok', 'msg' => 'TÅ‚umaczenie zostaÅ‚o zapisane.', 'id' => $id ]; - - echo json_encode( $response ); - exit; - } - - public static function translation_edit() - { - return \admin\view\Languages::translation_edit( - \admin\factory\Languages::translation_details( \S::get( 'id' ) ), - \admin\factory\Languages::languages_list() - ); - } - - public static function translation_list() - { - return \admin\view\Languages::translations_list(); - } -} -?> \ No newline at end of file diff --git a/autoload/admin/factory/class.Languages.php b/autoload/admin/factory/class.Languages.php index 97c8006..f77d6bf 100644 --- a/autoload/admin/factory/class.Languages.php +++ b/autoload/admin/factory/class.Languages.php @@ -1,141 +1,53 @@ delete( 'pp_langs_translations', [ 'id' => $translation_id ] ); + return new \Domain\Languages\LanguagesRepository($mdb); } - public static function translation_save( $translation_id, $text, $languages ) + public static function translation_delete($translation_id) { - global $mdb; - - if ( $translation_id ) - { - $mdb -> update( 'pp_langs_translations', [ 'text' => $text ], [ 'id' => $translation_id ] ); - if ( is_array( $languages ) and !empty( $languages ) ): foreach ( $languages as $key => $val ): - $mdb -> update( 'pp_langs_translations', [ $key => $val ], [ 'id' => $translation_id ] ); - endforeach; endif; - \S::htacces(); - \S::delete_dir( '../temp/' ); - return $translation_id; - } - else - { - $mdb -> insert( 'pp_langs_translations', [ 'text' => $text ] ); - if ( $translation_id = $mdb -> id() ) - { - if ( is_array( $languages ) and !empty( $languages ) ): foreach ( $languages as $key => $val ): - $mdb -> update( 'pp_langs_translations', [ $key => $val ], [ 'id' => $translation_id ] ); - endforeach; endif; - } - \S::htacces(); - \S::delete_dir( '../temp/' ); - return $translation_id; - } + return self::repository()->deleteTranslation((int)$translation_id); } - public static function translation_details( $translation_id ) + public static function translation_save($translation_id, $text, $languages) { - global $mdb; - return $mdb -> get( 'pp_langs_translations', '*', [ 'id' => $translation_id ] ); + return self::repository()->saveTranslation((int)$translation_id, (string)$text, is_array($languages) ? $languages : []); } - public static function language_delete( $language_id ) + public static function translation_details($translation_id) { - global $mdb; + return self::repository()->translationDetails((int)$translation_id); + } - if ( $mdb -> count( 'pp_langs' ) > 1 ) - { - if ( $mdb -> query( 'ALTER TABLE pp_langs_translations DROP ' . $language_id ) - and - $mdb -> delete( 'pp_langs', [ 'id' => $language_id ] ) - ) - return true; - } - return false; + public static function language_delete($language_id) + { + return self::repository()->deleteLanguage((string)$language_id); } public static function max_order() { - global $mdb; - return $mdb -> max( 'pp_langs', 'o' ); + return self::repository()->maxOrder(); } - public static function language_save( $language_id, $name, $status, $start, $o ) + public static function language_save($language_id, $name, $status, $start, $o) { - global $mdb; - - if ( $start == 'on' ) - $mdb -> update( 'pp_langs', [ - 'start' => 0 - ], [ - 'id[!]' => $language_id - ] ); - - if ( $mdb -> count( 'pp_langs', [ 'id' => $language_id ] ) ) - { - $mdb -> update( 'pp_langs', - [ - 'status' => $status == 'on' ? 1 : 0, - 'start' => $start == 'on' ? 1 : 0, - 'name' => $name, - 'o' => $o - ], [ - 'id' => $language_id - ] ); - - if ( !$mdb -> count( 'pp_langs', [ 'start' => 1 ] ) ) - { - $id_tmp = $mdb -> get( 'pp_langs', 'id', [ 'ORDER' => [ 'o' => 'ASC' ] ] ); - $mdb -> update( 'pp_langs', [ 'start' => 1 ], [ 'id' => $id_tmp ] ); - } - - \S::htacces(); - \S::delete_dir( '../temp/' ); - return $language_id; - } - else - { - if ( $mdb -> query( 'ALTER TABLE pp_langs_translations ADD ' . $language_id . ' TEXT NULL DEFAULT NULL' ) ) - { - $mdb -> insert( 'pp_langs', - [ - 'id' => $language_id, - 'name' => $name, - 'status' => $status == 'on' ? 1 : 0, - 'start' => $start == 'on' ? 1 : 0, - 'o' => $o - ] ); - - \S::htacces(); - \S::delete_dir( '../temp/' ); - return $language_id; - } - } - - return faslse; + return self::repository()->saveLanguage((string)$language_id, (string)$name, $status, $start, (int)$o); } - public static function language_details( $language_id ) + public static function language_details($language_id) { - global $mdb; - return $mdb -> get( 'pp_langs', '*', [ 'id' => $language_id ] ); + return self::repository()->languageDetails((string)$language_id); } - public static function languages_list( $only_active = false ) + public static function languages_list($only_active = false) { - global $mdb; - - $where = []; - if ( $only_active ) - $where['status'] = 1; - - return $mdb -> select( 'pp_langs', '*', array_merge( [ 'ORDER' => [ 'o' => 'ASC' ] ], $where ) ); + return self::repository()->languagesList((bool)$only_active); } } -?> \ No newline at end of file +?> + diff --git a/autoload/admin/view/class.Languages.php b/autoload/admin/view/class.Languages.php deleted file mode 100644 index de6453d..0000000 --- a/autoload/admin/view/class.Languages.php +++ /dev/null @@ -1,34 +0,0 @@ - languages = $languages; - $tpl -> translation = $translation; - return $tpl -> render( 'languages/translation-edit' ); - } - - public static function language_edit( $language, $order ) - { - $tpl = new \Tpl; - $tpl -> language = $language; - $tpl -> order = $order; - return $tpl -> render( 'languages/language-edit' ); - } - - public static function translations_list() - { - $tpl = new \Tpl; - return $tpl -> render( 'languages/translations-list' ); - } - - public static function languages_list() - { - $tpl = new \Tpl; - return $tpl -> render( 'languages/languages-list' ); - } -} -?> \ No newline at end of file diff --git a/tests/Unit/Domain/Languages/LanguagesRepositoryTest.php b/tests/Unit/Domain/Languages/LanguagesRepositoryTest.php new file mode 100644 index 0000000..36fa2bf --- /dev/null +++ b/tests/Unit/Domain/Languages/LanguagesRepositoryTest.php @@ -0,0 +1,116 @@ +createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->with('pp_langs', '*', ['id' => 'pl']) + ->willReturn(['id' => 'pl', 'name' => 'Polski']); + + $repository = new LanguagesRepository($mockDb); + $language = $repository->languageDetails('pl'); + + $this->assertIsArray($language); + $this->assertSame('pl', $language['id']); + } + + public function testLanguagesListReturnsArray(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('select') + ->with('pp_langs', '*', ['ORDER' => ['o' => 'ASC']]) + ->willReturn([ + ['id' => 'pl', 'name' => 'Polski', 'status' => 1], + ]); + + $repository = new LanguagesRepository($mockDb); + $list = $repository->languagesList(false); + + $this->assertCount(1, $list); + $this->assertSame('pl', $list[0]['id']); + } + + public function testSaveLanguageRejectsInvalidLanguageId(): void + { + $mockDb = $this->createMock(\medoo::class); + $repository = new LanguagesRepository($mockDb); + + $this->assertNull($repository->saveLanguage('pol', 'Polski', 1, 1, 1)); + $this->assertNull($repository->saveLanguage('p1', 'Polski', 1, 1, 1)); + } + + public function testSaveTranslationInsertsNewTranslationAndReturnsId(): void + { + $mockDb = $this->createMock(\medoo::class); + + $mockDb->expects($this->once()) + ->method('insert') + ->with('pp_langs_translations', ['text' => 'Hello']) + ->willReturn($this->createMock(\PDOStatement::class)); + + $mockDb->expects($this->once()) + ->method('id') + ->willReturn(15); + + $mockDb->expects($this->exactly(2)) + ->method('update') + ->withConsecutive( + ['pp_langs_translations', ['pl' => 'Czesc'], ['id' => 15]], + ['pp_langs_translations', ['en' => 'Hello'], ['id' => 15]] + ); + + $repository = new LanguagesRepository($mockDb); + $savedId = $repository->saveTranslation(0, 'Hello', ['pl' => 'Czesc', 'en' => 'Hello']); + + $this->assertSame(15, $savedId); + } + + public function testDeleteTranslationReturnsBoolean(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('delete') + ->with('pp_langs_translations', ['id' => 5]) + ->willReturn($this->createMock(\PDOStatement::class)); + + $repository = new LanguagesRepository($mockDb); + $this->assertTrue($repository->deleteTranslation(5)); + } + + public function testListForAdminReturnsItemsAndTotal(): void + { + $mockDb = $this->createMock(\medoo::class); + + $countStmt = $this->createMock(\PDOStatement::class); + $countStmt->expects($this->once()) + ->method('fetchAll') + ->willReturn([[1]]); + + $dataStmt = $this->createMock(\PDOStatement::class); + $dataStmt->expects($this->once()) + ->method('fetchAll') + ->willReturn([ + ['id' => 'pl', 'name' => 'Polski', 'status' => 1, 'start' => 1, 'o' => 1], + ]); + + $mockDb->expects($this->exactly(2)) + ->method('query') + ->willReturnOnConsecutiveCalls($countStmt, $dataStmt); + + $repository = new LanguagesRepository($mockDb); + $result = $repository->listForAdmin(['name' => '', 'status' => '', 'start' => ''], 'o', 'ASC', 1, 15); + + $this->assertSame(1, $result['total']); + $this->assertCount(1, $result['items']); + $this->assertSame('pl', $result['items'][0]['id']); + } +} + diff --git a/tests/Unit/admin/Controllers/LanguagesControllerTest.php b/tests/Unit/admin/Controllers/LanguagesControllerTest.php new file mode 100644 index 0000000..6a87661 --- /dev/null +++ b/tests/Unit/admin/Controllers/LanguagesControllerTest.php @@ -0,0 +1,63 @@ +mockRepository = $this->createMock(LanguagesRepository::class); + $this->controller = new LanguagesController($this->mockRepository); + } + + public function testConstructorAcceptsRepository(): void + { + $controller = new LanguagesController($this->mockRepository); + $this->assertInstanceOf(LanguagesController::class, $controller); + } + + public function testHasMainActionMethods(): void + { + $this->assertTrue(method_exists($this->controller, 'list')); + $this->assertTrue(method_exists($this->controller, 'view_list')); + $this->assertTrue(method_exists($this->controller, 'language_edit')); + $this->assertTrue(method_exists($this->controller, 'language_save')); + $this->assertTrue(method_exists($this->controller, 'language_delete')); + $this->assertTrue(method_exists($this->controller, 'translation_list')); + $this->assertTrue(method_exists($this->controller, 'translation_edit')); + $this->assertTrue(method_exists($this->controller, 'translation_save')); + $this->assertTrue(method_exists($this->controller, 'translation_delete')); + } + + public function testActionMethodReturnTypes(): void + { + $reflection = new \ReflectionClass($this->controller); + + $this->assertEquals('string', (string)$reflection->getMethod('list')->getReturnType()); + $this->assertEquals('string', (string)$reflection->getMethod('view_list')->getReturnType()); + $this->assertEquals('string', (string)$reflection->getMethod('language_edit')->getReturnType()); + $this->assertEquals('void', (string)$reflection->getMethod('language_save')->getReturnType()); + $this->assertEquals('void', (string)$reflection->getMethod('language_delete')->getReturnType()); + $this->assertEquals('string', (string)$reflection->getMethod('translation_list')->getReturnType()); + $this->assertEquals('string', (string)$reflection->getMethod('translation_edit')->getReturnType()); + $this->assertEquals('void', (string)$reflection->getMethod('translation_save')->getReturnType()); + $this->assertEquals('void', (string)$reflection->getMethod('translation_delete')->getReturnType()); + } + + public function testConstructorRequiresLanguagesRepository(): void + { + $reflection = new \ReflectionClass(LanguagesController::class); + $constructor = $reflection->getConstructor(); + $params = $constructor->getParameters(); + + $this->assertCount(1, $params); + $this->assertEquals('Domain\Languages\LanguagesRepository', $params[0]->getType()->getName()); + } +} + diff --git a/updates/0.20/ver_0.254.zip b/updates/0.20/ver_0.254.zip new file mode 100644 index 0000000000000000000000000000000000000000..947fe90381a723074cd8249a685e7a06fdabdb87 GIT binary patch literal 14334 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
  • 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 literal 0 HcmV?d00001 diff --git a/updates/0.20/ver_0.254_files.txt b/updates/0.20/ver_0.254_files.txt new file mode 100644 index 0000000..6b81089 --- /dev/null +++ b/updates/0.20/ver_0.254_files.txt @@ -0,0 +1,2 @@ +F: ../autoload/admin/controls/class.Languages.php +F: ../autoload/admin/view/class.Languages.php diff --git a/updates/changelog.php b/updates/changelog.php index e591143..3f34830 100644 --- a/updates/changelog.php +++ b/updates/changelog.php @@ -1,3 +1,11 @@ +ver. 0.254 - 12.02.2026
    +- UPDATE - modul `Languages` w panelu admin przepiety na `Domain\\Languages\\LanguagesRepository` + `admin\\Controllers\\LanguagesController` +- UPDATE - migracja widokow languages (`languages-list`, `language-edit`, `translations-list`, `translation-edit`) na `components/table-list` i `components/form-edit` +- UPDATE - routing DI dla `Languages` w `admin\\Site` oraz kompatybilna fasada `admin\\factory\\Languages` delegujaca do repozytorium +- UPDATE - naprawiono zapis edycji jezyka (ID jezyka pobierane z URL przy edycji) +- UPDATE - globalne poprawki UX filtrów w `components/table-list` (kompaktowe kolumny `Aktywny`/`Domyslny`, spacing i pelna szerokosc selecta) +- CLEANUP - usuniete legacy klasy: `autoload/admin/controls/class.Languages.php`, `autoload/admin/view/class.Languages.php` +
    ver. 0.253 - 12.02.2026
    - UPDATE - modul `Users` w panelu admin w pelni przepiety na `Domain\\User\\UserRepository` + `admin\\Controllers\\UsersController` - UPDATE - migracja widokow users z `grid/gridEdit` na nowe komponenty (`components/table-list`, `components/form-edit`) diff --git a/updates/shopPRO.zip b/updates/shopPRO.zip index 1b2a62dd00ea9e80af405134294d3ee45ce86e36..947fe90381a723074cd8249a685e7a06fdabdb87 100644 GIT binary patch literal 14334 zcmcJ$W0a)Lwk=%MWxLC^tuEWPjVv2owr$(CZQEV8tuFNKefHVA-|w9FobUd*PmTwf z88OF@Cf#V$8}FSP{b(6h>lTpw2ucAJTbUzj=+%jLM1M zi`_}JcK}KFf|e!Vl(?TvtrGcd4jI!M$ut1UH{d9&I4f2IMM57_K2m9)()*y9MAq+&vpx=Hing>*= zw1ZeUq3mEz%6yWYpKM2oB(B9A=sRC_zZ-vN_(hRnONLxr7FH)ORvym4y9O408+}gP zBF@j!L8vp=5t>~AuP&Pp_aTqVD zQ?t}eWJ{p~qYzw=wuAc7F5II&?tUphIig*&ILI5uNx5?u-zgGaP{ZK}@$ckZ(x&=A zMXHuTgU#fHLsbf4b;GSNZMvlAKN+XIGI9u_R@@8uZ(d8TCsMR-@-|Gl$HCV6=608t zy%BYp`1ahr8PxBDg#(0utec{b{UHbp&YIpDoQkwAUDU(u@nZO@zvW?PVU6u)ho(5_ zoTVXria^_$6F<#~O)fp-6tR6_{Z?KE%rkwImWm7e6f-L~sL+k-Jxc5|-OwrG8RGS{ zvQnbg<$n9KL5-lDy@n6p2R0d+1cG$NjLG1}HWR&@{!W``1jJDqgO#Sw45yG&S5$=e zX<#x2G@?N)VBiDb=Ef{GlSJfnYb@(Hp237FfjA9Y`#~w*ZHD$aTIfbTqT$Aya^!zd z+fGU$Fc^f|w{vs(4GNv_vv%Ex1L;uSuJwb`kVCWz?=e6rYthY*?f9m14AP2&C|=@B)#5g!kR z&->G{56blq%6GLGz=HBr&5G3Sfry|O1alGluT1{A3~D`v0r^07h+c-A2=SNs3gV99 z6gqnW#^^G0eTKXaRQwV0ulT-D_Sawn+~^p}c&nR_hDpRbOl4+=6j+)12Lyryux_LXB&pon&3-v~N@2vt-v&G^o(?^eL1=!K_kg$ffUwSfB`}{C}xy zAE;&O+(vNd*ykLo5lI2>`_oc9RFOARY($)uh!9b)MXi1Udjxu~nBr@?;-~-6YjK9n z|72!i%d~XFk*B}(aKYs5oEV9j!^3w=4Lhw4DKEksW?>xgYSr*(@WrEOBNtB>(1f2B7k^b8TZ0C*}V7 zVOhene?&6T3xnrbSu_($KT8&_49_|Ulp73!dWLP8Hk;kcDve&|E2WF1z=8nY5s)q) z5DjbMQ~=osZABG$gb@X_d}%Z$bvCt{H(j)$DL{foMZ5$5MExwm&ofEfEOZN1rRb@s zl5r5aEL)WbR78^l3uxi&>AM=g@BpD+SS*%AwaF9uT70-A7%Uw2!efP5<{KLxyp9x? zH0PfWl`r8}?cA}gus&ume+hsG?k5iZeY4J+b#-z9Tcx4{Bn zhW(_(;Hldv8I8trr~W}D?sQA&nwQ0O+AJs$k!XqE-TrGxy^5%MKW~89d3of`S-JX6 z#79{_YpAv2NbAsY3yz*zNcrt{^+&~{-1e_yd{L3U^??FP08;7~`Hf@g1F2E+ekldp z?6Q$BM$m5Ax`N|{+4htvM6sWB#yG*(Bel`Wklprztk zD!yhz4@Za^?+tVKDIHG@rNQn63+io!QY)JmbC@TVenXR+aw40g)>t}OT{Hy)v8T(H zN;(XSXqQxMP4v&qf$sTS27L`>=3{@IEvTg*kL%<=wn2;#k6^D4ZVQL(ZznYm7*PUm+xLq_bj`OGpQd#M;IEZzu@qz%Kp! zfARtYRI~V%@x)3uU0fRm1*G%+2f$Tr^SG$ijJ3Ufiga{~l6DK@x1FJ0#Z@&!6>g2Q z&Ap!D#vEh3x6jQtGjm>V4`C13v<9sNp3~Y^xnF%uM&YSiPE-x6aX*l8Tn&V3S@CJ! zFE-Dy%J`S0)6EF-s@@{tFuR2UClzLa2R%L9yZ$0^{Q279<9;bv6G3eDb;GzoN7Kdr z>Gu~~`kksjjrdd1?xNP-XDe1_oUY zd%ND8R1j1j2DFN1E5B-fN7jl}8-NDS1{>gHJ-l_eSsCY$e&M`f8jvvNN995tq0F7lLQJv_bbbNX9=CaoUT-L4W%tc+`7AKlISb?@w zGO23AX862jv*I#c%6=0UcOx&40;OY4vT_{IvYIYxKp z5`|10SXkQ~puV@|x+9u*8jgmug_RpH&!EPBaz?xyZ2ZQ4;5;>4TAulk_G)mixf~hr zEz{noZcihZ-eirfi7%@E*J)&VQTyiazm_s4u7Qi5Dr6sD#$2x+vqQAekGE(Z9U=WZ zst2Sk1!tc+DScNI4Yz>GPy~~`PQWDH7{#xaFdO`2^xehF9|%P#MT}dD8w(d75ASN} zIy{58mG1WvS8@^N3mtJWTvwvrH`w7$c9>)esgU;s#+3Dp(39OI=4c%u;PkHsuW2T)p+O)OFPtAjx=IjIm@qMNeEDC2a!;4jQn9_C4l4sCgaK%rHSZ_b) zT>~L*wGJq;0qAQ+Z5?DvL}tCJj*3O>wadl^iKhJVebh0NYv36tTBfnbO>~>(mX!9( zhZbiXk#7p!zl|MuB0Le?oXzyLT(%l~K@q*)bOt=hsi&#H-OGnF7EbUMu1K_xztp#v z^4V{x7n>gQuz5soLLhk84Eu)iE=TN$;BRP<*68+`OC{dM1HUKKh=g_)M*QKY!GOLn~oQ;2uvXlkp_$ zteE}4ASNl3!6mu|pE~%F=oGGOOuwxOuGh8bm~|2(;lq{+Y7%T1OS6396~zO~b+_lL z6U5T&lq-Kw;JZc%)l&HViG7oe6yA;);lB8!9UDT%Wm>lB64E`a3tet>Bsx(4wug6$2XqVQ83XY4?}00iD+_WpyR=k`9d&^%Hv5~3`|S9qolr~(3!A3)Q&aymi|x?FmN_gUk2zJrHHv= za#!FSrGf61C!Z7Kx!$IaHdL?*%QvG|keGb_Kz~Kk^4+RV_(I>0ui>;X|S_W2m!fCjf+*>1R7_?Gb`n)0> z+SFV%5PqRUxdvPRzp%nEc)-fl5NnKh5K$rdGzr*&-EfqoX!ZNwfFhhI8smDT*%nSRy4#UWK zye-t;-ySQJ2C!pb9ZofzTFX+D502J{x7onxLG0b+U56TGbR{r6GczZN>&0?U@0lr* zV8|LYQ$XYtNx?5Jbz7v~(xKUJ5ugln2jy!}xS6GnuA-{_^PZj|o*z$Kcj*EQAqD33 zTTqcpqffkK+He#d{+=>cO8lkM#`$T4r51Kgzr?5TNQL5y%eO9XKCqH@W+SW%A%GNa7tiTar@0 z@!L-K0R?BT_ot7pXdUUDwc;g(yWS>!uHxTuO0L4Ue8T)u++Eeu+K#eaQi8vgw#e_3 z`fn7sm4S_!vw_)frT(ucS`%Zye`xN8x)zmCD9X{gda4O&+7)@Z=^5J58L261%4N9; z`tcnMT4XU!tmzsU1ia^2Pmq@<(|=MDW(1^XZ>Cuc}&GB6>!>;-@d@ z#t!k<9M1Afk%SkoF5WlNwx?+x-%92<+Pl8iV#*O6+38wGD|&le zUOZv-9+9;eU&H zeJXgn+PoaODj1pm=yMH51dA=CdBh2=_lvp%%sirFyMKnFKC5h6cg=Ec6jK*MI@+tu$&Lq)Kg-&m7y zvmob|kgtaVuVZglgjMe#O`Gt_c8Fri3K23DWB2mgH3h`KT66AOrUzk~y1d1sp1U2r@~glAQGNA49^OP?oRNeqG8tDBc5nV(X-9hj?U1@DNU88l2{tMNekvs(T%6HrE zb#nm1^|~Lo^|^PR2ffaT##3x@xHxZS43g^+e^Pc zM~e}Dr;kJ@!wj`79EopB`DVm0rV@|_dImMLI`(QEc1%k!x6M{@Ki&88I7(m3Q- z#z=zFh1p*2NFAXVja(WzCO#2Kz~oO9!qr5<_V~CWwvbN;tJsRC9X$1+9~!rXo&|Nz z?&I8i3Xw2oC(@)g;>vr#*e^d=e@X_+dKLQs0(z-xRqkX(T#nk9vMB0h=*4MCaYvj- z6%U|)khD3-1W+Il#Lu#4E-s;+1PKgeXu1nyNo}2b_}Wv z7lGJcHGbC6{i1tijF5d;cfRj=)H);ZJO6Bm$VRjKvl{c_H4wSo)4Ze z3jYPQ(92kq+7Z{4HGhK4sx^9>J7d`o+4_NREy~>^$|vR!O1Jf z#`oRR=1n(Vccy0DXsOT4Iditw>|;x%HCON_2>4lMzAH9^I%8`97*Q>xVQ*uaUF0PJ z+%C(YabYi?$OBZ#ZYEgNY_}|fITcP2MUs+=;5#|agsd`Y_}ULgd*kW88Sq%0_j>V? zL`9`^@CRaVX*xB-rNXk;o*e~lSi|Y!^=!+Lx#id@pMyGf?ZzAls-g)Lv!qNljO3b+ zTd|!63Y}E24kO^j-|^_`C7g}@u|OmmLiwoC4l58|25G`TazDKZhO_g{;x(BdLaeBv zw@DTgIK88!+8?!NWIn_eDVPIPzEmkF&lkJyA(H5QQ}xN9K4|$9qZ+*ok#QNl$4ODz z@V+VQ*s`ov3BU^3Tz9KwS2{LJHcMcm=3rboI-Z|*3D`8kHl>$3v01-NnCnameZRk9 zdB2_WnXscX3|d68OrcN2cH8+{_)+Ed5bdz{b;F&c_iP^Rw=IwFnm}SSGij{h+Up}~*lxGI zwJeD>v0t1KKRig$=+8H6uC{7Uwrfsy=&yI1=yQ{C_Y%Egd9q#bbYHDdQpI$?0Fvv&v^yxTI1R6frQE&e=K7v~wP~MCy!r0G zi&iZD9Kb1JOs#{A6Dn7m%l^(blQvuCtH~JJ;OjrGN!Q$}Ctyr7)MG4*HH;@GZ7H_J zF|UZWTfd_ylFS`;Hm+q-gL}^s5#Z8JRJYz{?iw)M{f%81^hh!z$zQeyVqQTWKv1nF z44P#wDC;nMsD22!L~nu$_$18dt@oqzBk>r$HZuP=}5BssH6t zIC|mX0;}}NvDXF~gn?9)|I)W53|(IU9tR5dU7dbe5=P05zmmxih#7E=KNR|v#%`x; zt+3>yFs&}$_?MH|Elc(Fx@{LW=V13d-;kz8D-GPb3UmZrf71FgTE6ANC#~@ig8Ps5 zMX;u+OB{=StB_aTE@*~{Cbdy{BhI$@o~p>gHy$gGFD~C8*oHLi#Cn+;0D%g#d(~yO z?xz^VtLSiF;E7!cOVJUHm<6s%Cb+bZc9D4UqO$j=4ZS2L=qd3IVdxfv!^sSM84Vjj zV7MadBAZc)5r0SSr3QGglI$%|;oR3uLv1ZIS+Ce!yOf)$gWW1+q&N*Vxy#Q=Qd7G8 z7KM3^Nk9~m55BNKExD8CQ3_7!5dov=GlB;&tKJ*#w7qOu$F5rWN6ALndmqbXUFRinu_9Dq@QrGD3`W=hw!d-`6pg@-rMlJbx( z&Ei7@W3!2Tny@XD&l4=2uGM10x}Cgh+#*o%;zTq!nGAy=p<0Ne$~MEKCRHM$yF%g2 zs=p30ATLo}eFx=NoAESMK|R3cWPQ-DUGxPL5kuJ4D4F{t+6{wW{7$bdJE+vvJ+VQqrwoVx=8a zC&@hvcF@T|xkq&hob=({0^N~I7^u|?>$og9gwz?Ko5TAuz2vE1cz0WRK7xeSGvoqAuHId^__bF0LAyLkX~<90l9F{R!*;k2{;3iOtv`tIud^p&RL zb|NOiVfSZ(L*CkV2^H<7EK9%ICSZD`T!_gu!QRC+`E8%qbCOA5l~szev9Yih8HqN+ zCH(zRlk_&;ur}A|tbW@bkgSNyHWiNNNsYD6&tq-(rn(O!6 zPjCOW)Tr2@&)~eFG({0@(H89&npc3z*S1RVVC%!EPiC;Y7K+E^9Qjy-ERh$qwRjal zK185^>Q~s42s3tA65;Kj<^xSIlLwEBpV<}t2M1lTw&uXgaTyXniCT=xlzT8A^f2MW zJ5)Y6hb-!1YpjO_ zad6a@{+E*b^Wi@;ryMHJ8@wqpc1HHdpUz1gP?(tJ$OGo)aNW@+U~dlF^ii#J)+*J`RG$3x2MRmmpzZ zumYJIE0q9I4FVYp9>K?dWaB*T6thLEUp)syLPp19C*Xlwji@oYrwk9#82BFaOLg(y zwR+$7(2ThEjoY$350felo}5!mL-7!-C?MM#8+v}E`#k8q?{CHjg@}5fL1PmPtC1uV z{33de&_b8)9t{N@8$15ti}9>hoTsTvj%I6nKg9hz9MdN*{UrezIKkAP{mW#ri0>#R zIHZ)c`A(`BP&XZVl#VtMVvXdHDBV_cPCvz&c2+-mEeFh39~99b((>zGN^6LlO%)rl z*$=7B%&hw?=XrjdUEk}w-JA5{C9)@0mZ#|XF^1=g&#x{v9~~u%V`8|%KLSfZ_1q2} zyUVCoPq1F~DNVcS;Y6?RtT;Le+vt6}bP=LNiDQN2>WIMMuP@iQYSL~62m_H-2Q!tD z32`p$$9vnm4ry3g^O0yZ2Dyatu7 zP_~00;bzbAetKX-KYZ!3IRZ}Mw_^xOBVnfRq#$S3hv;T5r?c}GAd!9D|E==0B>&7Q_ zk7|D4!MUa`^Xp~EH9!~41r1x(Zye+4^QFazT>TN?)zdMxedTVx+B`;me=R(!w~Eom zvx_1*NuxJt+=y9dFBx)TD<)R6)6mccaYA(gU+Ev#|8!x#@<*^0I zZ`OhfS`3dNN(x!MrESDV`d(v_30ar}zBrXh53)K2UT%i`*XPUU`weX7)5v{6AD@0h zf9a<6MWLyGfQjkKhjMrmTj2W6J92Hylb#Qg{!zvVM?{zNT^oJ>FkTkx&*Q11O^ z=X(@E9o`q=y=TAtc?QeeRj{#s42U+e`TjCpK9%HF-i`w`VvXC9#{9nKvzcP_1$oGu zbIK*deQ|PjStrRZ@%FALDWp@%tbwI1uutKO1V2Tm(}(UeceA243n7wvV|OtQ`I^+H zFj6{$1h9ky9o2!#mR4uyFe5Simae)$3JymdK9^BV5|l42_+*gSZRclOVkQ+j!zpD> zhyPKHm|RwY`J%GiCClF2%Os-jTT6ARFZFgD7q`9JApbD=JazTg&QN0 zHBekORkc~-U8tOJ%~?MNiO4gSF&(!i+6xsD2{ik+#GkX?kL{9ryO`AB}2 zuSC%w{nB~~a{F7*j7M)w)DA;+mu0?(KCL#gFYg<{Y0sY$x3ph;>U~Qcq8+-eiI~4> z9o5O)1h5R|6l&QYlO!I1$;=+Qe?W17EvC4x>!26xVgBzf1)x~Buab3bCzBy2g4Zrb$F*6q5>z3p!K}s-{eo1XTstKdmCOKmz2wCEmvBjAFlshlcy* z%=<`68)d|XzQRhl)4nd3d3 zRpX`=)S&V5m>y(7Ul}f~n?^~XR@y9@C*NGNLV>zK56CO~dV-|G!uyD@ zE^@AZ`%8{kM(5!Eya*{VfUvKR2e{|0q2TGqHwzW*yq3uF5*LD-1{0FuUsjkOXj-DC zkiUYyi6y5vu*aZswTr??2;42TiysNDE8&3MwMN%8M%s)(UBCyT`L;il8q`+Auo|^f zr(FmGoWlIhog)(&J~CLNv^iEzuqh)wrO!uutCby+px(j>vKMI;S+D8m87wCI&@Zr- z=3n5{CUGnb5~^E>N8aI%sQizL7_q;e!}A6UxP%6X`0=`ByW%CzeU zmM;m$z#tg=8q2M-kcLULR(6Y1otL^7-S?%(=hZcnD8`rpav#0C`9)2S<{2-Y9U8JF zPXVlrumL6GJ!NRD;k^~YOY{*&??ZLojh+6N-*n+M&})DlX2V9;54pLX4$$(@Nk`QO zNYhC?*|28{)&;8A)VAqG3x-c%oKsa=ICYwm7OdHqvI+pBt^gof#(Gh_dzv?X9fjfa zDS{5&CRsl5CB+S9r^f(oMam7q11M`kF3t;MN%#HwBgv692&5LmLr_o}bZZqeyr1Sh zpad#{{SDCiWzI-T;lWEF1eYIfB}fY%cNdA1A`QZ63QjyORz_AQmQ5w;dBOe5EkfF; z&Nz(obPb~nb(kgFW_1v%EjBp&=@IP9*38GT_12dw$osTVO=>pUgrxT7&3uEhh|
  • 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 literal 15623 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 Cd;dNF diff --git a/updates/versions.php b/updates/versions.php index 72ca0f4..8f173d6 100644 --- a/updates/versions.php +++ b/updates/versions.php @@ -1,5 +1,5 @@