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