# Plan: Moduł Project-Pro Karuzela Produktów ## Analiza screenshota Karuzela wyświetla produkty jako karty z: dużym zdjęciem, etykietą kategorii (label w lewym górnym rogu), nazwą produktu, ceną z sufiksem `/m2`. Układ 3 kolumny na desktop, slider Swiper. ## Kluczowe decyzje architektoniczne **Dlaczego tabela SQL zamiast Configuration?** Moduł wymaga *dowolnej liczby* karuzeli — Configuration przechowuje pary klucz-wartość, nie nadaje się do list rekordów o zmiennej długości. Dedykowana tabela `pp_carousel` to jedyne poprawne rozwiązanie. **Dlaczego dynamiczne hooki?** Użytkownik chce przypisywać karuzele do hooków, w tym niestandardowych. Moduł rejestruje się na `displayHeader` + catch-all `actionDispatcher`, a wyświetlanie realizuje poprzez `Hook::exec()` z warunkiem `WHERE hook_name = ?` w bazie. Niestandardowe hooki dodawane są przez `Hook::getIdByName()` / ręczne INSERT. ## Struktura plików modułu ``` modules/pp_carousel/ ├── pp_carousel.php # Główna klasa modułu ├── config.xml ├── logo.png ├── sql/ │ ├── install.sql # CREATE TABLE │ └── uninstall.sql # DROP TABLE ├── views/ │ ├── css/ │ │ └── pp_carousel.css # Style karuzeli (front) │ ├── js/ │ │ └── pp_carousel.js # Inicjalizacja Swiper (front) │ ├── lib/ │ │ └── swiper/ # swiper-bundle.min.js/css (kopiujemy z project_pro_news) │ └── templates/ │ ├── hook/ │ │ └── pp_carousel.tpl # Szablon frontu karuzeli │ └── admin/ │ ├── list.tpl # Lista karuzeli w BO │ └── form.tpl # Formularz dodawania/edycji karuzeli ``` ## Tabela bazy danych: `ps_pp_carousel` | Kolumna | Typ | Opis | |---------|-----|------| | `id_carousel` | INT AUTO_INCREMENT PK | ID karuzeli | | `hook_name` | VARCHAR(128) | Nazwa hooka (np. `displayHome`, custom) | | `source_type` | ENUM('new','bestseller','category','manual') | Źródło produktów | | `id_category` | INT DEFAULT 0 | ID kategorii (gdy source_type=category) | | `product_ids` | TEXT | Ręczne ID produktów rozdzielone przecinkami | | `limit_products` | INT DEFAULT 12 | Limit produktów | | `title` | VARCHAR(255) | Tytuł karuzeli | | `subtitle` | VARCHAR(255) | Podtytuł | | `button_label` | VARCHAR(255) | Tekst przycisku | | `button_url` | VARCHAR(512) | URL przycisku | | `price_suffix` | VARCHAR(64) | Sufiks ceny (np. `/m²`) | | `position` | INT DEFAULT 0 | Kolejność sortowania | | `active` | TINYINT(1) DEFAULT 1 | Aktywna/nieaktywna | | `id_shop` | INT DEFAULT 1 | Multishop | | `date_add` | DATETIME | Data utworzenia | | `date_upd` | DATETIME | Data modyfikacji | > Wielojęzyczność: pola `title`, `subtitle`, `button_label`, `price_suffix` — dodatkowa tabela `ps_pp_carousel_lang` z kolumnami `id_carousel`, `id_lang`, `title`, `subtitle`, `button_label`, `price_suffix`. ## Implementacja krok po kroku ### Krok 1: Plik główny `pp_carousel.php` — szkielet - Klasa `Pp_Carousel extends Module` - Konstruktor: name=`pp_carousel`, author=`Project-Pro`, version=`1.0.0`, displayName=`Project-Pro Karuzela Produktów` - `install()`: wykonuje SQL, rejestruje hooki: `displayHeader`, `displayHome`, `displayBackOfficeHeader` - `uninstall()`: DROP tabeli, usunięcie konfiguracji - Metoda `getContent()` → panel admin z listą karuzeli + formularz CRUD ### Krok 2: SQL install/uninstall **install.sql:** ```sql CREATE TABLE IF NOT EXISTS `PREFIX_pp_carousel` ( `id_carousel` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, `hook_name` VARCHAR(128) NOT NULL DEFAULT 'displayHome', `source_type` VARCHAR(20) NOT NULL DEFAULT 'new', `id_category` INT(11) UNSIGNED NOT NULL DEFAULT 0, `product_ids` TEXT, `limit_products` INT(11) UNSIGNED NOT NULL DEFAULT 12, `button_url` VARCHAR(512) DEFAULT '', `position` INT(11) UNSIGNED NOT NULL DEFAULT 0, `active` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1, `id_shop` INT(11) UNSIGNED NOT NULL DEFAULT 1, `date_add` DATETIME NOT NULL, `date_upd` DATETIME NOT NULL, PRIMARY KEY (`id_carousel`), KEY `hook_name` (`hook_name`), KEY `active` (`active`) ) ENGINE=ENGINE_TYPE DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS `PREFIX_pp_carousel_lang` ( `id_carousel` INT(11) UNSIGNED NOT NULL, `id_lang` INT(11) UNSIGNED NOT NULL, `title` VARCHAR(255) DEFAULT '', `subtitle` VARCHAR(255) DEFAULT '', `button_label` VARCHAR(255) DEFAULT '', `price_suffix` VARCHAR(64) DEFAULT '', PRIMARY KEY (`id_carousel`, `id_lang`) ) ENGINE=ENGINE_TYPE DEFAULT CHARSET=utf8mb4; ``` ### Krok 3: Panel administracyjny (`getContent()`) Widok listy (domyślny): - Tabela HelperList: ID | Tytuł | Hook | Źródło | Limit | Aktywna | Akcje (edytuj/usuń) - Przycisk "Dodaj karuzelę" Widok formularza (dodaj/edytuj): - **Hook** — select z listą dostępnych hooków + pole tekstowe "Niestandardowy hook" (jeśli wpisany, tworzony jest nowy hook w `ps_hook`) - **Źródło produktów** — select: Nowości / Bestsellery / Kategoria / Ręczne ID - **Kategoria** — select z drzewem kategorii (widoczny gdy source_type=category) - **Ręczne ID produktów** — textarea, oddzielone przecinkami (widoczny gdy source_type=manual) - **Limit produktów** — input number - **Tytuł** — input text (wielojęzyczny) - **Podtytuł** — input text (wielojęzyczny) - **Tekst przycisku** — input text (wielojęzyczny) - **URL przycisku** — input text - **Sufiks ceny** — input text (wielojęzyczny), np. `/m²` - **Aktywna** — switch tak/nie ### Krok 4: Pobieranie produktów — metoda `getProductsByCarousel($carousel)` Switch po `source_type`: - `new` → `Product::getNewProducts($idLang, 0, $limit)` (wzorzec z `ps_newproducts`) - `bestseller` → `ProductSale::getBestSales($idLang, 0, $limit)` (wzorzec z `ps_bestsellers`) - `category` → `Category::getProducts($idCategory, $idLang, 1, $limit)` (wzorzec z `project_pro_news`) - `manual` → `SELECT` po `id_product IN (...)`, potem prezentacja Wszystkie wyniki przechodzą przez `ProductAssembler` + `ProductListingPresenter` (identycznie jak w istniejących modułach). ### Krok 5: Renderowanie hooków Mechanizm: moduł rejestruje się na najczęstsze hooki (`displayHome`, `displayFooterBefore`, `displayLeftColumn` itp.). W każdym hooku: ```php public function hookDisplayHome($params) { return $this->renderCarouselsForHook('displayHome'); } ``` Metoda `renderCarouselsForHook($hookName)`: 1. SELECT z `ps_pp_carousel` WHERE `hook_name` = $hookName AND `active` = 1 ORDER BY `position` 2. Dla każdej karuzeli: pobierz produkty, przypisz do Smarty, fetch szablonu 3. Zwróć połączony HTML Niestandardowe hooki: przy zapisie karuzeli z nowym hookiem → `$this->registerHook($hookName)` + ewentualnie INSERT do `ps_hook`. ### Krok 6: Szablon frontu `pp_carousel.tpl` Wzorowany na `project_pro_news.tpl` — ten sam layout Swiper: - Section z unikalnym `id="pp-carousel-{$carousel.id}"` - Nagłówek: tytuł + podtytuł - Swiper container z kartami produktów - Karta: zdjęcie (home_default), etykieta kategorii, nazwa, cena + sufiks - Nawigacja prev/next - Przycisk "Zobacz więcej" ### Krok 7: CSS + JS **CSS** — bazowany na `project_pro_news.css`, dostosowany do nowego namespace `.pp-carousel`. **JS** — inicjalizacja wielu instancji Swipera: ```js document.querySelectorAll('.pp-carousel__slider').forEach(function(el) { new Swiper(el, { /* config */ }); }); ``` ### Krok 8: Rejestracja assetów `hookDisplayHeader()` — rejestracja CSS/JS (swiper + pp_carousel) identycznie jak w `project_pro_news`. ## Hooki do rejestracji (install) Obowiązkowe: - `displayHeader` (assety CSS/JS) - `displayHome` (główna pozycja) - `displayBackOfficeHeader` (admin CSS/JS jeśli potrzebne) Dodatkowe (rejestrowane dynamicznie przy tworzeniu karuzeli z niestandardowym hookiem): - `displayFooterBefore`, `displayTopColumn`, `displayLeftColumn`, `displayFooter` itd. - Dowolne niestandardowe hooki ## Kolejność wdrożenia 1. **Pliki SQL** — tabele `pp_carousel` + `pp_carousel_lang` 2. **pp_carousel.php** — konstruktor, install/uninstall, hookDisplayHeader 3. **Panel admin** — getContent() z listą + formularzem CRUD 4. **Logika produktów** — getProductsByCarousel() z 4 źródłami 5. **Rendering hooków** — renderCarouselsForHook() + rejestracja dynamiczna 6. **Szablon .tpl** — karta produktu zgodna ze screenshotem 7. **CSS/JS** — style + Swiper init (wieloinstancyjny) 8. **Testowanie** — dodanie karuzeli w BO, weryfikacja na froncie