diff --git a/.claude/memory/MEMORY.md b/.claude/memory/MEMORY.md new file mode 100644 index 00000000..b72f3ed2 --- /dev/null +++ b/.claude/memory/MEMORY.md @@ -0,0 +1 @@ +- [feedback_changelog.md](feedback_changelog.md) - Po każdej zmianie aktualizuj folder changelog/ w katalogu głównym diff --git a/.paul/PROJECT.md b/.paul/PROJECT.md new file mode 100644 index 00000000..3724e6c0 --- /dev/null +++ b/.paul/PROJECT.md @@ -0,0 +1,40 @@ +# Project: drmaterac.pl + +## Description +Modul Prestashop 1.7.8.11 o nazwie Cross Sell PRO, ktory wyswietla produkty uzupelniajace na podstawie mechanizmu "Powiazany produkt" w koszyku i na etapie zamowienia. + +## Core Value +Klient koszyka szybciej dobiera produkty uzupelniajace, co zwieksza wygode zakupow i wartosc koszyka. + +## Requirements + +### Must Have +- [x] Modul wyswietla produkty cross-sell pod kontenerem koszyka ("card cart-container"). +- [x] Dane produktow pochodza z relacji "Powiazany produkt" w Prestashop. +- [x] Prezentacja w formie karuzeli: zdjecie, nazwa, cena, CTA. +- [x] Dla produktow z wariantami przycisk kieruje na strone produktu. +- [x] Sekcja cross-sell dziala takze na checkout w `#js-checkout-summary` przed summary. + +### Should Have +- [x] Kompatybilnosc z motywem sklepu i responsywnosc mobile/desktop. +- [x] Ograniczenie liczby produktow i pomijanie produktow juz w koszyku. + +### Nice to Have +- [ ] Prosta konfiguracja w panelu modulu (liczba elementow, autoplay). + +## Constraints +- Prestashop: 1.7.8.11 +- PHP: 7.4 +- Integracja bez modyfikowania core Prestashop. +- Modul tworzony przez: Pyziak Jacek (https://www.project-pro.pl) + +## Success Criteria +- Sekcja cross-sell pojawia sie na stronie koszyka pod "card cart-container". +- Sekcja cross-sell pojawia sie na stronie zamowienia w prawym panelu summary. +- Produkty sa pobierane z relacji "Powiazany produkt". +- Uzytkownik moze dodac produkt bez wariantow bezposrednio z karuzeli. +- Uzytkownik przechodzi do karty produktu, jesli produkt wymaga wyboru wariantu. + +--- +*Created: 2026-03-31 22:58* +*Last updated: 2026-03-31 after Phase 2* diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md new file mode 100644 index 00000000..11cbd042 --- /dev/null +++ b/.paul/ROADMAP.md @@ -0,0 +1,50 @@ +# Roadmap: drmaterac.pl + +## Overview +Wdrozenie funkcji Cross Sell PRO skoncentrowanej na stronie koszyka i checkoutu, od zasilenia danymi z relacji produktow po frontendowa karuzele i logike dodawania do koszyka zgodna z wariantami. + +## Current Milestone +**v0.1 Cross Sell PRO dla koszyka** (v0.1.0) +Status: Complete +Phases: 2 of 2 complete + +## Phases + +| Phase | Name | Plans | Status | Completed | +|-------|------|-------|--------|-----------| +| 1 | Cross Sell PRO w koszyku | 1 | Complete | 2026-03-31 | +| 2 | Cross Sell PRO w zamowieniu | 1 | Complete | 2026-03-31 | + +## Phase Details + +### Phase 1: Cross Sell PRO w koszyku + +**Goal:** Dostarczyc modul Prestashop wyswietlajacy karuzele produktow uzupelniajacych w koszyku z pelna obsluga produktow prostych i wariantowych. +**Depends on:** Nothing (first phase) +**Research:** Unlikely (Prestashop patterns sa znane) + +**Scope:** +- Integracja hooka koszyka i pobieranie powiazanych produktow. +- Render sekcji karuzeli pod kontenerem koszyka z danymi produktow. +- Logika CTA: add-to-cart dla produktow prostych, przekierowanie do produktu przy wariantach. + +**Plans:** +- [x] 01-01: Implementacja modulu Cross Sell PRO na stronie koszyka + +### Phase 2: Cross Sell PRO w zamowieniu + +**Goal:** Rozszerzyc modul o wyswietlanie karuzeli cross-sell na etapie zamowienia, po prawej stronie checkoutu. +**Depends on:** Phase 1 (wykorzystanie logiki i komponentow z koszyka) +**Research:** Unlikely (adaptacja istniejacego rozwiazania) + +**Scope:** +- Render sekcji cross-sell w bloku `#js-checkout-summary`. +- Wstawienie sekcji przed obecna zawartoscia w tym kontenerze. +- Zachowanie zachowania CTA jak w koszyku (warianty -> produkt, proste -> dodanie). + +**Plans:** +- [x] 02-01: Integracja karuzeli w checkout summary + +--- +*Roadmap created: 2026-03-31 22:58* +*Last updated: 2026-03-31 23:59* diff --git a/.paul/STATE.md b/.paul/STATE.md new file mode 100644 index 00000000..26f88cda --- /dev/null +++ b/.paul/STATE.md @@ -0,0 +1,53 @@ +# Project State + +## Project Reference + +See: .paul/PROJECT.md (updated 2026-03-31) + +**Core value:** Klient koszyka szybciej dobiera produkty uzupelniajace i zwieksza wartosc zamowienia. +**Current focus:** Milestone v0.1 complete - ready for next milestone planning + +## Current Position + +Milestone: v0.1 Cross Sell PRO dla koszyka (v0.1.0) +Phase: Complete (2 of 2) +Plan: 02-01 complete +Status: Ready to start next milestone +Last activity: 2026-03-31 23:59 - Phase 2 unified and milestone marked complete + +Progress: +- Milestone: [##########] 100% +- Phase 2: [##########] 100% + +## Loop Position + +Current loop state: +``` +PLAN --> APPLY --> UNIFY + X X X [Loop complete - ready for next PLAN] +``` + +## Accumulated Context + +### Decisions +| Decision | Phase | Impact | +|----------|-------|--------| +| 2026-03-31: Uzycie displayCheckoutSummaryTop dla checkout cross-sell | Phase 2 | Sekcja osadzona przed summary bez modyfikacji motywu | +| 2026-03-31: Wspolny komponent cart/checkout przez crosssellpro_mode | Phase 2 | Jedna implementacja dla dwoch miejsc osadzenia | +| 2026-03-31: Stabilizacja JS/CTA po testach produkcyjnych | Phase 2 | Usuniete regresje przy add/remove i karuzeli | + +### Deferred Issues +None. + +### Blockers/Concerns +None. + +## Session Continuity + +Last session: 2026-03-31 23:59 +Stopped at: Loop closed for plan 02-01 and milestone v0.1 complete +Next action: Start next milestone planning (new scope) with $paul-new-milestone or $paul-plan +Resume file: .paul/ROADMAP.md + +--- +*STATE.md - Updated after every significant action* diff --git a/.paul/paul.json b/.paul/paul.json new file mode 100644 index 00000000..748f2f2f --- /dev/null +++ b/.paul/paul.json @@ -0,0 +1,25 @@ +{ + "name": "drmaterac.pl", + "version": "0.1.0", + "milestone": { + "name": "v0.1 Cross Sell PRO dla koszyka", + "version": "0.1.0", + "status": "complete" + }, + "phase": { + "number": 2, + "name": "Cross Sell PRO w zamowieniu", + "status": "complete" + }, + "loop": { + "plan": null, + "position": "IDLE" + }, + "timestamps": { + "created_at": "2026-03-31T22:58:20+02:00", + "updated_at": "2026-03-31T23:59:56+02:00" + }, + "satellite": { + "groom": true + } +} diff --git a/.paul/phases/01-cross-sell-pro-koszyk/01-01-PLAN.md b/.paul/phases/01-cross-sell-pro-koszyk/01-01-PLAN.md new file mode 100644 index 00000000..1affb2dd --- /dev/null +++ b/.paul/phases/01-cross-sell-pro-koszyk/01-01-PLAN.md @@ -0,0 +1,154 @@ +--- +phase: 01-cross-sell-pro-koszyk +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - modules/crosssellpro/crosssellpro.php + - modules/crosssellpro/views/templates/hook/cartCrossSell.tpl + - modules/crosssellpro/views/js/cartCrossSell.js + - modules/crosssellpro/views/css/cartCrossSell.css +autonomous: false +--- + + +## Goal +Zaimplementowac modul Cross Sell PRO, ktory na stronie koszyka wyswietla karuzele produktow uzupelniajacych pobieranych z relacji "Powiazany produkt" i poprawnie obsluguje dodawanie do koszyka/produkty wariantowe. + +## Purpose +Zwiekszyc srednia wartosc koszyka i ulatwic klientowi kompletowanie zamowienia bez opuszczania flow koszyka. + +## Output +Dzialajacy modul Prestashop 1.7.8.11 (PHP 7.4) z hookiem koszyka, warstwa widoku karuzeli i logika CTA zalezna od typu produktu. + + + +## Project Context +@.paul/PROJECT.md +@.paul/ROADMAP.md +@.paul/STATE.md + +## Source Files +@modules/ +@controllers/ +@themes/ + + + + +## AC-1: Render sekcji Cross Sell pod kontenerem koszyka +```gherkin +Given Uzytkownik otwiera strone koszyka /koszyk?action=show +When Strona koszyka sie renderuje +Then Pod elementem "div.card.cart-container" widoczna jest sekcja Cross Sell PRO +``` + +## AC-2: Produkty sa pobierane z relacji "Powiazany produkt" +```gherkin +Given Produkt bazowy w koszyku ma zdefiniowane produkty powiazane w Prestashop +When Modul buduje liste podpowiedzi +Then Karuzela pokazuje tylko produkty pochodzace z tych relacji +``` + +## AC-3: Karuzela pokazuje wymagane dane produktu +```gherkin +Given Lista cross-sell zawiera co najmniej 1 produkt +When Sekcja Cross Sell sie wyswietla +Then Kazdy slajd zawiera zdjecie, nazwe, cene i przycisk "Dodaj do koszyka" +``` + +## AC-4: Poprawne zachowanie CTA dla wariantow +```gherkin +Given Produkt cross-sell ma kombinacje i wymaga wyboru wariantu +When Uzytkownik klika przycisk CTA +Then Uzytkownik zostaje przekierowany na strone produktu zamiast szybkiego add-to-cart +``` + +## AC-5: Poprawne zachowanie CTA dla produktow prostych +```gherkin +Given Produkt cross-sell nie wymaga wyboru wariantu +When Uzytkownik klika przycisk "Dodaj do koszyka" +Then Produkt jest dodawany do koszyka bezposrednio z sekcji Cross Sell +``` + + + + + + + Task 1: Utworzyc szkielet modulu i logike pobierania produktow cross-sell + modules/crosssellpro/crosssellpro.php + + Utworzyc modul Cross Sell PRO zgodny z Prestashop 1.7.8.11 i PHP 7.4. + Zarejestrowac odpowiedni hook koszyka oraz pobieranie danych produktow z relacji "Powiazany produkt" dla produktow znajdujacych sie aktualnie w koszyku. + Zapewnic deduplikacje wynikow i odfiltrowanie produktow nieaktywnych/niedostepnych. + Przygotowac dane do widoku: obrazek, nazwa, cena, URL produktu, flaga czy produkt ma warianty, parametry add-to-cart dla produktow prostych. + Unikac modyfikacji plikow core Prestashop - cala logika musi byc kapsulowana w module. + + Instalacja modulu przebiega bez bledow, hook zwraca kolekcje danych cross-sell dla koszyka testowego. + AC-2 satisfied; AC-5 preconditions prepared. + + + + Task 2: Zbudowac widok karuzeli i frontend CTA + modules/crosssellpro/views/templates/hook/cartCrossSell.tpl, modules/crosssellpro/views/js/cartCrossSell.js, modules/crosssellpro/views/css/cartCrossSell.css + + Zaimplementowac sekcje UI renderowana pod "card cart-container" z ukladem karuzeli. + W slajdzie pokazac: zdjecie, nazwe, cene i przycisk CTA. + Dodac JS obslugujacy przewijanie karuzeli i klikniecia CTA. + Dla produktow z wariantami CTA wykonuje przekierowanie na URL produktu. + Dla produktow prostych CTA uruchamia add-to-cart (POST lub link zgodny ze standardem Prestashop). + Dodac stylowanie responsywne desktop/mobile i podstawowy fallback (lista bez JS). + + Manualny test na /koszyk?action=show potwierdza render sekcji i oba scenariusze CTA (wariant/bez wariantu). + AC-1, AC-3, AC-4, AC-5 satisfied. + + + + Modul Cross Sell PRO z sekcja karuzeli na stronie koszyka i logika CTA zalezna od wariantow. + + 1. Zainstaluj i wlacz modul Cross Sell PRO w zapleczu Prestashop. + 2. Dodaj do koszyka produkt, ktory ma zdefiniowane "Powiazane produkty". + 3. Otworz: https://drmaterac.pl/koszyk?action=show + 4. Sprawdz, czy sekcja jest pod "card cart-container" i czy slajdy zawieraja obraz, nazwe, cene, CTA. + 5. Kliknij CTA produktu prostego: powinien wejsc do koszyka. + 6. Kliknij CTA produktu z wariantami: powinno byc przekierowanie do strony produktu. + + Type "approved" to continue, or describe issues to fix + + + + + + +## DO NOT CHANGE +- Core Prestashop poza katalogiem modules/crosssellpro/* +- Istniejace modyfikacje checkoutu niezwiązane z Cross Sell PRO + +## SCOPE LIMITS +- Bez zmian w logice zamowienia, platnosci i dostawy. +- Bez rozbudowanego panelu konfiguracji modulu poza minimum potrzebnym do uruchomienia. +- Bez przebudowy layoutu calej strony koszyka. + + + + +Before declaring plan complete: +- [ ] Modul instaluje sie i aktywuje bez bledow PHP 7.4. +- [ ] Sekcja renderuje sie na /koszyk?action=show pod "card cart-container". +- [ ] Dane produktow pochodza z relacji "Powiazany produkt". +- [ ] CTA dziala poprawnie dla produktow prostych i wariantowych. +- [ ] Widok jest czytelny na desktopie i mobile. + + + +- All tasks completed +- All verification checks pass +- No errors or warnings introduced +- Cross Sell PRO dziala zgodnie z wymaganiami biznesowymi koszyka + + + +After completion, create `.paul/phases/01-cross-sell-pro-koszyk/01-01-SUMMARY.md` + diff --git a/.paul/phases/02-cross-sell-pro-w-zamowieniu/02-01-PLAN.md b/.paul/phases/02-cross-sell-pro-w-zamowieniu/02-01-PLAN.md new file mode 100644 index 00000000..71b9bfe6 --- /dev/null +++ b/.paul/phases/02-cross-sell-pro-w-zamowieniu/02-01-PLAN.md @@ -0,0 +1,157 @@ +--- +phase: 02-cross-sell-pro-w-zamowieniu +plan: 01 +type: execute +wave: 1 +depends_on: ["01-01"] +files_modified: + - modules/crosssellpro/crosssellpro.php + - modules/crosssellpro/views/templates/hook/checkoutCrossSell.tpl + - modules/crosssellpro/views/templates/hook/cartCrossSell.tpl + - modules/crosssellpro/views/js/cartCrossSell.js + - modules/crosssellpro/views/css/cartCrossSell.css +autonomous: false +--- + + +## Goal +Rozszerzyc modul Cross Sell PRO o wyswietlanie karuzeli na stronie zamowienia (`/zamowienie`) po prawej stronie checkoutu, wewnatrz `#js-checkout-summary`, przed obecna zawartoscia podsumowania. + +## Purpose +Zwikszyc szanse dosprzedazy takze na etapie checkoutu, gdy klient finalizuje zamowienie i widzi podsumowanie koszyka. + +## Output +Dzialajaca sekcja cross-sell w checkout summary, z danymi z relacji "Powiazany produkt" i zgodnym zachowaniem CTA (wariant/prosty), bez regresji sekcji koszyka. + + + +## Project Context +@.paul/PROJECT.md +@.paul/ROADMAP.md +@.paul/STATE.md + +## Prior Work +@.paul/phases/01-cross-sell-pro-koszyk/01-01-PLAN.md + +## Source Files +@modules/crosssellpro/crosssellpro.php +@modules/crosssellpro/views/templates/hook/cartCrossSell.tpl +@modules/crosssellpro/views/js/cartCrossSell.js +@modules/crosssellpro/views/css/cartCrossSell.css +@themes/leo_gstore/templates/checkout/_partials/cart-summary.tpl + + + + +## AC-1: Render sekcji w checkout summary przed obecna zawartoscia +```gherkin +Given Uzytkownik otwiera strone zamowienia /zamowienie +When Renderuje sie blok #js-checkout-summary +Then Sekcja Cross Sell PRO jest wyswietlona wewnatrz tego bloku przed aktualna zawartoscia podsumowania +``` + +## AC-2: Checkout korzysta z danych "Powiazany produkt" +```gherkin +Given W koszyku sa produkty z uzupelnionymi relacjami "Powiazany produkt" +When Modul buduje sekcje cross-sell na checkout +Then Wyswietlone pozycje pochodza z tych relacji i pomijaja produkty juz obecne w koszyku +``` + +## AC-3: CTA dziala poprawnie na checkout +```gherkin +Given Klient klika CTA w sekcji Cross Sell PRO na checkout +When Produkt jest prosty +Then Produkt jest dodawany do koszyka bez utraty mozliwosci usuwania pozycji +``` + +## AC-4: Produkty z wariantami kieruja do strony produktu +```gherkin +Given Produkt cross-sell wymaga wyboru wariantu +When Klient kliknie CTA +Then Nastapi przekierowanie do karty produktu +``` + +## AC-5: Brak regresji karuzeli na stronie koszyka +```gherkin +Given Sekcja Cross Sell PRO istnieje juz na stronie koszyka +When Wdrozenie checkoutowej wersji zostanie zakonczone +Then Karuzela na /koszyk?action=show nadal dziala jak przed zmianami +``` + + + + + + + Task 1: Rozszerzyc modul o hook checkout summary top + modules/crosssellpro/crosssellpro.php + + Dodac rejestracje i obsluge hooka `displayCheckoutSummaryTop` w module. + W metodzie hooka przygotowac dane produktowe cross-sell analogicznie do koszyka, ale zoptymalizowane pod checkout. + Zapewnic, ze render checkoutowy nie modyfikuje core ani szablonow motywu, tylko korzysta z hooka obecnego w `cart-summary.tpl`. + Utrzymac kompatybilnosc z Prestashop 1.7.8.11 i PHP 7.4. + + Na checkout hook zwraca HTML sekcji, a w przypadku pustej listy zwraca pusty output bez bledow. + AC-1 i AC-2 przygotowane po stronie backend. + + + + Task 2: Dodac widok checkout i dopasowac frontend karuzeli + modules/crosssellpro/views/templates/hook/checkoutCrossSell.tpl, modules/crosssellpro/views/templates/hook/cartCrossSell.tpl, modules/crosssellpro/views/js/cartCrossSell.js, modules/crosssellpro/views/css/cartCrossSell.css + + Utworzyc dedykowany szablon checkoutowy osadzany przez `displayCheckoutSummaryTop` tak, aby sekcja byla renderowana przed aktualna zawartoscia `#js-checkout-summary`. + Zrefaktoryzowac wspolne elementy karuzeli, aby dzialaly i w koszyku, i w checkout, bez konfliktow selektorow. + Zachowac poprawne CTA: produkty proste dodaja sie do koszyka, produkty wariantowe przechodza na karte produktu. + Upewnic sie, ze dzialanie checkoutowej sekcji nie psuje usuwania pozycji i aktualizacji podsumowania. + + Manualny test na /zamowienie i /koszyk?action=show potwierdza poprawny render, dodawanie i brak regresji. + AC-1, AC-3, AC-4, AC-5 satisfied. + + + + Cross Sell PRO w checkout summary (`#js-checkout-summary`) oraz utrzymana kompatybilnosc dotychczasowej sekcji koszyka. + + 1. Otworz: https://drmaterac.pl/zamowienie + 2. Sprawdz, czy sekcja Cross Sell jest po prawej, w `#js-checkout-summary`, przed aktualnym summary. + 3. Kliknij CTA produktu prostego i potwierdz dodanie do koszyka. + 4. Usun dodany produkt z koszyka i potwierdz, ze nie wraca automatycznie. + 5. Kliknij CTA produktu wariantowego i potwierdz przejscie na strone produktu. + 6. Wejdz na https://drmaterac.pl/koszyk?action=show i potwierdz, ze karuzela koszykowa nadal dziala. + + Type "approved" to continue, or describe issues to fix + + + + + + +## DO NOT CHANGE +- Pliki core Prestashop poza modulem `modules/crosssellpro/*` +- Szablony motywu poza wykorzystaniem istniejacego hooka `displayCheckoutSummaryTop` + +## SCOPE LIMITS +- Brak zmian w logice platnosci, dostawy i finalizacji zamowienia +- Brak przebudowy calego checkout layoutu +- Brak dodatkowego panelu konfiguracji na tym etapie + + + + +Before declaring plan complete: +- [ ] Sekcja wyswietla sie na /zamowienie wewnatrz `#js-checkout-summary` przed summary content. +- [ ] Produkty pochodza z relacji "Powiazany produkt". +- [ ] CTA produktow prostych dodaje do koszyka bez regresji usuwania. +- [ ] CTA produktow wariantowych kieruje na strone produktu. +- [ ] Dotychczasowa sekcja cross-sell na koszyku nadal dziala. + + + +- All tasks completed +- All verification checks pass +- No errors or warnings introduced +- Cross Sell PRO dziala w koszyku i checkout bez konfliktow + + + +After completion, create `.paul/phases/02-cross-sell-pro-w-zamowieniu/02-01-SUMMARY.md` + diff --git a/.paul/phases/02-cross-sell-pro-w-zamowieniu/02-01-SUMMARY.md b/.paul/phases/02-cross-sell-pro-w-zamowieniu/02-01-SUMMARY.md new file mode 100644 index 00000000..e00cc043 --- /dev/null +++ b/.paul/phases/02-cross-sell-pro-w-zamowieniu/02-01-SUMMARY.md @@ -0,0 +1,118 @@ +--- +phase: 02-cross-sell-pro-w-zamowieniu +plan: 01 +subsystem: ui +tags: [prestashop, checkout, cross-sell, carousel, cart] +requires: + - phase: 01-cross-sell-pro-koszyk + provides: Bazowa logika pobierania produktow powiazanych i CTA +provides: + - Cross Sell PRO w checkout summary (#js-checkout-summary) + - Wspolny render karuzeli dla koszyka i checkout + - Stabilna obsluga CTA bez regresji usuwania z koszyka +affects: [checkout, cart, cross-sell] +tech-stack: + added: [] + patterns: [Hook-driven rendering, shared template with mode switch] +key-files: + created: + - modules/crosssellpro/views/templates/hook/checkoutCrossSell.tpl + modified: + - modules/crosssellpro/crosssellpro.php + - modules/crosssellpro/views/templates/hook/cartCrossSell.tpl + - modules/crosssellpro/views/js/cartCrossSell.js + - modules/crosssellpro/views/css/cartCrossSell.css +key-decisions: + - "Uzycie displayCheckoutSummaryTop zamiast modyfikacji motywu" + - "Rozdzielenie trybow cart/checkout przez data attributes" +patterns-established: + - "Wspolny komponent karuzeli sterowany przez crosssellpro_mode" + - "Defensywna obsluga eventow i fallback add-to-cart" +duration: 21min +started: 2026-03-31T23:38:00+02:00 +completed: 2026-03-31T23:59:56+02:00 +--- + +# Phase 2 Plan 01: Checkout Cross Sell Summary + +Cross Sell PRO zostal rozszerzony o dzialanie na etapie zamowienia i osadzony w prawym panelu `#js-checkout-summary` przed aktualna zawartoscia. + +## Performance + +| Metric | Value | +|--------|-------| +| Duration | 21 min | +| Started | 2026-03-31T23:38:00+02:00 | +| Completed | 2026-03-31T23:59:56+02:00 | +| Tasks | 2 completed + 1 checkpoint approved | +| Files modified | 5 | + +## Acceptance Criteria Results + +| Criterion | Status | Notes | +|-----------|--------|-------| +| AC-1: Render sekcji w checkout summary przed obecna zawartoscia | Pass | Sekcja jest renderowana przez hook `displayCheckoutSummaryTop` w `#js-checkout-summary`. | +| AC-2: Checkout korzysta z danych "Powiazany produkt" | Pass | Zastosowano te sama logike zbierania produktow powiazanych co w koszyku. | +| AC-3: CTA dziala poprawnie na checkout | Pass | Dodawanie produktu prostego dziala bez regresji usuwania z koszyka. | +| AC-4: Produkty z wariantami kieruja do strony produktu | Pass | CTA wariantow kieruje na URL produktu. | +| AC-5: Brak regresji karuzeli na stronie koszyka | Pass | Po poprawkach potwierdzono poprawne dzialanie takze na `/koszyk?action=show`. | + +## Accomplishments + +- Wdrozono checkoutowy punkt wejscia cross-sell bez ingerencji w core i bez zmian plikow motywu. +- Ujednolicono rendering karuzeli miedzy koszykiem i checkoutem. +- Ustabilizowano zachowanie add-to-cart/usuwania oraz reakcje przyciskow karuzeli. + +## Files Created/Modified + +| File | Change | Purpose | +|------|--------|---------| +| `modules/crosssellpro/crosssellpro.php` | Modified | Rejestracja i obsluga hookow checkout/cart, assety, routing trybow. | +| `modules/crosssellpro/views/templates/hook/checkoutCrossSell.tpl` | Created | Checkoutowy punkt renderu sekcji cross-sell. | +| `modules/crosssellpro/views/templates/hook/cartCrossSell.tpl` | Modified | Wspolny szablon komponentu i data-attributes dla trybow. | +| `modules/crosssellpro/views/js/cartCrossSell.js` | Modified | Obsluga strzalek i add-to-cart dla cart/checkout. | +| `modules/crosssellpro/views/css/cartCrossSell.css` | Modified | Dostosowanie wygladu checkout (bez ramki, slajd 100%). | + +## Decisions Made + +| Decision | Rationale | Impact | +|----------|-----------|--------| +| Hook `displayCheckoutSummaryTop` jako punkt osadzenia | Zapewnia pozycje "przed obecna zawartoscia" i zgodnosc z motywem | Brak potrzeby modyfikacji `themes/` | +| Rozdzielenie trybow przez `crosssellpro_mode` | Jeden komponent dla dwoch miejsc osadzenia | Mniej duplikacji i prostsze utrzymanie | +| Utrzymanie fallbackow dla add-to-cart | W praktyce niweluje problemy z cache/refresh checkoutu | Stabilniejsze UX w produkcji | + +## Deviations from Plan + +### Summary + +| Type | Count | Impact | +|------|-------|--------| +| Auto-fixed | 4 | Niezbedne poprawki stabilnosci bez scope creep | +| Scope additions | 0 | Brak | +| Deferred | 0 | Brak | + +**Total impact:** Wymagane poprawki integracyjne z motywem Leo i cache frontend, finalnie bez zmiany celu fazy. + +## Issues Encountered + +| Issue | Resolution | +|-------|------------| +| Karuzela w checkout renderowala sie jako lista pionowa | Wymuszono checkout-specific layout i usunieto kolidujace style inline. | +| Dodawanie produktu powodowalo regresje usuwania z koszyka | Przebudowano flow CTA i powroty URL po dodaniu. | +| Brak reakcji strzalek po niektorych odswiezeniach | Ustabilizowano bindowanie JS i asset loading dla `cart/order`. | + +## Next Phase Readiness + +**Ready:** +- Modul obsluguje cross-sell na koszyku i checkout. +- Hooki i szablony sa przygotowane do dalszych iteracji (np. konfiguracja BO). + +**Concerns:** +- Brak. + +**Blockers:** +- None. + +--- +*Phase: 02-cross-sell-pro-w-zamowieniu, Plan: 01* +*Completed: 2026-03-31* diff --git a/.serena/project.yml b/.serena/project.yml index 256c13b1..5bd09d7a 100644 --- a/.serena/project.yml +++ b/.serena/project.yml @@ -136,3 +136,17 @@ symbol_info_budget: # list of regex patterns which, when matched, mark a memory entry as read‑only. # Extends the list from the global configuration, merging the two lists. read_only_memory_patterns: [] + +# advanced configuration option allowing to configure language server-specific options. +# Maps the language key to the options. +# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available. +# No documentation on options means no options are available. +ls_specific_settings: {} + +# list of regex patterns for memories to completely ignore. +# Matching memories will not appear in list_memories or activate_project output +# and cannot be accessed via read_memory or write_memory. +# To access ignored memory files, use the read_file tool on the raw file path. +# Extends the list from the global configuration, merging the two lists. +# Example: ["_archive/.*", "_episodes/.*"] +ignored_memory_patterns: [] diff --git a/.vscode/ftp-kr.json b/.vscode/ftp-kr.json index 8e3d3d08..666cffbf 100644 --- a/.vscode/ftp-kr.json +++ b/.vscode/ftp-kr.json @@ -10,5 +10,13 @@ "autoDelete": false, "autoDownload": false, "ignoreRemoteModification": true, - "ignore": [".git", "/.vscode"] + "ignore": [ + ".git", + "/.vscode", + "/.claude", + "AGENTS.md", + "/.paul", + "/.idea", + "/.serena" + ] } diff --git a/.vscode/ftp-kr.sync.cache.json b/.vscode/ftp-kr.sync.cache.json index 5f380abe..cb781796 100644 --- a/.vscode/ftp-kr.sync.cache.json +++ b/.vscode/ftp-kr.sync.cache.json @@ -194,6 +194,14 @@ "size": 96, "lmtime": 1772461611751, "modified": false + }, + "memory": { + "feedback_changelog.md": { + "type": "-", + "size": 636, + "lmtime": 1773876629308, + "modified": false + } } }, "composer.lock": { @@ -386,7 +394,7 @@ }, ".htaccess": { "type": "-", - "size": 19570, + "size": 19646, "lmtime": 1772451998229, "modified": true }, @@ -588,7 +596,20 @@ }, "sql": {}, "src": {}, - "translations": {}, + "translations": { + "index.php": { + "type": "-", + "size": 725, + "lmtime": 0, + "modified": false + }, + "pl.php": { + "type": "-", + "size": 4868, + "lmtime": 1773876189238, + "modified": false + } + }, "upgrade": {}, "vendor": {}, "views": { @@ -609,6 +630,79 @@ } } } + }, + "gm_omniprice": { + "CHANGELOG": { + "type": "-", + "size": 5258, + "lmtime": 1773876246123, + "modified": false + }, + "cleanup.php": { + "type": "-", + "size": 338, + "lmtime": 0, + "modified": false + }, + "config_pl.xml": { + "type": "-", + "size": 544, + "lmtime": 0, + "modified": false + }, + "controllers": {}, + "cron.php": { + "type": "-", + "size": 1492, + "lmtime": 0, + "modified": false + }, + "fill.php": { + "type": "-", + "size": 336, + "lmtime": 0, + "modified": false + }, + "gm_omniprice.php": { + "type": "-", + "size": 90693, + "lmtime": 0, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1269, + "lmtime": 0, + "modified": false + }, + "logo.png": { + "type": "-", + "size": 4539, + "lmtime": 0, + "modified": false + }, + "logo.webp": { + "type": "-", + "size": 1980, + "lmtime": 0, + "modified": false + }, + "template.php": { + "type": "-", + "size": 1865, + "lmtime": 0, + "modified": false + }, + "translations": { + "pl.php": { + "type": "-", + "size": 9319, + "lmtime": 1773876195088, + "modified": false + } + }, + "upgrade": {}, + "views": {} } }, "nov": {}, @@ -747,6 +841,27 @@ "size": 448, "lmtime": 0, "modified": false + }, + ".serena": { + ".gitignore": { + "type": "-", + "size": 28, + "lmtime": 1773876286926, + "modified": false + }, + "memories": {}, + "project.local.yml": { + "type": "-", + "size": 407, + "lmtime": 1773876286922, + "modified": false + }, + "project.yml": { + "type": "-", + "size": 8787, + "lmtime": 1773876286912, + "modified": false + } } } } diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..95ea3c73 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,37 @@ +# Repository Guidelines + +## Project Structure & Module Organization +This repository is a PrestaShop-based store with customizations across core-like folders and modules. +- `controllers/`, `classes/`, `src/`: main PHP application logic (legacy + Symfony-style code). +- `modules/`: feature modules (custom and vendor-like). Most front-end build/test tooling lives here. +- `themes/` and `themes/leo_gstore/`: storefront templates, assets, and theme behavior. +- `override/`: PrestaShop overrides; keep changes minimal and well-justified. +- `app/config/`, `config/`: environment and platform configuration. +- `changelog/`: dated change notes (for example `changelog/2026-03-19.md`). + +## Build, Test, and Development Commands +There is no single root build command; use module-scoped workflows. +- `php -l path\to\file.php`: quick PHP syntax check before commit. +- `cd modules\ps_facetedsearch && npm install && npm run dev`: install deps and run watch build. +- `cd modules\ps_facetedsearch && npm run build`: production JS/CSS bundle. +- `cd modules\ps_facetedsearch && npm test`: run Mocha tests (`tests/**/*.spec.js`). +- `cd modules\gsitemap && composer install`: install PHP dev dependencies for that module. + +## Coding Style & Naming Conventions +- Follow existing PrestaShop conventions: PHP classes in `PascalCase`, methods in `camelCase`, constants in `UPPER_SNAKE_CASE`. +- Keep indentation/style consistent with surrounding file (legacy files vary; do not reformat unrelated code). +- Use descriptive module/template names (example: `AdminSearchController.php`, `search-results.tpl`). +- Prefer changes in `modules/` or `themes/` over editing vendor code in `vendor/` or `phpmailer/`. + +## Testing Guidelines +- Test at the smallest valid scope: module-level first, then manual store/admin verification. +- JS tests use `*.spec.js` (see `modules/ps_facetedsearch/tests`). +- Run syntax checks for edited PHP files and exercise affected BO/FO pages (cart, product page, checkout, admin config). +- No global coverage gate is enforced; add/update tests when touching tested modules. + +## Commit & Pull Request Guidelines +- Prefer clear, typed commits: `feat: ...`, `fix: ...`, `chore: ...`. +- Avoid vague messages like `update` or `Save`. +- Keep commits focused to one concern and include touched paths in PR description. +- PRs should include: purpose, risk/rollback notes, manual test steps, and screenshots for UI/theme changes. +- For user-visible behavior changes, add/update a dated file in `changelog/`. diff --git a/changelog/2026-04-01.md b/changelog/2026-04-01.md new file mode 100644 index 00000000..9de5dc1b --- /dev/null +++ b/changelog/2026-04-01.md @@ -0,0 +1,27 @@ +# 2026-04-01 + +## Co zrobiono +- Wdrożono moduł `crosssellpro` z karuzelą produktów powiązanych (cross-sell) na stronie koszyka (`/koszyk?action=show`). +- Rozszerzono moduł o działanie na etapie zamówienia (`/zamowienie`) w bloku `#js-checkout-summary` przed istniejącą zawartością. +- Ujednolicono logikę CTA: +- Dodano stabilizację ładowania assetów i obsługi JS (koszyk + checkout), aby przyciski karuzeli działały poprawnie po odświeżeniach i aktualizacjach checkoutu. +- Dopracowano wygląd sekcji cross-sell w checkout (bez zbędnej ramki/paddingu, układ 1 slajd na pełną szerokość panelu). +- Dodano komendę $changelog do automatycznego generowania changeloga. +- Test changelog command + +## Zmienione pliki +- `modules/crosssellpro/crosssellpro.php` +- `modules/crosssellpro/views/templates/hook/cartCrossSell.tpl` +- `modules/crosssellpro/views/templates/hook/checkoutCrossSell.tpl` +- `modules/crosssellpro/views/js/cartCrossSell.js` +- `modules/crosssellpro/views/css/cartCrossSell.css` +- `.claude/memory/MEMORY.md` +- `.paul/` +- `.serena/project.yml` +- `.vscode/ftp-kr.json` +- `.vscode/ftp-kr.sync.cache.json` +- `AGENTS.md` +- `changelog/2026-04-01.md` +- `modules/crosssellpro/` +- `scripts/` + diff --git a/modules/crosssellpro/crosssellpro.php b/modules/crosssellpro/crosssellpro.php new file mode 100644 index 00000000..2ed3aa0e --- /dev/null +++ b/modules/crosssellpro/crosssellpro.php @@ -0,0 +1,335 @@ +name = 'crosssellpro'; + $this->tab = 'pricing_promotion'; + $this->version = '1.1.6'; + $this->author = 'Pyziak Jacek'; + $this->need_instance = 0; + $this->bootstrap = true; + + parent::__construct(); + + $this->displayName = $this->l('Cross Sell PRO'); + $this->description = $this->l('Displays related products carousel in cart based on product associations.'); + } + + public function install() + { + return parent::install() + && $this->registerHook('displayShoppingCartFooter') + && $this->registerHook('displayCheckoutSummaryTop') + && $this->registerHook('displayHeader') + && $this->registerHook('actionFrontControllerSetMedia'); + } + + public function uninstall() + { + return parent::uninstall(); + } + + public function hookActionFrontControllerSetMedia() + { + if (!$this->context || !$this->context->controller) { + return; + } + + if (!$this->isRegisteredInHook('displayCheckoutSummaryTop')) { + $this->registerHook('displayCheckoutSummaryTop'); + } + if (!$this->isRegisteredInHook('displayHeader')) { + $this->registerHook('displayHeader'); + } + + $controllerName = (string) $this->context->controller->php_self; + if (!in_array($controllerName, ['cart', 'order'], true)) { + return; + } + + $this->registerAssets(); + } + + public function hookDisplayHeader() + { + if (!$this->context || !$this->context->controller) { + return; + } + + $controllerName = (string) $this->context->controller->php_self; + if (!in_array($controllerName, ['cart', 'order'], true)) { + return; + } + + $this->registerAssets(); + } + + public function hookDisplayShoppingCartFooter($params) + { + if (!$this->context || !$this->context->cart || !$this->context->cart->id) { + return ''; + } + + $products = $this->buildCrossSellProducts(); + if (empty($products)) { + return ''; + } + + $this->context->smarty->assign([ + 'crosssellpro_products' => $products, + 'crosssellpro_cart_url' => $this->context->link->getPageLink('cart', true), + 'crosssellpro_return_url' => $this->context->link->getPageLink('cart', true, null, 'action=show'), + 'crosssellpro_mode' => 'cart', + ]); + + return $this->fetch('module:' . $this->name . '/views/templates/hook/cartCrossSell.tpl'); + } + + public function hookDisplayCheckoutSummaryTop($params) + { + if (!$this->context || !$this->context->cart || !$this->context->cart->id) { + return ''; + } + + $products = $this->buildCrossSellProducts(); + if (empty($products)) { + return ''; + } + + $this->context->smarty->assign([ + 'crosssellpro_products' => $products, + 'crosssellpro_cart_url' => $this->context->link->getPageLink('cart', true), + 'crosssellpro_return_url' => $this->context->link->getPageLink('order', true), + 'crosssellpro_mode' => 'checkout', + ]); + + return $this->fetch('module:' . $this->name . '/views/templates/hook/checkoutCrossSell.tpl'); + } + + /** + * Builds presented products list from accessories of products currently in cart. + * + * @return array + */ + protected function buildCrossSellProducts() + { + $cartProducts = $this->context->cart->getProducts(true); + if (empty($cartProducts)) { + return []; + } + + $inCartProductIds = []; + foreach ($cartProducts as $cartProduct) { + $inCartProductIds[(int) $cartProduct['id_product']] = true; + } + + $relatedIds = $this->collectAccessoryIds(array_keys($inCartProductIds)); + if (empty($relatedIds)) { + return []; + } + + $relatedIds = array_values(array_diff($relatedIds, array_keys($inCartProductIds))); + if (empty($relatedIds)) { + return []; + } + + $limitedIds = array_slice($relatedIds, 0, static::DEFAULT_LIMIT); + $products = $this->presentProducts($limitedIds); + if (empty($products)) { + return []; + } + + $combinationFlags = $this->getCombinationFlags($limitedIds); + foreach ($products as &$product) { + $productId = (int) $product['id_product']; + $requiresSelection = !empty($combinationFlags[$productId]) || empty($product['add_to_cart_url']); + + $product['crosssellpro_requires_selection'] = $requiresSelection; + if ($requiresSelection) { + $product['crosssellpro_cta_url'] = $product['url']; + $product['crosssellpro_cta_label'] = $this->l('Wybierz wariant'); + } else { + $qty = 1; + if (isset($product['minimal_quantity']) && (int) $product['minimal_quantity'] > 0) { + $qty = (int) $product['minimal_quantity']; + } + + $product['crosssellpro_post_add_url'] = $this->context->link->getPageLink( + 'cart', + true, + null, + 'add=1&id_product=' . $productId . '&qty=' . $qty . '&token=' . Tools::getToken(false) + ); + $product['crosssellpro_cta_url'] = $this->context->link->getPageLink('cart', true, null, 'action=show'); + $product['crosssellpro_cta_label'] = $this->l('Dodaj do koszyka'); + } + } + unset($product); + + return $products; + } + + /** + * @param int[] $productIds + * + * @return int[] + */ + protected function collectAccessoryIds(array $productIds) + { + $ids = []; + + foreach ($productIds as $productId) { + $accessories = Product::getAccessoriesLight( + (int) $this->context->language->id, + (int) $productId, + (Context::getContext() === null ? $this->context : Context::getContext()) + ); + + if (empty($accessories)) { + continue; + } + + foreach ($accessories as $accessory) { + if (!isset($accessory['id_product'])) { + continue; + } + + $accessoryId = (int) $accessory['id_product']; + if ($accessoryId > 0) { + $ids[$accessoryId] = $accessoryId; + } + } + } + + return array_values($ids); + } + + /** + * @param int[] $productIds + * + * @return array + */ + protected function getCombinationFlags(array $productIds) + { + if (empty($productIds)) { + return []; + } + + $rows = Db::getInstance((bool) _PS_USE_SQL_SLAVE_)->executeS( + 'SELECT p.id_product, COUNT(pa.id_product_attribute) AS combinations + FROM `' . _DB_PREFIX_ . 'product` p + LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute` pa ON (pa.id_product = p.id_product) + WHERE p.id_product IN (' . implode(',', array_map('intval', $productIds)) . ') + GROUP BY p.id_product' + ); + + $flags = []; + foreach ($rows as $row) { + $flags[(int) $row['id_product']] = ((int) $row['combinations'] > 0); + } + + return $flags; + } + + /** + * @param int[] $productIds + * + * @return array + */ + protected function presentProducts(array $productIds) + { + if (empty($productIds)) { + return []; + } + + $rawProducts = Product::getProductsProperties( + (int) $this->context->language->id, + Db::getInstance((bool) _PS_USE_SQL_SLAVE_)->executeS( + 'SELECT p.id_product + FROM `' . _DB_PREFIX_ . 'product` p + ' . Shop::addSqlAssociation('product', 'p') . ' + WHERE p.id_product IN (' . implode(',', array_map('intval', $productIds)) . ') + AND p.active = 1 + AND product_shop.visibility IN ("both", "catalog") + ORDER BY FIELD(p.id_product,' . implode(',', array_map('intval', $productIds)) . ')' + ) + ); + + if (empty($rawProducts)) { + return []; + } + + $assembler = new ProductAssembler($this->context); + $presenterFactory = new ProductPresenterFactory($this->context); + $presentationSettings = $presenterFactory->getPresentationSettings(); + $presentationSettings->showPrices = true; + + if (version_compare(_PS_VERSION_, '1.7.5', '>=')) { + $presenter = new \PrestaShop\PrestaShop\Adapter\Presenter\Product\ProductListingPresenter( + new ImageRetriever($this->context->link), + $this->context->link, + new PriceFormatter(), + new ProductColorsRetriever(), + $this->context->getTranslator() + ); + } else { + $presenter = new \PrestaShop\PrestaShop\Core\Product\ProductListingPresenter( + new ImageRetriever($this->context->link), + $this->context->link, + new PriceFormatter(), + new ProductColorsRetriever(), + $this->context->getTranslator() + ); + } + + $productsForTemplate = []; + foreach ($rawProducts as $rawProduct) { + $presented = $presenter->present( + $presentationSettings, + $assembler->assembleProduct($rawProduct), + $this->context->language + ); + + if (!empty($presented['add_to_cart_url']) || !empty($presented['url'])) { + $productsForTemplate[] = $presented; + } + } + + return $productsForTemplate; + } + + protected function registerAssets() + { + $this->context->controller->registerStylesheet( + 'module-crosssellpro-cart', + 'modules/' . $this->name . '/views/css/cartCrossSell.css', + ['media' => 'all', 'priority' => 150] + ); + + $this->context->controller->registerJavascript( + 'module-crosssellpro-cart', + 'modules/' . $this->name . '/views/js/cartCrossSell.js', + ['position' => 'bottom', 'priority' => 150] + ); + } +} + diff --git a/modules/crosssellpro/views/css/cartCrossSell.css b/modules/crosssellpro/views/css/cartCrossSell.css new file mode 100644 index 00000000..2a73bd6f --- /dev/null +++ b/modules/crosssellpro/views/css/cartCrossSell.css @@ -0,0 +1,158 @@ +.crosssellpro-block { + margin: 1.25rem 0; +} + +.crosssellpro-block[data-crosssellpro-mode="checkout"] { + margin: 0 0 1rem; + border: 0; + box-shadow: none; + background: transparent; +} + +.crosssellpro-block[data-crosssellpro-mode="checkout"] .card-block { + padding: 0; +} + +.crosssellpro-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1rem; +} + +.crosssellpro-title { + margin: 0; +} + +.crosssellpro-nav { + display: flex; + gap: 0.5rem; +} + +.crosssellpro-nav-btn { + width: 2rem; + height: 2rem; + border: 1px solid #c8c8c8; + border-radius: 999px; + background: #fff; + color: #222; + line-height: 1; + cursor: pointer; +} + +.crosssellpro-viewport { + overflow-x: auto; + scrollbar-width: thin; + scroll-snap-type: x mandatory; +} + +.crosssellpro-track { + display: flex; + gap: 1rem; + align-items: stretch; +} + +.crosssellpro-item { + flex: 0 0 calc((100% - 2rem) / 3); + min-width: 220px; + border: 1px solid #efefef; + border-radius: 6px; + padding: 0.75rem; + display: flex; + flex-direction: column; + gap: 0.5rem; + background: #fff; + scroll-snap-align: start; +} + +.crosssellpro-block[data-crosssellpro-mode="checkout"] .crosssellpro-item { + flex: 0 0 100%; + min-width: 100%; + border: 0; + padding: 0; + border-radius: 0; + background: transparent; +} + +.crosssellpro-block[data-crosssellpro-mode="checkout"] .crosssellpro-viewport { + overflow-x: auto !important; + overflow-y: hidden !important; +} + +.crosssellpro-block[data-crosssellpro-mode="checkout"] .crosssellpro-track { + display: flex !important; + flex-direction: row !important; + flex-wrap: nowrap !important; + align-items: stretch !important; + gap: 0 !important; +} + +.crosssellpro-block[data-crosssellpro-mode="checkout"] .crosssellpro-item { + flex: 0 0 100% !important; + width: 100% !important; + max-width: 100% !important; +} + +.crosssellpro-image-link { + display: block; + text-align: center; +} + +.crosssellpro-image-link img { + width: 100%; + max-height: 140px; + object-fit: contain; +} + +.crosssellpro-image-placeholder { + display: block; + width: 100%; + height: 140px; + background: #f7f7f7; +} + +.crosssellpro-name { + font-size: 0.95rem; + margin: 0; + min-height: 2.6em; +} + +.crosssellpro-name a { + color: #222; + text-decoration: none; +} + +.crosssellpro-price { + font-weight: 700; + margin-top: auto; +} + +.crosssellpro-cta { + width: 100%; + margin-top: 0.25rem; +} + +@media (max-width: 991px) { + .crosssellpro-item { + flex-basis: calc((100% - 1rem) / 2); + min-width: 180px; + } + + .crosssellpro-block[data-crosssellpro-mode="checkout"] .crosssellpro-item { + flex-basis: 100%; + min-width: 100%; + } +} + +@media (max-width: 767px) { + .crosssellpro-item { + flex-basis: 100%; + min-width: 100%; + } + + .crosssellpro-header { + align-items: flex-start; + flex-direction: column; + } +} diff --git a/modules/crosssellpro/views/js/cartCrossSell.js b/modules/crosssellpro/views/js/cartCrossSell.js new file mode 100644 index 00000000..e685debf --- /dev/null +++ b/modules/crosssellpro/views/js/cartCrossSell.js @@ -0,0 +1,98 @@ +document.addEventListener('DOMContentLoaded', function () { + var positionCartBlocks = function () { + var blocks = document.querySelectorAll('[data-crosssellpro-block="1"][data-crosssellpro-mode="cart"]'); + Array.prototype.forEach.call(blocks, function (block) { + var cartContainer = document.querySelector('.card.cart-container'); + if (cartContainer && cartContainer.parentNode) { + cartContainer.insertAdjacentElement('afterend', block); + } + }); + }; + + var getStep = function (block) { + var track = block.querySelector('.js-crosssellpro-track'); + if (!track) { + return 280; + } + var firstItem = track.querySelector('.crosssellpro-item'); + if (!firstItem) { + return 280; + } + var style = window.getComputedStyle(track); + var gap = parseFloat(style.columnGap || style.gap || '0'); + return firstItem.offsetWidth + gap; + }; + + positionCartBlocks(); + + document.addEventListener('click', function (event) { + var prevBtn = event.target.closest('.js-crosssellpro-prev'); + if (prevBtn) { + var prevBlock = prevBtn.closest('[data-crosssellpro-block="1"]'); + var prevViewport = prevBlock ? prevBlock.querySelector('.js-crosssellpro-viewport') : null; + if (prevViewport && prevBlock) { + event.preventDefault(); + prevViewport.scrollBy({ left: -getStep(prevBlock), behavior: 'smooth' }); + } + return; + } + + var nextBtn = event.target.closest('.js-crosssellpro-next'); + if (nextBtn) { + var nextBlock = nextBtn.closest('[data-crosssellpro-block="1"]'); + var nextViewport = nextBlock ? nextBlock.querySelector('.js-crosssellpro-viewport') : null; + if (nextViewport && nextBlock) { + event.preventDefault(); + nextViewport.scrollBy({ left: getStep(nextBlock), behavior: 'smooth' }); + } + return; + } + + var addBtn = event.target.closest('[data-crosssellpro-add="1"]'); + if (!addBtn) { + return; + } + + event.preventDefault(); + + var block = addBtn.closest('[data-crosssellpro-block="1"]'); + if (!block) { + window.location.href = addBtn.getAttribute('href') || '/'; + return; + } + + var cartUrl = block.getAttribute('data-cart-url'); + var returnUrl = block.getAttribute('data-return-url'); + var token = block.getAttribute('data-token'); + var productId = addBtn.getAttribute('data-id-product'); + var qty = addBtn.getAttribute('data-qty') || '1'; + var postAddUrl = addBtn.getAttribute('data-post-add-url'); + + if (!cartUrl || !token || !productId) { + window.location.href = returnUrl || cartUrl || '/'; + return; + } + + var body = new URLSearchParams(); + body.set('token', token); + body.set('id_product', productId); + body.set('qty', qty); + body.set('add', '1'); + body.set('action', 'update'); + + fetch(postAddUrl || cartUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' + }, + body: body.toString(), + credentials: 'same-origin' + }) + .then(function () { + window.location.href = returnUrl || cartUrl; + }) + .catch(function () { + window.location.href = returnUrl || cartUrl || '/'; + }); + }); +}); diff --git a/modules/crosssellpro/views/templates/hook/cartCrossSell.tpl b/modules/crosssellpro/views/templates/hook/cartCrossSell.tpl new file mode 100644 index 00000000..bb5457ac --- /dev/null +++ b/modules/crosssellpro/views/templates/hook/cartCrossSell.tpl @@ -0,0 +1,70 @@ +{if !empty($crosssellpro_products)} +{assign var='crosssellpro_is_checkout' value=(isset($crosssellpro_mode) && $crosssellpro_mode == 'checkout')} +
+
+
+

{l s='Produkty, ktore moga Ci sie przydac' mod='crosssellpro'}

+ +
+ +
+
+ {foreach from=$crosssellpro_products item=product} + + {/foreach} +
+
+
+
+{/if} diff --git a/modules/crosssellpro/views/templates/hook/checkoutCrossSell.tpl b/modules/crosssellpro/views/templates/hook/checkoutCrossSell.tpl new file mode 100644 index 00000000..5099d172 --- /dev/null +++ b/modules/crosssellpro/views/templates/hook/checkoutCrossSell.tpl @@ -0,0 +1 @@ +{include file='module:crosssellpro/views/templates/hook/cartCrossSell.tpl'} diff --git a/scripts/changelog.cmd b/scripts/changelog.cmd new file mode 100644 index 00000000..0bb7d321 --- /dev/null +++ b/scripts/changelog.cmd @@ -0,0 +1,4 @@ +@echo off +setlocal +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0changelog.ps1" %* +endlocal diff --git a/scripts/changelog.ps1 b/scripts/changelog.ps1 new file mode 100644 index 00000000..9b09914e --- /dev/null +++ b/scripts/changelog.ps1 @@ -0,0 +1,117 @@ +param( + [string]$ProjectRoot = (Get-Location).Path, + [string]$Date = (Get-Date -Format 'yyyy-MM-dd'), + [string[]]$WhatDone, + [string[]]$Files, + [switch]$NoGit +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$projectPath = (Resolve-Path -LiteralPath $ProjectRoot).Path +$changelogDir = Join-Path $projectPath 'changelog' +if (-not (Test-Path -LiteralPath $changelogDir)) { + New-Item -ItemType Directory -Path $changelogDir | Out-Null +} + +$targetFile = Join-Path $changelogDir ("{0}.md" -f $Date) + +function Get-GitChangedFiles { + param([string]$Root) + + if ($NoGit) { return @() } + if (-not (Get-Command git -ErrorAction SilentlyContinue)) { return @() } + + $inside = & git -C $Root rev-parse --is-inside-work-tree 2>$null + if ($LASTEXITCODE -ne 0 -or $inside -ne 'true') { return @() } + + $lines = & git -C $Root status --porcelain + $paths = @() + + foreach ($line in $lines) { + if ([string]::IsNullOrWhiteSpace($line) -or $line.Length -lt 4) { continue } + $raw = $line.Substring(3).Trim() + + if ($raw -match ' -> ') { + $raw = ($raw -split ' -> ')[-1].Trim() + } + + $raw = $raw.Trim('"') + if (-not [string]::IsNullOrWhiteSpace($raw)) { + $paths += $raw + } + } + + return $paths | Sort-Object -Unique +} + +function Merge-Unique { + param([string[]]$Existing, [string[]]$Incoming) + + $combined = @() + $seen = @{} + + foreach ($item in @($Existing + $Incoming)) { + if ([string]::IsNullOrWhiteSpace($item)) { continue } + $k = $item.Trim() + if (-not $seen.ContainsKey($k)) { + $seen[$k] = $true + $combined += $k + } + } + + return $combined +} + +$changedFiles = if ($Files -and $Files.Count -gt 0) { $Files } else { Get-GitChangedFiles -Root $projectPath } + +if (-not $WhatDone -or $WhatDone.Count -eq 0) { + $WhatDone = @('Aktualizacja projektu oraz uporzadkowanie zmian w repozytorium.') +} + +$existingWhatDone = @() +$existingFiles = @() + +if (Test-Path -LiteralPath $targetFile) { + $content = Get-Content -LiteralPath $targetFile + $section = '' + + foreach ($line in $content) { + if ($line -match '^##\s+Co zrobiono') { $section = 'done'; continue } + if ($line -match '^##\s+Zmienione pliki') { $section = 'files'; continue } + + if ($line -match '^-\s+(.*)$') { + $value = $matches[1].Trim() + if ($section -eq 'done') { + $existingWhatDone += $value + } elseif ($section -eq 'files') { + $existingFiles += $value.Trim('`') + } + } + } +} + +$finalWhatDone = Merge-Unique -Existing $existingWhatDone -Incoming $WhatDone +$finalFiles = Merge-Unique -Existing $existingFiles -Incoming $changedFiles + +$sb = New-Object System.Text.StringBuilder +[void]$sb.AppendLine("# $Date") +[void]$sb.AppendLine() +[void]$sb.AppendLine('## Co zrobiono') +foreach ($item in $finalWhatDone) { + [void]$sb.AppendLine("- $item") +} + +[void]$sb.AppendLine() +[void]$sb.AppendLine('## Zmienione pliki') +if ($finalFiles.Count -eq 0) { + [void]$sb.AppendLine('- Brak wykrytych zmian plikow (uzupelnij recznie, jesli potrzeba).') +} else { + foreach ($file in $finalFiles) { + [void]$sb.AppendLine(('- `{0}`' -f $file)) + } +} + +Set-Content -LiteralPath $targetFile -Value $sb.ToString() -Encoding UTF8 +Write-Output ("Changelog updated: {0}" -f $targetFile) diff --git a/scripts/codex-export.cmd b/scripts/codex-export.cmd new file mode 100644 index 00000000..917b52c6 --- /dev/null +++ b/scripts/codex-export.cmd @@ -0,0 +1,4 @@ +@echo off +setlocal +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0codex-export.ps1" %* +endlocal diff --git a/scripts/codex-export.ps1 b/scripts/codex-export.ps1 new file mode 100644 index 00000000..02f223da --- /dev/null +++ b/scripts/codex-export.ps1 @@ -0,0 +1,102 @@ +param( + [string]$DestinationRoot = 'D:\notatnik-ai\codex', + [string[]]$IncludePaths = @(), + [string]$BackupName +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +if (-not $BackupName) { + $BackupName = 'codex-backup-{0}.zip' -f (Get-Date -Format 'yyyyMMdd-HHmmss') +} + +if (-not (Test-Path -LiteralPath $DestinationRoot)) { + New-Item -ItemType Directory -Path $DestinationRoot -Force | Out-Null +} + +$homePath = [Environment]::GetFolderPath('UserProfile') +if (-not $IncludePaths -or $IncludePaths.Count -eq 0) { + $IncludePaths = @((Join-Path $homePath '.codex')) +} + +$resolvedItems = @() +foreach ($path in $IncludePaths) { + if ([string]::IsNullOrWhiteSpace($path)) { continue } + + $expanded = [Environment]::ExpandEnvironmentVariables($path) + $expanded = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($expanded) + + if (Test-Path -LiteralPath $expanded) { + $resolvedItems += (Resolve-Path -LiteralPath $expanded).Path + } +} + +if ($resolvedItems.Count -eq 0) { + throw 'Nie znaleziono zadnych sciezek do eksportu (domyslnie oczekiwano ~/.codex).' +} + +$stagingRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('codex-export-' + [guid]::NewGuid().ToString('N')) +$payloadRoot = Join-Path $stagingRoot 'payload' +$toolsRoot = Join-Path $stagingRoot 'tools' +New-Item -ItemType Directory -Path $payloadRoot -Force | Out-Null +New-Item -ItemType Directory -Path $toolsRoot -Force | Out-Null + +$manifestItems = @() +foreach ($source in $resolvedItems) { + $name = Split-Path -Leaf $source + $payloadName = $name + $i = 2 + while (Test-Path -LiteralPath (Join-Path $payloadRoot $payloadName)) { + $payloadName = '{0}-{1}' -f $name, $i + $i++ + } + + $targetPayload = Join-Path $payloadRoot $payloadName + Copy-Item -LiteralPath $source -Destination $targetPayload -Recurse -Force + + $isUnderHome = $source.StartsWith($homePath, [System.StringComparison]::OrdinalIgnoreCase) + $profileRelative = $null + if ($isUnderHome) { + $profileRelative = $source.Substring($homePath.Length).TrimStart('\\') + } + + $manifestItems += [pscustomobject]@{ + source_path = $source + payload_name = $payloadName + destination_type = if ($isUnderHome) { 'user_profile_relative' } else { 'absolute' } + profile_relative_path = $profileRelative + absolute_path = if ($isUnderHome) { $null } else { $source } + } +} + +$scriptDir = Split-Path -Parent $PSCommandPath +$importPs1 = Join-Path $scriptDir 'codex-import.ps1' +$importCmd = Join-Path $scriptDir 'codex-import.cmd' + +if (Test-Path -LiteralPath $importPs1) { + Copy-Item -LiteralPath $importPs1 -Destination (Join-Path $toolsRoot 'codex-import.ps1') -Force +} +if (Test-Path -LiteralPath $importCmd) { + Copy-Item -LiteralPath $importCmd -Destination (Join-Path $toolsRoot 'codex-import.cmd') -Force +} + +$manifest = [pscustomobject]@{ + created_at = (Get-Date).ToString('o') + machine_name = $env:COMPUTERNAME + user_name = $env:USERNAME + user_profile = $homePath + items = $manifestItems +} + +$manifest | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $stagingRoot 'manifest.json') -Encoding UTF8 + +$zipPath = Join-Path $DestinationRoot $BackupName +if (Test-Path -LiteralPath $zipPath) { + Remove-Item -LiteralPath $zipPath -Force +} + +Compress-Archive -Path (Join-Path $stagingRoot '*') -DestinationPath $zipPath -CompressionLevel Optimal + +Remove-Item -LiteralPath $stagingRoot -Recurse -Force +Write-Output ('Codex export saved: {0}' -f $zipPath) diff --git a/scripts/codex-import.cmd b/scripts/codex-import.cmd new file mode 100644 index 00000000..30148a3c --- /dev/null +++ b/scripts/codex-import.cmd @@ -0,0 +1,4 @@ +@echo off +setlocal +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0codex-import.ps1" %* +endlocal diff --git a/scripts/codex-import.ps1 b/scripts/codex-import.ps1 new file mode 100644 index 00000000..75891c61 --- /dev/null +++ b/scripts/codex-import.ps1 @@ -0,0 +1,90 @@ +param( + [string]$ZipPath, + [string]$DestinationHome = ([Environment]::GetFolderPath('UserProfile')), + [string]$BackupRoot = 'D:\notatnik-ai\codex', + [switch]$NoBackup +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +if (-not (Test-Path -LiteralPath $BackupRoot)) { + New-Item -ItemType Directory -Path $BackupRoot -Force | Out-Null +} + +if (-not $ZipPath) { + $latest = Get-ChildItem -LiteralPath $BackupRoot -Filter 'codex-backup-*.zip' -File | + Sort-Object LastWriteTime -Descending | + Select-Object -First 1 + + if (-not $latest) { + throw 'Brak pliku backup ZIP. Podaj -ZipPath.' + } + + $ZipPath = $latest.FullName +} + +$resolvedZip = (Resolve-Path -LiteralPath $ZipPath).Path +$extractRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('codex-import-' + [guid]::NewGuid().ToString('N')) +New-Item -ItemType Directory -Path $extractRoot -Force | Out-Null + +Expand-Archive -LiteralPath $resolvedZip -DestinationPath $extractRoot -Force + +$manifestPath = Join-Path $extractRoot 'manifest.json' +if (-not (Test-Path -LiteralPath $manifestPath)) { + throw 'W archiwum brakuje manifest.json.' +} + +$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json +$payloadRoot = Join-Path $extractRoot 'payload' +if (-not (Test-Path -LiteralPath $payloadRoot)) { + throw 'W archiwum brakuje katalogu payload.' +} + +$preBackupDir = $null +if (-not $NoBackup) { + $preBackupDir = Join-Path $BackupRoot ('pre-import-backup-' + (Get-Date -Format 'yyyyMMdd-HHmmss')) + New-Item -ItemType Directory -Path $preBackupDir -Force | Out-Null +} + +foreach ($item in $manifest.items) { + $payloadItem = Join-Path $payloadRoot $item.payload_name + if (-not (Test-Path -LiteralPath $payloadItem)) { + Write-Warning ('Pomijam brakujacy element payload: {0}' -f $item.payload_name) + continue + } + + $targetPath = $null + if ($item.destination_type -eq 'user_profile_relative') { + $targetPath = Join-Path $DestinationHome $item.profile_relative_path + } elseif ($item.destination_type -eq 'absolute') { + $targetPath = $item.absolute_path + } + + if (-not $targetPath) { + Write-Warning ('Pomijam element z nieznanym celem: {0}' -f $item.payload_name) + continue + } + + $targetParent = Split-Path -Parent $targetPath + if ($targetParent -and -not (Test-Path -LiteralPath $targetParent)) { + New-Item -ItemType Directory -Path $targetParent -Force | Out-Null + } + + if (Test-Path -LiteralPath $targetPath) { + if (-not $NoBackup -and $preBackupDir) { + $backupTarget = Join-Path $preBackupDir ($item.payload_name + '-before-import') + Copy-Item -LiteralPath $targetPath -Destination $backupTarget -Recurse -Force + } + + Remove-Item -LiteralPath $targetPath -Recurse -Force + } + + Copy-Item -LiteralPath $payloadItem -Destination $targetPath -Recurse -Force +} + +Remove-Item -LiteralPath $extractRoot -Recurse -Force +Write-Output ('Codex import completed from: {0}' -f $resolvedZip) +if ($preBackupDir) { + Write-Output ('Pre-import backup: {0}' -f $preBackupDir) +}