This commit is contained in:
2026-04-01 01:17:17 +02:00
parent 0f6fe82843
commit acef745ccc
24 changed files with 1785 additions and 3 deletions

1
.claude/memory/MEMORY.md Normal file
View File

@@ -0,0 +1 @@
- [feedback_changelog.md](feedback_changelog.md) - Po każdej zmianie aktualizuj folder changelog/ w katalogu głównym

40
.paul/PROJECT.md Normal file
View File

@@ -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*

50
.paul/ROADMAP.md Normal file
View File

@@ -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*

53
.paul/STATE.md Normal file
View File

@@ -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*

25
.paul/paul.json Normal file
View File

@@ -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
}
}

View File

@@ -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
---
<objective>
## 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.
</objective>
<context>
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
## Source Files
@modules/
@controllers/
@themes/
</context>
<acceptance_criteria>
## 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
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Utworzyc szkielet modulu i logike pobierania produktow cross-sell</name>
<files>modules/crosssellpro/crosssellpro.php</files>
<action>
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.
</action>
<verify>Instalacja modulu przebiega bez bledow, hook zwraca kolekcje danych cross-sell dla koszyka testowego.</verify>
<done>AC-2 satisfied; AC-5 preconditions prepared.</done>
</task>
<task type="auto">
<name>Task 2: Zbudowac widok karuzeli i frontend CTA</name>
<files>modules/crosssellpro/views/templates/hook/cartCrossSell.tpl, modules/crosssellpro/views/js/cartCrossSell.js, modules/crosssellpro/views/css/cartCrossSell.css</files>
<action>
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).
</action>
<verify>Manualny test na /koszyk?action=show potwierdza render sekcji i oba scenariusze CTA (wariant/bez wariantu).</verify>
<done>AC-1, AC-3, AC-4, AC-5 satisfied.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>Modul Cross Sell PRO z sekcja karuzeli na stronie koszyka i logika CTA zalezna od wariantow.</what-built>
<how-to-verify>
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.
</how-to-verify>
<resume-signal>Type "approved" to continue, or describe issues to fix</resume-signal>
</task>
</tasks>
<boundaries>
## 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.
</boundaries>
<verification>
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.
</verification>
<success_criteria>
- All tasks completed
- All verification checks pass
- No errors or warnings introduced
- Cross Sell PRO dziala zgodnie z wymaganiami biznesowymi koszyka
</success_criteria>
<output>
After completion, create `.paul/phases/01-cross-sell-pro-koszyk/01-01-SUMMARY.md`
</output>

View File

@@ -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
---
<objective>
## 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.
</objective>
<context>
## 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
</context>
<acceptance_criteria>
## 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
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Rozszerzyc modul o hook checkout summary top</name>
<files>modules/crosssellpro/crosssellpro.php</files>
<action>
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.
</action>
<verify>Na checkout hook zwraca HTML sekcji, a w przypadku pustej listy zwraca pusty output bez bledow.</verify>
<done>AC-1 i AC-2 przygotowane po stronie backend.</done>
</task>
<task type="auto">
<name>Task 2: Dodac widok checkout i dopasowac frontend karuzeli</name>
<files>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</files>
<action>
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.
</action>
<verify>Manualny test na /zamowienie i /koszyk?action=show potwierdza poprawny render, dodawanie i brak regresji.</verify>
<done>AC-1, AC-3, AC-4, AC-5 satisfied.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>Cross Sell PRO w checkout summary (`#js-checkout-summary`) oraz utrzymana kompatybilnosc dotychczasowej sekcji koszyka.</what-built>
<how-to-verify>
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.
</how-to-verify>
<resume-signal>Type "approved" to continue, or describe issues to fix</resume-signal>
</task>
</tasks>
<boundaries>
## 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
</boundaries>
<verification>
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.
</verification>
<success_criteria>
- All tasks completed
- All verification checks pass
- No errors or warnings introduced
- Cross Sell PRO dziala w koszyku i checkout bez konfliktow
</success_criteria>
<output>
After completion, create `.paul/phases/02-cross-sell-pro-w-zamowieniu/02-01-SUMMARY.md`
</output>

View File

@@ -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*

View File

@@ -136,3 +136,17 @@ symbol_info_budget:
# list of regex patterns which, when matched, mark a memory entry as readonly.
# 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: []

10
.vscode/ftp-kr.json vendored
View File

@@ -10,5 +10,13 @@
"autoDelete": false,
"autoDownload": false,
"ignoreRemoteModification": true,
"ignore": [".git", "/.vscode"]
"ignore": [
".git",
"/.vscode",
"/.claude",
"AGENTS.md",
"/.paul",
"/.idea",
"/.serena"
]
}

View File

@@ -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
}
}
}
}

37
AGENTS.md Normal file
View File

@@ -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/`.

27
changelog/2026-04-01.md Normal file
View File

@@ -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/`

View File

@@ -0,0 +1,335 @@
<?php
/**
* Cross Sell PRO module for cart page upsell.
*
* @author Pyziak Jacek
* @copyright project-pro.pl
* @link https://www.project-pro.pl
*/
use PrestaShop\PrestaShop\Adapter\Image\ImageRetriever;
use PrestaShop\PrestaShop\Adapter\Product\PriceFormatter;
use PrestaShop\PrestaShop\Adapter\Product\ProductColorsRetriever;
if (!defined('_PS_VERSION_')) {
exit;
}
class Crosssellpro extends Module
{
const DEFAULT_LIMIT = 12;
public function __construct()
{
$this->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<int, bool>
*/
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]
);
}
}

View File

@@ -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;
}
}

View File

@@ -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 || '/';
});
});
});

View File

@@ -0,0 +1,70 @@
{if !empty($crosssellpro_products)}
{assign var='crosssellpro_is_checkout' value=(isset($crosssellpro_mode) && $crosssellpro_mode == 'checkout')}
<section
class="crosssellpro-block{if isset($crosssellpro_mode) && $crosssellpro_mode == 'cart'} card{/if}"
data-crosssellpro-block="1"
data-crosssellpro-mode="{if isset($crosssellpro_mode)}{$crosssellpro_mode|escape:'htmlall':'UTF-8'}{else}cart{/if}"
data-cart-url="{$crosssellpro_cart_url}"
data-return-url="{$crosssellpro_return_url}"
data-token="{$static_token}"
>
<div class="card-block">
<div class="crosssellpro-header">
<h2 class="h4 crosssellpro-title">{l s='Produkty, ktore moga Ci sie przydac' mod='crosssellpro'}</h2>
<div class="crosssellpro-nav" aria-hidden="true">
<button type="button" class="crosssellpro-nav-btn js-crosssellpro-prev" aria-label="{l s='Poprzednie produkty' mod='crosssellpro'}">&lsaquo;</button>
<button type="button" class="crosssellpro-nav-btn js-crosssellpro-next" aria-label="{l s='Nastepne produkty' mod='crosssellpro'}">&rsaquo;</button>
</div>
</div>
<div class="crosssellpro-viewport js-crosssellpro-viewport"{if $crosssellpro_is_checkout} style="overflow-x:auto;overflow-y:hidden;"{/if}>
<div class="crosssellpro-track js-crosssellpro-track"{if $crosssellpro_is_checkout} style="display:flex;flex-direction:row;flex-wrap:nowrap;gap:0;align-items:stretch;"{/if}>
{foreach from=$crosssellpro_products item=product}
<article class="crosssellpro-item"{if $crosssellpro_is_checkout} style="flex:0 0 100%;width:100%;max-width:100%;padding:0;border:0;background:transparent;border-radius:0;"{/if}>
<a href="{$product.url}" class="crosssellpro-image-link" title="{$product.name|escape:'htmlall':'UTF-8'}">
{if !empty($product.cover.bySize.home_default.url)}
<img
class="img-fluid"
src="{$product.cover.bySize.home_default.url}"
alt="{$product.name|escape:'htmlall':'UTF-8'}"
loading="lazy"
>
{else}
<span class="crosssellpro-image-placeholder"></span>
{/if}
</a>
<h3 class="crosssellpro-name">
<a href="{$product.url}">{$product.name}</a>
</h3>
<div class="crosssellpro-price">{$product.price}</div>
{if $product.crosssellpro_requires_selection}
<a
href="{$product.crosssellpro_cta_url}"
class="btn btn-primary crosssellpro-cta"
rel="nofollow"
>
{$product.crosssellpro_cta_label}
</a>
{else}
<a
href="{$crosssellpro_return_url}"
class="btn btn-primary crosssellpro-cta"
data-crosssellpro-add="1"
data-id-product="{$product.id_product|intval}"
data-qty="{if isset($product.minimal_quantity) && $product.minimal_quantity > 0}{$product.minimal_quantity|intval}{else}1{/if}"
data-post-add-url="{$product.crosssellpro_post_add_url}"
rel="nofollow"
>
{$product.crosssellpro_cta_label}
</a>
{/if}
</article>
{/foreach}
</div>
</div>
</div>
</section>
{/if}

View File

@@ -0,0 +1 @@
{include file='module:crosssellpro/views/templates/hook/cartCrossSell.tpl'}

4
scripts/changelog.cmd Normal file
View File

@@ -0,0 +1,4 @@
@echo off
setlocal
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0changelog.ps1" %*
endlocal

117
scripts/changelog.ps1 Normal file
View File

@@ -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)

4
scripts/codex-export.cmd Normal file
View File

@@ -0,0 +1,4 @@
@echo off
setlocal
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0codex-export.ps1" %*
endlocal

102
scripts/codex-export.ps1 Normal file
View File

@@ -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)

4
scripts/codex-import.cmd Normal file
View File

@@ -0,0 +1,4 @@
@echo off
setlocal
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0codex-import.ps1" %*
endlocal

90
scripts/codex-import.ps1 Normal file
View File

@@ -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)
}