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:
Jacek
2026-04-19 11:09:19 +02:00
parent 41e491c6b7
commit 9577d4944a
15 changed files with 856 additions and 42 deletions

View File

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

View File

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

View File

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

View 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`

View File

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

View File

@@ -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ść.

View File

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

View 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>

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

View File

@@ -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, '&quot;');
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( ' ', '' );

View File

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

View File

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

View File

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

View File

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

View File

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