From fe51a1f4c4a246393a6d990bfb493f096e03cab0 Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Sun, 15 Feb 2026 10:21:29 +0100 Subject: [PATCH] ver. 0.272 - ShopProductSets refactor + update package Co-Authored-By: Claude Opus 4.6 --- .../product-set-edit-custom-script.php | 64 ++++ .../shop-product-sets/product-set-edit.php | 2 + .../shop-product-sets/product-sets-list.php | 1 + .../templates/shop-product-sets/set-edit.php | 84 ----- .../templates/shop-product-sets/view-list.php | 76 ---- admin/templates/site/main-layout.php | 2 +- .../ProductSet/ProductSetRepository.php | 250 +++++++++++++ .../Controllers/ShopProductSetsController.php | 328 ++++++++++++++++++ autoload/admin/class.Site.php | 7 + .../admin/controls/class.ShopProductSets.php | 43 --- .../admin/factory/class.ShopProductSet.php | 68 ---- autoload/shop/class.ProductSet.php | 28 +- docs/CHANGELOG.md | 14 + docs/DATABASE_STRUCTURE.md | 24 ++ docs/PROJECT_STRUCTURE.md | 11 +- docs/REFACTORING_PLAN.md | 9 +- docs/TESTING.md | 17 +- .../ProductSet/ProductSetRepositoryTest.php | 185 ++++++++++ .../ShopProductSetsControllerTest.php | 62 ++++ updates/0.20/ver_0.272.zip | Bin 0 -> 13003 bytes updates/0.20/ver_0.272_files.txt | 4 + updates/changelog.php | 9 + updates/versions.php | 2 +- 23 files changed, 993 insertions(+), 297 deletions(-) create mode 100644 admin/templates/shop-product-sets/product-set-edit-custom-script.php create mode 100644 admin/templates/shop-product-sets/product-set-edit.php create mode 100644 admin/templates/shop-product-sets/product-sets-list.php delete mode 100644 admin/templates/shop-product-sets/set-edit.php delete mode 100644 admin/templates/shop-product-sets/view-list.php create mode 100644 autoload/Domain/ProductSet/ProductSetRepository.php create mode 100644 autoload/admin/Controllers/ShopProductSetsController.php delete mode 100644 autoload/admin/controls/class.ShopProductSets.php delete mode 100644 autoload/admin/factory/class.ShopProductSet.php create mode 100644 tests/Unit/Domain/ProductSet/ProductSetRepositoryTest.php create mode 100644 tests/Unit/admin/Controllers/ShopProductSetsControllerTest.php create mode 100644 updates/0.20/ver_0.272.zip create mode 100644 updates/0.20/ver_0.272_files.txt diff --git a/admin/templates/shop-product-sets/product-set-edit-custom-script.php b/admin/templates/shop-product-sets/product-set-edit-custom-script.php new file mode 100644 index 0000000..23d8702 --- /dev/null +++ b/admin/templates/shop-product-sets/product-set-edit-custom-script.php @@ -0,0 +1,64 @@ + + + + diff --git a/admin/templates/shop-product-sets/product-set-edit.php b/admin/templates/shop-product-sets/product-set-edit.php new file mode 100644 index 0000000..557b0ac --- /dev/null +++ b/admin/templates/shop-product-sets/product-set-edit.php @@ -0,0 +1,2 @@ + $this->form]); ?> + diff --git a/admin/templates/shop-product-sets/product-sets-list.php b/admin/templates/shop-product-sets/product-sets-list.php new file mode 100644 index 0000000..0f89c5b --- /dev/null +++ b/admin/templates/shop-product-sets/product-sets-list.php @@ -0,0 +1 @@ + $this->viewModel]); ?> diff --git a/admin/templates/shop-product-sets/set-edit.php b/admin/templates/shop-product-sets/set-edit.php deleted file mode 100644 index 4dafbe4..0000000 --- a/admin/templates/shop-product-sets/set-edit.php +++ /dev/null @@ -1,84 +0,0 @@ - - -
-
    -
  • Ustawienia
  • -
-
-
- 'Nazwa', - 'name' => 'name', - 'id' => 'name', - 'value' => $this -> set[ 'name' ], - ] ); - ?> - 'Aktywny', - 'name' => 'status', - 'checked' => $this -> set[ 'status' ] == 1 ? true : false - ] ); - ?> -
- -
- -
-
-
-
-
- id = 'set-edit'; -$grid -> gdb_opt = $gdb; -$grid -> include_plugins = true; -$grid -> title = 'Edycja kompletu produktów: ' . '' . ''; -$grid -> fields = [ - [ - 'db' => 'id', - 'type' => 'hidden', - 'value' => $this -> set[ 'id' ] - ] -]; -$grid -> actions = [ - 'save' => [ 'url' => '/admin/shop_product_sets/save/', 'back_url' => '/admin/shop_product_sets/view_list/' ], - 'cancel' => [ 'url' => '/admin/shop_product_sets/view_list/' ] -]; -$grid -> external_code = $out; -$grid -> persist_edit = true; -$grid -> id_param = 'id'; - -echo $grid -> draw(); -?> - - - - \ No newline at end of file diff --git a/admin/templates/shop-product-sets/view-list.php b/admin/templates/shop-product-sets/view-list.php deleted file mode 100644 index f86fb81..0000000 --- a/admin/templates/shop-product-sets/view-list.php +++ /dev/null @@ -1,76 +0,0 @@ - gdb_opt = $gdb; -$grid -> limit = 10; -$grid -> sql = 'SELECT *' - . 'FROM ( ' - . 'SELECT ' - . 'id, name, status ' - . 'FROM ' - . 'pp_shop_product_sets AS psps ' - . ') AS q1 ' - . 'WHERE ' - . '1=1 [where] ' - . 'ORDER BY ' - . '[order_p1] [order_p2]'; -$grid -> sql_count = 'SELECT ' - . 'COUNT(0) FROM ( ' - . 'SELECT ' - . 'id, name, status ' - . 'FROM ' - . 'pp_shop_product_sets AS psps ' - . ') 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 "" . htmlspecialchars( \'[name]\' ) . "";' - ], - [ - '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_product_sets/set_edit/id=[id]' ], - 'th' => [ 'class' => 'g-center', 'style' => 'width: 70px;' ], - 'td' => [ 'class' => 'g-center' ] - ], - [ - 'name' => 'Usuń', - 'action' => [ 'type' => 'delete', 'url' => '/admin/shop_product_sets/set_delete/id=[id]' ], - 'th' => [ 'class' => 'g-center', 'style' => 'width: 70px;' ], - 'td' => [ 'class' => 'g-center' ] - ] - ]; -$grid -> buttons = [ - [ - 'label' => 'Dodaj komplet produktów', - 'url' => '/admin/shop_product_sets/set_edit/', - 'icon' => 'fa-plus-circle', - 'class' => 'btn-success' - ] - ]; -echo $grid -> draw(); -?> \ No newline at end of file diff --git a/admin/templates/site/main-layout.php b/admin/templates/site/main-layout.php index 09abc89..ef7f00f 100644 --- a/admin/templates/site/main-layout.php +++ b/admin/templates/site/main-layout.php @@ -65,7 +65,7 @@
  • - + Komplety Produktów
  • diff --git a/autoload/Domain/ProductSet/ProductSetRepository.php b/autoload/Domain/ProductSet/ProductSetRepository.php new file mode 100644 index 0000000..0290430 --- /dev/null +++ b/autoload/Domain/ProductSet/ProductSetRepository.php @@ -0,0 +1,250 @@ +db = $db; + } + + /** + * @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' => 'ps.id', + 'name' => 'ps.name', + 'status' => 'ps.status', + ]; + + $sortSql = $allowedSortColumns[$sortColumn] ?? 'ps.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[] = 'ps.name LIKE :name'; + $params[':name'] = '%' . $name . '%'; + } + + $status = trim((string)($filters['status'] ?? '')); + if ($status === '0' || $status === '1') { + $where[] = 'ps.status = :status'; + $params[':status'] = (int)$status; + } + + $whereSql = implode(' AND ', $where); + + $sqlCount = " + SELECT COUNT(0) + FROM pp_shop_product_sets AS ps + WHERE {$whereSql} + "; + + $stmtCount = $this->db->query($sqlCount, $params); + $countRows = $stmtCount ? $stmtCount->fetchAll() : []; + $total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0; + + $sql = " + SELECT + ps.id, + ps.name, + ps.status + FROM pp_shop_product_sets AS ps + WHERE {$whereSql} + ORDER BY {$sortSql} {$sortDir}, ps.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, + ]; + } + + public function find(int $id): array + { + if ($id <= 0) { + return $this->defaultSet(); + } + + $set = $this->db->get('pp_shop_product_sets', '*', ['id' => $id]); + if (!is_array($set)) { + return $this->defaultSet(); + } + + $set['id'] = (int)($set['id'] ?? 0); + $set['status'] = $this->toSwitchValue($set['status'] ?? 0); + + $products = $this->db->select('pp_shop_product_sets_products', 'product_id', ['set_id' => $id]); + $set['products'] = is_array($products) ? array_map('intval', $products) : []; + + return $set; + } + + public function save(int $id, string $name, int $status, array $productIds): ?int + { + $row = [ + 'name' => trim($name), + 'status' => $status === 1 ? 1 : 0, + ]; + + if ($id <= 0) { + $this->db->insert('pp_shop_product_sets', $row); + $id = (int)$this->db->id(); + if ($id <= 0) { + return null; + } + } else { + $this->db->update('pp_shop_product_sets', $row, ['id' => $id]); + } + + $this->syncProducts($id, $productIds); + $this->clearTempAndCache(); + + return $id; + } + + public function delete(int $id): bool + { + if ($id <= 0) { + return false; + } + + $this->db->delete('pp_shop_product_sets_products', ['set_id' => $id]); + $result = (bool)$this->db->delete('pp_shop_product_sets', ['id' => $id]); + + if ($result) { + $this->clearTempAndCache(); + } + + return $result; + } + + /** + * @return array + */ + public function allSets(): array + { + $rows = $this->db->select('pp_shop_product_sets', ['id', 'name'], ['ORDER' => ['name' => 'ASC']]); + if (!is_array($rows)) { + return []; + } + + $sets = []; + foreach ($rows as $row) { + $sets[] = [ + 'id' => (int)($row['id'] ?? 0), + 'name' => (string)($row['name'] ?? ''), + ]; + } + + return $sets; + } + + /** + * @return array + */ + public function allProductsMap(): array + { + $rows = $this->db->select('pp_shop_products', 'id', ['parent_id' => null]); + if (!is_array($rows)) { + return []; + } + + $products = []; + foreach ($rows as $productId) { + $name = $this->db->get('pp_shop_products_langs', 'name', [ + 'AND' => ['product_id' => $productId, 'lang_id' => 'pl'], + ]); + if ($name) { + $products[(int)$productId] = (string)$name; + } + } + + return $products; + } + + private function syncProducts(int $setId, array $productIds): void + { + $this->db->delete('pp_shop_product_sets_products', ['set_id' => $setId]); + + $seen = []; + foreach ($productIds as $productId) { + $pid = (int)$productId; + if ($pid > 0 && !isset($seen[$pid])) { + $this->db->insert('pp_shop_product_sets_products', [ + 'set_id' => $setId, + 'product_id' => $pid, + ]); + $seen[$pid] = true; + } + } + } + + private function defaultSet(): array + { + return [ + 'id' => 0, + 'name' => '', + 'status' => 1, + 'products' => [], + ]; + } + + 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; + } + + private function clearTempAndCache(): void + { + \S::delete_dir('../temp/'); + \S::delete_dir('../thumbs/'); + } +} diff --git a/autoload/admin/Controllers/ShopProductSetsController.php b/autoload/admin/Controllers/ShopProductSetsController.php new file mode 100644 index 0000000..b963a09 --- /dev/null +++ b/autoload/admin/Controllers/ShopProductSetsController.php @@ -0,0 +1,328 @@ +repository = $repository; + } + + 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); + + $rows[] = [ + 'lp' => $lp++ . '.', + 'name' => '' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . '', + 'status' => $status === 1 ? 'tak' : 'nie', + '_actions' => [ + [ + 'label' => 'Edytuj', + 'url' => '/admin/shop_product_sets/edit/id=' . $id, + 'class' => 'btn btn-xs btn-primary', + ], + [ + 'label' => 'Usun', + 'url' => '/admin/shop_product_sets/delete/id=' . $id, + 'class' => 'btn btn-xs btn-danger', + 'confirm' => 'Na pewno chcesz usunac wybrany komplet produktow?', + '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' => '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_product_sets/list/', + 'Brak danych w tabeli.', + '/admin/shop_product_sets/edit/', + 'Dodaj komplet produktow' + ); + + return \Tpl::view('shop-product-sets/product-sets-list', [ + 'viewModel' => $viewModel, + ]); + } + + public function view_list(): string + { + return $this->list(); + } + + public function edit(): string + { + $set = $this->repository->find((int)\S::get('id')); + $products = $this->repository->allProductsMap(); + + return \Tpl::view('shop-product-sets/product-set-edit', [ + 'form' => $this->buildFormViewModel($set, $products), + ]); + } + + public function set_edit(): string + { + return $this->edit(); + } + + public function save(): void + { + $legacyValues = \S::get('values'); + + if ($legacyValues) { + $values = json_decode((string)$legacyValues, true); + $response = [ + 'status' => 'error', + 'msg' => 'Podczas zapisywania kompletu produktow wystapil blad. Prosze sprobowac ponownie.', + ]; + + if (is_array($values)) { + $productIds = $values['set_products_id'] ?? []; + if (!is_array($productIds)) { + $productIds = $productIds ? [$productIds] : []; + } + + $id = $this->repository->save( + (int)($values['id'] ?? 0), + (string)($values['name'] ?? ''), + $this->toSwitchValue($values['status'] ?? 0), + $productIds + ); + + if (!empty($id)) { + $response = [ + 'status' => 'ok', + 'msg' => 'Komplet produktow zostal zapisany.', + 'id' => (int)$id, + ]; + } + } + + echo json_encode($response); + exit; + } + + $payload = $_POST; + if (empty($payload['id'])) { + $routeId = (int)\S::get('id'); + if ($routeId > 0) { + $payload['id'] = $routeId; + } + } + + $productIds = $payload['set_products_id'] ?? []; + if (!is_array($productIds)) { + $productIds = $productIds ? [$productIds] : []; + } + + $id = $this->repository->save( + (int)($payload['id'] ?? 0), + (string)($payload['name'] ?? ''), + $this->toSwitchValue($payload['status'] ?? 0), + $productIds + ); + + if (!empty($id)) { + echo json_encode([ + 'success' => true, + 'id' => (int)$id, + 'message' => 'Komplet produktow zostal zapisany.', + ]); + exit; + } + + echo json_encode([ + 'success' => false, + 'errors' => ['general' => 'Podczas zapisywania kompletu produktow wystapil blad.'], + ]); + exit; + } + + public function delete(): void + { + if ($this->repository->delete((int)\S::get('id'))) { + \S::alert('Komplet produktow zostal usuniety.'); + } + + header('Location: /admin/shop_product_sets/list/'); + exit; + } + + public function set_delete(): void + { + $this->delete(); + } + + private function buildFormViewModel(array $set, array $products = []): FormEditViewModel + { + $id = (int)($set['id'] ?? 0); + $isNew = $id <= 0; + $selectedProducts = $set['products'] ?? []; + + $data = [ + 'id' => $id, + 'name' => (string)($set['name'] ?? ''), + 'status' => (int)($set['status'] ?? 1), + ]; + + $productsSelectHtml = $this->renderProductsSelect($products, $selectedProducts); + + $fields = [ + FormField::hidden('id', $id), + FormField::text('name', [ + 'label' => 'Nazwa', + 'tab' => 'settings', + 'required' => true, + ]), + FormField::switch('status', [ + 'label' => 'Aktywny', + 'tab' => 'settings', + 'value' => true, + ]), + FormField::custom('set_products', $productsSelectHtml, [ + 'tab' => 'settings', + ]), + ]; + + $tabs = [ + new FormTab('settings', 'Ustawienia', 'fa-wrench'), + ]; + + $actionUrl = '/admin/shop_product_sets/save/' . ($isNew ? '' : ('id=' . $id)); + $actions = [ + FormAction::save($actionUrl, '/admin/shop_product_sets/list/'), + FormAction::cancel('/admin/shop_product_sets/list/'), + ]; + + return new FormEditViewModel( + 'shop-product-set-edit', + $isNew ? 'Nowy komplet produktow' : ('Edycja kompletu produktow: ' . (string)($set['name'] ?? '')), + $data, + $fields, + $tabs, + $actions, + 'POST', + $actionUrl, + '/admin/shop_product_sets/list/', + true, + ['id' => $id] + ); + } + + private function toSwitchValue($value): int + { + 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; + } + + private function renderProductsSelect(array $products, array $selectedProducts): string + { + $html = '
    '; + $html .= ''; + $html .= '
    '; + $html .= ''; + $html .= '
    '; + $html .= '
    '; + + return $html; + } +} diff --git a/autoload/admin/class.Site.php b/autoload/admin/class.Site.php index 2f47fd5..72e2da8 100644 --- a/autoload/admin/class.Site.php +++ b/autoload/admin/class.Site.php @@ -362,6 +362,13 @@ class Site new \Domain\ShopStatus\ShopStatusRepository( $mdb ) ); }, + 'ShopProductSets' => function() { + global $mdb; + + return new \admin\Controllers\ShopProductSetsController( + new \Domain\ProductSet\ProductSetRepository( $mdb ) + ); + }, ]; return self::$newControllers; diff --git a/autoload/admin/controls/class.ShopProductSets.php b/autoload/admin/controls/class.ShopProductSets.php deleted file mode 100644 index 757a2d5..0000000 --- a/autoload/admin/controls/class.ShopProductSets.php +++ /dev/null @@ -1,43 +0,0 @@ - 'error', 'msg' => 'Podczas zapisywania kompletu produktów wystąpił błąd. Proszę spróbować ponownie.' ]; - $values = json_decode( \S::get( 'values' ), true ); - - if ( $id = \admin\factory\ShopProductSet::save( - (int)$values['id'], $values['name'], (string) $values['status'], $values['set_products_id'] - ) ) { - $response = [ 'status' => 'ok', 'msg' => 'Komplet produktów został zapisany.', 'id' => $id ]; - } - - echo json_encode( $response ); - exit; - } - - static public function set_edit() - { - return \Tpl::view( 'shop-product-sets/set-edit', [ - 'set' => new \shop\ProductSet( (int) \S::get( 'id' ) ), - 'products' => \admin\factory\ShopProduct::products_list() - ] ); - } - - static public function view_list() - { - return \Tpl::view( 'shop-product-sets/view-list' ); - } -} \ No newline at end of file diff --git a/autoload/admin/factory/class.ShopProductSet.php b/autoload/admin/factory/class.ShopProductSet.php deleted file mode 100644 index d3276cf..0000000 --- a/autoload/admin/factory/class.ShopProductSet.php +++ /dev/null @@ -1,68 +0,0 @@ - insert('pp_shop_product_sets', [ - 'name' => $name, - 'status' => 'on' === $status ? 1 : 0 - ] ); - - $id = $mdb -> id(); - - if ( $set_products_id == null ) - $not_in = [ 0 ]; - elseif ( !is_array( $set_products_id ) ) - $not_in = [ 0, $set_products_id ]; - elseif ( is_array( $set_products_id ) ) - $not_in = $set_products_id; - - foreach ( $not_in as $product_id ) - { - if ( $product_id != 0 ) - $mdb -> insert( 'pp_shop_product_sets_products', [ 'set_id' => $id, 'product_id' => $product_id ] ); - } - - \S::delete_dir('../temp/'); - \S::delete_dir('../thumbs/'); - - return $id; - } - else - { - $mdb -> update('pp_shop_product_sets', [ - 'name' => $name, - 'status' => 'on' === $status ? 1 : 0 - ], [ - 'id' => (int)$set_id, - ] ); - - $mdb -> delete( 'pp_shop_product_sets_products', [ 'set_id' => $set_id ] ); - - if ( $set_products_id == null ) - $not_in = [ 0 ]; - elseif ( !is_array( $set_products_id ) ) - $not_in = [ 0, $set_products_id ]; - elseif ( is_array( $set_products_id ) ) - $not_in = $set_products_id; - - foreach ( $not_in as $product_id ) - { - if ( $product_id != 0 ) - $mdb -> insert( 'pp_shop_product_sets_products', [ 'set_id' => $set_id, 'product_id' => $product_id ] ); - } - - \S::delete_dir('../temp/'); - \S::delete_dir('../thumbs/'); - - return $set_id; - } - } -} \ No newline at end of file diff --git a/autoload/shop/class.ProductSet.php b/autoload/shop/class.ProductSet.php index 78dda25..cca00cb 100644 --- a/autoload/shop/class.ProductSet.php +++ b/autoload/shop/class.ProductSet.php @@ -23,32 +23,26 @@ class ProductSet implements \ArrayAccess { global $mdb; - $result = $mdb -> get( 'pp_shop_product_sets', '*', [ 'id' => $set_id ] ); - if ( \S::is_array_fix( $result ) ) foreach ( $result as $key => $val ) - $this -> $key = $val; + $repo = new \Domain\ProductSet\ProductSetRepository( $mdb ); + $data = $repo->find( $set_id ); - $this -> products = $mdb -> select( 'pp_shop_product_sets_products', 'product_id', [ 'set_id' => $set_id ] ); + foreach ( $data as $key => $val ) + $this->$key = $val; } - //lista dostępnych kompletów + //lista dostepnych kompletow (fasada do repozytorium) static public function sets_list() { global $mdb; - return $mdb -> select( 'pp_shop_product_sets', [ 'id', 'name' ], [ 'ORDER' => [ 'name' => 'ASC' ] ] ); + $repo = new \Domain\ProductSet\ProductSetRepository( $mdb ); + return $repo->allSets(); } - // usuwanie kompletu produktów + // usuwanie kompletu produktow (fasada do repozytorium) static public function set_delete( int $set_id ) { global $mdb; - - if ( - $mdb -> delete( 'pp_shop_product_sets_products', [ 'set_id' => $set_id ] ) - and - $mdb -> delete( 'pp_shop_product_sets', [ 'id' => $set_id ] ) - ) - return true; - - return false; + $repo = new \Domain\ProductSet\ProductSetRepository( $mdb ); + return $repo->delete( $set_id ); } -} \ No newline at end of file +} diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index a71517a..354840a 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,6 +4,20 @@ Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze. --- +## ver. 0.272 (2026-02-15) - ShopProductSets + +- **ShopProductSets** - migracja `/admin/shop_product_sets` na Domain + DI + nowe widoki + - NOWE: `Domain\ProductSet\ProductSetRepository` (`listForAdmin`, `find`, `save`, `delete`, `allSets`, `allProductsMap`) + - NOWE: `admin\Controllers\ShopProductSetsController` (DI) z akcjami `list`, `edit`, `save`, `delete` + - UPDATE: modul `/admin/shop_product_sets/*` dziala na `components/table-list` i `components/form-edit` + Selectize multi-select produktow + - UPDATE: routing i menu admin na kanoniczny URL `/admin/shop_product_sets/list/` + - UPDATE: `shop\ProductSet` przepiety na fasade do `Domain\ProductSet\ProductSetRepository` + - CLEANUP: usuniete 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` + - TEST: dodane `tests/Unit/Domain/ProductSet/ProductSetRepositoryTest.php` i `tests/Unit/admin/Controllers/ShopProductSetsControllerTest.php` +- Testy: **OK (324 tests, 1000 assertions)** + +--- + ## ver. 0.271 (2026-02-14) - ShopAttribute - **ShopAttribute** - migracja `/admin/shop_attribute` na Domain + DI + nowe widoki diff --git a/docs/DATABASE_STRUCTURE.md b/docs/DATABASE_STRUCTURE.md index 6a5853e..5ca1d44 100644 --- a/docs/DATABASE_STRUCTURE.md +++ b/docs/DATABASE_STRUCTURE.md @@ -516,3 +516,27 @@ Statusy zamowien sklepu (modul `/admin/shop_statuses`). Statusy sa predefiniowan **Uzywane w:** `Domain\ShopStatus\ShopStatusRepository`, `admin\Controllers\ShopStatusesController`, `front\factory\ShopStatuses`, `shop\Order`, `cron.php` **Aktualizacja 2026-02-14 (ver. 0.267):** modul `/admin/shop_statuses` korzysta z `Domain\ShopStatus\ShopStatusRepository` przez `admin\Controllers\ShopStatusesController`. Usunieto legacy klasy `admin\controls\ShopStatuses` i `admin\factory\ShopStatuses`. `front\factory\ShopStatuses` dziala jako fasada do repozytorium. + +## pp_shop_product_sets +Komplety produktow (modul `/admin/shop_product_sets`). + +| Kolumna | Opis | +|---------|------| +| id | PK | +| name | Nazwa kompletu | +| status | Status: 1 = aktywny, 0 = nieaktywny | + +**Uzywane w:** `Domain\ProductSet\ProductSetRepository`, `admin\Controllers\ShopProductSetsController`, `shop\ProductSet`, `shop\Product` + +## pp_shop_product_sets_products +Powiazanie kompletow z produktami (tabela lacznikowa). + +| Kolumna | Opis | +|---------|------| +| id | PK | +| set_id | FK do pp_shop_product_sets | +| product_id | FK do pp_shop_products | + +**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. diff --git a/docs/PROJECT_STRUCTURE.md b/docs/PROJECT_STRUCTURE.md index c02b230..915fe3c 100644 --- a/docs/PROJECT_STRUCTURE.md +++ b/docs/PROJECT_STRUCTURE.md @@ -220,6 +220,8 @@ autoload/ │ │ └── ShopStatusRepository.php │ ├── Transport/ │ │ └── TransportRepository.php +│ ├── ProductSet/ +│ │ └── ProductSetRepository.php │ └── ... ├── admin/ │ ├── Controllers/ # Nowe kontrolery (namespace \admin\Controllers\) @@ -285,5 +287,12 @@ Pelna dokumentacja testow: `TESTING.md` - Przepieto zaleznosci kombinacji produktu na `Domain\Attribute\AttributeRepository` i `shop\ProductAttribute`. - Dla `ShopAttribute` routing celowo nie wykonuje fallbacku akcji do legacy kontrolera. +## Dodatkowa aktualizacja 2026-02-15 (ver. 0.272) +- Dodano modul domenowy `Domain/ProductSet/ProductSetRepository.php`. +- Dodano kontroler DI `admin/Controllers/ShopProductSetsController.php`. +- Modul `/admin/shop_product_sets/*` dziala na nowych widokach (`product-sets-list`, `product-set-edit`). +- 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`. + --- -*Dokument aktualizowany: 2026-02-14* +*Dokument aktualizowany: 2026-02-15* diff --git a/docs/REFACTORING_PLAN.md b/docs/REFACTORING_PLAN.md index 449864a..564c71b 100644 --- a/docs/REFACTORING_PLAN.md +++ b/docs/REFACTORING_PLAN.md @@ -151,6 +151,7 @@ grep -r "Product::getQuantity" . | 20 | ShopPaymentMethod | 0.268 | listForAdmin, find, save, allActive, mapowanie Apilo, DI kontroler | | 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 | ### Product - szczegolowy status - ✅ getQuantity (ver. 0.238) @@ -168,11 +169,11 @@ grep -r "Product::getQuantity" . ## Kolejność refaktoryzacji (priorytet) -1-22: ✅ Cache, Product, Banner, Settings, Dictionaries, ProductArchive, Filemanager, Users, Pages, Integrations, ShopPromotion, ShopCoupon, ShopStatuses, ShopPaymentMethod, ShopTransport, ShopAttribute +1-23: ✅ Cache, Product, Banner, Settings, Dictionaries, ProductArchive, Filemanager, Users, Pages, Integrations, ShopPromotion, ShopCoupon, ShopStatuses, ShopPaymentMethod, ShopTransport, ShopAttribute, ShopProductSets Nastepne: -23. **Order** -24. **Category** +24. **Order** +25. **Category** ## Form Edit System @@ -279,5 +280,5 @@ Pelna dokumentacja testow: `TESTING.md` --- *Rozpoczęto: 2025-02-05* -*Ostatnia aktualizacja: 2026-02-14* +*Ostatnia aktualizacja: 2026-02-15* *Changelog zmian: `docs/CHANGELOG.md`* diff --git a/docs/TESTING.md b/docs/TESTING.md index 743dd1a..5489e5e 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -33,10 +33,10 @@ Alternatywnie (Git Bash): ## Aktualny stan suite -Ostatnio zweryfikowano: 2026-02-14 +Ostatnio zweryfikowano: 2026-02-15 ```text -OK (312 tests, 948 assertions) +OK (324 tests, 1000 assertions) ``` ## Struktura testow @@ -55,6 +55,7 @@ tests/ | | |-- Integrations/IntegrationsRepositoryTest.php | | |-- PaymentMethod/PaymentMethodRepositoryTest.php | | |-- Product/ProductRepositoryTest.php +| | |-- ProductSet/ProductSetRepositoryTest.php | | |-- Promotion/PromotionRepositoryTest.php | | |-- Settings/SettingsRepositoryTest.php | | |-- ShopStatus/ShopStatusRepositoryTest.php @@ -70,6 +71,7 @@ tests/ | |-- ShopAttributeControllerTest.php | |-- ShopCouponControllerTest.php | |-- ShopPaymentMethodControllerTest.php +| |-- ShopProductSetsControllerTest.php | |-- ShopPromotionControllerTest.php | |-- ShopStatusesControllerTest.php | |-- ShopTransportControllerTest.php @@ -398,3 +400,14 @@ OK (312 tests, 948 assertions) Nowe testy dodane 2026-02-14: - `tests/Unit/Domain/Attribute/AttributeRepositoryTest.php` (5 testow: domyslne dane cechy, whitelist sortowania/paginacji, zapis wartosci i domyslnej, usuwanie pustych tlumaczen, jezyk domyslny) - `tests/Unit/admin/Controllers/ShopAttributeControllerTest.php` (7 testow: kontrakty metod, brak aliasow legacy, return types, DI konstruktora, walidacja `validateValuesRows`) + +## Aktualizacja suite (ShopProductSets refactor, ver. 0.272) +Ostatnio zweryfikowano: 2026-02-15 + +```text +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) diff --git a/tests/Unit/Domain/ProductSet/ProductSetRepositoryTest.php b/tests/Unit/Domain/ProductSet/ProductSetRepositoryTest.php new file mode 100644 index 0000000..2998676 --- /dev/null +++ b/tests/Unit/Domain/ProductSet/ProductSetRepositoryTest.php @@ -0,0 +1,185 @@ +createMock(\medoo::class); + $repository = new ProductSetRepository($mockDb); + + $result = $repository->find(0); + + $this->assertIsArray($result); + $this->assertSame(0, $result['id']); + $this->assertSame('', $result['name']); + $this->assertSame(1, $result['status']); + $this->assertSame([], $result['products']); + } + + public function testFindNormalizesSetData(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->with('pp_shop_product_sets', '*', ['id' => 5]) + ->willReturn([ + 'id' => '5', + 'name' => 'Komplet A', + 'status' => '1', + ]); + + $mockDb->expects($this->once()) + ->method('select') + ->with('pp_shop_product_sets_products', 'product_id', ['set_id' => 5]) + ->willReturn(['10', '20', '30']); + + $repository = new ProductSetRepository($mockDb); + $result = $repository->find(5); + + $this->assertSame(5, $result['id']); + $this->assertSame('Komplet A', $result['name']); + $this->assertSame(1, $result['status']); + $this->assertSame([10, 20, 30], $result['products']); + } + + public function testSaveInsertsNewSetAndSyncsProducts(): void + { + $mockDb = $this->createMock(\medoo::class); + $insertCalls = []; + $deleteCalls = []; + + $mockDb->method('insert') + ->willReturnCallback(function ($table, $row) use (&$insertCalls) { + $insertCalls[] = ['table' => $table, 'row' => $row]; + }); + + $mockDb->expects($this->once()) + ->method('id') + ->willReturn(42); + + $mockDb->method('delete') + ->willReturnCallback(function ($table, $where) use (&$deleteCalls) { + $deleteCalls[] = ['table' => $table, 'where' => $where]; + return true; + }); + + $repository = new ProductSetRepository($mockDb); + $id = $repository->save(0, 'Nowy komplet', 1, [10, 20, 20]); + + $this->assertSame(42, $id); + + $this->assertSame('pp_shop_product_sets', $insertCalls[0]['table']); + $this->assertSame('Nowy komplet', $insertCalls[0]['row']['name']); + $this->assertSame(1, $insertCalls[0]['row']['status']); + + // Sync: delete old + insert 2 unique products (10, 20) + $this->assertSame('pp_shop_product_sets_products', $deleteCalls[0]['table']); + $this->assertSame(['set_id' => 42], $deleteCalls[0]['where']); + + $productInserts = array_filter($insertCalls, fn($c) => $c['table'] === 'pp_shop_product_sets_products'); + $this->assertCount(2, $productInserts); + } + + public function testSaveUpdatesExistingSet(): void + { + $mockDb = $this->createMock(\medoo::class); + $updateRow = null; + + $mockDb->expects($this->once()) + ->method('update') + ->willReturnCallback(function ($table, $row, $where) use (&$updateRow) { + $this->assertSame('pp_shop_product_sets', $table); + $this->assertSame(['id' => 7], $where); + $updateRow = $row; + return true; + }); + + $mockDb->expects($this->never())->method('id'); + + $mockDb->method('delete')->willReturn(true); + $mockDb->method('insert')->willReturn(null); + + $repository = new ProductSetRepository($mockDb); + $id = $repository->save(7, 'Zaktualizowany', 0, [15]); + + $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 ProductSetRepository($mockDb); + $this->assertFalse($repository->delete(0)); + } + + 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, + ]]; + } + }; + }); + + $repository = new ProductSetRepository($mockDb); + $result = $repository->listForAdmin( + [], + 'name DESC; DROP TABLE pp_shop_product_sets; --', + 'DESC; DELETE FROM pp_users; --', + 1, + 999 + ); + + $this->assertCount(2, $queries); + $dataSql = $queries[1]['sql']; + + $this->assertMatchesRegularExpression('/ORDER BY\s+ps\.name\s+ASC,\s+ps\.id\s+DESC/i', $dataSql); + $this->assertStringNotContainsString('DROP TABLE', $dataSql); + $this->assertMatchesRegularExpression('/LIMIT\s+100\s+OFFSET\s+0/i', $dataSql); + } + + public function testAllSetsReturnsFormattedList(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('select') + ->with('pp_shop_product_sets', ['id', 'name'], ['ORDER' => ['name' => 'ASC']]) + ->willReturn([ + ['id' => '1', 'name' => 'Komplet A'], + ['id' => '2', 'name' => 'Komplet B'], + ]); + + $repository = new ProductSetRepository($mockDb); + $result = $repository->allSets(); + + $this->assertCount(2, $result); + $this->assertSame(1, $result[0]['id']); + $this->assertSame('Komplet A', $result[0]['name']); + $this->assertSame(2, $result[1]['id']); + } +} diff --git a/tests/Unit/admin/Controllers/ShopProductSetsControllerTest.php b/tests/Unit/admin/Controllers/ShopProductSetsControllerTest.php new file mode 100644 index 0000000..9f8a4ac --- /dev/null +++ b/tests/Unit/admin/Controllers/ShopProductSetsControllerTest.php @@ -0,0 +1,62 @@ +repository = $this->createMock(ProductSetRepository::class); + $this->controller = new ShopProductSetsController($this->repository); + } + + public function testConstructorAcceptsRepository(): void + { + $controller = new ShopProductSetsController($this->repository); + $this->assertInstanceOf(ShopProductSetsController::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, 'set_edit')); + $this->assertTrue(method_exists($this->controller, 'set_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('set_edit')->getReturnType()); + $this->assertEquals('void', (string)$reflection->getMethod('save')->getReturnType()); + $this->assertEquals('void', (string)$reflection->getMethod('delete')->getReturnType()); + $this->assertEquals('void', (string)$reflection->getMethod('set_delete')->getReturnType()); + } + + public function testConstructorRequiresProductSetRepository(): void + { + $reflection = new \ReflectionClass(ShopProductSetsController::class); + $constructor = $reflection->getConstructor(); + $params = $constructor->getParameters(); + + $this->assertCount(1, $params); + $this->assertEquals('Domain\ProductSet\ProductSetRepository', $params[0]->getType()->getName()); + } +} diff --git a/updates/0.20/ver_0.272.zip b/updates/0.20/ver_0.272.zip new file mode 100644 index 0000000000000000000000000000000000000000..852a9c87623d2221713236c236bf95c212094e4a GIT binary patch literal 13003 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 WrNF@d^%mrxix3z9NTUDO>c0Ty!9Okl literal 0 HcmV?d00001 diff --git a/updates/0.20/ver_0.272_files.txt b/updates/0.20/ver_0.272_files.txt new file mode 100644 index 0000000..008f9fc --- /dev/null +++ b/updates/0.20/ver_0.272_files.txt @@ -0,0 +1,4 @@ +F: ../admin/templates/shop-product-sets/set-edit.php +F: ../admin/templates/shop-product-sets/view-list.php +F: ../autoload/admin/controls/class.ShopProductSets.php +F: ../autoload/admin/factory/class.ShopProductSet.php diff --git a/updates/changelog.php b/updates/changelog.php index a2c7dec..1413388 100644 --- a/updates/changelog.php +++ b/updates/changelog.php @@ -1,3 +1,12 @@ +ver. 0.272 - 15.02.2026
    +- NEW - migracja modulu `ShopProductSets` do architektury Domain + DI (`Domain\ProductSet\ProductSetRepository`, `admin\Controllers\ShopProductSetsController`) +- UPDATE - modul `/admin/shop_product_sets/*` przepiety z legacy `grid/gridEdit` na `components/table-list` i `components/form-edit` + multi-select Selectize +- UPDATE - routing i menu admin przepiete na kanoniczny URL `/admin/shop_product_sets/list/` +- UPDATE - `shop\ProductSet` przepiety na fasade do `Domain\ProductSet\ProductSetRepository` +- CLEANUP - usuniete legacy klasy/pliki: `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` +- UPDATE - testy: `OK (324 tests, 1000 assertions)` + nowe pliki testowe `ProductSetRepositoryTest`, `ShopProductSetsControllerTest` +- UPDATE - pliki aktualizacji: `updates/0.20/ver_0.272.zip`, `ver_0.272_files.txt` +
    ver. 0.271 - 14.02.2026
    - NEW - migracja modulu `ShopAttribute` do architektury Domain + DI (`Domain\Attribute\AttributeRepository`, `admin\Controllers\ShopAttributeController`) - UPDATE - modul `/admin/shop_attribute/*` przepiety z legacy `grid/gridEdit` na `components/table-list`, `components/form-edit` oraz nowy edytor wartosci (`values-edit`) diff --git a/updates/versions.php b/updates/versions.php index f9a47b1..3c5a3bd 100644 --- a/updates/versions.php +++ b/updates/versions.php @@ -1,5 +1,5 @@