From 3bac7616e737922b00c7c79c12d6c70e31609ad9 Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Sun, 15 Feb 2026 10:46:55 +0100 Subject: [PATCH] ver. 0.273 - ShopProducer refactor + cleanup 6 factory facades - Domain\Producer\ProducerRepository (CRUD + frontend) - admin\Controllers\ShopProducerController (DI) - Nowe widoki: producers-list, producer-edit (table-list/form-edit) - shop\Producer -> fasada do ProducerRepository - Przepiecie ShopProduct factory na TransportRepository - Usuniete 6 pustych factory facades: Languages, Newsletter, Scontainers, ShopProducer, ShopTransport, Layouts - Usuniete legacy: controls\ShopProducer, stare szablony - Testy: 338 tests, 1063 assertions OK --- admin/templates/shop-producer/edit.php | 180 --------- admin/templates/shop-producer/list.php | 73 ---- .../templates/shop-producer/producer-edit.php | 1 + .../shop-producer/producers-list.php | 1 + .../Domain/Producer/ProducerRepository.php | 332 +++++++++++++++ .../Controllers/ShopProducerController.php | 379 ++++++++++++++++++ autoload/admin/class.Site.php | 8 + .../admin/controls/class.ShopProducer.php | 37 -- autoload/admin/controls/class.ShopProduct.php | 8 +- autoload/admin/factory/class.Languages.php | 51 --- autoload/admin/factory/class.Layouts.php | 65 --- autoload/admin/factory/class.Newsletter.php | 48 --- autoload/admin/factory/class.Scontainers.php | 32 -- autoload/admin/factory/class.ShopProducer.php | 89 ---- autoload/admin/factory/class.ShopProduct.php | 4 +- .../admin/factory/class.ShopTransport.php | 10 - autoload/shop/class.Producer.php | 38 +- docs/CHANGELOG.md | 17 + docs/DATABASE_STRUCTURE.md | 32 +- docs/PROJECT_STRUCTURE.md | 13 + docs/REFACTORING_PLAN.md | 17 +- docs/TESTING.md | 15 +- temp/update_build/delete_files_0.273.txt | 9 + temp/update_build/update_0.273.zip | Bin 0 -> 45279 bytes .../Producer/ProducerRepositoryTest.php | 240 +++++++++++ .../ShopProducerControllerTest.php | 67 ++++ 26 files changed, 1134 insertions(+), 632 deletions(-) delete mode 100644 admin/templates/shop-producer/edit.php delete mode 100644 admin/templates/shop-producer/list.php create mode 100644 admin/templates/shop-producer/producer-edit.php create mode 100644 admin/templates/shop-producer/producers-list.php create mode 100644 autoload/Domain/Producer/ProducerRepository.php create mode 100644 autoload/admin/Controllers/ShopProducerController.php delete mode 100644 autoload/admin/controls/class.ShopProducer.php delete mode 100644 autoload/admin/factory/class.Languages.php delete mode 100644 autoload/admin/factory/class.Layouts.php delete mode 100644 autoload/admin/factory/class.Newsletter.php delete mode 100644 autoload/admin/factory/class.Scontainers.php delete mode 100644 autoload/admin/factory/class.ShopProducer.php delete mode 100644 autoload/admin/factory/class.ShopTransport.php create mode 100644 temp/update_build/delete_files_0.273.txt create mode 100644 temp/update_build/update_0.273.zip create mode 100644 tests/Unit/Domain/Producer/ProducerRepositoryTest.php create mode 100644 tests/Unit/admin/Controllers/ShopProducerControllerTest.php diff --git a/admin/templates/shop-producer/edit.php b/admin/templates/shop-producer/edit.php deleted file mode 100644 index 6a03486..0000000 --- a/admin/templates/shop-producer/edit.php +++ /dev/null @@ -1,180 +0,0 @@ - - - -
- -
-
- 'Nazwa', - 'class' => 'require', - 'name' => 'name', - 'id' => 'name', - 'value' => $this -> producer['name'] - ] );?> - 'Aktywny', - 'name' => 'status', - 'checked' => $this -> producer['status'] == 1 ? true : false - ] );?> - 'Logo', - 'name' => 'img', - 'id' => 'img', - 'value' => $this -> producer['img'], - 'icon_content' => 'przeglądaj', - 'icon_js' => "window.open ( '/libraries/filemanager-9.14.2/dialog.php?type=1&popup=1&field_id=img&akey=" . $rfmAkeyJS . "', 'mywindow', 'location=1,status=1,scrollbars=1, width=1100,height=700');" - ] ); - ?> -
-
-
-
    - languages ) ): foreach ( $this -> languages as $lg ):?> - -
  • ';?>
  • - - -
-
- languages ) ): foreach ( $this -> languages as $lg ):?> - languages ) ) foreach ( $this -> languages as $lg_tmp ) - { - if ( $lg_tmp[ 'id' ] != $lg[ 'id' ] ) - $languages[ $lg_tmp[ 'id' ] ] = $lg_tmp[ 'name' ]; - } - ?> - -
- 'Opis', - 'name' => 'description[' . $lg['id'] . ']', - 'id' => 'description_' . $lg['id'], - 'value' => $this -> producer['languages'][$lg[ 'id']]['description'], - 'inline' => true - ] );?> - 'Dane producenta', - 'name' => 'data[' . $lg['id'] . ']', - 'id' => 'data_' . $lg['id'], - 'value' => $this -> producer['languages'][$lg[ 'id']]['data'], - 'inline' => true - ] );?> - -
- - -
-
-
-
-
-
-
    - languages ) ): foreach ( $this -> languages as $lg ):?> - -
  • ';?>
  • - - -
-
- languages ) ): foreach ( $this -> languages as $lg ):?> - languages ) ) foreach ( $this -> languages as $lg_tmp ) - { - if ( $lg_tmp[ 'id' ] != $lg[ 'id' ] ) - $languages[ $lg_tmp[ 'id' ] ] = $lg_tmp[ 'name' ]; - } - ?> - -
- 'Meta title', - 'name' => 'meta_title[' . $lg['id'] . ']', - 'id' => 'meta_title_' . $lg[ 'id' ], - 'value' => $this -> producer['languages'][$lg['id']]['meta_title'] - ] );?> -
- - -
-
-
-
-
-
- id = 'producer-edit'; -$grid -> gdb_opt = $gdb; -$grid -> include_plugins = true; -$grid -> title = 'Edycja producenta'; -$grid -> fields = [ - [ - 'db' => 'id', - 'type' => 'hidden', - 'value' => $this -> producer['id'] - ] - ]; -$grid -> actions = [ - 'save' => [ 'url' => '/admin/shop_producer/save/', 'back_url' => '/admin/shop_producer/list/' ], - 'cancel' => [ 'url' => '/admin/shop_producer/list/' ] - ]; -$grid -> external_code = $out; -$grid -> persist_edit = true; -$grid -> id_param = 'id'; -echo $grid -> draw(); -?> - diff --git a/admin/templates/shop-producer/list.php b/admin/templates/shop-producer/list.php deleted file mode 100644 index 11dd1b1..0000000 --- a/admin/templates/shop-producer/list.php +++ /dev/null @@ -1,73 +0,0 @@ - gdb_opt = $gdb; -$grid -> sql = 'SELECT *' - . 'FROM ( ' - . 'SELECT ' - . 'id, name, status, img ' - . 'FROM ' - . 'pp_shop_producer ' - . ') AS q1 ' - . 'WHERE ' - . '1=1 [where] ' - . 'ORDER BY ' - . '[order_p1] [order_p2]'; -$grid -> sql_count = 'SELECT ' - . 'COUNT(0) FROM ( ' - . 'SELECT ' - . 'id, name, status, img ' - . 'FROM ' - . 'pp_shop_producer ' - . ') AS q1 ' - . 'WHERE ' - . '1=1 [where] '; -$grid -> debug = true; -$grid -> order = [ 'column' => 'name', 'type' => 'ASC' ]; -$grid -> search = [ - [ 'name' => 'Nazwa', 'db' => 'name', 'type' => 'text' ], - [ 'name' => 'Aktywny', 'db' => 'status', 'type' => 'select', 'replace' => [ 'array' => [ 0 => 'nie', 1 => 'tak' ] ] ] - ]; -$grid -> columns_view = [ - [ - 'name' => 'Lp.', - 'th' => [ 'class' => 'g-lp' ], - 'td' => [ 'class' => 'g-center' ], - 'autoincrement' => true - ], [ - 'name' => 'Nazwa', - 'db' => 'name', - 'sort' => true, - 'php' => 'echo "[name]";' - ], [ - 'name' => 'Logo', - 'db' => 'img' - ], [ - 'name' => 'Aktywny', - 'db' => 'status', - 'replace' => [ 'array' => [ 0 => 'nie', 1 => 'tak' ] ], - 'td' => [ 'class' => 'g-center' ], - 'th' => [ 'class' => 'g-center', 'style' => 'width: 150px;' ], - 'sort' => true - ], [ - 'name' => 'Edytuj', - 'action' => [ 'type' => 'edit', 'url' => '/admin/shop_producer/edit/id=[id]' ], - 'th' => [ 'class' => 'g-center', 'style' => 'width: 70px;' ], - 'td' => [ 'class' => 'g-center' ] - ], [ - 'name' => 'Usuń', - 'action' => [ 'type' => 'delete', 'url' => '/admin/shop_producer/delete/id=[id]' ], - 'th' => [ 'class' => 'g-center', 'style' => 'width: 70px;' ], - 'td' => [ 'class' => 'g-center' ] - ] - ]; -$grid -> buttons = [ - [ - 'label' => 'Dodaj producenta', - 'url' => '/admin/shop_producer/edit/', - 'icon' => 'fa-plus-circle', - 'class' => 'btn-success' - ] - ]; -echo $grid -> draw(); \ No newline at end of file diff --git a/admin/templates/shop-producer/producer-edit.php b/admin/templates/shop-producer/producer-edit.php new file mode 100644 index 0000000..5fe1611 --- /dev/null +++ b/admin/templates/shop-producer/producer-edit.php @@ -0,0 +1 @@ + $this->form]); ?> diff --git a/admin/templates/shop-producer/producers-list.php b/admin/templates/shop-producer/producers-list.php new file mode 100644 index 0000000..0f89c5b --- /dev/null +++ b/admin/templates/shop-producer/producers-list.php @@ -0,0 +1 @@ + $this->viewModel]); ?> diff --git a/autoload/Domain/Producer/ProducerRepository.php b/autoload/Domain/Producer/ProducerRepository.php new file mode 100644 index 0000000..1e3cf09 --- /dev/null +++ b/autoload/Domain/Producer/ProducerRepository.php @@ -0,0 +1,332 @@ +db = $db; + } + + /** + * Lista producentów dla panelu admin (paginowana, filtrowalna, sortowalna). + * + * @return array{items: array>, total: int} + */ + public function listForAdmin( + array $filters, + string $sortColumn = 'name', + string $sortDir = 'ASC', + int $page = 1, + int $perPage = 15 + ): array { + $allowedSortColumns = [ + 'id' => 'p.id', + 'name' => 'p.name', + 'status' => 'p.status', + ]; + + $sortSql = $allowedSortColumns[$sortColumn] ?? 'p.name'; + $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[] = 'p.name LIKE :name'; + $params[':name'] = '%' . $name . '%'; + } + + $status = trim((string)($filters['status'] ?? '')); + if ($status === '0' || $status === '1') { + $where[] = 'p.status = :status'; + $params[':status'] = (int)$status; + } + + $whereSql = implode(' AND ', $where); + + $sqlCount = " + SELECT COUNT(0) + FROM pp_shop_producer AS p + WHERE {$whereSql} + "; + + $stmtCount = $this->db->query($sqlCount, $params); + $countRows = $stmtCount ? $stmtCount->fetchAll() : []; + $total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0; + + $sql = " + SELECT + p.id, + p.name, + p.status, + p.img + FROM pp_shop_producer AS p + WHERE {$whereSql} + ORDER BY {$sortSql} {$sortDir}, p.id DESC + LIMIT {$perPage} OFFSET {$offset} + "; + + $stmt = $this->db->query($sql, $params); + $items = $stmt ? $stmt->fetchAll() : []; + + if (!is_array($items)) { + $items = []; + } + + foreach ($items as &$item) { + $item['id'] = (int)($item['id'] ?? 0); + $item['status'] = $this->toSwitchValue($item['status'] ?? 0); + } + unset($item); + + return [ + 'items' => $items, + 'total' => $total, + ]; + } + + /** + * Pobiera producenta z tłumaczeniami. + */ + public function find(int $id): array + { + if ($id <= 0) { + return $this->defaultProducer(); + } + + $producer = $this->db->get('pp_shop_producer', '*', ['id' => $id]); + if (!is_array($producer)) { + return $this->defaultProducer(); + } + + $producer['id'] = (int)($producer['id'] ?? 0); + $producer['status'] = $this->toSwitchValue($producer['status'] ?? 0); + + // Tłumaczenia + $rows = $this->db->select('pp_shop_producer_lang', '*', ['producer_id' => $id]); + $languages = []; + if (is_array($rows)) { + foreach ($rows as $row) { + $langId = $row['lang_id'] ?? ''; + $languages[$langId] = [ + 'description' => $row['description'] ?? null, + 'data' => $row['data'] ?? null, + 'meta_title' => $row['meta_title'] ?? null, + ]; + } + } + $producer['languages'] = $languages; + + return $producer; + } + + /** + * Zapisuje producenta (insert / update) wraz z tłumaczeniami. + * + * @return int|null ID producenta lub null przy błędzie + */ + public function save( + int $id, + string $name, + int $status, + ?string $img, + array $description, + array $data, + array $metaTitle, + array $langs + ): ?int { + $row = [ + 'name' => trim($name), + 'status' => $status === 1 ? 1 : 0, + 'img' => $img, + ]; + + if ($id <= 0) { + $this->db->insert('pp_shop_producer', $row); + $id = (int)$this->db->id(); + if ($id <= 0) { + return null; + } + } else { + $this->db->update('pp_shop_producer', $row, ['id' => $id]); + } + + // Tłumaczenia + foreach ($langs as $lg) { + $langId = $lg['id'] ?? ''; + $translationData = [ + 'description' => $description[$langId] ?? null, + 'data' => $data[$langId] ?? null, + 'meta_title' => $metaTitle[$langId] ?? null, + ]; + + $translationId = $this->db->get( + 'pp_shop_producer_lang', + 'id', + ['AND' => ['producer_id' => $id, 'lang_id' => $langId]] + ); + + if ($translationId) { + $this->db->update('pp_shop_producer_lang', $translationData, ['id' => $translationId]); + } else { + $this->db->insert('pp_shop_producer_lang', array_merge($translationData, [ + 'producer_id' => $id, + 'lang_id' => $langId, + ])); + } + } + + return $id; + } + + /** + * Usuwa producenta (kaskadowo z pp_shop_producer_lang przez FK). + */ + public function delete(int $id): bool + { + if ($id <= 0) { + return false; + } + + $result = (bool)$this->db->delete('pp_shop_producer', ['id' => $id]); + + return $result; + } + + /** + * Wszystkie producenty (do select w edycji produktu). + * + * @return array + */ + public function allProducers(): array + { + $rows = $this->db->select('pp_shop_producer', ['id', 'name'], ['ORDER' => ['name' => 'ASC']]); + if (!is_array($rows)) { + return []; + } + + $producers = []; + foreach ($rows as $row) { + $producers[] = [ + 'id' => (int)($row['id'] ?? 0), + 'name' => (string)($row['name'] ?? ''), + ]; + } + + return $producers; + } + + /** + * Pobiera producenta z tłumaczeniami dla danego języka (frontend). + */ + public function findForFrontend(int $id, string $langId): ?array + { + if ($id <= 0) { + return null; + } + + $producer = $this->db->get('pp_shop_producer', '*', ['id' => $id]); + if (!is_array($producer)) { + return null; + } + + $producer['id'] = (int)($producer['id'] ?? 0); + + $langRow = $this->db->get('pp_shop_producer_lang', '*', [ + 'AND' => ['producer_id' => $id, 'lang_id' => $langId], + ]); + + $producer['languages'] = []; + if (is_array($langRow)) { + $producer['languages'][$langId] = [ + 'description' => $langRow['description'] ?? null, + 'data' => $langRow['data'] ?? null, + 'meta_title' => $langRow['meta_title'] ?? null, + ]; + } + + return $producer; + } + + /** + * Produkty producenta (paginowane) — frontend. + * + * @return array{products: array, ls: int} + */ + public function producerProducts(int $producerId, int $perPage = 12, int $page = 1): array + { + $count = $this->db->count('pp_shop_products', [ + 'AND' => ['producer_id' => $producerId, 'status' => 1], + ]); + + $totalPages = max(1, (int)ceil($count / $perPage)); + $page = max(1, min($page, $totalPages)); + $offset = $perPage * ($page - 1); + + $products = $this->db->select('pp_shop_products', 'id', [ + 'AND' => ['producer_id' => $producerId, 'status' => 1], + 'LIMIT' => [$offset, $perPage], + ]); + + return [ + 'products' => is_array($products) ? $products : [], + 'ls' => $totalPages, + ]; + } + + /** + * Aktywni producenci (frontend lista). + * + * @return array + */ + public function allActiveIds(): array + { + $rows = $this->db->select('pp_shop_producer', 'id', [ + 'status' => 1, + 'ORDER' => ['name' => 'ASC'], + ]); + + return is_array($rows) ? array_map('intval', $rows) : []; + } + + private function defaultProducer(): array + { + return [ + 'id' => 0, + 'name' => '', + 'status' => 1, + 'img' => null, + 'languages' => [], + ]; + } + + private function toSwitchValue($value): int + { + if (is_bool($value)) { + return $value ? 1 : 0; + } + + if (is_numeric($value)) { + return ((int)$value) === 1 ? 1 : 0; + } + + if (is_string($value)) { + $normalized = strtolower(trim($value)); + return in_array($normalized, ['1', 'on', 'true', 'yes'], true) ? 1 : 0; + } + + return 0; + } +} diff --git a/autoload/admin/Controllers/ShopProducerController.php b/autoload/admin/Controllers/ShopProducerController.php new file mode 100644 index 0000000..58e9f8d --- /dev/null +++ b/autoload/admin/Controllers/ShopProducerController.php @@ -0,0 +1,379 @@ +repository = $repository; + $this->languagesRepository = $languagesRepository; + $this->formHandler = new FormRequestHandler(); + } + + public function list(): string + { + $sortableColumns = ['id', 'name', 'status']; + $filterDefinitions = [ + [ + 'key' => 'name', + 'label' => 'Nazwa', + 'type' => 'text', + ], + [ + 'key' => 'status', + 'label' => 'Aktywny', + 'type' => 'select', + 'options' => [ + '' => '- aktywny -', + '1' => 'tak', + '0' => 'nie', + ], + ], + ]; + + $listRequest = \admin\Support\TableListRequestFactory::fromRequest( + $filterDefinitions, + $sortableColumns, + 'name' + ); + + $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 = (int)($item['id'] ?? 0); + $name = trim((string)($item['name'] ?? '')); + $status = (int)($item['status'] ?? 0); + $img = trim((string)($item['img'] ?? '')); + + $imgHtml = ''; + if ($img !== '') { + $imgHtml = ''; + } + + $rows[] = [ + 'lp' => $lp++ . '.', + 'name' => '' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . '', + 'img' => $imgHtml, + 'status' => $status === 1 ? 'tak' : 'nie', + '_actions' => [ + [ + 'label' => 'Edytuj', + 'url' => '/admin/shop_producer/edit/id=' . $id, + 'class' => 'btn btn-xs btn-primary', + ], + [ + 'label' => 'Usun', + 'url' => '/admin/shop_producer/delete/id=' . $id, + 'class' => 'btn btn-xs btn-danger', + 'confirm' => 'Na pewno chcesz usunac wybranego producenta?', + 'confirm_ok' => 'Usun', + 'confirm_cancel' => 'Anuluj', + ], + ], + ]; + } + + $total = (int)$result['total']; + $totalPages = max(1, (int)ceil($total / $listRequest['perPage'])); + + $viewModel = new PaginatedTableViewModel( + [ + ['key' => 'lp', 'label' => 'Lp.', 'class' => 'text-center', 'sortable' => false], + ['key' => 'name', 'sort_key' => 'name', 'label' => 'Nazwa', 'sortable' => true, 'raw' => true], + ['key' => 'img', 'label' => 'Logo', 'sortable' => false, 'raw' => true], + ['key' => 'status', 'sort_key' => 'status', 'label' => 'Aktywny', 'class' => 'text-center', '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/shop_producer/list/', + 'Brak danych w tabeli.', + '/admin/shop_producer/edit/', + 'Dodaj producenta' + ); + + return \Tpl::view('shop-producer/producers-list', [ + 'viewModel' => $viewModel, + ]); + } + + public function view_list(): string + { + return $this->list(); + } + + public function edit(): string + { + $producer = $this->repository->find((int)\S::get('id')); + $languages = $this->languagesRepository->languagesList(); + $validationErrors = $_SESSION['form_errors'][$this->formId()] ?? null; + if ($validationErrors) { + unset($_SESSION['form_errors'][$this->formId()]); + } + + return \Tpl::view('shop-producer/producer-edit', [ + 'form' => $this->buildFormViewModel($producer, $languages, $validationErrors), + ]); + } + + public function producer_edit(): string + { + return $this->edit(); + } + + public function save(): void + { + // Legacy JSON (gridEdit) + $legacyValues = \S::get('values'); + if ($legacyValues) { + $values = json_decode((string)$legacyValues, true); + $response = [ + 'status' => 'error', + 'msg' => 'Podczas zapisywania producenta wystapil blad. Prosze sprobowac ponownie.', + ]; + + if (is_array($values)) { + $langs = $this->languagesRepository->languagesList(true); + + $id = $this->repository->save( + (int)($values['id'] ?? 0), + (string)($values['name'] ?? ''), + $this->toSwitchValue($values['status'] ?? 0), + $values['img'] ?? null, + $values['description'] ?? [], + $values['data'] ?? [], + $values['meta_title'] ?? [], + $langs + ); + + if (!empty($id)) { + \S::htacces(); + \S::delete_dir('../temp/'); + $response = [ + 'status' => 'ok', + 'msg' => 'Producent zostal zapisany.', + 'id' => (int)$id, + ]; + } + } + + echo json_encode($response); + exit; + } + + // Nowy flow (form-edit) + $producer = $this->repository->find((int)\S::get('id')); + $languages = $this->languagesRepository->languagesList(); + $form = $this->buildFormViewModel($producer, $languages); + + $result = $this->formHandler->handleSubmit($form, $_POST); + if (!$result['success']) { + $_SESSION['form_errors'][$this->formId()] = $result['errors']; + echo json_encode(['success' => false, 'errors' => $result['errors']]); + exit; + } + + $data = $result['data']; + $langs = $this->languagesRepository->languagesList(true); + + $translations = $data['translations'] ?? []; + $description = []; + $metaData = []; + $metaTitle = []; + foreach ($translations as $langId => $fields) { + $description[$langId] = $fields['description'] ?? null; + $metaData[$langId] = $fields['data'] ?? null; + $metaTitle[$langId] = $fields['meta_title'] ?? null; + } + + $savedId = $this->repository->save( + (int)($data['id'] ?? 0), + (string)($data['name'] ?? ''), + $this->toSwitchValue($data['status'] ?? 0), + $data['img'] ?? null, + $description, + $metaData, + $metaTitle, + $langs + ); + + if ($savedId) { + \S::htacces(); + \S::delete_dir('../temp/'); + echo json_encode([ + 'success' => true, + 'id' => $savedId, + 'message' => 'Producent zostal zapisany.', + ]); + exit; + } + + echo json_encode([ + 'success' => false, + 'errors' => ['general' => 'Podczas zapisywania producenta wystapil blad.'], + ]); + exit; + } + + public function producer_save(): void + { + $this->save(); + } + + public function delete(): void + { + if ($this->repository->delete((int)\S::get('id'))) { + \S::htacces(); + \S::delete_dir('../temp/'); + \S::alert('Producent zostal usuniety.'); + } + + header('Location: /admin/shop_producer/list/'); + exit; + } + + public function producer_delete(): void + { + $this->delete(); + } + + private function buildFormViewModel(array $producer, array $languages, ?array $errors = null): FormEditViewModel + { + $id = (int)($producer['id'] ?? 0); + $isNew = $id <= 0; + + $data = [ + 'id' => $id, + 'name' => (string)($producer['name'] ?? ''), + 'status' => (int)($producer['status'] ?? 1), + 'img' => $producer['img'] ?? null, + 'languages' => is_array($producer['languages'] ?? null) ? $producer['languages'] : [], + ]; + + $fields = [ + FormField::hidden('id', $id), + FormField::text('name', [ + 'label' => 'Nazwa', + 'required' => true, + 'tab' => 'general', + ]), + FormField::switch('status', [ + 'label' => 'Aktywny', + 'tab' => 'general', + 'value' => true, + ]), + FormField::image('img', [ + 'label' => 'Logo', + 'tab' => 'general', + ]), + FormField::langSection('translations', 'description', [ + FormField::editor('description', [ + 'label' => 'Opis', + 'height' => 250, + ]), + FormField::editor('data', [ + 'label' => 'Dane producenta', + 'height' => 250, + ]), + ]), + FormField::langSection('translations', 'seo', [ + FormField::text('meta_title', [ + 'label' => 'Meta title', + ]), + ]), + ]; + + $tabs = [ + new FormTab('general', 'Ogolne', 'fa-file'), + new FormTab('description', 'Opis', 'fa-file'), + new FormTab('seo', 'SEO', 'fa-globe'), + ]; + + $actionUrl = '/admin/shop_producer/save/' . ($isNew ? '' : ('id=' . $id)); + $actions = [ + FormAction::save($actionUrl, '/admin/shop_producer/list/'), + FormAction::cancel('/admin/shop_producer/list/'), + ]; + + return new FormEditViewModel( + $this->formId(), + 'Edycja producenta', + $data, + $fields, + $tabs, + $actions, + 'POST', + $actionUrl, + '/admin/shop_producer/list/', + true, + [], + $languages, + $errors + ); + } + + private function formId(): string + { + return 'shop-producer-edit'; + } + + private function toSwitchValue($value): int + { + if (is_bool($value)) { + return $value ? 1 : 0; + } + + if (is_numeric($value)) { + return ((int)$value) === 1 ? 1 : 0; + } + + if (is_string($value)) { + $normalized = strtolower(trim($value)); + return in_array($normalized, ['1', 'on', 'true', 'yes'], true) ? 1 : 0; + } + + return 0; + } +} diff --git a/autoload/admin/class.Site.php b/autoload/admin/class.Site.php index 72e2da8..9dadd20 100644 --- a/autoload/admin/class.Site.php +++ b/autoload/admin/class.Site.php @@ -369,6 +369,14 @@ class Site new \Domain\ProductSet\ProductSetRepository( $mdb ) ); }, + 'ShopProducer' => function() { + global $mdb; + + return new \admin\Controllers\ShopProducerController( + new \Domain\Producer\ProducerRepository( $mdb ), + new \Domain\Languages\LanguagesRepository( $mdb ) + ); + }, ]; return self::$newControllers; diff --git a/autoload/admin/controls/class.ShopProducer.php b/autoload/admin/controls/class.ShopProducer.php deleted file mode 100644 index 815e695..0000000 --- a/autoload/admin/controls/class.ShopProducer.php +++ /dev/null @@ -1,37 +0,0 @@ - 'error', 'msg' => 'Podczas zapisywania producenta wystąpił błąd. Proszę spróbować ponownie.' ]; - $values = json_decode( \S::get( 'values' ), true ); - - if ( $producer_id = \admin\factory\ShopProducer::save( $values['id'], $values['name'], $values['status'] == 'on' ? 1 : 0, $values['img'], $values['description'], $values['data'], $values['meta_title'] ) ) - $response = [ 'status' => 'ok', 'msg' => 'Producent został zapisany.', 'id' => $producer_id ]; - - echo json_encode( $response ); - exit; - } - - static public function edit() - { - return \Tpl::view( 'shop-producer/edit', [ - 'producer' => \S::get( 'id' ) ? new \shop\Producer( \S::get( 'id' ) ) : null, - 'languages' => ( new \Domain\Languages\LanguagesRepository( $GLOBALS['mdb'] ) )->languagesList() - ] ); - } - - static public function list() - { - return \Tpl::view( 'shop-producer/list' ); - } -} diff --git a/autoload/admin/controls/class.ShopProduct.php b/autoload/admin/controls/class.ShopProduct.php index e65be73..f7dba45 100644 --- a/autoload/admin/controls/class.ShopProduct.php +++ b/autoload/admin/controls/class.ShopProduct.php @@ -248,7 +248,7 @@ class ShopProduct 'products' => \admin\factory\ShopProduct::products_list(), 'dlang' => \front\factory\Languages::default_language(), 'sets' => \shop\ProductSet::sets_list(), - 'producers' => \admin\factory\ShopProducer::all(), + 'producers' => ( new \Domain\Producer\ProducerRepository( $mdb ) )->allProducers(), 'units' => ( new \Domain\Dictionaries\DictionariesRepository( $mdb ) ) -> allUnits(), 'user' => $user ] ); @@ -262,12 +262,6 @@ class ShopProduct return is_array( $rows ) ? $rows : []; } - if ( class_exists( '\admin\factory\Layouts' ) ) - { - $rows = \admin\factory\Layouts::layouts_list(); - return is_array( $rows ) ? $rows : []; - } - return []; } diff --git a/autoload/admin/factory/class.Languages.php b/autoload/admin/factory/class.Languages.php deleted file mode 100644 index 3e46b4f..0000000 --- a/autoload/admin/factory/class.Languages.php +++ /dev/null @@ -1,51 +0,0 @@ -deleteTranslation((int)$translation_id); - } - - public static function translation_save($translation_id, $text, $languages) - { - return self::repository()->saveTranslation((int)$translation_id, (string)$text, is_array($languages) ? $languages : []); - } - - public static function translation_details($translation_id) - { - return self::repository()->translationDetails((int)$translation_id); - } - - public static function language_delete($language_id) - { - return self::repository()->deleteLanguage((string)$language_id); - } - - public static function max_order() - { - return self::repository()->maxOrder(); - } - - public static function language_save($language_id, $name, $status, $start, $o) - { - return self::repository()->saveLanguage((string)$language_id, (string)$name, $status, $start, (int)$o); - } - - public static function language_details($language_id) - { - return self::repository()->languageDetails((string)$language_id); - } - - public static function languages_list($only_active = false) - { - return self::repository()->languagesList((bool)$only_active); - } -} diff --git a/autoload/admin/factory/class.Layouts.php b/autoload/admin/factory/class.Layouts.php deleted file mode 100644 index 51e52bf..0000000 --- a/autoload/admin/factory/class.Layouts.php +++ /dev/null @@ -1,65 +0,0 @@ -delete((int)$layout_id); - } - - public static function layout_details($layout_id) - { - return self::repository()->find((int)$layout_id); - } - - public static function layout_save( - $layout_id, - $name, - $status, - $pages, - $html, - $css, - $js, - $m_html, - $m_css, - $m_js, - $categories, - $categories_default - ) { - return self::repository()->save([ - 'id' => $layout_id, - 'name' => $name, - 'status' => $status, - 'pages' => $pages, - 'html' => $html, - 'css' => $css, - 'js' => $js, - 'm_html' => $m_html, - 'm_css' => $m_css, - 'm_js' => $m_js, - 'categories' => $categories, - 'categories_default' => $categories_default, - ]); - } - - public static function menus_list() - { - global $mdb; - return (new PagesRepository($mdb))->menusWithPages(); - } - - public static function layouts_list() - { - return self::repository()->listAll(); - } - - private static function repository(): LayoutsRepository - { - global $mdb; - return new LayoutsRepository($mdb); - } -} diff --git a/autoload/admin/factory/class.Newsletter.php b/autoload/admin/factory/class.Newsletter.php deleted file mode 100644 index ddd14f7..0000000 --- a/autoload/admin/factory/class.Newsletter.php +++ /dev/null @@ -1,48 +0,0 @@ - isAdminTemplate( (int)$template_id ); - } - - public static function newsletter_template_delete( $template_id ) - { - return self::repository() -> deleteTemplate( (int)$template_id ); - } - - public static function send( $dates, $template ) - { - return self::repository() -> queueSend( (string)$dates, (int)$template ); - } - - public static function email_template_detalis( $id_template ) - { - return self::repository() -> templateDetails( (int)$id_template ); - } - - public static function template_save( $id, $name, $text ) - { - return self::repository() -> saveTemplate( (int)$id, (string)$name, (string)$text ); - } - - public static function templates_list() - { - return self::repository() -> listTemplatesSimple( false ); - } - - private static function repository(): NewsletterRepository - { - global $mdb; - - return new NewsletterRepository( - $mdb, - new SettingsRepository($mdb) - ); - } -} \ No newline at end of file diff --git a/autoload/admin/factory/class.Scontainers.php b/autoload/admin/factory/class.Scontainers.php deleted file mode 100644 index 3f1d819..0000000 --- a/autoload/admin/factory/class.Scontainers.php +++ /dev/null @@ -1,32 +0,0 @@ -delete((int)$container_id); - } - - public static function container_save($container_id, $title, $text, $status, $show_title) - { - return self::repository()->save([ - 'id' => (int)$container_id, - 'title' => is_array($title) ? $title : [], - 'text' => is_array($text) ? $text : [], - 'status' => $status, - 'show_title' => $show_title, - ]); - } - - public static function container_details($container_id) - { - return self::repository()->find((int)$container_id); - } -} \ No newline at end of file diff --git a/autoload/admin/factory/class.ShopProducer.php b/autoload/admin/factory/class.ShopProducer.php deleted file mode 100644 index 6c3aae5..0000000 --- a/autoload/admin/factory/class.ShopProducer.php +++ /dev/null @@ -1,89 +0,0 @@ - select( 'pp_shop_producer', '*', [ 'ORDER' => [ 'name' => 'ASC' ] ] ); - } - - static public function delete( int $producer_id ) - { - global $mdb; - return $mdb -> delete( 'pp_shop_producer', [ 'id' => $producer_id ] ); - } - - static public function save( $producer_id, $name, int $status, $img, $description, $data, $meta_title ) - { - global $mdb; - - if ( !$producer_id ) - { - $mdb -> insert( 'pp_shop_producer', [ - 'name' => $name, - 'status' => $status, - 'img' => $img - ] ); - - $id = $mdb -> id(); - - $langs = ( new \Domain\Languages\LanguagesRepository( $mdb ) )->languagesList( true ); - foreach ( $langs as $lg ) - { - $mdb -> insert( 'pp_shop_producer_lang', [ - 'producer_id' => $id, - 'lang_id' => $lg['id'], - 'description' => $description[ $lg['id'] ] ?? null, - 'data' => $data[ $lg['id'] ] ?? null, - 'meta_title' => $meta_title[ $lg['id'] ] ?? null - ] ); - } - - \S::htacces(); - \S::delete_dir( '../temp/' ); - - return $id; - } - else - { - $mdb -> update( 'pp_shop_producer', [ - 'name' => $name, - 'status' => $status, - 'img' => $img - ], [ - 'id' => (int) $producer_id - ] ); - - $langs = ( new \Domain\Languages\LanguagesRepository( $mdb ) )->languagesList( true ); - foreach ( $langs as $lg ) - { - if ( $translation_id = $mdb -> get( 'pp_shop_producer_lang', 'id', [ 'AND' => [ 'producer_id' => $producer_id, 'lang_id' => $lg['id'] ] ] ) ) - { - $mdb -> update( 'pp_shop_producer_lang', [ - 'description' => $description[ $lg['id'] ] ?? null, - 'meta_title' => $meta_title[ $lg['id'] ] ?? null, - 'data' => $data[ $lg['id'] ] ?? null - ], [ - 'id' => $translation_id - ] ); - } - else - { - $mdb -> insert( 'pp_shop_producer_lang', [ - 'producer_id' => $producer_id, - 'lang_id' => $lg['id'], - 'description' => $description[ $lg['id'] ] ?? null, - 'data' => $data[ $lg['id'] ] ?? null, - 'meta_title' => $meta_title[ $lg['id'] ] ?? null - ] ); - } - } - - \S::htacces(); - \S::delete_dir( '../temp/' ); - return $producer_id; - } - return false; - } -} diff --git a/autoload/admin/factory/class.ShopProduct.php b/autoload/admin/factory/class.ShopProduct.php index 196d655..5fd0a9e 100644 --- a/autoload/admin/factory/class.ShopProduct.php +++ b/autoload/admin/factory/class.ShopProduct.php @@ -397,7 +397,7 @@ class ShopProduct $p_gshipping = $itemNode -> appendChild( $doc -> createElement( 'g:shipping' ) ); $p_gcountry = $p_gshipping -> appendChild( $doc -> createElement( 'g:country', 'PL' ) ); $p_gservice = $p_gshipping -> appendChild( $doc -> createElement( 'g:service', '1 dzień roboczy' ) ); - $p_gprice = $p_gshipping -> appendChild( $doc -> createElement( 'g:price', \admin\factory\ShopTransport::lowest_transport_price( (int) $product -> wp ) . ' PLN' ) ); + $p_gprice = $p_gshipping -> appendChild( $doc -> createElement( 'g:price', ( new \Domain\Transport\TransportRepository( $mdb ) )->lowestTransportPrice( (int) $product -> wp ) . ' PLN' ) ); } } } @@ -484,7 +484,7 @@ class ShopProduct $p_gshipping = $itemNode -> appendChild( $doc -> createElement( 'g:shipping' ) ); $p_gcountry = $p_gshipping -> appendChild( $doc -> createElement( 'g:country', 'PL' ) ); $p_gservice = $p_gshipping -> appendChild( $doc -> createElement( 'g:service', '1 dzień roboczy' ) ); - $p_gprice = $p_gshipping -> appendChild( $doc -> createElement( 'g:price', \admin\factory\ShopTransport::lowest_transport_price( (int) $product -> wp ) . ' PLN' ) ); + $p_gprice = $p_gshipping -> appendChild( $doc -> createElement( 'g:price', ( new \Domain\Transport\TransportRepository( $mdb ) )->lowestTransportPrice( (int) $product -> wp ) . ' PLN' ) ); } } file_put_contents('../google-feed.xml', $doc -> saveXML()); diff --git a/autoload/admin/factory/class.ShopTransport.php b/autoload/admin/factory/class.ShopTransport.php deleted file mode 100644 index 5c00ae2..0000000 --- a/autoload/admin/factory/class.ShopTransport.php +++ /dev/null @@ -1,10 +0,0 @@ -lowestTransportPrice($wp); - } -} diff --git a/autoload/shop/class.Producer.php b/autoload/shop/class.Producer.php index 16315b9..430ff82 100644 --- a/autoload/shop/class.Producer.php +++ b/autoload/shop/class.Producer.php @@ -1,4 +1,4 @@ - get( 'pp_shop_producer', '*', [ 'id' => $producer_id ] ); - foreach ( $result as $key => $val ) - $this -> $key = $val; + $repo = new \Domain\Producer\ProducerRepository( $mdb ); + $data = $repo->find( $producer_id ); - $rows = $mdb -> select( 'pp_shop_producer_lang', '*', [ 'producer_id' => $producer_id ] ); - foreach ( $rows as $row ) - { - $languages[ $row['lang_id'] ]['description'] = $row['description']; - $languages[ $row['lang_id'] ]['data'] = $row['data']; - $languages[ $row['lang_id'] ]['meta_title'] = $row['meta_title']; - } - - $this -> languages = $languages; + foreach ( $data as $key => $val ) + $this->$key = $val; } static public function producer_products( $producer_id, $lang_id, $bs ) { global $mdb; - $count = $mdb -> count( 'pp_shop_products', [ 'AND' => [ 'producer_id' => $producer_id, 'status' => 1 ] ] ); - $ls = ceil( $count / 12 ); - - if ( $bs < 1 ) - $bs = 1; - else if ( $bs > $ls ) - $bs = $ls; - - $from = 12 * ( $bs - 1 ); - - if ( $from < 0 ) - $from = 0; - - $results['products'] = $mdb -> select( 'pp_shop_products', 'id', [ 'AND' => [ 'producer_id' => $producer_id, 'status' => 1 ], 'LIMIT' => [ $from, 12 ] ] ); - - $results['ls'] = $ls; - - return $results; + $repo = new \Domain\Producer\ProducerRepository( $mdb ); + return $repo->producerProducts( (int) $producer_id, 12, (int) $bs ); } public function __get( $variable ) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 354840a..b952091 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,6 +4,23 @@ Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze. --- +## ver. 0.273 (2026-02-15) - ShopProducer + +- **ShopProducer** - migracja `/admin/shop_producer` na Domain + DI + nowe widoki + - NOWE: `Domain\Producer\ProducerRepository` (`listForAdmin`, `find`, `save`, `delete`, `allProducers`, `findForFrontend`, `producerProducts`, `allActiveIds`) + - NOWE: `admin\Controllers\ShopProducerController` (DI) z akcjami `list`, `edit`, `save`, `delete` + - UPDATE: modul `/admin/shop_producer/*` dziala na `components/table-list` i `components/form-edit` z zakladkami jezykowymi (Opis + SEO) + - UPDATE: routing i menu admin na kanoniczny URL `/admin/shop_producer/list/` + - UPDATE: `shop\Producer` przepiety na fasade do `Domain\Producer\ProducerRepository` + - UPDATE: `admin\factory\ShopProduct` - 2 wywolania `admin\factory\ShopTransport` przepiete na `Domain\Transport\TransportRepository` + - UPDATE: `admin\controls\ShopProduct` - usuniety fallback do `admin\factory\Layouts` + - CLEANUP: usuniete legacy `autoload/admin/controls/class.ShopProducer.php`, `admin/templates/shop-producer/list.php`, `admin/templates/shop-producer/edit.php` + - CLEANUP: usuniete 6 pustych factory facades: `admin\factory\Languages`, `admin\factory\Newsletter`, `admin\factory\Scontainers`, `admin\factory\ShopProducer`, `admin\factory\ShopTransport`, `admin\factory\Layouts` + - TEST: dodane `tests/Unit/Domain/Producer/ProducerRepositoryTest.php` i `tests/Unit/admin/Controllers/ShopProducerControllerTest.php` +- Testy: **OK (338 tests, 1063 assertions)** + +--- + ## ver. 0.272 (2026-02-15) - ShopProductSets - **ShopProductSets** - migracja `/admin/shop_product_sets` na Domain + DI + nowe widoki diff --git a/docs/DATABASE_STRUCTURE.md b/docs/DATABASE_STRUCTURE.md index 5ca1d44..69bdb6f 100644 --- a/docs/DATABASE_STRUCTURE.md +++ b/docs/DATABASE_STRUCTURE.md @@ -199,7 +199,7 @@ Jezyki panelu i frontendu. | start | 1 = domyslny jezyk | | o | Kolejnosc | -**Uzywane w:** `Domain\\Languages\\LanguagesRepository`, `admin\\Controllers\\LanguagesController`, `admin\\factory\\Languages`, `front\\factory\\Languages` +**Uzywane w:** `Domain\\Languages\\LanguagesRepository`, `admin\\Controllers\\LanguagesController`, `front\\factory\\Languages` ## pp_langs_translations Slownik tlumaczen panelu/frontendu. @@ -462,7 +462,7 @@ Rodzaje transportu sklepu (modul `/admin/shop_transport`). | apilo_carrier_account_id | ID konta przewoznika w Apilo (NULL gdy brak mapowania) | | o | Kolejnosc wyswietlania | -**Uzywane w:** `Domain\Transport\TransportRepository`, `admin\Controllers\ShopTransportController`, `front\factory\ShopTransport`, `admin\factory\ShopTransport` +**Uzywane w:** `Domain\Transport\TransportRepository`, `admin\Controllers\ShopTransportController`, `front\factory\ShopTransport` ## pp_shop_transport_payment_methods Powiazanie metod transportu z metodami platnosci (tabela lacznikowa). @@ -540,3 +540,31 @@ Powiazanie kompletow z produktami (tabela lacznikowa). **Uzywane w:** `Domain\ProductSet\ProductSetRepository`, `shop\Product`, `front\factory\ShopProduct`, `admin\factory\ShopProduct` **Aktualizacja 2026-02-15 (ver. 0.272):** modul `/admin/shop_product_sets` korzysta z `Domain\ProductSet\ProductSetRepository` przez `admin\Controllers\ShopProductSetsController`. Usunieto legacy klasy `admin\controls\ShopProductSets` i `admin\factory\ShopProductSet`. `shop\ProductSet` dziala jako fasada do repozytorium. + +## pp_shop_producer +Producenci produktow (modul `/admin/shop_producer`). + +| Kolumna | Opis | +|---------|------| +| id | PK | +| name | Nazwa producenta | +| status | Status: 1 = aktywny, 0 = nieaktywny | +| img | Sciezka do logo producenta (NULL gdy brak) | + +**Uzywane w:** `Domain\Producer\ProducerRepository`, `admin\Controllers\ShopProducerController`, `shop\Producer`, `shop\Product`, `front\controls\ShopProducer` + +## pp_shop_producer_lang +Tlumaczenia producentow (per jezyk). FK kaskadowe ON DELETE CASCADE. + +| Kolumna | Opis | +|---------|------| +| id | PK | +| producer_id | FK do pp_shop_producer | +| lang_id | ID jezyka (np. pl, en) | +| description | Opis producenta (TEXT) | +| data | Dane producenta (TEXT, HTML) | +| meta_title | Meta title SEO (VARCHAR 255) | + +**Uzywane w:** `Domain\Producer\ProducerRepository`, `shop\Producer`, `shop\Product` + +**Aktualizacja 2026-02-15 (ver. 0.273):** modul `/admin/shop_producer` korzysta z `Domain\Producer\ProducerRepository` przez `admin\Controllers\ShopProducerController`. Usunieto legacy `admin\controls\ShopProducer` i `admin\factory\ShopProducer`. `shop\Producer` dziala jako fasada do repozytorium. diff --git a/docs/PROJECT_STRUCTURE.md b/docs/PROJECT_STRUCTURE.md index 915fe3c..88e05bc 100644 --- a/docs/PROJECT_STRUCTURE.md +++ b/docs/PROJECT_STRUCTURE.md @@ -222,6 +222,8 @@ autoload/ │ │ └── TransportRepository.php │ ├── ProductSet/ │ │ └── ProductSetRepository.php +│ ├── Producer/ +│ │ └── ProducerRepository.php │ └── ... ├── admin/ │ ├── Controllers/ # Nowe kontrolery (namespace \admin\Controllers\) @@ -294,5 +296,16 @@ Pelna dokumentacja testow: `TESTING.md` - Usunieto legacy: `autoload/admin/controls/class.ShopProductSets.php`, `autoload/admin/factory/class.ShopProductSet.php`, `admin/templates/shop-product-sets/view-list.php`, `admin/templates/shop-product-sets/set-edit.php`. - `shop\ProductSet` przepiety na fasade do `Domain\ProductSet\ProductSetRepository`. +## Dodatkowa aktualizacja 2026-02-15 (ver. 0.273) +- Dodano modul domenowy `Domain/Producer/ProducerRepository.php`. +- Dodano kontroler DI `admin/Controllers/ShopProducerController.php`. +- Modul `/admin/shop_producer/*` dziala na nowych widokach (`producers-list`, `producer-edit`). +- Usunieto legacy: `autoload/admin/controls/class.ShopProducer.php`, `admin/templates/shop-producer/list.php`, `admin/templates/shop-producer/edit.php`. +- `shop\Producer` przepiety na fasade do `Domain\Producer\ProducerRepository`. +- `admin\controls\ShopProduct` uzywa `ProducerRepository::allProducers()`. +- Usunieto 6 pustych factory facades: `admin\factory\Languages`, `admin\factory\Newsletter`, `admin\factory\Scontainers`, `admin\factory\ShopProducer`, `admin\factory\ShopTransport`, `admin\factory\Layouts`. +- Przepieto 2 wywolania `admin\factory\ShopTransport` w `admin\factory\ShopProduct` na `Domain\Transport\TransportRepository`. +- Usuniety fallback do `admin\factory\Layouts` w `admin\controls\ShopProduct`. + --- *Dokument aktualizowany: 2026-02-15* diff --git a/docs/REFACTORING_PLAN.md b/docs/REFACTORING_PLAN.md index 564c71b..f8125f2 100644 --- a/docs/REFACTORING_PLAN.md +++ b/docs/REFACTORING_PLAN.md @@ -152,6 +152,7 @@ grep -r "Product::getQuantity" . | 21 | ShopTransport | 0.269 | listForAdmin, find, save, allActive, allForAdmin, findActiveById, getTransportCost, lowestTransportPrice, getApiloCarrierAccountId, powiazanie z PaymentMethod, DI kontroler | | 22 | ShopAttribute | 0.271 | list/edit/save/delete/values, nowy edytor wartosci, cleanup legacy, przepiecie zaleznosci kombinacji | | 23 | ShopProductSets | 0.272 | listForAdmin, find, save, delete, allSets, allProductsMap, multi-select Selectize, DI kontroler | +| 24 | ShopProducer | 0.273 | listForAdmin, find, save, delete, allProducers, producerProducts, fasada shop\Producer, DI kontroler | ### Product - szczegolowy status - ✅ getQuantity (ver. 0.238) @@ -169,11 +170,11 @@ grep -r "Product::getQuantity" . ## Kolejność refaktoryzacji (priorytet) -1-23: ✅ Cache, Product, Banner, Settings, Dictionaries, ProductArchive, Filemanager, Users, Pages, Integrations, ShopPromotion, ShopCoupon, ShopStatuses, ShopPaymentMethod, ShopTransport, ShopAttribute, ShopProductSets +1-24: ✅ Cache, Product, Banner, Settings, Dictionaries, ProductArchive, Filemanager, Users, Pages, Integrations, ShopPromotion, ShopCoupon, ShopStatuses, ShopPaymentMethod, ShopTransport, ShopAttribute, ShopProductSets, ShopProducer Nastepne: -24. **Order** -25. **Category** +25. **Order** +26. **Category** ## Form Edit System @@ -251,7 +252,9 @@ tests/ │ │ ├── Dictionaries/DictionariesRepositoryTest.php │ │ ├── Integrations/IntegrationsRepositoryTest.php │ │ ├── PaymentMethod/PaymentMethodRepositoryTest.php +│ │ ├── Producer/ProducerRepositoryTest.php │ │ ├── Product/ProductRepositoryTest.php +│ │ ├── ProductSet/ProductSetRepositoryTest.php │ │ ├── Promotion/PromotionRepositoryTest.php │ │ ├── Settings/SettingsRepositoryTest.php │ │ ├── ShopStatus/ShopStatusRepositoryTest.php @@ -265,12 +268,18 @@ tests/ │ ├── SettingsControllerTest.php │ ├── ShopCouponControllerTest.php │ ├── ShopPaymentMethodControllerTest.php +│ ├── ShopProducerControllerTest.php +│ ├── ShopProductSetsControllerTest.php │ ├── ShopPromotionControllerTest.php │ ├── ShopStatusesControllerTest.php │ └── UsersControllerTest.php └── Integration/ ``` -**Lacznie: 312 testow, 948 asercji** +**Lacznie: 338 testow, 1063 asercji** + +Aktualizacja 2026-02-15 (ver. 0.273): +- dodano testy `tests/Unit/Domain/Producer/ProducerRepositoryTest.php` +- dodano testy `tests/Unit/admin/Controllers/ShopProducerControllerTest.php` Aktualizacja 2026-02-14 (ver. 0.271): - dodano testy `tests/Unit/Domain/Attribute/AttributeRepositoryTest.php` diff --git a/docs/TESTING.md b/docs/TESTING.md index 5489e5e..07f5d6b 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -36,7 +36,7 @@ Alternatywnie (Git Bash): Ostatnio zweryfikowano: 2026-02-15 ```text -OK (324 tests, 1000 assertions) +OK (338 tests, 1063 assertions) ``` ## Struktura testow @@ -54,6 +54,7 @@ tests/ | | |-- Dictionaries/DictionariesRepositoryTest.php | | |-- Integrations/IntegrationsRepositoryTest.php | | |-- PaymentMethod/PaymentMethodRepositoryTest.php +| | |-- Producer/ProducerRepositoryTest.php | | |-- Product/ProductRepositoryTest.php | | |-- ProductSet/ProductSetRepositoryTest.php | | |-- Promotion/PromotionRepositoryTest.php @@ -71,6 +72,7 @@ tests/ | |-- ShopAttributeControllerTest.php | |-- ShopCouponControllerTest.php | |-- ShopPaymentMethodControllerTest.php +| |-- ShopProducerControllerTest.php | |-- ShopProductSetsControllerTest.php | |-- ShopPromotionControllerTest.php | |-- ShopStatusesControllerTest.php @@ -411,3 +413,14 @@ OK (324 tests, 1000 assertions) Nowe testy dodane 2026-02-15: - `tests/Unit/Domain/ProductSet/ProductSetRepositoryTest.php` (7 testow: find default/normalize, save insert/update, delete invalid, whitelist sortowania/paginacji, allSets) - `tests/Unit/admin/Controllers/ShopProductSetsControllerTest.php` (5 testow: kontrakty metod, aliasy legacy, return types, DI konstruktora) + +## Aktualizacja suite (ShopProducer refactor, ver. 0.273) +Ostatnio zweryfikowano: 2026-02-15 + +```text +OK (338 tests, 1063 assertions) +``` + +Nowe testy dodane 2026-02-15: +- `tests/Unit/Domain/Producer/ProducerRepositoryTest.php` (9 testow: find default/normalize, save insert/update, delete invalid/success, whitelist sortowania/paginacji, allProducers, producerProducts) +- `tests/Unit/admin/Controllers/ShopProducerControllerTest.php` (5 testow: kontrakty metod, aliasy legacy, return types, DI konstruktora) diff --git a/temp/update_build/delete_files_0.273.txt b/temp/update_build/delete_files_0.273.txt new file mode 100644 index 0000000..2f1a8af --- /dev/null +++ b/temp/update_build/delete_files_0.273.txt @@ -0,0 +1,9 @@ +admin/templates/shop-producer/edit.php +admin/templates/shop-producer/list.php +autoload/admin/controls/class.ShopProducer.php +autoload/admin/factory/class.Languages.php +autoload/admin/factory/class.Layouts.php +autoload/admin/factory/class.Newsletter.php +autoload/admin/factory/class.Scontainers.php +autoload/admin/factory/class.ShopProducer.php +autoload/admin/factory/class.ShopTransport.php diff --git a/temp/update_build/update_0.273.zip b/temp/update_build/update_0.273.zip new file mode 100644 index 0000000000000000000000000000000000000000..b277f343642c976c854bc0e9aa6d067581fd25a1 GIT binary patch literal 45279 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\medoo::class); + $repository = new ProducerRepository($mockDb); + + $result = $repository->find(0); + + $this->assertIsArray($result); + $this->assertSame(0, $result['id']); + $this->assertSame('', $result['name']); + $this->assertSame(1, $result['status']); + $this->assertNull($result['img']); + $this->assertSame([], $result['languages']); + } + + public function testFindNormalizesProducerData(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->with('pp_shop_producer', '*', ['id' => 5]) + ->willReturn([ + 'id' => '5', + 'name' => 'Apple', + 'status' => '1', + 'img' => '/logo.png', + ]); + + $mockDb->expects($this->once()) + ->method('select') + ->with('pp_shop_producer_lang', '*', ['producer_id' => 5]) + ->willReturn([ + [ + 'lang_id' => 'pl', + 'description' => 'Opis PL', + 'data' => 'Dane PL', + 'meta_title' => 'Meta PL', + ], + [ + 'lang_id' => 'en', + 'description' => 'Desc EN', + 'data' => null, + 'meta_title' => null, + ], + ]); + + $repository = new ProducerRepository($mockDb); + $result = $repository->find(5); + + $this->assertSame(5, $result['id']); + $this->assertSame('Apple', $result['name']); + $this->assertSame(1, $result['status']); + $this->assertSame('/logo.png', $result['img']); + $this->assertArrayHasKey('pl', $result['languages']); + $this->assertArrayHasKey('en', $result['languages']); + $this->assertSame('Opis PL', $result['languages']['pl']['description']); + $this->assertSame('Meta PL', $result['languages']['pl']['meta_title']); + $this->assertNull($result['languages']['en']['meta_title']); + } + + public function testSaveInsertsNewProducer(): void + { + $mockDb = $this->createMock(\medoo::class); + $insertCalls = []; + + $mockDb->method('insert') + ->willReturnCallback(function ($table, $row) use (&$insertCalls) { + $insertCalls[] = ['table' => $table, 'row' => $row]; + }); + + $mockDb->expects($this->once()) + ->method('id') + ->willReturn(42); + + $langs = [['id' => 'pl'], ['id' => 'en']]; + + $repository = new ProducerRepository($mockDb); + $id = $repository->save( + 0, 'Samsung', 1, '/samsung.png', + ['pl' => 'Opis PL', 'en' => 'Desc EN'], + ['pl' => 'Dane', 'en' => null], + ['pl' => 'Meta PL', 'en' => 'Meta EN'], + $langs + ); + + $this->assertSame(42, $id); + + // 1st insert: pp_shop_producer + $this->assertSame('pp_shop_producer', $insertCalls[0]['table']); + $this->assertSame('Samsung', $insertCalls[0]['row']['name']); + $this->assertSame(1, $insertCalls[0]['row']['status']); + + // 2nd and 3rd insert: pp_shop_producer_lang for each language + $langInserts = array_filter($insertCalls, fn($c) => $c['table'] === 'pp_shop_producer_lang'); + $this->assertCount(2, $langInserts); + } + + public function testSaveUpdatesExistingProducer(): void + { + $mockDb = $this->createMock(\medoo::class); + $updateRow = null; + + $mockDb->expects($this->atLeastOnce()) + ->method('update') + ->willReturnCallback(function ($table, $row, $where) use (&$updateRow) { + if ($table === 'pp_shop_producer') { + $updateRow = $row; + $this->assertSame(['id' => 7], $where); + } + return true; + }); + + $mockDb->expects($this->never())->method('id'); + + // get for translation id check + $mockDb->method('get') + ->willReturn(100); + + $langs = [['id' => 'pl']]; + + $repository = new ProducerRepository($mockDb); + $id = $repository->save(7, 'Zaktualizowany', 0, null, ['pl' => 'Nowy opis'], ['pl' => null], ['pl' => 'Meta'], $langs); + + $this->assertSame(7, $id); + $this->assertSame('Zaktualizowany', $updateRow['name']); + $this->assertSame(0, $updateRow['status']); + } + + public function testDeleteReturnsFalseForInvalidId(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('delete'); + + $repository = new ProducerRepository($mockDb); + $this->assertFalse($repository->delete(0)); + } + + public function testDeleteReturnsTrueOnSuccess(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('delete') + ->with('pp_shop_producer', ['id' => 3]) + ->willReturn(true); + + $repository = new ProducerRepository($mockDb); + $this->assertTrue($repository->delete(3)); + } + + public function testListForAdminWhitelistsSortAndPagination(): void + { + $mockDb = $this->createMock(\medoo::class); + $queries = []; + + $mockDb->method('query') + ->willReturnCallback(function ($sql, $params = []) use (&$queries) { + $queries[] = ['sql' => $sql, 'params' => $params]; + + if (strpos($sql, 'COUNT(0)') !== false) { + return new class { + public function fetchAll() { return [[1]]; } + }; + } + + return new class { + public function fetchAll() { + return [[ + 'id' => 1, + 'name' => 'Test', + 'status' => 1, + 'img' => null, + ]]; + } + }; + }); + + $repository = new ProducerRepository($mockDb); + $result = $repository->listForAdmin( + [], + 'name DESC; DROP TABLE pp_shop_producer; --', + 'DESC; DELETE FROM pp_users; --', + 1, + 999 + ); + + $this->assertCount(2, $queries); + $dataSql = $queries[1]['sql']; + + $this->assertMatchesRegularExpression('/ORDER BY\s+p\.name\s+ASC,\s+p\.id\s+DESC/i', $dataSql); + $this->assertStringNotContainsString('DROP TABLE', $dataSql); + $this->assertMatchesRegularExpression('/LIMIT\s+100\s+OFFSET\s+0/i', $dataSql); + } + + public function testAllProducersReturnsFormattedList(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('select') + ->with('pp_shop_producer', ['id', 'name'], ['ORDER' => ['name' => 'ASC']]) + ->willReturn([ + ['id' => '1', 'name' => 'Apple'], + ['id' => '2', 'name' => 'Samsung'], + ]); + + $repository = new ProducerRepository($mockDb); + $result = $repository->allProducers(); + + $this->assertCount(2, $result); + $this->assertSame(1, $result[0]['id']); + $this->assertSame('Apple', $result[0]['name']); + $this->assertSame(2, $result[1]['id']); + } + + public function testProducerProductsReturnsPaginatedResults(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('count') + ->willReturn(30); + + $mockDb->expects($this->once()) + ->method('select') + ->willReturn([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + + $repository = new ProducerRepository($mockDb); + $result = $repository->producerProducts(5, 12, 1); + + $this->assertArrayHasKey('products', $result); + $this->assertArrayHasKey('ls', $result); + $this->assertSame(3, $result['ls']); + } +} diff --git a/tests/Unit/admin/Controllers/ShopProducerControllerTest.php b/tests/Unit/admin/Controllers/ShopProducerControllerTest.php new file mode 100644 index 0000000..582a0ab --- /dev/null +++ b/tests/Unit/admin/Controllers/ShopProducerControllerTest.php @@ -0,0 +1,67 @@ +repository = $this->createMock(ProducerRepository::class); + $this->languagesRepository = $this->createMock(LanguagesRepository::class); + $this->controller = new ShopProducerController($this->repository, $this->languagesRepository); + } + + public function testConstructorAcceptsRepositories(): void + { + $controller = new ShopProducerController($this->repository, $this->languagesRepository); + $this->assertInstanceOf(ShopProducerController::class, $controller); + } + + public function testHasMainActionMethods(): void + { + $this->assertTrue(method_exists($this->controller, 'list')); + $this->assertTrue(method_exists($this->controller, 'edit')); + $this->assertTrue(method_exists($this->controller, 'save')); + $this->assertTrue(method_exists($this->controller, 'delete')); + } + + public function testHasLegacyAliasMethods(): void + { + $this->assertTrue(method_exists($this->controller, 'view_list')); + $this->assertTrue(method_exists($this->controller, 'producer_edit')); + $this->assertTrue(method_exists($this->controller, 'producer_save')); + $this->assertTrue(method_exists($this->controller, 'producer_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('edit')->getReturnType()); + $this->assertEquals('string', (string)$reflection->getMethod('producer_edit')->getReturnType()); + $this->assertEquals('void', (string)$reflection->getMethod('save')->getReturnType()); + $this->assertEquals('void', (string)$reflection->getMethod('delete')->getReturnType()); + $this->assertEquals('void', (string)$reflection->getMethod('producer_delete')->getReturnType()); + } + + public function testConstructorRequiresBothRepositories(): void + { + $reflection = new \ReflectionClass(ShopProducerController::class); + $constructor = $reflection->getConstructor(); + $params = $constructor->getParameters(); + + $this->assertCount(2, $params); + $this->assertEquals('Domain\Producer\ProducerRepository', $params[0]->getType()->getName()); + $this->assertEquals('Domain\Languages\LanguagesRepository', $params[1]->getType()->getName()); + } +}