--- phase: 01-products-cl1-column plan: 01 status: completed unified_at: 2026-04-22 --- # Summary 01-01 — Products CL1 Column ## Co zostało zrobione ### Task 1 — Migracja DB - Utworzono `migrations/028_products_custom_label_1.sql` (idempotentna, wzorzec 016) - Dodaje `products.custom_label_1 VARCHAR(255) NULL DEFAULT NULL AFTER min_roas` ### Task 2 — Factory (`autoload/factory/class.Products.php`) - `is_product_core_field()`: dodano `custom_label_1` (obok zachowanych `custom_label_3`/`custom_label_4`) - `get_products()`: zmieniono sygnaturę o parametr `$custom_label_1`, SELECT/GROUP BY/order_map przepięte z `custom_label_3` na `custom_label_1`, dodano LIKE search i WHERE filter dla CL1 - `get_roas_bounds()`: nowy parametr `$custom_label_1`, analogiczny filtr WHERE - `get_records_total_products()`: nowy parametr `$custom_label_1`, analogiczny filtr WHERE - Dodano metodę `get_distinct_custom_label_1( $client_id )` (kopia metody CL4) ### Task 3 — Controller (`autoload/controls/class.Products.php`) - `list()`: - Nowa zmienna `$filter_cl1` z `\S::get('filter_cl1')` - Przekazana do `get_roas_bounds`, `get_products`, `get_records_total_products` - W pętli: pobiera `$custom_label_1` przez `get_product_data`, wylicza `$custom_label_1_color` (analogiczna paleta: bestseller/deleted/zombie/pla_single/pla/paused) - Komórka CL3 (`htmlspecialchars`) zastąpiona inputem `` z kolorem - Dodano endpoint `get_distinct_cl1()` → zwraca unikalne wartości CL1 dla klienta - Dodano endpoint `save_custom_label_1()` → zapis + komentarz w historii produktu ### Task 4 — Template (`templates/products/main_view.php`) - Nagłówek tabeli: `CL3` → `CL1` - Nowy filtr nagłówkowy `#products_cl1` przed CL4 - columnDefs: `name: 'custom_label_3'` → `name: 'custom_label_1'` - `ajax.data`: dodano `d.filter_cl1 = $('#products_cl1').val()` - Debounce 400ms dla filtra CL1 + `localStorage.setItem('products_cl1', ...)` - Reset filtrów (change #client_id): dodano usuwanie `products_cl1` z localStorage i czyszczenie pola - Odczyt na starcie: `savedCl1 = localStorage.getItem('products_cl1')` + `load_cl1_suggestions()` - Pełny blok autocomplete CL1: `load_cl1_suggestions`, `render_cl1_datalist`, `bind_cl1_datalist`, `draw.dt` hook, listener change #client_id, `refresh_cl1_cache_after_save` - Handler `change .custom_label_1` → AJAX POST do `/products/save_custom_label_1/` + toast ## Zmienione pliki - `migrations/028_products_custom_label_1.sql` (nowy) - `autoload/factory/class.Products.php` - `autoload/controls/class.Products.php` - `templates/products/main_view.php` ## Weryfikacja statyczna - [x] `php -l autoload/factory/class.Products.php` — OK - [x] `php -l autoload/controls/class.Products.php` — OK - [x] Zero pozostałości `custom_label_3` / `CL3` w `templates/products/main_view.php` - [x] Zero pozostałości `custom_label_3` w rendererze listy kontrolera - [x] `custom_label_3` zachowany w `SupplementalFeed.php` (linie 166/171/190/199/202/212) — AC-4 - [x] `custom_label_3` zachowany w `is_product_core_field` w factory (potrzebny dla get/set z merchant sync) - [x] `custom_label_1` w field_map `GoogleAdsApi.php:438` — już istniało, bez zmian ## Weryfikacja runtime (do wykonania) - [ ] `php install.php` — zaaplikuj migrację 028 - [ ] Otwórz `/products`, wybierz klienta — nagłówek pokazuje CL1 zamiast CL3 - [ ] Edytuj wartość CL1 → toast „Custom Label 1 zapisany", po F5 wartość utrzymana - [ ] Wpisz w filtrze nagłówkowym `#products_cl1` — tabela zawęża wyniki po 400ms - [ ] Datalist (autocomplete) pokazuje poprzednie wartości CL1 dla klienta - [ ] Generowanie feedu: `feeds/supplemental_{client_id}.tsv` zawiera kolumnę `custom_label_3` (bez zmian) - [ ] Regresja CL4: wszystkie funkcje CL4 działają jak wcześniej (edycja, filtr, autocomplete, color coding) ## AC status - **AC-1** (migracja) — kod gotowy, wymaga `php install.php` do aktywacji - **AC-2** (CL1 zamiast CL3 w widoku) — zrealizowane - **AC-3** (CL1 pełne zachowanie CL4) — zrealizowane - **AC-4** (custom_label_3 w feed) — zachowane bez zmian, zweryfikowano grepem ## Deviations (odchylenia od planu) - **DEV-1**: columnDefs szerokość CL1 zmieniona z 50px (plan: 1:1 z CL3) na 120px (zgodność z CL4 jako kolumna z inputem). Zasugerowane przez użytkownika po pierwszym renderze — wąski input CL3 był nieczytelny dla edytowalnego pola. - **DEV-2**: Pytanie diagnostyczne user nt. nadpisywania CL4 przez sync Google Ads — potwierdzono czytając `Cron.php` (linie 1366, 1401, 928) oraz `Api.php` (407): import GAds pisze tylko `client_id`/`offer_id`/`name` przy INSERT i `product_url`/`merchant_url_not_found` przy UPDATE. `custom_label_*` są nietykane. Jedyny automat piszący do CL4 — `SupplementalFeed::refresh_bestseller_labels_for_client` — respektuje ręczne etykiety (`continue` dla wartości ≠ '' i ≠ 'bestseller'). ## Odroczone / Known issues - Istniejące produkty mają pustą wartość `custom_label_1` — spodziewane (nowa kolumna) - GoogleAdsApi `field_map` już obsługuje `custom_label_1` → `customLabel1`, więc ewentualny push do Merchant Center zadziała bez dodatkowych zmian - Nie dodano automatyki label-owania dla CL1 (poza scope; bestseller rules nadal wiążą się wyłącznie z CL4)