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 @@ - - -
- -
-
- '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 0000000..852a9c8 Binary files /dev/null and b/updates/0.20/ver_0.272.zip differ 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 @@