feat: custom labels toggle and inline editing in product list
Adds session-based show/hide toggle for custom labels in admin product list, inline editable fields for custom_label_0..4, and label suggestions with custom entry support. Includes repository/controller updates, UI fixes, tests, and PAUL docs release updates. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@ Właściciel sklepu internetowego ma pełną kontrolę nad sprzedażą online -
|
||||
|-----------|-------|
|
||||
| Version | 0.333 |
|
||||
| Status | Production |
|
||||
| Last Updated | 2026-04-18 |
|
||||
| Last Updated | 2026-04-19 |
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -30,6 +30,7 @@ Właściciel sklepu internetowego ma pełną kontrolę nad sprzedażą online -
|
||||
- [x] Redis caching
|
||||
- [x] Ochrona przed podwójnym składaniem zamówienia
|
||||
- [x] Domain-Driven Architecture (migracja z legacy zakończona)
|
||||
- [x] Szybka edycja custom_label_0..4 na liscie produktow admina (toggle sesyjny + autocomplete)
|
||||
|
||||
### Active (In Progress)
|
||||
|
||||
@@ -79,6 +80,7 @@ Właściciel sklepu internetowego ma pełną kontrolę nad sprzedażą online -
|
||||
| PHP < 8.0 kompatybilność | Klienci na starszych serwerach | 2025 | Active |
|
||||
| Własny silnik zamiast frameworka | Pełna kontrola, brak narzutów | - | Active |
|
||||
| `id` w tabbed FormEdit przez `hiddenFields` | Zapobiega insert zamiast update przy edycji encji | 2026-04-18 | Active |
|
||||
| Inline custom labels w product list przez sesyjny toggle | Szybszy workflow dla Google XML bez wejscia w edycje produktu | 2026-04-19 | Active |
|
||||
|
||||
## Success Metrics
|
||||
|
||||
@@ -113,4 +115,4 @@ Quick Reference:
|
||||
|
||||
---
|
||||
*PROJECT.md - Updated when requirements or context change*
|
||||
*Last updated: 2026-04-18 after Phase 15*
|
||||
*Last updated: 2026-04-19 after Phase 16*
|
||||
|
||||
@@ -6,9 +6,9 @@ shopPRO to autorski silnik sklepu internetowego rozwijany iteracyjnie. Projekt j
|
||||
|
||||
## Current Milestone
|
||||
|
||||
**Hotfix backlog**
|
||||
**Feature — Product list custom labels quick edit**
|
||||
Status: Complete
|
||||
Phases: 4 of 4 complete
|
||||
Phases: 1 of 1 complete
|
||||
|
||||
## Phases
|
||||
|
||||
@@ -47,6 +47,7 @@ Status: Planning
|
||||
| 12 | summaryView redirect fix — double order block | 1 | Done | 2026-03-25 |
|
||||
| 13 | Basket logging + TTL token fix | 1 | Done | 2026-03-25 |
|
||||
| 14 | Custom fields delete bug — usunięcie wszystkich pól | 1 | Done | 2026-04-16 |
|
||||
| 16 | Product list custom labels quick edit | 1 | Done | 2026-04-19 |
|
||||
|
||||
## Phase Details
|
||||
|
||||
@@ -111,5 +112,11 @@ Status: Planning
|
||||
|
||||
**Scope:** Poprawić przekazywanie `id` w nowym flow formularza ScontainersController + dodać test regresyjny dla edycji, bez zmian globalnych w innych kontrolerach.
|
||||
|
||||
### Phase 16 - Product list custom labels quick edit
|
||||
|
||||
**Problem:** Na liscie produktow brakuje szybkiego trybu uzupelniania `custom_label_0..4`. Administrator musi wchodzic do edycji produktu, co spowalnia uzupelnianie danych Google XML.
|
||||
|
||||
**Scope:** Dodac przycisk "Pokaz etykiety niestandardowe" obok "Dodaj produkt", zapisywac jego stan w sesji, pokazac 5 pol custom label pod nazwa produktu, zapisac wartosci do bazy i zapewnic podpowiedzi z juz istniejacych wartosci.
|
||||
|
||||
---
|
||||
*Last updated: 2026-04-18*
|
||||
*Last updated: 2026-04-19 (Phase 16 complete)*
|
||||
|
||||
@@ -5,19 +5,19 @@
|
||||
See: .paul/PROJECT.md (updated 2026-04-18)
|
||||
|
||||
**Core value:** Właściciel sklepu ma pełną kontrolę nad sprzedażą online w jednym systemie pisanym od podstaw, bez narzutów zewnętrznych platform.
|
||||
**Current focus:** Phase 15 complete - loop closed (scontainers edit save fix)
|
||||
**Current focus:** Phase 16 complete - loop closed
|
||||
|
||||
## Current Position
|
||||
|
||||
Milestone: Hotfix
|
||||
Phase: 15 of 15 (Scontainers edit save fix) - Complete
|
||||
Plan: 15-01 complete
|
||||
Status: UNIFY complete, ready for next planning loop
|
||||
Last activity: 2026-04-18 - Closed loop for .paul/phases/15-scontainers-edit-save-fix/15-01-PLAN.md
|
||||
Milestone: Feature
|
||||
Phase: 16 of 16 (Product list custom labels quick edit) - Complete
|
||||
Plan: 16-01 complete
|
||||
Status: UNIFY complete, ready for next PLAN loop
|
||||
Last activity: 2026-04-19 - Closed loop for .paul/phases/16-product-list-custom-labels/16-01-PLAN.md
|
||||
|
||||
Progress:
|
||||
- Milestone: [##########] 100%
|
||||
- Phase 15: [##########] 100%
|
||||
- Phase 16: [##########] 100%
|
||||
|
||||
## Loop Position
|
||||
|
||||
@@ -41,10 +41,18 @@ Phase 12: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-03-25]
|
||||
Phase 13: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-03-25]
|
||||
Phase 14: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-04-16]
|
||||
Phase 15: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-04-18]
|
||||
Phase 16: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-04-19]
|
||||
```
|
||||
## Accumulated Context
|
||||
|
||||
### Decisions
|
||||
- 2026-04-19: Created Phase 16 plan at .paul/phases/16-product-list-custom-labels/16-01-PLAN.md
|
||||
- 2026-04-19: Phase 16 scope includes session toggle + inline custom_label_0..4 edit + suggestions on product list
|
||||
- 2026-04-19: Override approved by user - proceeded without required /feature-dev skill in Phase 16 APPLY
|
||||
- 2026-04-19: /koniec-pracy acknowledged by user as available for session close workflow
|
||||
- 2026-04-19: Human verify checkpoint approved after UX fixes (button style + autocomplete in single input)
|
||||
- 2026-04-19: Product list custom labels final UX: single input with autocomplete, no separate select under field
|
||||
- 2026-04-19: Transition-phase git commit for Phase 16 not executed in this UNIFY run
|
||||
- 2026-04-18: Transition-phase git commit step pending (not executed during this UNIFY run)
|
||||
- 2026-04-18: Phase 15 loop closed with SUMMARY at .paul/phases/15-scontainers-edit-save-fix/15-01-SUMMARY.md
|
||||
- 2026-04-18: Override - proceeded without required /feature-dev skill for Phase 15 APPLY
|
||||
@@ -74,17 +82,17 @@ None.
|
||||
### Blockers/Concerns
|
||||
None.
|
||||
|
||||
### Skill Audit (Phase 15)
|
||||
### Skill Audit (Phase 16)
|
||||
| Expected | Invoked | Notes |
|
||||
|----------|---------|-------|
|
||||
| /feature-dev | ○ | User-approved override during APPLY |
|
||||
| /koniec-pracy | ○ | Mapped to `.claude/commands/koniec-pracy.md`; release flow not executed in this loop |
|
||||
| /koniec-pracy | ○ | Marked as available by user; execute on session close workflow |
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-04-18
|
||||
Stopped at: Phase 15 complete, loop closed
|
||||
Next action: Start next work with $paul-plan (or run /koniec-pracy for release flow)
|
||||
Resume file: .paul/phases/15-scontainers-edit-save-fix/15-01-SUMMARY.md
|
||||
Last session: 2026-04-19
|
||||
Stopped at: Phase 16 complete, loop closed
|
||||
Next action: Start next milestone or create next phase plan
|
||||
Resume file: .paul/phases/16-product-list-custom-labels/16-01-SUMMARY.md
|
||||
---
|
||||
*STATE.md — Updated after every significant action*
|
||||
|
||||
22
.paul/changelog/2026-04-19.md
Normal file
22
.paul/changelog/2026-04-19.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# 2026-04-19
|
||||
|
||||
## Co zrobiono
|
||||
|
||||
- [Phase 16, Plan 01] Dodano szybka edycje custom_label_0..4 w `/admin/shop_product/view_list/` z przelacznikiem sesyjnym.
|
||||
- Dodano zapis wartosci custom labels do bazy oraz walidacje dozwolonych `label_type` po stronie kontrolera.
|
||||
- Dodano podpowiedzi istniejacych wartosci jako autocomplete w jednym input (z mozliwoscia wpisania wartosci wlasnej).
|
||||
- Poprawiono UX przycisku toggla (kolorystyka, rozmiar, hover i czytelnosc).
|
||||
- Rozszerzono testy jednostkowe kontrolera i repozytorium dla nowej funkcjonalnosci.
|
||||
|
||||
## Zmienione pliki
|
||||
|
||||
- `autoload/admin/Controllers/ShopProductController.php`
|
||||
- `autoload/Domain/Product/ProductRepository.php`
|
||||
- `admin/templates/shop-product/products-list.php`
|
||||
- `admin/templates/shop-product/products-list-custom-script.php`
|
||||
- `tests/Unit/admin/Controllers/ShopProductControllerTest.php`
|
||||
- `tests/Unit/Domain/Product/ProductRepositoryTest.php`
|
||||
- `.paul/phases/16-product-list-custom-labels/16-01-SUMMARY.md`
|
||||
- `.paul/PROJECT.md`
|
||||
- `.paul/ROADMAP.md`
|
||||
- `.paul/STATE.md`
|
||||
@@ -1,11 +1,11 @@
|
||||
# Testing Patterns
|
||||
# Testing Patterns
|
||||
|
||||
## Overview
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Total tests | **810** |
|
||||
| Total assertions | **2264** |
|
||||
| Total tests | **828** |
|
||||
| Total assertions | **2306** |
|
||||
| Framework | PHPUnit 9.6 (`phpunit.phar`) |
|
||||
| Bootstrap | `tests/bootstrap.php` |
|
||||
| Config | `phpunit.xml` |
|
||||
@@ -13,7 +13,7 @@
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
# Full suite (PowerShell — recommended)
|
||||
# Full suite (PowerShell — recommended)
|
||||
./test.ps1
|
||||
|
||||
# Specific file
|
||||
@@ -36,16 +36,16 @@ Tests mirror source structure:
|
||||
|
||||
```
|
||||
tests/Unit/
|
||||
├── Domain/
|
||||
│ ├── Product/ProductRepositoryTest.php
|
||||
│ ├── Category/CategoryRepositoryTest.php
|
||||
│ ├── Order/OrderRepositoryTest.php
|
||||
│ └── ... (all 29 modules covered)
|
||||
├── admin/Controllers/
|
||||
│ ├── ShopCategoryControllerTest.php
|
||||
│ └── ...
|
||||
└── api/
|
||||
└── ...
|
||||
├── Domain/
|
||||
│ ├── Product/ProductRepositoryTest.php
|
||||
│ ├── Category/CategoryRepositoryTest.php
|
||||
│ ├── Order/OrderRepositoryTest.php
|
||||
│ └── ... (all 29 modules covered)
|
||||
├── admin/Controllers/
|
||||
│ ├── ShopCategoryControllerTest.php
|
||||
│ └── ...
|
||||
└── api/
|
||||
└── ...
|
||||
```
|
||||
|
||||
## Test Class Pattern
|
||||
@@ -229,13 +229,13 @@ $this->assertInstanceOf(ClassName::class, $obj);
|
||||
|
||||
## What's Covered
|
||||
|
||||
- All 29 Domain repositories ✓
|
||||
- Core business logic (quantity, pricing, category tree) ✓
|
||||
- Query behavior with mocked Medoo ✓
|
||||
- Cache patterns ✓
|
||||
- Controller constructor injection ✓
|
||||
- `FormValidator` behavior ✓
|
||||
- API controllers ✓
|
||||
- All 29 Domain repositories âś“
|
||||
- Core business logic (quantity, pricing, category tree) âś“
|
||||
- Query behavior with mocked Medoo âś“
|
||||
- Cache patterns âś“
|
||||
- Controller constructor injection âś“
|
||||
- `FormValidator` behavior âś“
|
||||
- API controllers âś“
|
||||
|
||||
## What's Lightly Covered
|
||||
|
||||
@@ -243,3 +243,4 @@ $this->assertInstanceOf(ClassName::class, $obj);
|
||||
- Session state in tests
|
||||
- AJAX response integration
|
||||
- Frontend Views (static classes)
|
||||
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
# TECH_CHANGELOG
|
||||
|
||||
> Chronologiczny log zmian technicznych — co i dlaczego.
|
||||
|
||||
## v0.348 (2026-04-19)
|
||||
|
||||
- Dodano przełącznik widoczności etykiet niestandardowych na liście produktów w panelu admina, z zapisem stanu w sesji.
|
||||
- Po włączeniu opcji renderowane jest 5 pól custom_label_0..4 bezpośrednio pod sekcją zdjęcie/nazwa produktu.
|
||||
- Dodano zapisywanie wartości etykiet niestandardowych do bazy oraz walidację dozwolonych typów etykiet po stronie kontrolera.
|
||||
- Wprowadzono podpowiedzi istniejących wartości jako wybieralne sugestie z możliwością wpisania własnej wartości.
|
||||
- Rozszerzono testy jednostkowe dla ShopProductController i ProductRepository pod nową funkcjonalność.
|
||||
|
||||
|
||||
@@ -3608,3 +3608,81 @@ Dodać możliwość ustawienia limitu znaków w wiadomościach do produktu
|
||||
- [ ] [MINOR] templates/wiki/main-view.php:77 — Replace "and" with "&&". (php:S2010)
|
||||
- [ ] [MINOR] templates/wiki/main-view.php:85 — Anchors must have content and the content must be accessible by a screen reader. (Web:S6827)
|
||||
|
||||
|
||||
## SonarQube - 0.348 (2026-04-19)
|
||||
|
||||
### Code Smells
|
||||
|
||||
- [ ] [CRITICAL] autoload/admin/Controllers/ShopProductController.php:109 - Define a constant instead of duplicating this literal "" value="" 4 times. (php:S1192)
|
||||
- [ ] [CRITICAL] autoload/admin/Controllers/ShopProductController.php:39 - Refactor this function to reduce its Cognitive Complexity from 16 to the 15 allowed. (php:S3776)
|
||||
- [ ] [CRITICAL] autoload/admin/Controllers/ShopProductController.php:713 - Define a constant instead of duplicating this literal " selected" 3 times. (php:S1192)
|
||||
- [ ] [CRITICAL] autoload/admin/Controllers/ShopProductController.php:788 - Define a constant instead of duplicating this literal "Produkt został zapisany." 3 times. (php:S1192)
|
||||
- [ ] [CRITICAL] autoload/admin/Controllers/ShopProductController.php:809 - Define a constant instead of duplicating this literal "Location: /admin/shop_product/view_list/" 3 times. (php:S1192)
|
||||
- [ ] [CRITICAL] autoload/Domain/Product/ProductRepository.php:2303 - Refactor this function to reduce its Cognitive Complexity from 21 to the 15 allowed. (php:S3776)
|
||||
- [ ] [CRITICAL] autoload/Domain/Product/ProductRepository.php:2329 - Add curly braces around the nested statement(s). (php:S121)
|
||||
- [ ] [CRITICAL] autoload/Domain/Product/ProductRepository.php:2384 - Define a constant instead of duplicating this literal "in stock" 6 times. (php:S1192)
|
||||
- [ ] [CRITICAL] autoload/Domain/Product/ProductRepository.php:2387 - Define a constant instead of duplicating this literal "out of stock" 3 times. (php:S1192)
|
||||
- [ ] [CRITICAL] autoload/Domain/Product/ProductRepository.php:2417 - Refactor this function to reduce its Cognitive Complexity from 17 to the 15 allowed. (php:S3776)
|
||||
- [ ] [CRITICAL] autoload/Domain/Product/ProductRepository.php:2899 - Refactor this function to reduce its Cognitive Complexity from 38 to the 15 allowed. (php:S3776)
|
||||
- [ ] [CRITICAL] autoload/Domain/Product/ProductRepository.php:3061 - Refactor this function to reduce its Cognitive Complexity from 62 to the 15 allowed. (php:S3776)
|
||||
- [ ] [CRITICAL] autoload/Domain/Product/ProductRepository.php:3158 - Add curly braces around the nested statement(s). (php:S121)
|
||||
- [ ] [CRITICAL] autoload/Domain/Product/ProductRepository.php:3159 - Add curly braces around the nested statement(s). (php:S121)
|
||||
- [ ] [CRITICAL] autoload/Domain/Product/ProductRepository.php:3160 - Add curly braces around the nested statement(s). (php:S121)
|
||||
- [ ] [CRITICAL] autoload/Domain/Product/ProductRepository.php:3161 - Add curly braces around the nested statement(s). (php:S121)
|
||||
- [ ] [CRITICAL] autoload/Domain/Product/ProductRepository.php:3178 - Refactor this function to reduce its Cognitive Complexity from 24 to the 15 allowed. (php:S3776)
|
||||
- [ ] [CRITICAL] autoload/Domain/Product/ProductRepository.php:3246 - Refactor this function to reduce its Cognitive Complexity from 19 to the 15 allowed. (php:S3776)
|
||||
- [ ] [CRITICAL] autoload/Domain/Product/ProductRepository.php:3369 - Refactor this function to reduce its Cognitive Complexity from 16 to the 15 allowed. (php:S3776)
|
||||
- [ ] [CRITICAL] autoload/Domain/Product/ProductRepository.php:3425 - Refactor this function to reduce its Cognitive Complexity from 18 to the 15 allowed. (php:S3776)
|
||||
- [ ] [CRITICAL] autoload/Domain/Product/ProductRepository.php:3439 - Add curly braces around the nested statement(s). (php:S121)
|
||||
- [ ] [CRITICAL] autoload/Domain/Product/ProductRepository.php:3510 - Add curly braces around the nested statement(s). (php:S121)
|
||||
- [ ] [CRITICAL] autoload/Domain/Product/ProductRepository.php:3521 - Refactor this function to reduce its Cognitive Complexity from 37 to the 15 allowed. (php:S3776)
|
||||
- [ ] [CRITICAL] autoload/Domain/Product/ProductRepository.php:3556 - Refactor this function to reduce its Cognitive Complexity from 75 to the 15 allowed. (php:S3776)
|
||||
- [ ] [CRITICAL] autoload/Domain/Product/ProductRepository.php:3604 - Refactor this function to reduce its Cognitive Complexity from 16 to the 15 allowed. (php:S3776)
|
||||
- [ ] [MAJOR] autoload/admin/Controllers/ShopProductController.php:272 - This function "buildProductFormViewModel" has 281 lines, which is greater than the 150 lines authorized. Split it into smaller functions. (php:S138)
|
||||
- [ ] [MAJOR] autoload/admin/Controllers/ShopProductController.php:272 - This function has 9 parameters, which is greater than the 7 authorized. (php:S107)
|
||||
- [ ] [MAJOR] autoload/admin/Controllers/ShopProductController.php:39 - This function "view_list" has 151 lines, which is greater than the 150 lines authorized. Split it into smaller functions. (php:S138)
|
||||
- [ ] [MAJOR] autoload/Domain/Product/ProductRepository.php:2641 - This method has 4 returns, which is more than the 3 allowed. (php:S1142)
|
||||
- [ ] [MAJOR] autoload/Domain/Product/ProductRepository.php:2663 - This method has 4 returns, which is more than the 3 allowed. (php:S1142)
|
||||
- [ ] [MAJOR] autoload/Domain/Product/ProductRepository.php:2858 - Remove the unused function parameter "$limit". (php:S1172)
|
||||
- [ ] [MAJOR] autoload/Domain/Product/ProductRepository.php:2899 - This method has 4 returns, which is more than the 3 allowed. (php:S1142)
|
||||
- [ ] [MAJOR] autoload/Domain/Product/ProductRepository.php:3029 - This method has 4 returns, which is more than the 3 allowed. (php:S1142)
|
||||
- [ ] [MAJOR] autoload/Domain/Product/ProductRepository.php:3061 - This method has 4 returns, which is more than the 3 allowed. (php:S1142)
|
||||
- [ ] [MAJOR] autoload/Domain/Product/ProductRepository.php:3435 - Extract this nested ternary operation into an independent statement. (php:S3358)
|
||||
- [ ] [MAJOR] autoload/Domain/Product/ProductRepository.php:3544 - Extract this nested ternary operation into an independent statement. (php:S3358)
|
||||
- [ ] [MAJOR] autoload/Domain/Product/ProductRepository.php:3564 - Extract this nested ternary operation into an independent statement. (php:S3358)
|
||||
- [ ] [MAJOR] autoload/Domain/Product/ProductRepository.php:3591 - Extract this nested ternary operation into an independent statement. (php:S3358)
|
||||
- [ ] [MAJOR] autoload/Domain/Product/ProductRepository.php:3592 - Extract this nested ternary operation into an independent statement. (php:S3358)
|
||||
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:1004 - Rename function "generate_combination" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
|
||||
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:1025 - Rename function "delete_combination" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
|
||||
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:1040 - Rename function "product_combination_stock_0_buy_save" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
|
||||
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:1050 - Rename function "product_combination_sku_save" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
|
||||
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:1060 - Rename function "product_combination_quantity_save" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
|
||||
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:1070 - Rename function "product_combination_price_save" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
|
||||
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:1080 - Rename function "delete_combination_ajax" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
|
||||
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:1097 - Rename function "image_delete" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
|
||||
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:1112 - Rename function "images_order_save" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
|
||||
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:1124 - Rename function "image_alt_change" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
|
||||
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:1139 - Rename function "product_file_delete" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
|
||||
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:1154 - Rename function "product_file_name_change" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
|
||||
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:1169 - Rename function "product_image_delete" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
|
||||
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:1186 - Rename function "mass_edit" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
|
||||
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:1200 - Rename function "mass_edit_save" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
|
||||
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:1205 - Use empty() to check whether the array is empty or not. (php:S1155)
|
||||
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:1226 - Rename function "get_products_by_category" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
|
||||
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:215 - Rename function "product_custom_labels_toggle" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
|
||||
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:228 - Rename function "product_edit" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
|
||||
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:39 - Rename function "view_list" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
|
||||
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:801 - Rename function "duplicate_product" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
|
||||
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:816 - Rename function "product_archive" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
|
||||
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:831 - Rename function "product_unarchive" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
|
||||
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:846 - Rename function "product_delete" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
|
||||
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:861 - Rename function "change_product_status" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
|
||||
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:874 - Rename function "product_change_price_brutto" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
|
||||
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:889 - Rename function "product_change_price_brutto_promo" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
|
||||
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:904 - Rename function "product_change_custom_label" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
|
||||
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:919 - Rename function "product_custom_label_suggestions" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
|
||||
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:941 - Rename function "product_custom_label_save" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
|
||||
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:962 - Rename function "ajax_product_url" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
|
||||
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:971 - Rename function "generate_sku_code" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
|
||||
- [ ] [MINOR] autoload/admin/Controllers/ShopProductController.php:989 - Rename function "product_combination" to match the regular expression ^[a-z][a-zA-Z0-9]*$. (php:S100)
|
||||
|
||||
|
||||
201
.paul/phases/16-product-list-custom-labels/16-01-PLAN.md
Normal file
201
.paul/phases/16-product-list-custom-labels/16-01-PLAN.md
Normal file
@@ -0,0 +1,201 @@
|
||||
---
|
||||
phase: 16-product-list-custom-labels
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- autoload/admin/Controllers/ShopProductController.php
|
||||
- autoload/Domain/Product/ProductRepository.php
|
||||
- admin/templates/shop-product/products-list-custom-script.php
|
||||
- tests/Unit/admin/Controllers/ShopProductControllerTest.php
|
||||
- tests/Unit/Domain/Product/ProductRepositoryTest.php
|
||||
autonomous: false
|
||||
delegation: off
|
||||
---
|
||||
|
||||
<objective>
|
||||
## Goal
|
||||
Dodac w liscie produktow (`/admin/shop_product/view_list/`) przelacznik "Pokaz etykiety niestandardowe", ktory zapisuje stan w sesji i po wlaczeniu pokazuje szybka edycje 5 pol `custom_label_0..4` z podpowiedziami.
|
||||
|
||||
## Purpose
|
||||
Administrator ma szybciej uzupelniac etykiety Google XML bez wchodzenia do edycji kazdego produktu, z zachowaniem spojnosc danych i wygodnych podpowiedzi istniejacych wartosci.
|
||||
|
||||
## Output
|
||||
- Nowy przycisk obok "Dodaj produkt", sterujacy widocznoscia custom labels i zapisujacy stan w sesji
|
||||
- Render 5 pol `custom_label_0..4` pod nazwa/SKU produktu w tabeli, tylko przy wlaczonej opcji
|
||||
- Zapis kazdego pola do `pp_shop_products` oraz system podpowiedzi z istniejacych wartosci w bazie
|
||||
- Odczyt nazw etykiet z bazy (z fallbackiem) zamiast hardcodu w widoku listy
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## Project Context
|
||||
@.paul/PROJECT.md
|
||||
@.paul/ROADMAP.md
|
||||
@.paul/STATE.md
|
||||
|
||||
## Source Files
|
||||
@autoload/admin/Controllers/ShopProductController.php
|
||||
@autoload/Domain/Product/ProductRepository.php
|
||||
@admin/templates/components/table-list.php
|
||||
@admin/templates/shop-product/products-list.php
|
||||
@admin/templates/shop-product/products-list-custom-script.php
|
||||
@tests/Unit/admin/Controllers/ShopProductControllerTest.php
|
||||
@tests/Unit/Domain/Product/ProductRepositoryTest.php
|
||||
</context>
|
||||
|
||||
<skills>
|
||||
## Required Skills (from SPECIAL-FLOWS.md)
|
||||
|
||||
| Skill | Priority | When to Invoke | Loaded? |
|
||||
|-------|----------|----------------|---------|
|
||||
| /feature-dev | required | Before implementation in APPLY | ○ |
|
||||
| /koniec-pracy | required | After implementation/release wrap-up | ○ |
|
||||
|
||||
**BLOCKING:** Required skills MUST be loaded before APPLY proceeds.
|
||||
Run each skill command or confirm already loaded.
|
||||
|
||||
## Skill Invocation Checklist
|
||||
- [ ] /feature-dev loaded (run command or confirm)
|
||||
- [ ] /koniec-pracy loaded (run command or confirm)
|
||||
|
||||
</skills>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## AC-1: Przelacznik widocznosci custom labels dziala i jest zapamietywany
|
||||
```gherkin
|
||||
Given administrator jest na /admin/shop_product/view_list/
|
||||
When kliknie przycisk "Pokaz etykiety niestandardowe"
|
||||
Then stan opcji zostanie zapisany w sesji
|
||||
And po odswiezeniu/listowaniu tabela zachowa ustawiony stan (wlaczony lub wylaczony)
|
||||
```
|
||||
|
||||
## AC-2: Lista produktow pokazuje 5 pol custom_label po wlaczeniu opcji
|
||||
```gherkin
|
||||
Given opcja "Pokaz etykiety niestandardowe" jest wlaczona
|
||||
When lista produktow sie renderuje
|
||||
Then pod sekcja zdjecie/nazwa/SKU-EAN dla kazdego produktu widoczne sa pola custom_label_0..custom_label_4
|
||||
And etykiety tych pol sa pobrane dynamicznie z bazy danych (z fallbackiem tylko gdy brak konfiguracji)
|
||||
```
|
||||
|
||||
## AC-3: Zapis i podpowiedzi wartosci dzialaja dla kazdego custom_label
|
||||
```gherkin
|
||||
Given administrator wpisuje wartosc w jednym z pol custom_label_0..custom_label_4
|
||||
When wybierze podpowiedz lub zatwierdzi wpis
|
||||
Then wartosc zostanie zapisana w pp_shop_products dla danego produktu i pola
|
||||
And podpowiedzi sa budowane z juz istniejacych wartosci tego samego custom_label w bazie
|
||||
```
|
||||
|
||||
## AC-4: Walidacja i bezpieczenstwo endpointow sa zachowane
|
||||
```gherkin
|
||||
Given zapytanie AJAX podaje nieprawidlowy typ labela spoza custom_label_0..4
|
||||
When backend przetwarza request
|
||||
Then operacja zostaje odrzucona bez zapisu
|
||||
And odpowiedz zwraca status bledu bez ingerencji w dane produktu
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Dodac backend przelacznika sesyjnego i danych dla widoku listy</name>
|
||||
<files>autoload/admin/Controllers/ShopProductController.php, autoload/Domain/Product/ProductRepository.php</files>
|
||||
<action>
|
||||
Rozszerzyc `ShopProductController::view_list()` o flage sesyjna dla widocznosci custom labels
|
||||
oraz przekazanie do widoku nazw etykiet pobieranych z bazy.
|
||||
|
||||
Dodac akcje kontrolera do przelaczania flagi (toggle) i zwracania prostego JSON.
|
||||
|
||||
W `ProductRepository` dodac metode pobierajaca nazwy etykiet custom_label_0..4 z bazy
|
||||
(np. tabela ustawien), z bezpiecznym fallbackiem "Custom label N" gdy wartosc nie istnieje.
|
||||
Nie stosowac konkatenacji SQL dla danych wejsciowych.
|
||||
</action>
|
||||
<verify>Uruchomic testy kontrolera/repo oraz sprawdzic recznie, ze zmiana flagi utrzymuje sie po reloadzie listy</verify>
|
||||
<done>AC-1 i AC-2 satisfied</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Dodac UI i logike AJAX dla custom labels na liscie produktow</name>
|
||||
<files>autoload/admin/Controllers/ShopProductController.php, admin/templates/shop-product/products-list-custom-script.php</files>
|
||||
<action>
|
||||
Wygenerowac HTML 5 pol custom_label pod kolumna nazwy produktu tylko gdy flaga sesyjna jest wlaczona.
|
||||
Uzyc klas zgodnych z istniejacym stylem (`custom-labels`, `custom_label_X_container`, listy sugestii).
|
||||
|
||||
Dodac przycisk "Pokaz etykiety niestandardowe" obok "Dodaj produkt" (hook przez custom script listy)
|
||||
oraz obsluge klikniecia przez AJAX do nowej akcji toggle + odswiezenie aktualnego URL.
|
||||
|
||||
Podlaczyc dla kazdego inputa:
|
||||
- pobieranie sugestii przez `/admin/shop_product/product_custom_label_suggestions/`
|
||||
- zapis przez `/admin/shop_product/product_custom_label_save/`
|
||||
z walidacja odpowiedzi i obsluga bledow UI.
|
||||
</action>
|
||||
<verify>Manual: wlaczyc opcje, wpisac i zapisac wartosc custom_label, odswiezyc strone, potwierdzic widocznosc i dane</verify>
|
||||
<done>AC-2 i AC-3 satisfied</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Dodac testy regresyjne dla nowego zachowania</name>
|
||||
<files>tests/Unit/admin/Controllers/ShopProductControllerTest.php, tests/Unit/Domain/Product/ProductRepositoryTest.php</files>
|
||||
<action>
|
||||
Rozszerzyc testy kontrolera o przypadki:
|
||||
- toggle flagi sesyjnej
|
||||
- odrzucenie nieprawidlowych typow labeli
|
||||
- poprawne przekazanie danych do widoku listy przy wlaczonej opcji.
|
||||
|
||||
Rozszerzyc testy repozytorium o:
|
||||
- pobieranie nazw custom labels z bazy z fallbackiem
|
||||
- sugestie i zapis tylko dla dozwolonych label_type.
|
||||
</action>
|
||||
<verify>./test.ps1 tests/Unit/admin/Controllers/ShopProductControllerTest.php oraz ./test.ps1 tests/Unit/Domain/Product/ProductRepositoryTest.php</verify>
|
||||
<done>AC-4 covered and AC-1..AC-3 protected by tests</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<what-built>Nowy przycisk sesyjny + szybka edycja custom labels 0..4 z podpowiedziami na liscie produktow</what-built>
|
||||
<how-to-verify>
|
||||
1. Otworz: /admin/shop_product/view_list/
|
||||
2. Kliknij: "Pokaz etykiety niestandardowe"
|
||||
3. Potwierdz: pola custom_label_0..4 pojawiaja sie pod nazwa produktu
|
||||
4. Wpisz wartosc, wybierz podpowiedz i odswiez strone
|
||||
5. Potwierdz: wartosc zostala zapisana i toggle pozostaje aktywny
|
||||
</how-to-verify>
|
||||
<resume-signal>Type "approved" to continue, or describe issues to fix</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## DO NOT CHANGE
|
||||
- Globalnych komponentow listy niezwiązanych z produktami (`admin/templates/components/table-list.php`) poza minimalnym, koniecznym hookiem
|
||||
- Endpointow API (`autoload/api/*`)
|
||||
- Logiki produktow frontendowych (`autoload/front/*`, `templates/shop-product/*`)
|
||||
|
||||
## SCOPE LIMITS
|
||||
- Zakres ograniczony do admin listy produktow i quick-edit custom labels
|
||||
- Bez migracji DB w tym planie (odczyt nazw z istniejacych danych konfiguracyjnych)
|
||||
- Bez refaktoru calego modułu integracji Google XML
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
Before declaring plan complete:
|
||||
- [ ] ./test.ps1 tests/Unit/admin/Controllers/ShopProductControllerTest.php
|
||||
- [ ] ./test.ps1 tests/Unit/Domain/Product/ProductRepositoryTest.php
|
||||
- [ ] Manual check: toggle zapisuje sie w sesji i zachowuje po reloadzie
|
||||
- [ ] Manual check: podpowiedzi i zapis custom labels dzialaja dla 0..4
|
||||
- [ ] All acceptance criteria met
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Przycisk "Pokaz etykiety niestandardowe" dziala i przechowuje stan w sesji
|
||||
- Lista produktow pokazuje i zapisuje custom_label_0..4 bez wejscia w edycje produktu
|
||||
- Nazwy etykiet sa pobierane z bazy z fallbackiem
|
||||
- Testy regresyjne dla backendu i repozytorium przechodza
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.paul/phases/16-product-list-custom-labels/16-01-SUMMARY.md`
|
||||
</output>
|
||||
149
.paul/phases/16-product-list-custom-labels/16-01-SUMMARY.md
Normal file
149
.paul/phases/16-product-list-custom-labels/16-01-SUMMARY.md
Normal file
@@ -0,0 +1,149 @@
|
||||
---
|
||||
phase: 16-product-list-custom-labels
|
||||
plan: 01
|
||||
subsystem: admin
|
||||
tags: [shop-product, custom-label, session-toggle, autocomplete, quick-edit]
|
||||
|
||||
requires: []
|
||||
provides:
|
||||
- Szybka edycja custom_label_0..4 na liscie produktow
|
||||
- Przelacznik widocznosci etykiet w sesji admina
|
||||
- Podpowiedzi istniejacych wartosci + wpisywanie wartosci wlasnej w jednym polu
|
||||
affects: [shop-product-list, google-xml-label-flow]
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns: [inline quick-edit in table list, datalist autocomplete in single input, settings fallback mapping]
|
||||
|
||||
key-files:
|
||||
created: []
|
||||
modified:
|
||||
- autoload/admin/Controllers/ShopProductController.php
|
||||
- autoload/Domain/Product/ProductRepository.php
|
||||
- admin/templates/shop-product/products-list.php
|
||||
- admin/templates/shop-product/products-list-custom-script.php
|
||||
- tests/Unit/admin/Controllers/ShopProductControllerTest.php
|
||||
- tests/Unit/Domain/Product/ProductRepositoryTest.php
|
||||
|
||||
key-decisions:
|
||||
- "Przelacznik widocznosci custom labels zapisany w sesji (per admin session)"
|
||||
- "Nazwy custom labels pobierane z pp_settings z fallbackiem do domyslnych nazw"
|
||||
- "UX: jedno pole input z autocomplete (datalist), bez osobnego select pod spodem"
|
||||
|
||||
patterns-established:
|
||||
- "Dla list admina: toggle funkcji przez dedykowany endpoint JSON + reload widoku"
|
||||
- "Quick-edit text fields: walidacja typu po stronie kontrolera przed zapisem/sugestiami"
|
||||
|
||||
duration: ~120min
|
||||
completed: 2026-04-19
|
||||
---
|
||||
|
||||
# Phase 16 Plan 01: Product list custom labels quick edit - Summary
|
||||
|
||||
**Wdrozono szybka edycje custom labels na liscie produktow z przelacznikiem sesyjnym i autocomplete w pojedynczym polu.**
|
||||
|
||||
## Performance
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Duration | ~120min |
|
||||
| Completed | 2026-04-19 |
|
||||
| Tasks | 3 completed + 1 checkpoint approved |
|
||||
| Files modified | 6 |
|
||||
|
||||
## Acceptance Criteria Results
|
||||
|
||||
| Criterion | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| AC-1: Przelacznik widocznosci custom labels dziala i jest zapamietywany | Pass | Dodano przycisk toggle + zapis stanu w sesji admina |
|
||||
| AC-2: Lista pokazuje 5 pol custom_label po wlaczeniu opcji | Pass | Pola custom_label_0..4 renderowane pod nazwa/SKU/EAN tylko przy wlaczonej opcji |
|
||||
| AC-3: Zapis i podpowiedzi wartosci dzialaja dla custom_label | Pass | Zapis AJAX do bazy + autocomplete istniejacych wartosci i mozliwosc wpisu wlasnego |
|
||||
| AC-4: Walidacja i bezpieczenstwo endpointow zachowane | Pass | Kontroler odrzuca niedozwolone label_type przed zapisem i pobraniem sugestii |
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- Dodano nowy toggle "Pokaz/Ukryj etykiety niestandardowe" przy liscie produktow, sterowany sesja.
|
||||
- Dodano inline quick-edit custom_label_0..4 bez wchodzenia do edycji produktu.
|
||||
- Podpowiedzi dzialaja jako autocomplete w tym samym input (nie osobny kontrolka pod polem).
|
||||
- Ujednolicono UX przycisku toggle (kolorystyka, rozmiar, hover, czytelnosc).
|
||||
- Rozszerzono testy kontrolera i repozytorium o nowe przypadki.
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
| File | Change | Purpose |
|
||||
|------|--------|---------|
|
||||
| `autoload/admin/Controllers/ShopProductController.php` | Modified | Toggle sesyjny, render custom labels w tabeli, walidacja label_type |
|
||||
| `autoload/Domain/Product/ProductRepository.php` | Modified | Pobieranie nazw custom_label z ustawien (fallback) |
|
||||
| `admin/templates/shop-product/products-list.php` | Modified | Przekazanie flagi custom_labels_enabled do skryptu |
|
||||
| `admin/templates/shop-product/products-list-custom-script.php` | Modified | UI toggle, zapis/sugestie custom label, autocomplete, poprawki wygladu |
|
||||
| `tests/Unit/admin/Controllers/ShopProductControllerTest.php` | Modified | Testy nowych metod i walidacji kontrolera |
|
||||
| `tests/Unit/Domain/Product/ProductRepositoryTest.php` | Modified | Testy customLabelNames + walidacji sugestii/zapisu |
|
||||
|
||||
## Decisions Made
|
||||
|
||||
| Decision | Rationale | Impact |
|
||||
|----------|-----------|--------|
|
||||
| Session key dla toggla (`shop_product_show_custom_labels`) | Funkcja ma byc osobnym trybem widoku admina | Stabilny stan po odswiezeniu strony |
|
||||
| Nazwy etykiet z `pp_settings` + fallback | Wymaganie "nazwy z bazy" i bezpieczne zachowanie gdy brak konfiguracji | Elastyczne nazewnictwo bez hardcodu |
|
||||
| Autocomplete w jednym polu zamiast input + oddzielny select | UX feedback od usera podczas checkpointu | Czytelniejszy i szybszy flow edycji |
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Summary
|
||||
|
||||
| Type | Count | Impact |
|
||||
|------|-------|--------|
|
||||
| Auto-fixed | 1 | Niski, techniczny (BOM w pliku kontrolera) |
|
||||
| Scope additions | 2 | Niski, UX polish po feedbacku checkpoint |
|
||||
| Deferred | 0 | Brak |
|
||||
|
||||
**Total impact:** Niezbedne poprawki techniczne i UX bez scope creep funkcjonalnego.
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. Encoding/BOM in controller file**
|
||||
- **Found during:** Task 1 implementation verification
|
||||
- **Issue:** Parser PHP zglaszal blad namespace przez BOM na poczatku pliku
|
||||
- **Fix:** Zapisano plik `ShopProductController.php` jako UTF-8 bez BOM
|
||||
- **Files:** `autoload/admin/Controllers/ShopProductController.php`
|
||||
- **Verification:** `php -l autoload/admin/Controllers/ShopProductController.php`
|
||||
|
||||
### Deferred Items
|
||||
|
||||
None.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
| Issue | Resolution |
|
||||
|-------|------------|
|
||||
| Drobne regresje UX przycisku toggle | Iteracyjna poprawka stylu i hover po feedbacku usera |
|
||||
| Forma podpowiedzi (select pod inputem) nieakceptowalna UX | Zmieniono na jedno pole z autocomplete (datalist) |
|
||||
|
||||
## Verification Results
|
||||
|
||||
- `php -l autoload/admin/Controllers/ShopProductController.php` -> OK
|
||||
- `php -l autoload/Domain/Product/ProductRepository.php` -> OK
|
||||
- `php -l admin/templates/shop-product/products-list-custom-script.php` -> OK
|
||||
- `php phpunit.phar tests/Unit/admin/Controllers/ShopProductControllerTest.php` -> OK (15 tests, 71 assertions)
|
||||
- `php phpunit.phar tests/Unit/Domain/Product/ProductRepositoryTest.php` -> OK (64 tests, 131 assertions)
|
||||
- Checkpoint human-verify: approved by user after final UX adjustments
|
||||
|
||||
Skill audit:
|
||||
- `/feature-dev` - not invoked (user-approved override)
|
||||
- `/koniec-pracy` - acknowledged by user as available for end-of-session flow
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
**Ready:**
|
||||
- Admin ma szybki i praktyczny workflow uzupelniania custom labels bez przechodzenia do edycji produktu.
|
||||
- Kod posiada testy regresyjne dla nowej logiki backendowej.
|
||||
|
||||
**Concerns:**
|
||||
- Brak.
|
||||
|
||||
**Blockers:**
|
||||
- None.
|
||||
|
||||
---
|
||||
*Phase: 16-product-list-custom-labels, Plan: 01*
|
||||
*Completed: 2026-04-19*
|
||||
@@ -1,3 +1,20 @@
|
||||
<?php $customLabelsEnabled = !empty( $this->custom_labels_enabled ); ?>
|
||||
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
var $header = $( '.panel-heading .col-sm-8' );
|
||||
if ( !$header.length ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $header.find( '.btn-toggle-custom-labels' ).length === 0 ) {
|
||||
var buttonClass = <?= $customLabelsEnabled ? "'btn-danger'" : "'btn-success'" ?>;
|
||||
var buttonText = <?= $customLabelsEnabled ? "'Ukryj etykiety niestandardowe'" : "'Pokaż etykiety niestandardowe'" ?>;
|
||||
$header.append( ' <a href=\"#\" class=\"btn btn-sm btn-toggle-custom-labels ' + buttonClass + '\"><i class=\"fa fa-tags mr5\"></i>' + buttonText + '</a>' );
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<? if ( $this -> shoppro_enabled ):?>
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
@@ -27,10 +44,19 @@
|
||||
.product-categories {
|
||||
display: block;
|
||||
}
|
||||
.custom-label-suggestions {
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.custom-labels small {
|
||||
display: block;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
var customLabelSuggestionTimers = {};
|
||||
|
||||
// --- Inline price save ---
|
||||
$( 'body' ).on( 'change', '.product-price', function() {
|
||||
@@ -54,6 +80,139 @@ $(function() {
|
||||
});
|
||||
});
|
||||
|
||||
// --- Toggle custom labels ---
|
||||
$( 'body' ).on( 'click', '.btn-toggle-custom-labels', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
cache: false,
|
||||
url: '/admin/shop_product/product_custom_labels_toggle/',
|
||||
beforeSend: function() { $( '#overlay' ).show(); },
|
||||
success: function( response ) {
|
||||
$( '#overlay' ).hide();
|
||||
|
||||
var data = jQuery.parseJSON( response );
|
||||
if ( data.status === 'ok' ) {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
create_error( 'Nie udało się przełączyć widoku etykiet.' );
|
||||
},
|
||||
error: function() {
|
||||
$( '#overlay' ).hide();
|
||||
create_error( 'Nie udało się przełączyć widoku etykiet.' );
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// --- Custom label suggestions + save ---
|
||||
function hideCustomLabelSuggestions( $input ) {
|
||||
var $container = $input.closest( '[class*=\"custom_label_\"]' );
|
||||
$container.find( '.custom-label-suggestions' ).hide().empty();
|
||||
var datalistId = $input.attr( 'data-datalist-id' ) || '';
|
||||
if ( datalistId ) {
|
||||
$( '#' + datalistId ).empty();
|
||||
}
|
||||
}
|
||||
|
||||
function saveCustomLabelValue( $input ) {
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
cache: false,
|
||||
url: '/admin/shop_product/product_custom_label_save/',
|
||||
data: {
|
||||
product_id: $input.attr( 'data-product-id' ),
|
||||
label_type: $input.attr( 'data-label-type' ),
|
||||
custom_label: $input.val()
|
||||
},
|
||||
success: function( response ) {
|
||||
var data = jQuery.parseJSON( response );
|
||||
if ( data.status !== 'ok' ) {
|
||||
create_error( data.msg || 'Nie udało się zapisać etykiety.' );
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
create_error( 'Nie udało się zapisać etykiety.' );
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadCustomLabelSuggestions( $input ) {
|
||||
var labelType = $input.attr( 'data-label-type' );
|
||||
var query = $input.val();
|
||||
var $suggestions = $input.closest( '[class*=\"custom_label_\"]' ).find( '.custom-label-suggestions' );
|
||||
var datalistId = $input.attr( 'data-datalist-id' ) || '';
|
||||
var $datalist = datalistId ? $( '#' + datalistId ) : $();
|
||||
|
||||
if ( !labelType ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
cache: false,
|
||||
url: '/admin/shop_product/product_custom_label_suggestions/',
|
||||
data: {
|
||||
label_type: labelType,
|
||||
custom_label: query
|
||||
},
|
||||
success: function( response ) {
|
||||
var data = jQuery.parseJSON( response );
|
||||
if ( data.status !== 'ok' || !data.suggestions || !data.suggestions.length ) {
|
||||
$suggestions.hide().empty();
|
||||
$datalist.empty();
|
||||
return;
|
||||
}
|
||||
|
||||
var html = '';
|
||||
$.each( data.suggestions, function( index, item ) {
|
||||
if ( !item.label ) {
|
||||
return;
|
||||
}
|
||||
var safe = String( item.label ).replace(/\"/g, '"');
|
||||
html += '<option value=\"' + safe + '\">' + item.label + '</option>';
|
||||
});
|
||||
|
||||
if ( html ) {
|
||||
$datalist.html( html );
|
||||
$suggestions.hide().empty();
|
||||
} else {
|
||||
$suggestions.hide().empty();
|
||||
$datalist.empty();
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$suggestions.hide().empty();
|
||||
$datalist.empty();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$( 'body' ).on( 'input', '.product-custom-label', function() {
|
||||
var $input = $( this );
|
||||
var inputKey = $input.attr( 'data-product-id' ) + ':' + $input.attr( 'data-label-type' );
|
||||
|
||||
if ( customLabelSuggestionTimers[inputKey] ) {
|
||||
clearTimeout( customLabelSuggestionTimers[inputKey] );
|
||||
}
|
||||
|
||||
customLabelSuggestionTimers[inputKey] = setTimeout( function() {
|
||||
loadCustomLabelSuggestions( $input );
|
||||
}, 250 );
|
||||
});
|
||||
|
||||
$( 'body' ).on( 'change', '.product-custom-label', function() {
|
||||
saveCustomLabelValue( $( this ) );
|
||||
});
|
||||
|
||||
$( document ).on( 'click', function(e) {
|
||||
if ( $( e.target ).closest( '.custom-labels' ).length === 0 ) {
|
||||
$( '.custom-label-suggestions' ).hide().empty();
|
||||
}
|
||||
});
|
||||
|
||||
$( 'body' ).on( 'change', '.product-price-promo', function() {
|
||||
var $el = $( this );
|
||||
var price = $el.val().replace( ' ', '' );
|
||||
|
||||
@@ -5,5 +5,6 @@
|
||||
'list' => $this->viewModel,
|
||||
'apilo_enabled' => $this->apilo_enabled,
|
||||
'shoppro_enabled' => $this->shoppro_enabled,
|
||||
'custom_labels_enabled' => $this->custom_labels_enabled,
|
||||
]); ?>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -2205,6 +2205,44 @@ class ProductRepository
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Pobiera nazwy etykiet custom_label_0..4 z bazy ustawien.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function customLabelNames(): array
|
||||
{
|
||||
$names = [];
|
||||
for ( $index = 0; $index < 5; $index++ ) {
|
||||
$fieldName = 'custom_label_' . $index;
|
||||
$names[$fieldName] = 'Custom label ' . $index;
|
||||
}
|
||||
|
||||
$settingsKeys = [];
|
||||
for ( $index = 0; $index < 5; $index++ ) {
|
||||
$settingsKeys[] = 'custom_label_' . $index . '_name';
|
||||
$settingsKeys[] = 'google_custom_label_' . $index . '_name';
|
||||
}
|
||||
|
||||
$settingsRows = $this->db->select( 'pp_settings', [ 'param', 'value' ], [ 'param' => $settingsKeys ] );
|
||||
if ( is_array( $settingsRows ) ) {
|
||||
foreach ( $settingsRows as $settingRow ) {
|
||||
$param = (string) ( $settingRow['param'] ?? '' );
|
||||
$value = trim( (string) ( $settingRow['value'] ?? '' ) );
|
||||
|
||||
if ( $value === '' ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( preg_match( '/^(?:google_)?custom_label_([0-4])_name$/', $param, $match ) ) {
|
||||
$names[ 'custom_label_' . $match[1] ] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $names;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pobiera sugestie custom label.
|
||||
*/
|
||||
|
||||
@@ -18,6 +18,8 @@ use admin\Support\TableListRequestFactory;
|
||||
*/
|
||||
class ShopProductController
|
||||
{
|
||||
private const CUSTOM_LABELS_SESSION_KEY = 'shop_product_show_custom_labels';
|
||||
|
||||
private ProductRepository $repository;
|
||||
private IntegrationsRepository $integrationsRepository;
|
||||
private LanguagesRepository $languagesRepository;
|
||||
@@ -39,6 +41,8 @@ class ShopProductController
|
||||
$apiloEnabled = $this->integrationsRepository->getSetting( 'apilo', 'enabled' );
|
||||
$shopproEnabled = $this->integrationsRepository->getSetting( 'shoppro', 'enabled' );
|
||||
$dlang = $this->languagesRepository->defaultLanguage();
|
||||
$customLabelsEnabled = $this->customLabelsEnabled();
|
||||
$customLabelNames = $this->repository->customLabelNames();
|
||||
|
||||
$sortableColumns = [ 'id', 'name', 'price_brutto', 'status', 'promoted', 'quantity' ];
|
||||
|
||||
@@ -98,6 +102,10 @@ class ShopProductController
|
||||
. '<small class="text-muted product-categories product-categories--cats" title="' . $categories . '">' . $categories . '</small>'
|
||||
. '<small class="text-muted product-categories">SKU: ' . $sku . ', EAN: ' . $ean . '</small>';
|
||||
|
||||
if ( $customLabelsEnabled ) {
|
||||
$nameHtml .= $this->renderCustomLabelsEditor( $product, $id, $customLabelNames );
|
||||
}
|
||||
|
||||
$priceHtml = '<input type="text" class="product-price form-control text-right" product-id="' . $id . '" value="' . htmlspecialchars( (string) $product['price_brutto'], ENT_QUOTES, 'UTF-8' ) . '" style="width: 75px;">';
|
||||
$promoHtml = '<input type="text" class="product-price-promo form-control text-right" product-id="' . $id . '" value="' . htmlspecialchars( (string) $product['price_brutto_promo'], ENT_QUOTES, 'UTF-8' ) . '" style="width: 75px;">';
|
||||
$promotedHtml = $product['promoted'] ? '<span class="text-success text-bold">tak</span>' : 'nie';
|
||||
@@ -195,11 +203,25 @@ class ShopProductController
|
||||
'viewModel' => $viewModel,
|
||||
'apilo_enabled' => $apiloEnabled,
|
||||
'shoppro_enabled' => $shopproEnabled,
|
||||
'custom_labels_enabled' => $customLabelsEnabled,
|
||||
] );
|
||||
}
|
||||
|
||||
// ─── Krok 7: Edycja i zapis ─────────────────────────────────────
|
||||
|
||||
/**
|
||||
* AJAX: przelacza widok custom labels na liscie produktow i zapisuje stan w sesji.
|
||||
*/
|
||||
public function product_custom_labels_toggle(): void
|
||||
{
|
||||
$currentState = $this->customLabelsEnabled();
|
||||
$newState = $currentState ? 0 : 1;
|
||||
\Shared\Helpers\Helpers::set_session( self::CUSTOM_LABELS_SESSION_KEY, $newState );
|
||||
|
||||
echo json_encode( [ 'status' => 'ok', 'enabled' => (bool) $newState ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formularz edycji produktu.
|
||||
*/
|
||||
@@ -897,9 +919,15 @@ class ShopProductController
|
||||
public function product_custom_label_suggestions(): void
|
||||
{
|
||||
$response = [ 'status' => 'error', 'msg' => 'Podczas pobierania sugestii dla custom label wystąpił błąd. Proszę spróbować ponownie.' ];
|
||||
$labelType = (string) \Shared\Helpers\Helpers::get( 'label_type' );
|
||||
|
||||
$suggestions = $this->repository->customLabelSuggestions( \Shared\Helpers\Helpers::get( 'custom_label' ), \Shared\Helpers\Helpers::get( 'label_type' ) );
|
||||
if ( $suggestions ) {
|
||||
if ( !$this->isAllowedCustomLabelType( $labelType ) ) {
|
||||
echo json_encode( $response );
|
||||
exit;
|
||||
}
|
||||
|
||||
$suggestions = $this->repository->customLabelSuggestions( (string) \Shared\Helpers\Helpers::get( 'custom_label' ), $labelType );
|
||||
if ( is_array( $suggestions ) ) {
|
||||
$response = [ 'status' => 'ok', 'suggestions' => $suggestions ];
|
||||
}
|
||||
|
||||
@@ -913,8 +941,14 @@ class ShopProductController
|
||||
public function product_custom_label_save(): void
|
||||
{
|
||||
$response = [ 'status' => 'error', 'msg' => 'Podczas zapisywania custom label wystąpił błąd. Proszę spróbować ponownie.' ];
|
||||
$labelType = (string) \Shared\Helpers\Helpers::get( 'label_type' );
|
||||
|
||||
if ( $this->repository->saveCustomLabel( (int) \Shared\Helpers\Helpers::get( 'product_id' ), \Shared\Helpers\Helpers::get( 'custom_label' ), \Shared\Helpers\Helpers::get( 'label_type' ) ) ) {
|
||||
if ( !$this->isAllowedCustomLabelType( $labelType ) ) {
|
||||
echo json_encode( $response );
|
||||
exit;
|
||||
}
|
||||
|
||||
if ( $this->repository->saveCustomLabel( (int) \Shared\Helpers\Helpers::get( 'product_id' ), (string) \Shared\Helpers\Helpers::get( 'custom_label' ), $labelType ) ) {
|
||||
$response = [ 'status' => 'ok' ];
|
||||
}
|
||||
|
||||
@@ -1197,4 +1231,36 @@ class ShopProductController
|
||||
echo json_encode( [ 'status' => 'ok', 'products' => $products ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
private function customLabelsEnabled(): bool
|
||||
{
|
||||
return isset( $_SESSION[ self::CUSTOM_LABELS_SESSION_KEY ] ) && (int) $_SESSION[ self::CUSTOM_LABELS_SESSION_KEY ] === 1;
|
||||
}
|
||||
|
||||
private function isAllowedCustomLabelType(string $labelType): bool
|
||||
{
|
||||
return in_array( $labelType, [ 'custom_label_0', 'custom_label_1', 'custom_label_2', 'custom_label_3', 'custom_label_4' ], true );
|
||||
}
|
||||
|
||||
private function renderCustomLabelsEditor(array $product, int $productId, array $customLabelNames): string
|
||||
{
|
||||
$customLabelsHtml = '<div class="custom-labels mt10">';
|
||||
|
||||
for ( $index = 0; $index < 5; $index++ ) {
|
||||
$fieldName = 'custom_label_' . $index;
|
||||
$labelText = htmlspecialchars( (string) ( $customLabelNames[$fieldName] ?? 'Custom label ' . $index ), ENT_QUOTES, 'UTF-8' );
|
||||
$valueText = htmlspecialchars( (string) ( $product[$fieldName] ?? '' ), ENT_QUOTES, 'UTF-8' );
|
||||
|
||||
$customLabelsHtml .= '<div class="' . $fieldName . '_container">';
|
||||
$customLabelsHtml .= '<small class="text-muted">' . $labelText . '</small>';
|
||||
$datalistId = 'custom-label-list-' . $productId . '-' . $fieldName;
|
||||
$customLabelsHtml .= '<input type="text" class="form-control input-sm product-custom-label" data-label-type="' . $fieldName . '" data-product-id="' . $productId . '" data-datalist-id="' . $datalistId . '" list="' . $datalistId . '" value="' . $valueText . '" placeholder="' . $labelText . '">';
|
||||
$customLabelsHtml .= '<datalist id="' . $datalistId . '"></datalist>';
|
||||
$customLabelsHtml .= '<div class="' . $fieldName . '_suggestions custom-label-suggestions"></div>';
|
||||
$customLabelsHtml .= '</div>';
|
||||
}
|
||||
|
||||
$customLabelsHtml .= '</div>';
|
||||
return $customLabelsHtml;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1313,4 +1313,55 @@ class ProductRepositoryTest extends TestCase
|
||||
$method->setAccessible(true);
|
||||
$method->invoke($repository, 55, [], [], []);
|
||||
}
|
||||
|
||||
public function testCustomLabelNamesUsesDbSettingsWithFallback(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
|
||||
$mockDb->expects($this->once())
|
||||
->method('select')
|
||||
->with(
|
||||
'pp_settings',
|
||||
['param', 'value'],
|
||||
$this->callback(function ($where) {
|
||||
return isset($where['param']) && is_array($where['param']) && count($where['param']) === 10;
|
||||
})
|
||||
)
|
||||
->willReturn([
|
||||
['param' => 'custom_label_0_name', 'value' => 'Sezon'],
|
||||
['param' => 'google_custom_label_2_name', 'value' => 'Kampania'],
|
||||
['param' => 'custom_label_4_name', 'value' => ''],
|
||||
]);
|
||||
|
||||
$repository = new ProductRepository($mockDb);
|
||||
$names = $repository->customLabelNames();
|
||||
|
||||
$this->assertSame('Sezon', $names['custom_label_0']);
|
||||
$this->assertSame('Custom label 1', $names['custom_label_1']);
|
||||
$this->assertSame('Kampania', $names['custom_label_2']);
|
||||
$this->assertSame('Custom label 3', $names['custom_label_3']);
|
||||
$this->assertSame('Custom label 4', $names['custom_label_4']);
|
||||
}
|
||||
|
||||
public function testCustomLabelSuggestionsReturnsEmptyForInvalidLabelType(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
$mockDb->expects($this->never())->method('query');
|
||||
|
||||
$repository = new ProductRepository($mockDb);
|
||||
$result = $repository->customLabelSuggestions('abc', 'custom_label_10');
|
||||
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
|
||||
public function testSaveCustomLabelReturnsFalseForInvalidLabelType(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
$mockDb->expects($this->never())->method('update');
|
||||
|
||||
$repository = new ProductRepository($mockDb);
|
||||
$result = $repository->saveCustomLabel(1, 'abc', 'custom_label_10');
|
||||
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ class ShopProductControllerTest extends TestCase
|
||||
$this->assertTrue(method_exists($this->controller, 'product_unarchive'));
|
||||
$this->assertTrue(method_exists($this->controller, 'product_delete'));
|
||||
$this->assertTrue(method_exists($this->controller, 'change_product_status'));
|
||||
$this->assertTrue(method_exists($this->controller, 'product_custom_labels_toggle'));
|
||||
$this->assertTrue(method_exists($this->controller, 'product_change_price_brutto'));
|
||||
$this->assertTrue(method_exists($this->controller, 'product_change_price_brutto_promo'));
|
||||
$this->assertTrue(method_exists($this->controller, 'product_change_custom_label'));
|
||||
@@ -128,6 +129,9 @@ class ShopProductControllerTest extends TestCase
|
||||
'renderCustomFieldsBox',
|
||||
'escapeHtml',
|
||||
'resolveSavePayload',
|
||||
'customLabelsEnabled',
|
||||
'isAllowedCustomLabelType',
|
||||
'renderCustomLabelsEditor',
|
||||
];
|
||||
|
||||
foreach ($expectedPrivate as $method) {
|
||||
@@ -147,4 +151,22 @@ class ShopProductControllerTest extends TestCase
|
||||
$reflection = new \ReflectionClass($this->controller);
|
||||
$this->assertEquals('void', (string)$reflection->getMethod('save')->getReturnType());
|
||||
}
|
||||
|
||||
public function testToggleCustomLabelsMethodReturnsVoid(): void
|
||||
{
|
||||
$reflection = new \ReflectionClass($this->controller);
|
||||
$this->assertEquals('void', (string)$reflection->getMethod('product_custom_labels_toggle')->getReturnType());
|
||||
}
|
||||
|
||||
public function testAllowedCustomLabelTypeValidation(): void
|
||||
{
|
||||
$reflection = new \ReflectionClass($this->controller);
|
||||
$method = $reflection->getMethod('isAllowedCustomLabelType');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$this->assertTrue($method->invoke($this->controller, 'custom_label_0'));
|
||||
$this->assertTrue($method->invoke($this->controller, 'custom_label_4'));
|
||||
$this->assertFalse($method->invoke($this->controller, 'custom_label_5'));
|
||||
$this->assertFalse($method->invoke($this->controller, 'invalid'));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user