update
This commit is contained in:
1
.claude/memory/MEMORY.md
Normal file
1
.claude/memory/MEMORY.md
Normal 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
40
.paul/PROJECT.md
Normal 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
50
.paul/ROADMAP.md
Normal 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
53
.paul/STATE.md
Normal 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
25
.paul/paul.json
Normal 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
|
||||
}
|
||||
}
|
||||
154
.paul/phases/01-cross-sell-pro-koszyk/01-01-PLAN.md
Normal file
154
.paul/phases/01-cross-sell-pro-koszyk/01-01-PLAN.md
Normal 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>
|
||||
157
.paul/phases/02-cross-sell-pro-w-zamowieniu/02-01-PLAN.md
Normal file
157
.paul/phases/02-cross-sell-pro-w-zamowieniu/02-01-PLAN.md
Normal 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>
|
||||
118
.paul/phases/02-cross-sell-pro-w-zamowieniu/02-01-SUMMARY.md
Normal file
118
.paul/phases/02-cross-sell-pro-w-zamowieniu/02-01-SUMMARY.md
Normal 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*
|
||||
@@ -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: []
|
||||
|
||||
10
.vscode/ftp-kr.json
vendored
10
.vscode/ftp-kr.json
vendored
@@ -10,5 +10,13 @@
|
||||
"autoDelete": false,
|
||||
"autoDownload": false,
|
||||
"ignoreRemoteModification": true,
|
||||
"ignore": [".git", "/.vscode"]
|
||||
"ignore": [
|
||||
".git",
|
||||
"/.vscode",
|
||||
"/.claude",
|
||||
"AGENTS.md",
|
||||
"/.paul",
|
||||
"/.idea",
|
||||
"/.serena"
|
||||
]
|
||||
}
|
||||
|
||||
119
.vscode/ftp-kr.sync.cache.json
vendored
119
.vscode/ftp-kr.sync.cache.json
vendored
@@ -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
37
AGENTS.md
Normal 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
27
changelog/2026-04-01.md
Normal 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/`
|
||||
|
||||
335
modules/crosssellpro/crosssellpro.php
Normal file
335
modules/crosssellpro/crosssellpro.php
Normal 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]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
158
modules/crosssellpro/views/css/cartCrossSell.css
Normal file
158
modules/crosssellpro/views/css/cartCrossSell.css
Normal 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;
|
||||
}
|
||||
}
|
||||
98
modules/crosssellpro/views/js/cartCrossSell.js
Normal file
98
modules/crosssellpro/views/js/cartCrossSell.js
Normal 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 || '/';
|
||||
});
|
||||
});
|
||||
});
|
||||
70
modules/crosssellpro/views/templates/hook/cartCrossSell.tpl
Normal file
70
modules/crosssellpro/views/templates/hook/cartCrossSell.tpl
Normal 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'}">‹</button>
|
||||
<button type="button" class="crosssellpro-nav-btn js-crosssellpro-next" aria-label="{l s='Nastepne produkty' mod='crosssellpro'}">›</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}
|
||||
@@ -0,0 +1 @@
|
||||
{include file='module:crosssellpro/views/templates/hook/cartCrossSell.tpl'}
|
||||
4
scripts/changelog.cmd
Normal file
4
scripts/changelog.cmd
Normal file
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
setlocal
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0changelog.ps1" %*
|
||||
endlocal
|
||||
117
scripts/changelog.ps1
Normal file
117
scripts/changelog.ps1
Normal 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
4
scripts/codex-export.cmd
Normal file
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
setlocal
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0codex-export.ps1" %*
|
||||
endlocal
|
||||
102
scripts/codex-export.ps1
Normal file
102
scripts/codex-export.ps1
Normal 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
4
scripts/codex-import.cmd
Normal file
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
setlocal
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0codex-import.ps1" %*
|
||||
endlocal
|
||||
90
scripts/codex-import.ps1
Normal file
90
scripts/codex-import.ps1
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user