diff --git a/admin/templates/shop-category/category-edit.php b/admin/templates/shop-category/category-edit.php index 1c41f7d..0db5211 100644 --- a/admin/templates/shop-category/category-edit.php +++ b/admin/templates/shop-category/category-edit.php @@ -7,7 +7,7 @@ ob_start(); ?>
@@ -17,7 +17,7 @@ ob_start(); @@ -45,7 +45,7 @@ ob_start(); );?> 'Opis kategorii (rozwiniÄ™cie)', + 'label' => 'Opis kategorii (rozwinięcie)', 'name' => 'text_hidden[' . $lg['id'] . ']', 'id' => 'text_hidden_' . $lg['id'], 'value' => $this -> category['languages'][ $lg['id'] ]['text_hidden'], @@ -84,7 +84,7 @@ ob_start(); );?> 'Sortowanie produktĂłw', + 'label' => 'Sortowanie produktów', 'name' => 'sort_type', 'id' => 'sort_type', 'values' => is_array( $this -> sort_types ) ? $this -> sort_types : [], @@ -93,7 +93,7 @@ ob_start(); );?> 'WyĹ›wietlić podkategorie', + 'label' => 'Wyświetlić podkategorie', 'name' => 'view_subcategories', 'checked' => $this -> category['view_subcategories'] == 1 ? true : false ) @@ -104,7 +104,7 @@ ob_start(); @@ -124,7 +124,7 @@ ob_start(); );?> 'TytuĹ‚ kategorii (h1)', + 'label' => 'Tytuł kategorii (h1)', 'name' => 'category_title[' . $lg['id'] . ']', 'id' => 'category_title_' . $lg['id'], 'value' => $this -> category['languages' ][ $lg['id'] ]['category_title'] @@ -156,7 +156,7 @@ ob_start(); );?> 'Blokuj indeksacjÄ™', + 'label' => 'Blokuj indeksację', 'name' => 'noindex[' . $lg['id'] . ']', 'id' => 'noindex_' . $lg['id'], 'values' => array( diff --git a/admin/templates/update/main-view.php b/admin/templates/update/main-view.php index ebabea3..dfcd62f 100644 --- a/admin/templates/update/main-view.php +++ b/admin/templates/update/main-view.php @@ -52,15 +52,6 @@
-
-
- Changelog -
-
- ver ); ?> -
-
- + +
+
+ Changelog +
+
+ ver ); + $changelog = preg_replace( '//s', '', $changelog ); + $changelog = strip_tags( $changelog, '


diff --git a/autoload/Domain/Product/ProductRepository.php b/autoload/Domain/Product/ProductRepository.php index 562db10..ecc5c24 100644 --- a/autoload/Domain/Product/ProductRepository.php +++ b/autoload/Domain/Product/ProductRepository.php @@ -1335,8 +1335,9 @@ class ProductRepository $this->saveImagesOrder( $productId, $d['gallery_order'] ); } - // Zapisz custom fields tylko gdy jawnie podane (partial update przez API może nie zawierać tego klucza) - if ( array_key_exists( 'custom_field_name', $d ) ) { + // Zapisz custom fields tylko gdy formularz edycji renderował sekcję (marker hidden field) + // API partial update nie zawiera tego markera — custom fields pominięte + if ( array_key_exists( 'custom_field_name_present', $d ) ) { $this->saveCustomFields( $productId, $d['custom_field_name'] ?? [], $d['custom_field_type'] ?? [], $d['custom_field_required'] ?? [] ); } @@ -2204,6 +2205,44 @@ class ProductRepository ] ); } + /** + * Pobiera nazwy etykiet custom_label_0..4 z bazy ustawien. + * + * @return array + */ + public function customLabelNames(): array + { + $names = []; + for ( $index = 0; $index < 5; $index++ ) { + $fieldName = 'custom_label_' . $index; + $names[$fieldName] = 'Custom label ' . $index; + } + + $settingsKeys = []; + for ( $index = 0; $index < 5; $index++ ) { + $settingsKeys[] = 'custom_label_' . $index . '_name'; + $settingsKeys[] = 'google_custom_label_' . $index . '_name'; + } + + $settingsRows = $this->db->select( 'pp_settings', [ 'param', 'value' ], [ 'param' => $settingsKeys ] ); + if ( is_array( $settingsRows ) ) { + foreach ( $settingsRows as $settingRow ) { + $param = (string) ( $settingRow['param'] ?? '' ); + $value = trim( (string) ( $settingRow['value'] ?? '' ) ); + + if ( $value === '' ) { + continue; + } + + if ( preg_match( '/^(?:google_)?custom_label_([0-4])_name$/', $param, $match ) ) { + $names[ 'custom_label_' . $match[1] ] = $value; + } + } + } + + return $names; + } + /** * Pobiera sugestie custom label. */ @@ -2330,9 +2369,9 @@ class ProductRepository $itemNode->appendChild( $doc->createElement( 'g:description', html_entity_decode( strip_tags( $desc ) ) ) ); if ( $product['language']['seo_link'] ) { - $link = $domainPrefix . '://' . $url . '/' . \Shared\Helpers\Helpers::seo( $product['language']['seo_link'] ) . '/' . str_replace( '|', '/', $combination['permutation_hash'] ); + $link = $domainPrefix . '://' . $url . '/' . \Shared\Helpers\Helpers::seo( $product['language']['seo_link'] ) . '/' . str_replace( '|', '_', $combination['permutation_hash'] ); } else { - $link = $domainPrefix . '://' . $url . '/p-' . $product['id'] . '-' . \Shared\Helpers\Helpers::seo( $product['language']['name'] ) . '/' . str_replace( '|', '/', $combination['permutation_hash'] ); + $link = $domainPrefix . '://' . $url . '/p-' . $product['id'] . '-' . \Shared\Helpers\Helpers::seo( $product['language']['name'] ) . '/' . str_replace( '|', '_', $combination['permutation_hash'] ); } $itemNode->appendChild( $doc->createElement( 'link', $link ) ); diff --git a/autoload/Shared/Helpers/Helpers.php b/autoload/Shared/Helpers/Helpers.php index 0d42ad6..9956002 100644 --- a/autoload/Shared/Helpers/Helpers.php +++ b/autoload/Shared/Helpers/Helpers.php @@ -691,12 +691,12 @@ class Helpers if ( $row2['seo_link'] ) { $mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . self::seo( $row2['seo_link'] ) . '$', 'destination' => 'index.php?product=' . $row2['product_id'] ] ); - $mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . self::seo( $row2['seo_link'] ) . '/([0-9-]+)$', 'destination' => 'index.php?product=' . $row2['product_id'] . '&permutation_hash=$1' ] ); + $mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . self::seo( $row2['seo_link'] ) . '/([0-9_-]+)$', 'destination' => 'index.php?product=' . $row2['product_id'] . '&permutation_hash=$1' ] ); } else { $mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . 'p-' . $row2['product_id'] . '-' . self::seo( $row2['name'] ) . '$', 'destination' => 'index.php?product=' . $row2['product_id'] ] ); - $mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . 'p-' . $row2['product_id'] . '-' . self::seo( $row2['name'] ) . '/([0-9-]+)$', 'destination' => 'index.php?product=' . $row2['product_id'] . '&permutation_hash=$1' ] ); + $mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . 'p-' . $row2['product_id'] . '-' . self::seo( $row2['name'] ) . '/([0-9_-]+)$', 'destination' => 'index.php?product=' . $row2['product_id'] . '&permutation_hash=$1' ] ); } } } diff --git a/autoload/admin/Controllers/ScontainersController.php b/autoload/admin/Controllers/ScontainersController.php index a680082..6d6d064 100644 --- a/autoload/admin/Controllers/ScontainersController.php +++ b/autoload/admin/Controllers/ScontainersController.php @@ -184,8 +184,16 @@ class ScontainersController } $data = $result['data']; + $containerId = (int)($data['id'] ?? 0); + if ($containerId <= 0) { + $routeId = (int)\Shared\Helpers\Helpers::get('id'); + if ($routeId > 0) { + $containerId = $routeId; + } + } + $savedId = $this->repository->save([ - 'id' => (int)($data['id'] ?? 0), + 'id' => $containerId, 'status' => $data['status'] ?? 0, 'show_title' => $data['show_title'] ?? 0, 'translations' => $data['translations'] ?? [], @@ -240,7 +248,6 @@ class ScontainersController ]; $fields = [ - FormField::hidden('id', $id), FormField::langSection('translations', 'content', [ FormField::text('title', [ 'label' => 'Tytul', @@ -283,7 +290,7 @@ class ScontainersController $actionUrl, '/admin/scontainers/list/', true, - [], + ['id' => $id], $languages, $errors ); diff --git a/autoload/admin/Controllers/ShopProductController.php b/autoload/admin/Controllers/ShopProductController.php index bdc288a..55ff61f 100644 --- a/autoload/admin/Controllers/ShopProductController.php +++ b/autoload/admin/Controllers/ShopProductController.php @@ -18,6 +18,8 @@ use admin\Support\TableListRequestFactory; */ class ShopProductController { + private const CUSTOM_LABELS_SESSION_KEY = 'shop_product_show_custom_labels'; + private ProductRepository $repository; private IntegrationsRepository $integrationsRepository; private LanguagesRepository $languagesRepository; @@ -39,6 +41,8 @@ class ShopProductController $apiloEnabled = $this->integrationsRepository->getSetting( 'apilo', 'enabled' ); $shopproEnabled = $this->integrationsRepository->getSetting( 'shoppro', 'enabled' ); $dlang = $this->languagesRepository->defaultLanguage(); + $customLabelsEnabled = $this->customLabelsEnabled(); + $customLabelNames = $this->repository->customLabelNames(); $sortableColumns = [ 'id', 'name', 'price_brutto', 'status', 'promoted', 'quantity' ]; @@ -98,6 +102,10 @@ class ShopProductController . '' . $categories . '' . 'SKU: ' . $sku . ', EAN: ' . $ean . ''; + if ( $customLabelsEnabled ) { + $nameHtml .= $this->renderCustomLabelsEditor( $product, $id, $customLabelNames ); + } + $priceHtml = ''; $promoHtml = ''; $promotedHtml = $product['promoted'] ? 'tak' : 'nie'; @@ -195,11 +203,25 @@ class ShopProductController 'viewModel' => $viewModel, 'apilo_enabled' => $apiloEnabled, 'shoppro_enabled' => $shopproEnabled, + 'custom_labels_enabled' => $customLabelsEnabled, ] ); } // ─── Krok 7: Edycja i zapis ───────────────────────────────────── + /** + * AJAX: przelacza widok custom labels na liscie produktow i zapisuje stan w sesji. + */ + public function product_custom_labels_toggle(): void + { + $currentState = $this->customLabelsEnabled(); + $newState = $currentState ? 0 : 1; + \Shared\Helpers\Helpers::set_session( self::CUSTOM_LABELS_SESSION_KEY, $newState ); + + echo json_encode( [ 'status' => 'ok', 'enabled' => (bool) $newState ] ); + exit; + } + /** * Formularz edycji produktu. */ @@ -699,7 +721,8 @@ class ShopProductController private function renderCustomFieldsBox( array $product ): string { - $html = ' dodaj niestandardowe pole'; + $html = ''; + $html .= ' dodaj niestandardowe pole'; $html .= '
'; $customFields = is_array( $product['custom_fields'] ?? null ) ? $product['custom_fields'] : []; @@ -896,9 +919,15 @@ class ShopProductController public function product_custom_label_suggestions(): void { $response = [ 'status' => 'error', 'msg' => 'Podczas pobierania sugestii dla custom label wystąpił błąd. Proszę spróbować ponownie.' ]; + $labelType = (string) \Shared\Helpers\Helpers::get( 'label_type' ); - $suggestions = $this->repository->customLabelSuggestions( \Shared\Helpers\Helpers::get( 'custom_label' ), \Shared\Helpers\Helpers::get( 'label_type' ) ); - if ( $suggestions ) { + if ( !$this->isAllowedCustomLabelType( $labelType ) ) { + echo json_encode( $response ); + exit; + } + + $suggestions = $this->repository->customLabelSuggestions( (string) \Shared\Helpers\Helpers::get( 'custom_label' ), $labelType ); + if ( is_array( $suggestions ) ) { $response = [ 'status' => 'ok', 'suggestions' => $suggestions ]; } @@ -912,8 +941,14 @@ class ShopProductController public function product_custom_label_save(): void { $response = [ 'status' => 'error', 'msg' => 'Podczas zapisywania custom label wystąpił błąd. Proszę spróbować ponownie.' ]; + $labelType = (string) \Shared\Helpers\Helpers::get( 'label_type' ); - if ( $this->repository->saveCustomLabel( (int) \Shared\Helpers\Helpers::get( 'product_id' ), \Shared\Helpers\Helpers::get( 'custom_label' ), \Shared\Helpers\Helpers::get( 'label_type' ) ) ) { + if ( !$this->isAllowedCustomLabelType( $labelType ) ) { + echo json_encode( $response ); + exit; + } + + if ( $this->repository->saveCustomLabel( (int) \Shared\Helpers\Helpers::get( 'product_id' ), (string) \Shared\Helpers\Helpers::get( 'custom_label' ), $labelType ) ) { $response = [ 'status' => 'ok' ]; } @@ -1196,4 +1231,36 @@ class ShopProductController echo json_encode( [ 'status' => 'ok', 'products' => $products ] ); exit; } + + private function customLabelsEnabled(): bool + { + return isset( $_SESSION[ self::CUSTOM_LABELS_SESSION_KEY ] ) && (int) $_SESSION[ self::CUSTOM_LABELS_SESSION_KEY ] === 1; + } + + private function isAllowedCustomLabelType(string $labelType): bool + { + return in_array( $labelType, [ 'custom_label_0', 'custom_label_1', 'custom_label_2', 'custom_label_3', 'custom_label_4' ], true ); + } + + private function renderCustomLabelsEditor(array $product, int $productId, array $customLabelNames): string + { + $customLabelsHtml = '
'; + + for ( $index = 0; $index < 5; $index++ ) { + $fieldName = 'custom_label_' . $index; + $labelText = htmlspecialchars( (string) ( $customLabelNames[$fieldName] ?? 'Custom label ' . $index ), ENT_QUOTES, 'UTF-8' ); + $valueText = htmlspecialchars( (string) ( $product[$fieldName] ?? '' ), ENT_QUOTES, 'UTF-8' ); + + $customLabelsHtml .= '
'; + $customLabelsHtml .= '' . $labelText . ''; + $datalistId = 'custom-label-list-' . $productId . '-' . $fieldName; + $customLabelsHtml .= ''; + $customLabelsHtml .= ''; + $customLabelsHtml .= '
'; + $customLabelsHtml .= '
'; + } + + $customLabelsHtml .= '
'; + return $customLabelsHtml; + } } diff --git a/autoload/front/Controllers/ShopBasketController.php b/autoload/front/Controllers/ShopBasketController.php index e26e7fa..7477b91 100644 --- a/autoload/front/Controllers/ShopBasketController.php +++ b/autoload/front/Controllers/ShopBasketController.php @@ -280,20 +280,71 @@ class ShopBasketController $client = \Shared\Helpers\Helpers::get_session( 'client' ); $orderSubmitToken = $this->createOrderSubmitToken(); + $basket = \Shared\Helpers\Helpers::get_session( 'basket' ); + $coupon = \Shared\Helpers\Helpers::get_session( 'coupon' ); + $transport = ( new \Domain\Transport\TransportRepository( $GLOBALS['mdb'] ) )->findActiveByIdCached( \Shared\Helpers\Helpers::get_session( 'basket-transport-method-id' ) ); + + $productsSummary = (float)\Domain\Basket\BasketCalculator::summaryPrice( $basket, $coupon ); + $freeDeliveryThreshold = isset( $settings['free_delivery'] ) ? (float)$settings['free_delivery'] : 0.0; + $transportCalc = $this->calculateTransportCostForSummary( $transport, $productsSummary, $freeDeliveryThreshold ); + return \Shared\Tpl\Tpl::view( 'shop-basket/summary-view', [ 'lang_id' => $lang_id, 'client' => \Shared\Helpers\Helpers::get_session( 'client' ), - 'basket' => \Shared\Helpers\Helpers::get_session( 'basket' ), - 'transport' => ( new \Domain\Transport\TransportRepository( $GLOBALS['mdb'] ) )->findActiveByIdCached( \Shared\Helpers\Helpers::get_session( 'basket-transport-method-id' ) ), + 'basket' => $basket, + 'transport' => $transport, + 'transport_cost_effective' => $transportCalc['transport_cost_effective'], + 'free_delivery_applies' => $transportCalc['free_delivery_applies'], 'payment_method' => $this->paymentMethodRepository->paymentMethodCached( (int)\Shared\Helpers\Helpers::get_session( 'basket-payment-method-id' ) ), 'addresses' => ( new \Domain\Client\ClientRepository( $GLOBALS['mdb'] ) )->clientAddresses( (int)$client['id'] ), 'settings' => $settings, - 'coupon' => \Shared\Helpers\Helpers::get_session( 'coupon' ), + 'coupon' => $coupon, 'basket_message' => \Shared\Helpers\Helpers::get_session( 'basket_message' ), 'order_submit_token' => $orderSubmitToken ] ); } + /** + * Wylicza efektywny koszt transportu dla widoku /koszyk-podsumowanie. + * Koszt spada do 0, gdy transport ma flage delivery_free=1 ORAZ wartosc koszyka + * (po kuponie) osiaga prog darmowej dostawy $freeDeliveryThreshold. + * + * @param array|null $transport Aktywny transport (lub null gdy nie wybrany) + * @param float $productsSummary Wartosc koszyka po kuponie + * @param float $freeDeliveryThreshold Prog darmowej dostawy z settings.free_delivery + * @return array{transport_cost_effective: float, free_delivery_applies: bool} + */ + protected function calculateTransportCostForSummary( $transport, $productsSummary, $freeDeliveryThreshold ) + { + if ( !is_array( $transport ) ) + { + return [ + 'transport_cost_effective' => 0.0, + 'free_delivery_applies' => false, + ]; + } + + $deliveryFree = isset( $transport['delivery_free'] ) ? (int)$transport['delivery_free'] : 0; + $cost = isset( $transport['cost'] ) ? (float)$transport['cost'] : 0.0; + + $applies = false; + if ( $deliveryFree === 1 && $freeDeliveryThreshold > 0 ) + { + $summaryNormalized = \Shared\Helpers\Helpers::normalize_decimal( $productsSummary ); + $thresholdNormalized = \Shared\Helpers\Helpers::normalize_decimal( $freeDeliveryThreshold ); + + if ( $summaryNormalized >= $thresholdNormalized ) + { + $applies = true; + } + } + + return [ + 'transport_cost_effective' => $applies ? 0.0 : $cost, + 'free_delivery_applies' => $applies, + ]; + } + public function basketSave() { $orderSubmitToken = (string)\Shared\Helpers\Helpers::get( 'order_submit_token', true ); diff --git a/autoload/front/LayoutEngine.php b/autoload/front/LayoutEngine.php index f384f70..5679298 100644 --- a/autoload/front/LayoutEngine.php +++ b/autoload/front/LayoutEngine.php @@ -153,15 +153,9 @@ class LayoutEngine { $category = $categoryRepo->frontCategoryDetails( (int)\Shared\Helpers\Helpers::get( 'category' ), $lang_id ); - if ( $category['language']['meta_title'] ) - $page['language']['title'] = $category['language']['meta_title']; - else - $page['language']['title'] = $category['language']['title']; - + $page = self::applyEntityMetaToPage( $page, isset( $category['language'] ) ? $category['language'] : null, isset( $category['language']['title'] ) ? $category['language']['title'] : '' ); $page['show_title'] = true; - $page['language']['meta_keywords'] = $category['language']['meta_keywords']; - $page['language']['meta_description'] = $category['language']['meta_description']; - $page['language']['page_title'] = $category['language']['category_title'] ? $category['language']['category_title'] : $category['language']['title']; + $page['language']['page_title'] = !empty( $category['language']['category_title'] ) ? $category['language']['category_title'] : ( isset( $category['language']['title'] ) ? $category['language']['title'] : '' ); // CANONICAL $html = str_replace( '[CANONICAL]', '', $html ); @@ -175,14 +169,8 @@ class LayoutEngine { $article = $articleRepo->articleDetailsFrontend( (int)\Shared\Helpers\Helpers::get( 'article' ), $lang_id ); - if ( $article['language']['meta_title'] ) - $page['language']['title'] = $article['language']['meta_title']; - else - $page['language']['title'] = $article['language']['title']; - + $page = self::applyEntityMetaToPage( $page, isset( $article['language'] ) ? $article['language'] : null, isset( $article['language']['title'] ) ? $article['language']['title'] : '' ); $page['show_title'] = false; - $page['language']['meta_keywords'] = $article['language']['meta_keywords']; - $page['language']['meta_description'] = $article['language']['meta_description']; // CANONICAL $html = str_replace( '[CANONICAL]', '', $html ); @@ -193,16 +181,11 @@ class LayoutEngine // if ( \Shared\Helpers\Helpers::get( 'product' ) ) { - $product = ( new \Domain\Product\ProductRepository( $GLOBALS['mdb'] ) )->findCached( \Shared\Helpers\Helpers::get( 'product' ), $lang_id, $_GET['permutation_hash'] ?? null ); - - if ( $product['language']['meta_title'] ) - $page['language']['title'] = $product['language']['meta_title']; - else - $page['language']['title'] = $product['language']['name']; + $permutation_hash = isset( $_GET['permutation_hash'] ) ? str_replace( '_', '|', $_GET['permutation_hash'] ) : null; + $product = ( new \Domain\Product\ProductRepository( $GLOBALS['mdb'] ) )->findCached( \Shared\Helpers\Helpers::get( 'product' ), $lang_id, $permutation_hash ); + $page = self::applyEntityMetaToPage( $page, isset( $product['language'] ) ? $product['language'] : null, isset( $product['language']['name'] ) ? $product['language']['name'] : '' ); $page['show_title'] = false; - $page['language']['meta_keywords'] = $product['language']['meta_keywords']; - $page['language']['meta_description'] = $product['language']['meta_description']; // CANONICAL if ( $product['language']['canonical'] ) @@ -439,6 +422,35 @@ class LayoutEngine ] ); } + /** + * Przepisuje meta encji (kategoria/artykuł/produkt) do $page['language']. + * + * Dlaczego: domyślne $page jest stroną główną CMS. Jeśli nie nadpiszemy + * meta_title encji (nawet pustym), meta_title homepage wycieka do + * na podstronie kategorii/produktu (linia podstawienia [TITLE]). + * + * @param array $page obecne $page (z homepage lub session) + * @param array|null $entityLanguage wiersz *_langs encji (może być null) + * @param string $fallbackTitle nazwa encji używana jako $page.language.title + * @return array zmodyfikowany $page + */ + public static function applyEntityMetaToPage( $page, $entityLanguage, $fallbackTitle ) + { + if ( !is_array( $page ) ) { + $page = []; + } + if ( !isset( $page['language'] ) or !is_array( $page['language'] ) ) { + $page['language'] = []; + } + + $page['language']['title'] = $fallbackTitle; + $page['language']['meta_title'] = is_array( $entityLanguage ) && isset( $entityLanguage['meta_title'] ) ? $entityLanguage['meta_title'] : null; + $page['language']['meta_keywords'] = is_array( $entityLanguage ) && isset( $entityLanguage['meta_keywords'] ) ? $entityLanguage['meta_keywords'] : null; + $page['language']['meta_description'] = is_array( $entityLanguage ) && isset( $entityLanguage['meta_description'] ) ? $entityLanguage['meta_description'] : null; + + return $page; + } + public static function alert() { if ( $alert = \Shared\Helpers\Helpers::get_session( 'alert' ) )