update
This commit is contained in:
39
.paul/STATE.md
Normal file
39
.paul/STATE.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# STATE
|
||||||
|
|
||||||
|
## Current Position
|
||||||
|
|
||||||
|
Milestone: (ad-hoc) Products CL1 column swap
|
||||||
|
Phase: 1 of 1 (Products CL1 Column) — Completed
|
||||||
|
Plan: 01-01 unified (loop closed)
|
||||||
|
Status: UNIFY complete
|
||||||
|
Last activity: 2026-04-22 — Closed loop for plan 01-01
|
||||||
|
|
||||||
|
Progress:
|
||||||
|
- Milestone: [██████████] 100%
|
||||||
|
- Phase 1: [██████████] 100%
|
||||||
|
|
||||||
|
## Loop Position
|
||||||
|
|
||||||
|
```
|
||||||
|
PLAN ──▶ APPLY ──▶ UNIFY
|
||||||
|
✓ ✓ ✓ [Pętla zamknięta]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Session Continuity
|
||||||
|
|
||||||
|
Last session: 2026-04-22
|
||||||
|
Stopped at: Loop closed, pętla gotowa do nowego /paul:plan
|
||||||
|
Next action: Deploy na adspro.projectpro.pl — uruchom `php install.php` na serwerze (migracja 028) po wgraniu plików przez FTP
|
||||||
|
Resume file: .paul/phases/01-products-cl1-column/01-01-SUMMARY.md
|
||||||
|
|
||||||
|
## Historia zrealizowanych planów
|
||||||
|
|
||||||
|
- `01-01-PLAN.md` — CL3 → CL1 w tabeli /products (completed 2026-04-22)
|
||||||
|
- 4 pliki zmodyfikowane: migration 028, factory, controller, template
|
||||||
|
- 4 AC spełnione
|
||||||
|
- 2 odchylenia udokumentowane (szerokość kolumny 50→120px, weryfikacja sync GAds)
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- PAUL framework działa w trybie ad-hoc (bez pełnej roadmapy/PROJECT.md).
|
||||||
|
- Następne zmiany: uruchom `/paul:plan` z opisem kolejnego zadania.
|
||||||
32
.paul/governance/governance_2026-04-22.jsonl
Normal file
32
.paul/governance/governance_2026-04-22.jsonl
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{"ts":"2026-04-21T22:21:29Z","tool":"Bash","cmd":"mkdir -p .paul/phases/01-products-cl1-column\",\"description\":\"Create plan phase directory\"},\"tool_response\":{\"stdout\":\"\",\"stderr\":\"\",\"interrupted\":false,\"isImage\":false,\"noOutputExp","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||||
|
{"ts":"2026-04-21T22:23:02Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\.paul\\\\phases\\\\01-products-cl1-column\\\\01-01-PLAN.md","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||||
|
{"ts":"2026-04-21T22:23:12Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||||
|
{"ts":"2026-04-21T22:24:22Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\migrations\\\\028_products_custom_label_1.sql","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||||
|
{"ts":"2026-04-21T22:24:34Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\factory\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||||
|
{"ts":"2026-04-21T22:24:41Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\factory\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||||
|
{"ts":"2026-04-21T22:24:46Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\factory\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||||
|
{"ts":"2026-04-21T22:24:59Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\factory\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||||
|
{"ts":"2026-04-21T22:25:07Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\factory\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||||
|
{"ts":"2026-04-21T22:25:21Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\factory\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||||
|
{"ts":"2026-04-21T22:25:39Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\factory\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||||
|
{"ts":"2026-04-21T22:26:29Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\controls\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||||
|
{"ts":"2026-04-21T22:26:36Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\controls\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||||
|
{"ts":"2026-04-21T22:26:44Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\controls\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||||
|
{"ts":"2026-04-21T22:26:55Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\controls\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||||
|
{"ts":"2026-04-21T22:27:01Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\controls\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||||
|
{"ts":"2026-04-21T22:27:15Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\autoload\\\\controls\\\\class.Products.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||||
|
{"ts":"2026-04-21T22:27:25Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\templates\\\\products\\\\main_view.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||||
|
{"ts":"2026-04-21T22:27:30Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\templates\\\\products\\\\main_view.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||||
|
{"ts":"2026-04-21T22:27:34Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\templates\\\\products\\\\main_view.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||||
|
{"ts":"2026-04-21T22:28:19Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\templates\\\\products\\\\main_view.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||||
|
{"ts":"2026-04-21T22:28:38Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\templates\\\\products\\\\main_view.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||||
|
{"ts":"2026-04-21T22:28:54Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\templates\\\\products\\\\main_view.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||||
|
{"ts":"2026-04-21T22:29:05Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\templates\\\\products\\\\main_view.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||||
|
{"ts":"2026-04-21T22:29:32Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\templates\\\\products\\\\main_view.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||||
|
{"ts":"2026-04-21T22:29:37Z","tool":"Bash","cmd":"php -l autoload/factory/class.Products.php && php -l autoload/controls/class.Products.php\",\"description\":\"PHP lint edited files\"},\"tool_response\":{\"stdout\":\"No syntax errors detected in auto","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||||
|
{"ts":"2026-04-21T22:30:11Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||||
|
{"ts":"2026-04-21T22:30:47Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\.paul\\\\phases\\\\01-products-cl1-column\\\\01-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||||
|
{"ts":"2026-04-21T22:34:00Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\templates\\\\products\\\\main_view.php","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||||
|
{"ts":"2026-04-21T22:37:41Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\.paul\\\\phases\\\\01-products-cl1-column\\\\01-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||||
|
{"ts":"2026-04-21T22:37:55Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\.paul\\\\phases\\\\01-products-cl1-column\\\\01-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||||
|
{"ts":"2026-04-21T22:38:08Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/adsPRO"}
|
||||||
226
.paul/phases/01-products-cl1-column/01-01-PLAN.md
Normal file
226
.paul/phases/01-products-cl1-column/01-01-PLAN.md
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
---
|
||||||
|
phase: 01-products-cl1-column
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- migrations/028_products_custom_label_1.sql
|
||||||
|
- autoload/factory/class.Products.php
|
||||||
|
- autoload/controls/class.Products.php
|
||||||
|
- templates/products/main_view.php
|
||||||
|
autonomous: true
|
||||||
|
delegation: off
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
## Goal
|
||||||
|
Zastąpić w tabeli `/products` kolumnę **CL3** (read-only) kolumną **CL1** o pełnej funkcjonalności identycznej z CL4 (edytowalny input, autocomplete datalist, kolorowanie tła, filtr nagłówkowy, zapis AJAX, utrzymywanie filtra w localStorage). Utrzymać bieżące włączenie `custom_label_3` w supplemental feed (już obecne — tylko weryfikacja).
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Użytkownik potrzebuje ręcznej edycji `custom_label_1` z poziomu UI (analogicznie do CL4), a CL3 pozostaje polem systemowym uzupełnianym wyłącznie przez feed i nie wymaga obecności w widoku produktów.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
- Nowa kolumna DB `products.custom_label_1` + migracja
|
||||||
|
- Edytowalna kolumna CL1 w tabeli produktów z kolorem, autocomplete, filtrem nagłówkowym i zapisem AJAX
|
||||||
|
- Brak kolumny CL3 w widoku (nadal w DB, nadal w feedzie)
|
||||||
|
- Endpointy: `save_custom_label_1`, `get_distinct_cl1`
|
||||||
|
- Supplemental feed TSV bez zmian — `custom_label_3` już generowany (weryfikacja w AC-4)
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@CLAUDE.md
|
||||||
|
@autoload/controls/class.Products.php
|
||||||
|
@autoload/factory/class.Products.php
|
||||||
|
@autoload/services/class.SupplementalFeed.php
|
||||||
|
@autoload/services/class.GoogleAdsApi.php
|
||||||
|
@templates/products/main_view.php
|
||||||
|
@migrations/016_products_model_unification.sql
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<acceptance_criteria>
|
||||||
|
|
||||||
|
## AC-1: Migracja dodaje kolumnę custom_label_1
|
||||||
|
```gherkin
|
||||||
|
Given baza produkcyjna bez kolumny products.custom_label_1
|
||||||
|
When uruchomię `php install.php`
|
||||||
|
Then migracja 028 dodaje VARCHAR(255) NULL `custom_label_1` AFTER `min_roas` (idempotentnie)
|
||||||
|
And ponowne uruchomienie nie błędzi
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-2: Kolumna CL1 zastępuje CL3 w widoku /products
|
||||||
|
```gherkin
|
||||||
|
Given zalogowany użytkownik na /products z wybranym klientem
|
||||||
|
When załaduje się tabela produktów
|
||||||
|
Then nagłówek tabeli pokazuje "CL1" zamiast "CL3"
|
||||||
|
And w wierszu jest edytowalny input z klasą .custom_label_1 i atrybutem product_id
|
||||||
|
And kolumna CL3 jest usunięta z nagłówka, ciała i columnDefs
|
||||||
|
And szerokość/porządek innych kolumn pozostaje bez zmian
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-3: CL1 ma pełne zachowanie CL4
|
||||||
|
```gherkin
|
||||||
|
Given produkt z pustym custom_label_1 i aktywny input nagłówkowy #products_cl1
|
||||||
|
When wpiszę wartość w input wiersza i opuszczę pole (event change)
|
||||||
|
Then AJAX POST /products/save_custom_label_1 zapisuje wartość do DB
|
||||||
|
And wartość zostaje dodana do datalist (autocomplete) po przeładowaniu
|
||||||
|
And filtr #products_cl1 zawęża wyniki tabeli po debounce 400ms
|
||||||
|
And wartość filtra jest zapamiętywana w localStorage pod kluczem products_cl1
|
||||||
|
And kolorowanie tła działa według tej samej palety wartości co CL4 (neutralnie dla nieznanych)
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-4: custom_label_3 nadal trafia do supplemental feed
|
||||||
|
```gherkin
|
||||||
|
Given produkt z wypełnionym custom_label_3
|
||||||
|
When uruchomię generate_for_client(client_id)
|
||||||
|
Then plik TSV zawiera nagłówek "id\ttitle\tdescription\tgoogle_product_category\tcustom_label_3\tcustom_label_4"
|
||||||
|
And produkt jest obecny w pliku (custom_label_3 wystarcza do obecności)
|
||||||
|
And żadna linijka CL3 nie została usunięta w ramach tej zmiany
|
||||||
|
```
|
||||||
|
|
||||||
|
</acceptance_criteria>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Migracja DB — dodaj kolumnę custom_label_1</name>
|
||||||
|
<files>migrations/028_products_custom_label_1.sql</files>
|
||||||
|
<action>
|
||||||
|
Utwórz plik migracji zgodnie z konwencją z `migrations/016_products_model_unification.sql`:
|
||||||
|
- Idempotentnie dodaje `custom_label_1 VARCHAR(255) NULL DEFAULT NULL AFTER min_roas`
|
||||||
|
- Sprawdza INFORMATION_SCHEMA.COLUMNS przed ALTER
|
||||||
|
- Używa PREPARE/EXECUTE/DEALLOCATE
|
||||||
|
Nie dodawaj custom_label_0/2 — nie są w scope.
|
||||||
|
Nie modyfikuj products_data ani migracji 016.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
1. `php install.php` — brak błędu, migracja 028 w `schema_migrations`
|
||||||
|
2. `DESCRIBE products` — widoczna kolumna `custom_label_1`
|
||||||
|
3. Drugie uruchomienie `php install.php` — bez zmian (idempotentne)
|
||||||
|
</verify>
|
||||||
|
<done>AC-1 spełniony</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Factory — wymień CL3 na CL1 w odczytach listy + dodaj helpery CL1</name>
|
||||||
|
<files>autoload/factory/class.Products.php</files>
|
||||||
|
<action>
|
||||||
|
W `class.Products.php`:
|
||||||
|
1. W `is_product_core_field()` — dodaj `'custom_label_1'` do tablicy (obok już istniejących). Zachowaj `custom_label_3` (feed nadal go potrzebuje przez get/set).
|
||||||
|
2. W `get_products()`:
|
||||||
|
- W mapie `$order_map` zamień `'custom_label_3' => 'custom_label_3'` na `'custom_label_1' => 'custom_label_1'`
|
||||||
|
- W SELECT zamień `COALESCE(NULLIF(TRIM(p.custom_label_3),''),'') AS custom_label_3` na analog dla `custom_label_1`
|
||||||
|
- W `GROUP BY` zamień `p.custom_label_3` na `p.custom_label_1`
|
||||||
|
- Dodaj parametr `$custom_label_1 = ''` (sygnatura metody) i gałąź WHERE `AND p.custom_label_1 LIKE :custom_label_1`
|
||||||
|
- W LIKE-search dodaj `OR p.custom_label_1 LIKE :search` (CL4 już jest)
|
||||||
|
3. W `get_records_total_products()` i `get_roas_bounds()` dodaj identyczny parametr `$custom_label_1` + WHERE (aby licznik i bounds szanowały filtr — analogicznie do `$custom_label_4`).
|
||||||
|
4. Dodaj metodę `get_distinct_custom_label_1( $client_id )` — kopia `get_distinct_custom_label_4` z podmianą kolumny.
|
||||||
|
NIE usuwaj żadnych odwołań do `custom_label_3` w metodach set/get/field_map używanych przez feed/merchant sync — kolumna CL3 pozostaje w DB i jest eksportowana do feedu.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
1. `php -l autoload/factory/class.Products.php` — OK
|
||||||
|
2. AJAX `/products/list` zwraca JSON bez błędu dla klienta z custom_label_1
|
||||||
|
3. `filter_cl1=foo` w POST zawęża wynik (sprawdź SQL w logu/XDebug lub ręcznie)
|
||||||
|
</verify>
|
||||||
|
<done>AC-2 spełniony (warstwa danych) + baza pod AC-3 filtr</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Controller — renderuj CL1 jako edytowalny input + endpointy CL1</name>
|
||||||
|
<files>autoload/controls/class.Products.php</files>
|
||||||
|
<action>
|
||||||
|
W `class.Products.php`:
|
||||||
|
1. Akcja `list()`:
|
||||||
|
- Pobierz `$filter_cl1 = trim((string)\S::get('filter_cl1'))`
|
||||||
|
- Przekaż `$filter_cl1` do `get_roas_bounds`, `get_products`, `get_records_total_products` (nowy parametr — analogicznie do `filter_cl4`)
|
||||||
|
- W pętli `foreach ( $db_results as $row )` pobierz `$custom_label_1 = \factory\Products::get_product_data($row['product_id'], 'custom_label_1')` i wylicz `$custom_label_1_color` analogicznie do CL4 (te same mapy kolorów dla bestseller/deleted/zombie/pla_single/pla/paused — użyj JEDNEJ lokalnej funkcji lambda żeby nie duplikować)
|
||||||
|
- W tablicy danych wiersza USUŃ komórkę CL3 (`htmlspecialchars((string)($row['custom_label_3'] ?? ''))`) i zastąp ją inputem CL1: `'<input type="text" class="form-control custom_label_1" product_id="' . $row['product_id'] . '" value="' . $custom_label_1 . '" style="' . $custom_label_1_color . '">'`
|
||||||
|
- Pozycja komórki: dokładnie tam, gdzie była CL3 (przed komórką CL4), aby indeksy data[] w JS się nie rozjechały
|
||||||
|
2. Dodaj metodę `get_distinct_cl1()` — analog `get_distinct_cl4` wywołująca `\factory\Products::get_distinct_custom_label_1`
|
||||||
|
3. Dodaj metodę `save_custom_label_1()` — analog `save_custom_label_4` (walidacja, `set_product_data(..., 'custom_label_1', $v)`, `add_product_comment($product_id, 'Zmiana etykiety 1 na: ' . $custom_label_1)`)
|
||||||
|
Zachowaj:
|
||||||
|
- Istniejące użycia `custom_label_3` w innych akcjach (get_product_data, product_history, edycje) bez zmian
|
||||||
|
- Indeksowanie kolumn DataTables (liczba kolumn pozostaje ta sama: 1-na-1 zamiana)
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
1. `php -l autoload/controls/class.Products.php` — OK
|
||||||
|
2. POST `/products/save_custom_label_1/` z product_id i custom_label_1=test → `{"status":"ok"}`
|
||||||
|
3. GET `/products/get_distinct_cl1/client_id=X` → `{"values":[...]}`
|
||||||
|
</verify>
|
||||||
|
<done>AC-2 + AC-3 spełnione (warstwa serwerowa)</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 4: Template — zamień CL3 na CL1 w UI (nagłówek, filtr, columnDefs, JS)</name>
|
||||||
|
<files>templates/products/main_view.php</files>
|
||||||
|
<action>
|
||||||
|
W `templates/products/main_view.php`:
|
||||||
|
1. Nagłówek tabeli: zamień `<th>CL3</th>` na `<th>CL1</th>` (linia ~120). NIE dodawaj nowego `<th>` — nadal dokładnie zastępujesz.
|
||||||
|
2. Filtry nagłówkowe: obok istniejącego bloku `.filter-group-cl4` dodaj analogiczny `.filter-group-cl1` z `<input id="products_cl1">` (placeholder „np. bestseller, deleted..."). Możesz postawić go PRZED filtrem CL4 (spójność z kolejnością kolumn).
|
||||||
|
3. DataTables columnDefs: zamień `{ width: '50px', name: 'custom_label_3' }` na `{ width: '50px', name: 'custom_label_1' }`.
|
||||||
|
4. W `ajax.data` callbacku: dodaj `d.filter_cl1 = $('#products_cl1').val() || '';` obok `d.filter_cl4`.
|
||||||
|
5. `createdRow` callback: obecnie czyta `data[20]` (CL4). Po zamianie CL3→CL1 indeksy pozostają takie same (1:1 swap), więc zostaw bez zmian — chyba że weryfikacja wykaże przesunięcie.
|
||||||
|
6. Debounce filtra: skopiuj blok `_cl4Timer` na `_cl1Timer` (listener keyup #products_cl1, `localStorage.setItem('products_cl1', ...)`, reload po 400ms).
|
||||||
|
7. Reset filtrów (`localStorage.removeItem`, `.val('')`) — dodaj `products_cl1` do wszystkich list.
|
||||||
|
8. Odczyt z localStorage przy starcie: `var savedCl1 = localStorage.getItem('products_cl1') || '';` + `$('#products_cl1').val(savedCl1);`.
|
||||||
|
9. Autocomplete/datalist dla CL1:
|
||||||
|
- zduplikuj blok „CL4 autocomplete" (funkcje `load_cl4_suggestions`, `render_cl4_datalist`, `bind_cl4_datalist`, `refresh_cl4_cache_after_save`) pod nową nazwą „CL1 autocomplete" z endpointem `/products/get_distinct_cl1/` i selektorem `.custom_label_1`
|
||||||
|
- podepnij `bind_cl1_datalist()` do `draw.dt` (obok CL4)
|
||||||
|
- `load_cl1_suggestions($(this).val())` w change #client_id
|
||||||
|
10. Zapis CL1:
|
||||||
|
- zduplikuj handler `$('body').on('change', '.custom_label_4', ...)` na `.custom_label_1` → endpoint `/products/save_custom_label_1/`, po sukcesie `refresh_cl1_cache_after_save()`
|
||||||
|
Nie dotykaj żadnej logiki poza CL3→CL1 i duplikacją CL4→CL1.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
1. Otwórz `/products` → widać kolumnę CL1 (edytowalny input), brak CL3
|
||||||
|
2. Wpisz wartość w CL1 i blur → toast sukcesu, reload pokazuje zapisaną wartość
|
||||||
|
3. Wpisz wartość w filtrze nagłówkowym #products_cl1 → tabela zawęża wyniki po 400ms
|
||||||
|
4. Po F5 filtr CL1 jest przywracany z localStorage
|
||||||
|
5. Datalist pokazuje unikalne wartości po zmianie klienta
|
||||||
|
</verify>
|
||||||
|
<done>AC-2 + AC-3 spełnione (UI)</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<boundaries>
|
||||||
|
|
||||||
|
## DO NOT CHANGE
|
||||||
|
- `autoload/services/class.SupplementalFeed.php` — custom_label_3 już tam jest; weryfikacja tylko czytaniem (AC-4)
|
||||||
|
- `autoload/services/class.GoogleAdsApi.php` — `field_map` już ma `custom_label_1` → `customLabel1`, nic nie ruszamy
|
||||||
|
- Kolumna `products.custom_label_3` w DB — zostaje (nadal używana przez feed i historię)
|
||||||
|
- Migracja `016_products_model_unification.sql` — bez modyfikacji
|
||||||
|
- Logika bestseller (`refresh_bestseller_labels_for_client`) — pozostaje związana wyłącznie z CL4
|
||||||
|
- `products_data` tabela/logika — bez zmian
|
||||||
|
- `class.js` funkcje pomocnicze wspólne (escape_html, show_toast) — reużywamy, nie duplikujemy
|
||||||
|
|
||||||
|
## SCOPE LIMITS
|
||||||
|
- Nie dodajemy kolumn custom_label_0 ani custom_label_2
|
||||||
|
- Nie wprowadzamy automatyki dla CL1 (żadnych reguł auto-etykietowania)
|
||||||
|
- Nie zmieniamy struktury feedu TSV (kolejność kolumn pozostaje: id, title, description, google_product_category, custom_label_3, custom_label_4)
|
||||||
|
- Nie dotykamy merchant-sync cronów (CL3 nadal populowany po staremu, CL1 to nowa kolumna, pusta dla istniejących produktów)
|
||||||
|
- Nie zmieniamy indeksów kolumn DataTables inaczej niż przez 1:1 zamianę CL3→CL1
|
||||||
|
|
||||||
|
</boundaries>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- [ ] `php -l` na wszystkich zmienionych plikach PHP — brak błędów
|
||||||
|
- [ ] `php install.php` — migracja 028 zastosowana idempotentnie
|
||||||
|
- [ ] Otwarcie `/products` z wybranym klientem — tabela ładuje się bez błędów konsoli
|
||||||
|
- [ ] Kolumna CL1 edytowalna, zapis AJAX działa, filtr działa, datalist działa
|
||||||
|
- [ ] Generowanie supplemental feed dla klienta → TSV zawiera niezmieniony blok custom_label_3 (AC-4)
|
||||||
|
- [ ] Brak odwołań do `custom_label_3` w widoku (grep templates/products + controller `list()` renderer komórek)
|
||||||
|
- [ ] Odwołania do `custom_label_3` pozostają w feed/GoogleAdsApi/get_product_data/set_product_data
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Wszystkie 4 zadania ukończone
|
||||||
|
- Wszystkie 4 acceptance criteria spełnione
|
||||||
|
- Brak błędów PHP ani JS console
|
||||||
|
- Regresja: CL4 działa jak przed zmianą (te same color coding, autocomplete, filtr, zapis)
|
||||||
|
- Supplemental feed TSV bit-identyczny z poprzednim wynikiem dla tych samych danych testowych
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
Po ukończeniu utwórz `.paul/phases/01-products-cl1-column/01-01-SUMMARY.md` z listą zmienionych plików, wynikami weryfikacji i ewentualnymi odroczonymi kwestiami.
|
||||||
|
</output>
|
||||||
86
.paul/phases/01-products-cl1-column/01-01-SUMMARY.md
Normal file
86
.paul/phases/01-products-cl1-column/01-01-SUMMARY.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
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 `<input class="custom_label_1">` 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: `<th>CL3</th>` → `<th>CL1</th>`
|
||||||
|
- 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)
|
||||||
20
.vscode/ftp-kr.sync.cache.json
vendored
20
.vscode/ftp-kr.sync.cache.json
vendored
@@ -101,8 +101,8 @@
|
|||||||
},
|
},
|
||||||
"class.Cron.php": {
|
"class.Cron.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 184735,
|
"size": 184760,
|
||||||
"lmtime": 1773134833337,
|
"lmtime": 1774557774097,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"class.FacebookAds.php": {
|
"class.FacebookAds.php": {
|
||||||
@@ -125,8 +125,8 @@
|
|||||||
},
|
},
|
||||||
"class.Products.php": {
|
"class.Products.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 50269,
|
"size": 50620,
|
||||||
"lmtime": 1773703439429,
|
"lmtime": 1774556928712,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"class.Site.php": {
|
"class.Site.php": {
|
||||||
@@ -157,8 +157,8 @@
|
|||||||
},
|
},
|
||||||
"class.Campaigns.php": {
|
"class.Campaigns.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 14333,
|
"size": 14536,
|
||||||
"lmtime": 1772671326827,
|
"lmtime": 1774557850564,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"class.Clients.php": {
|
"class.Clients.php": {
|
||||||
@@ -193,8 +193,8 @@
|
|||||||
},
|
},
|
||||||
"class.Products.php": {
|
"class.Products.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 42617,
|
"size": 42493,
|
||||||
"lmtime": 1773703548911,
|
"lmtime": 1774557838303,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"class.Users.php": {
|
"class.Users.php": {
|
||||||
@@ -793,8 +793,8 @@
|
|||||||
"products": {
|
"products": {
|
||||||
"main_view.php": {
|
"main_view.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 77733,
|
"size": 79768,
|
||||||
"lmtime": 1773703439430,
|
"lmtime": 1774561858674,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"product_history.php": {
|
"product_history.php": {
|
||||||
|
|||||||
@@ -925,9 +925,10 @@ class Products
|
|||||||
$order_name = $_POST['order'][0]['name'] ? $_POST['order'][0]['name'] : 'clicks';
|
$order_name = $_POST['order'][0]['name'] ? $_POST['order'][0]['name'] : 'clicks';
|
||||||
$search = trim( (string) \S::get( 'search_text' ) );
|
$search = trim( (string) \S::get( 'search_text' ) );
|
||||||
$filter_cl4 = trim( (string) \S::get( 'filter_cl4' ) );
|
$filter_cl4 = trim( (string) \S::get( 'filter_cl4' ) );
|
||||||
|
$filter_cl1 = trim( (string) \S::get( 'filter_cl1' ) );
|
||||||
|
|
||||||
// ➊ MIN/MAX ROAS dla kontekstu klienta (opcjonalnie z filtrem search)
|
// ➊ MIN/MAX ROAS dla kontekstu klienta (opcjonalnie z filtrem search)
|
||||||
$bounds = \factory\Products::get_roas_bounds( (int) $client_id, $search, $campaign_id, $ad_group_id, $filter_cl4 );
|
$bounds = \factory\Products::get_roas_bounds( (int) $client_id, $search, $campaign_id, $ad_group_id, $filter_cl4, $filter_cl1 );
|
||||||
$roas_min = (float)$bounds['min'];
|
$roas_min = (float)$bounds['min'];
|
||||||
$roas_max = (float)$bounds['max'];
|
$roas_max = (float)$bounds['max'];
|
||||||
// zabezpieczenie przed dzieleniem przez 0
|
// zabezpieczenie przed dzieleniem przez 0
|
||||||
@@ -962,8 +963,8 @@ class Products
|
|||||||
</div>';
|
</div>';
|
||||||
};
|
};
|
||||||
|
|
||||||
$db_results = \factory\Products::get_products( $client_id, $search, $limit, $start, $order_name, $order_dir, $campaign_id, $ad_group_id, $filter_cl4 );
|
$db_results = \factory\Products::get_products( $client_id, $search, $limit, $start, $order_name, $order_dir, $campaign_id, $ad_group_id, $filter_cl4, $filter_cl1 );
|
||||||
$recordsTotal = \factory\Products::get_records_total_products( $client_id, $search, $campaign_id, $ad_group_id, $filter_cl4 );
|
$recordsTotal = \factory\Products::get_records_total_products( $client_id, $search, $campaign_id, $ad_group_id, $filter_cl4, $filter_cl1 );
|
||||||
|
|
||||||
// Sredni CR konta — do obliczenia progu klikniec
|
// Sredni CR konta — do obliczenia progu klikniec
|
||||||
$account_cr = \factory\Products::get_account_conversion_rate( (int) $client_id );
|
$account_cr = \factory\Products::get_account_conversion_rate( (int) $client_id );
|
||||||
@@ -978,6 +979,7 @@ class Products
|
|||||||
{
|
{
|
||||||
$custom_class = '';
|
$custom_class = '';
|
||||||
$custom_label_4 = \factory\Products::get_product_data( $row['product_id'], 'custom_label_4' );
|
$custom_label_4 = \factory\Products::get_product_data( $row['product_id'], 'custom_label_4' );
|
||||||
|
$custom_label_1 = \factory\Products::get_product_data( $row['product_id'], 'custom_label_1' );
|
||||||
$custom_name = \factory\Products::get_product_data( $row['product_id'], 'title' );
|
$custom_name = \factory\Products::get_product_data( $row['product_id'], 'title' );
|
||||||
$product_url = trim( (string) \factory\Products::get_product_data( $row['product_id'], 'product_url' ) );
|
$product_url = trim( (string) \factory\Products::get_product_data( $row['product_id'], 'product_url' ) );
|
||||||
|
|
||||||
@@ -1004,6 +1006,20 @@ class Products
|
|||||||
else if ( $custom_label_4 == 'paused' )
|
else if ( $custom_label_4 == 'paused' )
|
||||||
$custom_label_4_color = 'background-color:rgb(143, 143, 143); color: #FFF;';
|
$custom_label_4_color = 'background-color:rgb(143, 143, 143); color: #FFF;';
|
||||||
|
|
||||||
|
$custom_label_1_color = '';
|
||||||
|
if ( $custom_label_1 == 'bestseller' )
|
||||||
|
$custom_label_1_color = 'background-color:rgb(96, 119, 102); color: #FFF;';
|
||||||
|
else if ( $custom_label_1 == 'deleted' )
|
||||||
|
$custom_label_1_color = 'background-color:rgb(255, 0, 0); color: #FFF;';
|
||||||
|
else if ( $custom_label_1 == 'zombie' )
|
||||||
|
$custom_label_1_color = 'background-color:rgb(58, 58, 58); color: #FFF;';
|
||||||
|
else if ( $custom_label_1 == 'pla_single' )
|
||||||
|
$custom_label_1_color = 'background-color:rgb(49, 184, 9); color: #FFF;';
|
||||||
|
else if ( $custom_label_1 == 'pla' )
|
||||||
|
$custom_label_1_color = 'background-color:rgb(74, 63, 136); color: #FFF;';
|
||||||
|
else if ( $custom_label_1 == 'paused' )
|
||||||
|
$custom_label_1_color = 'background-color:rgb(143, 143, 143); color: #FFF;';
|
||||||
|
|
||||||
// ➌ ROAS – liczba + pasek performance
|
// ➌ ROAS – liczba + pasek performance
|
||||||
$roasValue = (float)$row['roas'];
|
$roasValue = (float)$row['roas'];
|
||||||
$roasDisplay = (int) round( $roasValue, 0 );
|
$roasDisplay = (int) round( $roasValue, 0 );
|
||||||
@@ -1096,7 +1112,7 @@ class Products
|
|||||||
\S::number_display( $row['conversions_value'] ),
|
\S::number_display( $row['conversions_value'] ),
|
||||||
$roasCellHtml,
|
$roasCellHtml,
|
||||||
'<input type="text" class="form-control min_roas" product_id="' . $row['product_id'] . '" value="' . $row['min_roas'] . '" style="width: 100px;">',
|
'<input type="text" class="form-control min_roas" product_id="' . $row['product_id'] . '" value="' . $row['min_roas'] . '" style="width: 100px;">',
|
||||||
htmlspecialchars( (string) ( $row['custom_label_3'] ?? '' ) ),
|
'<input type="text" class="form-control custom_label_1" product_id="' . $row['product_id'] . '" value="' . $custom_label_1 . '" style="' . $custom_label_1_color . '">',
|
||||||
'<input type="text" class="form-control custom_label_4" product_id="' . $row['product_id'] . '" value="' . $custom_label_4 . '" style="' . $custom_label_4_color . '">',
|
'<input type="text" class="form-control custom_label_4" product_id="' . $row['product_id'] . '" value="' . $custom_label_4 . '" style="' . $custom_label_4_color . '">',
|
||||||
'<div class="btn-group btn-group-sm products-row-actions" role="group">'
|
'<div class="btn-group btn-group-sm products-row-actions" role="group">'
|
||||||
. '<button type="button" class="btn btn-primary assign-product-scope" product_id="' . $row['product_id'] . '" title="Dodaj produkt do kampanii/grupy"><i class="fa-solid fa-diagram-project"></i></button>'
|
. '<button type="button" class="btn btn-primary assign-product-scope" product_id="' . $row['product_id'] . '" title="Dodaj produkt do kampanii/grupy"><i class="fa-solid fa-diagram-project"></i></button>'
|
||||||
@@ -1172,6 +1188,29 @@ class Products
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static public function get_distinct_cl1()
|
||||||
|
{
|
||||||
|
$client_id = (int) \S::get( 'client_id' );
|
||||||
|
$values = \factory\Products::get_distinct_custom_label_1( $client_id );
|
||||||
|
echo json_encode( [ 'status' => 'ok', 'values' => $values ] );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
static public function save_custom_label_1()
|
||||||
|
{
|
||||||
|
$product_id = \S::get( 'product_id' );
|
||||||
|
$custom_label_1 = \S::get( 'custom_label_1' );
|
||||||
|
|
||||||
|
if ( \factory\Products::set_product_data( $product_id, 'custom_label_1', $custom_label_1 ) )
|
||||||
|
{
|
||||||
|
\factory\Products::add_product_comment( $product_id, 'Zmiana etykiety 1 na: ' . $custom_label_1 );
|
||||||
|
echo json_encode( [ 'status' => 'ok' ] );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
echo json_encode( [ 'status' => 'error' ] );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
static public function product_history()
|
static public function product_history()
|
||||||
{
|
{
|
||||||
$client_id = \S::get( 'client_id' );
|
$client_id = \S::get( 'client_id' );
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ class Products
|
|||||||
return in_array( (string) $field, [
|
return in_array( (string) $field, [
|
||||||
'custom_label_4',
|
'custom_label_4',
|
||||||
'custom_label_3',
|
'custom_label_3',
|
||||||
|
'custom_label_1',
|
||||||
'title',
|
'title',
|
||||||
'description',
|
'description',
|
||||||
'google_product_category',
|
'google_product_category',
|
||||||
@@ -486,7 +487,7 @@ class Products
|
|||||||
$sql .= ' AND ag.status = \'active\'';
|
$sql .= ' AND ag.status = \'active\'';
|
||||||
}
|
}
|
||||||
|
|
||||||
static public function get_products( $client_id, $search, $limit, $start, $order_name, $order_dir, $campaign_id = 0, $ad_group_id = 0, $custom_label_4 = '' )
|
static public function get_products( $client_id, $search, $limit, $start, $order_name, $order_dir, $campaign_id = 0, $ad_group_id = 0, $custom_label_4 = '', $custom_label_1 = '' )
|
||||||
{
|
{
|
||||||
global $mdb;
|
global $mdb;
|
||||||
|
|
||||||
@@ -499,7 +500,7 @@ class Products
|
|||||||
'campaign_name' => 'campaign_name',
|
'campaign_name' => 'campaign_name',
|
||||||
'ad_group_name' => 'ad_group_name',
|
'ad_group_name' => 'ad_group_name',
|
||||||
'name' => 'name',
|
'name' => 'name',
|
||||||
'custom_label_3' => 'custom_label_3',
|
'custom_label_1' => 'custom_label_1',
|
||||||
'impressions' => 'impressions',
|
'impressions' => 'impressions',
|
||||||
'impressions_30' => 'impressions_30',
|
'impressions_30' => 'impressions_30',
|
||||||
'clicks' => 'clicks',
|
'clicks' => 'clicks',
|
||||||
@@ -520,7 +521,7 @@ class Products
|
|||||||
p.id AS product_id,
|
p.id AS product_id,
|
||||||
p.offer_id,
|
p.offer_id,
|
||||||
p.min_roas,
|
p.min_roas,
|
||||||
COALESCE( NULLIF( TRIM( p.custom_label_3 ), \'\' ), \'\' ) AS custom_label_3,
|
COALESCE( NULLIF( TRIM( p.custom_label_1 ), \'\' ), \'\' ) AS custom_label_1,
|
||||||
pa.campaign_id AS campaign_id,
|
pa.campaign_id AS campaign_id,
|
||||||
COALESCE( NULLIF( TRIM( c.campaign_name ), \'\' ), \'--- brak kampanii ---\' ) AS campaign_name,
|
COALESCE( NULLIF( TRIM( c.campaign_name ), \'\' ), \'--- brak kampanii ---\' ) AS campaign_name,
|
||||||
CASE
|
CASE
|
||||||
@@ -565,6 +566,7 @@ class Products
|
|||||||
OR p.title LIKE :search
|
OR p.title LIKE :search
|
||||||
OR p.offer_id LIKE :search
|
OR p.offer_id LIKE :search
|
||||||
OR p.custom_label_4 LIKE :search
|
OR p.custom_label_4 LIKE :search
|
||||||
|
OR p.custom_label_1 LIKE :search
|
||||||
OR c.campaign_name LIKE :search
|
OR c.campaign_name LIKE :search
|
||||||
OR ag.ad_group_name LIKE :search
|
OR ag.ad_group_name LIKE :search
|
||||||
)';
|
)';
|
||||||
@@ -577,13 +579,19 @@ class Products
|
|||||||
$params[':custom_label_4'] = '%' . $custom_label_4 . '%';
|
$params[':custom_label_4'] = '%' . $custom_label_4 . '%';
|
||||||
}
|
}
|
||||||
|
|
||||||
$sql .= ' GROUP BY p.id, p.offer_id, p.min_roas, p.custom_label_3, p.name, p.title, pa.campaign_id, c.campaign_name, pa.ad_group_id, ag.ad_group_name';
|
if ( $custom_label_1 !== '' )
|
||||||
|
{
|
||||||
|
$sql .= ' AND p.custom_label_1 LIKE :custom_label_1';
|
||||||
|
$params[':custom_label_1'] = '%' . $custom_label_1 . '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql .= ' GROUP BY p.id, p.offer_id, p.min_roas, p.custom_label_1, p.name, p.title, pa.campaign_id, c.campaign_name, pa.ad_group_id, ag.ad_group_name';
|
||||||
$sql .= ' ORDER BY ' . $order_sql . ' ' . $order_dir . ', product_id DESC LIMIT ' . $start . ', ' . $limit;
|
$sql .= ' ORDER BY ' . $order_sql . ' ' . $order_dir . ', product_id DESC LIMIT ' . $start . ', ' . $limit;
|
||||||
|
|
||||||
return $mdb -> query( $sql, $params ) -> fetchAll( \PDO::FETCH_ASSOC );
|
return $mdb -> query( $sql, $params ) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function get_roas_bounds( int $client_id, ?string $search = null, int $campaign_id = 0, int $ad_group_id = 0, string $custom_label_4 = '' ): array
|
public static function get_roas_bounds( int $client_id, ?string $search = null, int $campaign_id = 0, int $ad_group_id = 0, string $custom_label_4 = '', string $custom_label_1 = '' ): array
|
||||||
{
|
{
|
||||||
global $mdb;
|
global $mdb;
|
||||||
|
|
||||||
@@ -612,6 +620,7 @@ class Products
|
|||||||
OR p.title LIKE :search
|
OR p.title LIKE :search
|
||||||
OR p.offer_id LIKE :search
|
OR p.offer_id LIKE :search
|
||||||
OR p.custom_label_4 LIKE :search
|
OR p.custom_label_4 LIKE :search
|
||||||
|
OR p.custom_label_1 LIKE :search
|
||||||
OR c.campaign_name LIKE :search
|
OR c.campaign_name LIKE :search
|
||||||
OR ag.ad_group_name LIKE :search
|
OR ag.ad_group_name LIKE :search
|
||||||
)';
|
)';
|
||||||
@@ -624,6 +633,12 @@ class Products
|
|||||||
$params[':custom_label_4'] = '%' . $custom_label_4 . '%';
|
$params[':custom_label_4'] = '%' . $custom_label_4 . '%';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( $custom_label_1 !== '' )
|
||||||
|
{
|
||||||
|
$sql .= ' AND p.custom_label_1 LIKE :custom_label_1';
|
||||||
|
$params[':custom_label_1'] = '%' . $custom_label_1 . '%';
|
||||||
|
}
|
||||||
|
|
||||||
$row = $mdb -> query( $sql, $params ) -> fetch( \PDO::FETCH_ASSOC );
|
$row = $mdb -> query( $sql, $params ) -> fetch( \PDO::FETCH_ASSOC );
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -656,7 +671,7 @@ class Products
|
|||||||
return $total_conversions / $total_clicks;
|
return $total_conversions / $total_clicks;
|
||||||
}
|
}
|
||||||
|
|
||||||
static public function get_records_total_products( $client_id, $search, $campaign_id = 0, $ad_group_id = 0, $custom_label_4 = '' )
|
static public function get_records_total_products( $client_id, $search, $campaign_id = 0, $ad_group_id = 0, $custom_label_4 = '', $custom_label_1 = '' )
|
||||||
{
|
{
|
||||||
global $mdb;
|
global $mdb;
|
||||||
|
|
||||||
@@ -679,6 +694,7 @@ class Products
|
|||||||
OR p.title LIKE :search
|
OR p.title LIKE :search
|
||||||
OR p.offer_id LIKE :search
|
OR p.offer_id LIKE :search
|
||||||
OR p.custom_label_4 LIKE :search
|
OR p.custom_label_4 LIKE :search
|
||||||
|
OR p.custom_label_1 LIKE :search
|
||||||
OR c.campaign_name LIKE :search
|
OR c.campaign_name LIKE :search
|
||||||
OR ag.ad_group_name LIKE :search
|
OR ag.ad_group_name LIKE :search
|
||||||
)';
|
)';
|
||||||
@@ -691,6 +707,12 @@ class Products
|
|||||||
$params[':custom_label_4'] = '%' . $custom_label_4 . '%';
|
$params[':custom_label_4'] = '%' . $custom_label_4 . '%';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( $custom_label_1 !== '' )
|
||||||
|
{
|
||||||
|
$sql .= ' AND p.custom_label_1 LIKE :custom_label_1';
|
||||||
|
$params[':custom_label_1'] = '%' . $custom_label_1 . '%';
|
||||||
|
}
|
||||||
|
|
||||||
$sql .= ' GROUP BY p.id, pa.campaign_id, pa.ad_group_id
|
$sql .= ' GROUP BY p.id, pa.campaign_id, pa.ad_group_id
|
||||||
) AS grouped_rows';
|
) AS grouped_rows';
|
||||||
|
|
||||||
@@ -760,6 +782,30 @@ class Products
|
|||||||
return $rows ?: [];
|
return $rows ?: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static public function get_distinct_custom_label_1( $client_id )
|
||||||
|
{
|
||||||
|
global $mdb;
|
||||||
|
|
||||||
|
$client_id = (int) $client_id;
|
||||||
|
|
||||||
|
if ( $client_id <= 0 )
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = $mdb -> query(
|
||||||
|
"SELECT DISTINCT p.custom_label_1
|
||||||
|
FROM products p
|
||||||
|
WHERE p.client_id = :client_id
|
||||||
|
AND p.custom_label_1 IS NOT NULL
|
||||||
|
AND p.custom_label_1 != ''
|
||||||
|
ORDER BY p.custom_label_1 ASC",
|
||||||
|
[ ':client_id' => $client_id ]
|
||||||
|
) -> fetchAll( \PDO::FETCH_COLUMN );
|
||||||
|
|
||||||
|
return $rows ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
static public function get_product_data( $product_id, $field )
|
static public function get_product_data( $product_id, $field )
|
||||||
{
|
{
|
||||||
global $mdb;
|
global $mdb;
|
||||||
|
|||||||
17
migrations/028_products_custom_label_1.sql
Normal file
17
migrations/028_products_custom_label_1.sql
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
-- Migracja: dodanie kolumny custom_label_1 do tabeli products
|
||||||
|
-- Cel: wsparcie edytowalnej kolumny CL1 w widoku /products (analogicznie do custom_label_4)
|
||||||
|
|
||||||
|
SET @sql = IF(
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'products'
|
||||||
|
AND COLUMN_NAME = 'custom_label_1'
|
||||||
|
),
|
||||||
|
'DO 1',
|
||||||
|
'ALTER TABLE `products` ADD COLUMN `custom_label_1` VARCHAR(255) NULL DEFAULT NULL AFTER `min_roas`'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
@@ -35,6 +35,10 @@
|
|||||||
<label for="products_search"><i class="fa-solid fa-magnifying-glass"></i> Szukaj</label>
|
<label for="products_search"><i class="fa-solid fa-magnifying-glass"></i> Szukaj</label>
|
||||||
<input type="text" id="products_search" class="form-control" placeholder="Nazwa, ID oferty..." />
|
<input type="text" id="products_search" class="form-control" placeholder="Nazwa, ID oferty..." />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="filter-group filter-group-cl1">
|
||||||
|
<label for="products_cl1"><i class="fa-solid fa-tag"></i> CL1</label>
|
||||||
|
<input type="text" id="products_cl1" class="form-control" placeholder="np. bestseller, deleted..." />
|
||||||
|
</div>
|
||||||
<div class="filter-group filter-group-cl4">
|
<div class="filter-group filter-group-cl4">
|
||||||
<label for="products_cl4"><i class="fa-solid fa-tag"></i> CL4</label>
|
<label for="products_cl4"><i class="fa-solid fa-tag"></i> CL4</label>
|
||||||
<input type="text" id="products_cl4" class="form-control" placeholder="np. bestseller, deleted..." />
|
<input type="text" id="products_cl4" class="form-control" placeholder="np. bestseller, deleted..." />
|
||||||
@@ -117,7 +121,7 @@
|
|||||||
<th>Wart. konw.</th>
|
<th>Wart. konw.</th>
|
||||||
<th>ROAS</th>
|
<th>ROAS</th>
|
||||||
<th>Min. ROAS</th>
|
<th>Min. ROAS</th>
|
||||||
<th>CL3</th>
|
<th>CL1</th>
|
||||||
<th>CL4</th>
|
<th>CL4</th>
|
||||||
<th>Akcje</th>
|
<th>Akcje</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -541,6 +545,7 @@ $( function()
|
|||||||
d.ad_group_id = $( '#products_ad_group_id' ).val() || '';
|
d.ad_group_id = $( '#products_ad_group_id' ).val() || '';
|
||||||
d.search_text = $( '#products_search' ).val() || '';
|
d.search_text = $( '#products_search' ).val() || '';
|
||||||
d.filter_cl4 = $( '#products_cl4' ).val() || '';
|
d.filter_cl4 = $( '#products_cl4' ).val() || '';
|
||||||
|
d.filter_cl1 = $( '#products_cl1' ).val() || '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
processing: true,
|
processing: true,
|
||||||
@@ -572,7 +577,7 @@ $( function()
|
|||||||
{ width: '90px', name: 'conversions_value', className: "dt-type-numeric" },
|
{ width: '90px', name: 'conversions_value', className: "dt-type-numeric" },
|
||||||
{ width: '60px', name: 'roas' },
|
{ width: '60px', name: 'roas' },
|
||||||
{ width: '70px', name: 'min_roas' },
|
{ width: '70px', name: 'min_roas' },
|
||||||
{ width: '50px', name: 'custom_label_3' },
|
{ width: '120px', name: 'custom_label_1' },
|
||||||
{ width: '120px', orderable: false },
|
{ width: '120px', orderable: false },
|
||||||
{ width: '190px', orderable: false, className: 'dt-center' }
|
{ width: '190px', orderable: false, className: 'dt-center' }
|
||||||
],
|
],
|
||||||
@@ -621,6 +626,14 @@ $( function()
|
|||||||
_cl4Timer = setTimeout( function() { reload_products_table(); }, 400 );
|
_cl4Timer = setTimeout( function() { reload_products_table(); }, 400 );
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Filtr: custom_label_1 (debounce 400ms)
|
||||||
|
var _cl1Timer = null;
|
||||||
|
$( '#products_cl1' ).on( 'keyup', function() {
|
||||||
|
localStorage.setItem( 'products_cl1', $( this ).val() || '' );
|
||||||
|
clearTimeout( _cl1Timer );
|
||||||
|
_cl1Timer = setTimeout( function() { reload_products_table(); }, 400 );
|
||||||
|
});
|
||||||
|
|
||||||
function submit_delete_campaign_ad_group( campaign_id, ad_group_id, delete_scope, on_success )
|
function submit_delete_campaign_ad_group( campaign_id, ad_group_id, delete_scope, on_success )
|
||||||
{
|
{
|
||||||
function parse_json_loose( raw )
|
function parse_json_loose( raw )
|
||||||
@@ -1320,8 +1333,10 @@ $( function()
|
|||||||
localStorage.removeItem( 'products_ad_group_id' );
|
localStorage.removeItem( 'products_ad_group_id' );
|
||||||
localStorage.removeItem( 'products_search' );
|
localStorage.removeItem( 'products_search' );
|
||||||
localStorage.removeItem( 'products_cl4' );
|
localStorage.removeItem( 'products_cl4' );
|
||||||
|
localStorage.removeItem( 'products_cl1' );
|
||||||
$( '#products_search' ).val( '' );
|
$( '#products_search' ).val( '' );
|
||||||
$( '#products_cl4' ).val( '' );
|
$( '#products_cl4' ).val( '' );
|
||||||
|
$( '#products_cl1' ).val( '' );
|
||||||
update_delete_ad_group_button_state();
|
update_delete_ad_group_button_state();
|
||||||
load_client_bestseller_settings( client_id );
|
load_client_bestseller_settings( client_id );
|
||||||
|
|
||||||
@@ -1435,6 +1450,7 @@ $( function()
|
|||||||
var savedAdGroup = localStorage.getItem( 'products_ad_group_id' ) || '';
|
var savedAdGroup = localStorage.getItem( 'products_ad_group_id' ) || '';
|
||||||
var savedSearch = localStorage.getItem( 'products_search' ) || '';
|
var savedSearch = localStorage.getItem( 'products_search' ) || '';
|
||||||
var savedCl4 = localStorage.getItem( 'products_cl4' ) || '';
|
var savedCl4 = localStorage.getItem( 'products_cl4' ) || '';
|
||||||
|
var savedCl1 = localStorage.getItem( 'products_cl1' ) || '';
|
||||||
|
|
||||||
if ( savedClient && $( '#client_id option[value="' + savedClient + '"]' ).length )
|
if ( savedClient && $( '#client_id option[value="' + savedClient + '"]' ).length )
|
||||||
{
|
{
|
||||||
@@ -1443,9 +1459,11 @@ $( function()
|
|||||||
|
|
||||||
$( '#products_search' ).val( savedSearch );
|
$( '#products_search' ).val( savedSearch );
|
||||||
$( '#products_cl4' ).val( savedCl4 );
|
$( '#products_cl4' ).val( savedCl4 );
|
||||||
|
$( '#products_cl1' ).val( savedCl1 );
|
||||||
load_client_bestseller_settings( $( '#client_id' ).val() || '' );
|
load_client_bestseller_settings( $( '#client_id' ).val() || '' );
|
||||||
|
|
||||||
load_cl4_suggestions( $( '#client_id' ).val() || '' );
|
load_cl4_suggestions( $( '#client_id' ).val() || '' );
|
||||||
|
load_cl1_suggestions( $( '#client_id' ).val() || '' );
|
||||||
|
|
||||||
load_products_campaigns( $( '#client_id' ).val() || '', savedCampaign ).done( function() {
|
load_products_campaigns( $( '#client_id' ).val() || '', savedCampaign ).done( function() {
|
||||||
var selected_campaign_id = $( '#products_campaign_id' ).val() || '';
|
var selected_campaign_id = $( '#products_campaign_id' ).val() || '';
|
||||||
@@ -1771,6 +1789,113 @@ $( function()
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// CL1 autocomplete — datalist z unikalnymi wartościami
|
||||||
|
var cl1_values_cache = [];
|
||||||
|
var cl1_datalist_id = 'cl1-suggestions';
|
||||||
|
|
||||||
|
function load_cl1_suggestions( client_id )
|
||||||
|
{
|
||||||
|
if ( !client_id )
|
||||||
|
{
|
||||||
|
cl1_values_cache = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/products/get_distinct_cl1/client_id=' + client_id,
|
||||||
|
type: 'GET',
|
||||||
|
dataType: 'json'
|
||||||
|
}).done( function( res ) {
|
||||||
|
cl1_values_cache = ( res && res.values ) ? res.values : [];
|
||||||
|
render_cl1_datalist();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function render_cl1_datalist()
|
||||||
|
{
|
||||||
|
var $dl = $( '#' + cl1_datalist_id );
|
||||||
|
|
||||||
|
if ( !$dl.length )
|
||||||
|
{
|
||||||
|
$dl = $( '<datalist id="' + cl1_datalist_id + '"></datalist>' );
|
||||||
|
$( 'body' ).append( $dl );
|
||||||
|
}
|
||||||
|
|
||||||
|
var html = '';
|
||||||
|
|
||||||
|
for ( var i = 0; i < cl1_values_cache.length; i++ )
|
||||||
|
{
|
||||||
|
html += '<option value="' + escape_html( cl1_values_cache[i] ) + '">';
|
||||||
|
}
|
||||||
|
|
||||||
|
$dl.html( html );
|
||||||
|
}
|
||||||
|
|
||||||
|
function bind_cl1_datalist()
|
||||||
|
{
|
||||||
|
$( '.custom_label_1' ).attr( 'list', cl1_datalist_id );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Podłącz datalist po każdym renderze tabeli
|
||||||
|
$( '#products' ).on( 'draw.dt', function() {
|
||||||
|
bind_cl1_datalist();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Załaduj sugestie po zmianie klienta
|
||||||
|
$( 'body' ).on( 'change', '#client_id', function() {
|
||||||
|
load_cl1_suggestions( $( this ).val() || '' );
|
||||||
|
});
|
||||||
|
|
||||||
|
// Odśwież cache po zapisie CL1
|
||||||
|
function refresh_cl1_cache_after_save()
|
||||||
|
{
|
||||||
|
var client_id = $( '#client_id' ).val() || '';
|
||||||
|
if ( client_id )
|
||||||
|
{
|
||||||
|
load_cl1_suggestions( client_id );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zapis custom_label_1
|
||||||
|
$( 'body' ).on( 'change', '.custom_label_1', function()
|
||||||
|
{
|
||||||
|
var product_id = $( this ).attr( 'product_id' );
|
||||||
|
var custom_label_1 = $( this ).val();
|
||||||
|
$.ajax({
|
||||||
|
url: '/products/save_custom_label_1/',
|
||||||
|
type: 'POST',
|
||||||
|
data: { product_id: product_id, custom_label_1: custom_label_1 },
|
||||||
|
success: function( response )
|
||||||
|
{
|
||||||
|
var data;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
data = JSON.parse( response );
|
||||||
|
}
|
||||||
|
catch ( e )
|
||||||
|
{
|
||||||
|
show_toast( 'Custom Label 1: nieprawidłowa odpowiedź serwera.', 'error' );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( data.status === 'ok' )
|
||||||
|
{
|
||||||
|
show_toast( 'Custom Label 1 zapisany.', 'success' );
|
||||||
|
refresh_cl1_cache_after_save();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
show_toast( 'Custom Label 1: zapis nie powiódł się.', 'error' );
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function()
|
||||||
|
{
|
||||||
|
show_toast( 'Custom Label 1: błąd połączenia podczas zapisu.', 'error' );
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Edycja produktu (tytuł, opis, kategoria Google)
|
// Edycja produktu (tytuł, opis, kategoria Google)
|
||||||
$( 'body' ).on( 'click', '.edit-product-title', function( e )
|
$( 'body' ).on( 'click', '.edit-product-title', function( e )
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user