update
This commit is contained in:
@@ -1,56 +1,47 @@
|
||||
# STATE
|
||||
# STATE
|
||||
|
||||
## Current Position
|
||||
|
||||
Milestone: (ad-hoc) Products — widok "wszystkie kampanie"
|
||||
Phase: 3 of 3 (Products All Campaigns View) — Completed
|
||||
Plan: 03-01 unified (loop closed)
|
||||
Milestone: (ad-hoc) Products - aggregate + breakdown
|
||||
Phase: 4 of 4 (Products Aggregate Breakdown) - Completed
|
||||
Plan: 04-01 unified (loop closed)
|
||||
Status: UNIFY complete
|
||||
Last activity: 2026-04-24 — Zamknięto pętlę planu 03-01
|
||||
Last activity: 2026-04-25T17:28:08+02:00 - Zamknieto petle planu 04-01
|
||||
|
||||
Progress:
|
||||
- Milestone: [██████████] 100%
|
||||
- Phase 3: [██████████] 100%
|
||||
- Phase 4: [██████████] 100%
|
||||
|
||||
## Loop Position
|
||||
|
||||
```
|
||||
PLAN ──▶ APPLY ──▶ UNIFY
|
||||
✓ ✓ ✓ [Pętla zamknięta]
|
||||
PLAN --> APPLY --> UNIFY
|
||||
✓ ✓ ✓ [Petla zamknieta]
|
||||
```
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-04-24
|
||||
Stopped at: Loop closed, pętla gotowa do nowego /paul:plan
|
||||
Next action: Wdróż `templates/products/main_view.php` na produkcję (FTP sync); w razie kolejnego zadania — `/paul:plan` z opisem
|
||||
Resume file: .paul/phases/03-products-all-campaigns-view/03-01-SUMMARY.md
|
||||
Last session: 2026-04-25
|
||||
Stopped at: Loop closed, gotowe do nowego /paul:plan
|
||||
Next action: Jesli chcesz kontynuowac - uruchom /paul:plan z kolejnym zadaniem
|
||||
Resume file: .paul/phases/04-products-aggregate-breakdown/04-01-SUMMARY.md
|
||||
|
||||
## Historia zrealizowanych planów
|
||||
## Historia zrealizowanych planow
|
||||
|
||||
- `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)
|
||||
- `02-01-PLAN.md` — custom_label_1 w supplemental feed TSV (completed 2026-04-22)
|
||||
- 1 plik zmodyfikowany: autoload/services/class.SupplementalFeed.php (+6/-4)
|
||||
- 4 AC spełnione
|
||||
- 0 odchyleń — plan 1:1
|
||||
- `03-01-PLAN.md` — Powrót do widoku "wszystkie kampanie" w /products (completed 2026-04-24)
|
||||
- 1 plik zmodyfikowany: templates/products/main_view.php (+1/-3)
|
||||
- Zmiana: usunięto `placeholder: '- wybierz -'` i `allowClear: true` z Select2 dla `#products_campaign_id` / `#products_ad_group_id`
|
||||
- 4 AC spełnione
|
||||
- 1 kosmetyczna różnica w verify (allowClear na linii 2097 to niezwiązany Google taxonomy picker, poza zakresem)
|
||||
- `01-01-PLAN.md` - CL3 -> CL1 w tabeli /products (completed 2026-04-22)
|
||||
- `02-01-PLAN.md` - custom_label_1 w supplemental feed TSV (completed 2026-04-22)
|
||||
- `03-01-PLAN.md` - Powrot do widoku "wszystkie kampanie" w /products (completed 2026-04-24)
|
||||
- `04-01-PLAN.md` - Agregat produktu + rozwijane podwiersze kampania/grupa w /products (completed 2026-04-25)
|
||||
|
||||
## Decisions
|
||||
|
||||
| Date | Decision | Phase | Impact |
|
||||
|------|----------|-------|--------|
|
||||
| 2026-04-24 | Usunięcie placeholdera zamiast wprowadzania wartości sentinel `0`/`all` — minimalny blast radius | 3 | 1-linijkowa zmiana w JS, brak zmian w kontrolerze/factory |
|
||||
| 2026-04-24 | Usuniecie placeholdera zamiast sentinel `0/all` | 3 | Minimalny blast radius, bez zmian w kontrolerze/factory |
|
||||
| 2026-04-25 | Bez wybranej grupy: glowny agregat per produkt + rozwijane podwiersze per kampania/grupa | 4 | Czytelniejsza analiza produktu i szybki drill-down |
|
||||
| 2026-04-25 | Podwiersze tylko readonly, edycja tylko w parent row | 4 | Spojnosc UX i brak konfliktu akcji edycyjnych |
|
||||
|
||||
## Notes
|
||||
|
||||
- PAUL framework działa w trybie ad-hoc (bez pełnej roadmapy/PROJECT.md).
|
||||
- Backend `factory\Products::build_scope_filters` już dziś traktuje `campaign_id ≤ 0` jako "bez filtra" — fix był wyłącznie po stronie JS.
|
||||
- Ustalony wzorzec: **Select2 + `<option value="">agregat</option>` ⇒ NIE konfigurować `placeholder` ani `allowClear`**, żeby opcja pozostała widoczna w dropdownie po selekcji.
|
||||
- Deploy: wymaga FTP sync szablonu + hard reload (Ctrl+Shift+R) w przeglądarce klientów.
|
||||
- PAUL framework dziala tutaj w trybie ad-hoc (bez ROADMAP.md i PROJECT.md).
|
||||
- Human-verify checkpoint dla planu 04-01 zatwierdzony (`approved`).
|
||||
|
||||
13
.paul/changelog/2026-04-25.md
Normal file
13
.paul/changelog/2026-04-25.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# 2026-04-25
|
||||
|
||||
## Co zrobiono
|
||||
|
||||
- [Phase 4, Plan 01] Wdrozono agregacje listy produktow: 1 produkt = 1 wiersz glowny przy braku wybranej grupy reklam.
|
||||
- Dodano rozwijane podwiersze z pelnym rozbiciem na kampanie i grupy reklam.
|
||||
- Pozostawiono edycje (min_roas, CL1, CL4, akcje) tylko w wierszu glownym.
|
||||
|
||||
## Zmienione pliki
|
||||
|
||||
- `autoload/factory/class.Products.php`
|
||||
- `autoload/controls/class.Products.php`
|
||||
- `templates/products/main_view.php`
|
||||
206
.paul/phases/04-products-aggregate-breakdown/04-01-PLAN.md
Normal file
206
.paul/phases/04-products-aggregate-breakdown/04-01-PLAN.md
Normal file
@@ -0,0 +1,206 @@
|
||||
---
|
||||
phase: 04-products-aggregate-breakdown
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- autoload/factory/class.Products.php
|
||||
- autoload/controls/class.Products.php
|
||||
- templates/products/main_view.php
|
||||
autonomous: false
|
||||
delegation: off
|
||||
---
|
||||
|
||||
<objective>
|
||||
## Goal
|
||||
Na stronie `/products` zmienic tryb listy tak, aby przy braku wybranej grupy reklam (`ad_group_id` puste/0) tabela pokazywala 1 wiersz glowny na produkt z metrykami zagregowanymi, oraz rozwijane podwiersze z pelnym rozbiciem na kampanie i grupy reklam.
|
||||
|
||||
## Purpose
|
||||
Obecny widok miesza poziomy agregacji i utrudnia szybka analize produktu jako calosci. Wiersz glowny ma pokazywac wynik laczny produktu, a szczegoly (kampania/grupa) maja byc dostepne na zadanie przez rozwiniecie.
|
||||
|
||||
## Output
|
||||
- Backend zwracajacy dane zagregowane per produkt oraz szczegoly per scope (kampania/grupa) dla aktualnej strony.
|
||||
- Frontend DataTable z przyciskiem rozwin/zwin i podwierszem z pelnymi metrykami.
|
||||
- Brak mozliwosci edycji w podwierszach (edycja tylko w wierszu glownym).
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## Project Context
|
||||
@.paul/STATE.md
|
||||
@.paul/phases/03-products-all-campaigns-view/03-01-SUMMARY.md
|
||||
|
||||
## Source Files
|
||||
@autoload/factory/class.Products.php
|
||||
@autoload/controls/class.Products.php
|
||||
@templates/products/main_view.php
|
||||
|
||||
## Constraints from user
|
||||
- Podwiersze maja byc zwijane/rozwijane przyciskiem.
|
||||
- Podwiersze maja pokazywac pelny zestaw metryk.
|
||||
- Edycja (`min_roas`, `CL1`, `CL4`, akcje) tylko w wierszu glownym produktu.
|
||||
|
||||
## Notes
|
||||
Repo dziala w trybie ad-hoc PAUL (bez ROADMAP.md / PROJECT.md). Plan tworzony jako kolejna faza ad-hoc.
|
||||
</context>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## AC-1: Agregacja produktu przy braku wybranej grupy reklam
|
||||
```gherkin
|
||||
Given uzytkownik jest na `/products` i ma wybranego klienta
|
||||
And filtr grupy reklam jest pusty (`#products_ad_group_id` = "")
|
||||
When tabela laduje dane
|
||||
Then kazdy produkt wystepuje tylko raz jako wiersz glowny
|
||||
And metryki wiersza glownego (wyswietlenia, klikniecia, koszt, konwersje, wartosc konwersji, ROAS itd.) sa suma ze wszystkich pasujacych scope
|
||||
```
|
||||
|
||||
## AC-2: Rozwijane podwiersze z pelnym rozbiciem
|
||||
```gherkin
|
||||
Given tabela produktow jest zaladowana w trybie z AC-1
|
||||
When uzytkownik kliknie przycisk rozwin przy produkcie
|
||||
Then pod wierszem glownym pojawia sie podwiersz(e) z rozbiciem per kampania+grupa reklam
|
||||
And kazdy podwiersz pokazuje pelen zestaw kolumn metryk jak w widoku glownym (w formie tylko-do-odczytu)
|
||||
And ponowne klikniecie przycisku zwija szczegoly
|
||||
```
|
||||
|
||||
## AC-3: Edycja tylko w wierszu glownym
|
||||
```gherkin
|
||||
Given produkt ma rozwiniete podwiersze
|
||||
When uzytkownik przeglada podwiersze
|
||||
Then nie ma tam inputow ani przyciskow edycji/usuwania
|
||||
And inputy `min_roas`, `custom_label_1`, `custom_label_4` oraz akcje pozostaja dostepne tylko w wierszu glownym produktu
|
||||
```
|
||||
|
||||
## AC-4: Brak regresji dla filtrowania i wydajnosci listy
|
||||
```gherkin
|
||||
Given nowy tryb agregacji i podwierszy
|
||||
When uzytkownik zmienia filtry (kampania, grupa, search, CL1, CL4), sortowanie i paginacje
|
||||
Then tabela zachowuje poprawna liczbe rekordow i sortowanie
|
||||
And przy wybranej konkretnej grupie reklam zachowanie pozostaje logicznie spojne (bez duplikacji i bez bledow JS/PHP)
|
||||
And endpoint DataTables nie zwraca bledow SQL ani warningow
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Przebudowa warstwy danych na agregat produktu + szczegoly scope</name>
|
||||
<files>autoload/factory/class.Products.php</files>
|
||||
<action>
|
||||
Rozszerzyc factory tak, aby `get_products()` zwracalo dane glownych wierszy agregowane per `p.id` przy `ad_group_id <= 0`, oraz by mozna bylo pobrac szczegoly per kampania/grupa dla zestawu `product_id` z biezacej strony.
|
||||
|
||||
Zakres:
|
||||
- Utrzymac medoo/prepared statements.
|
||||
- Dostosowac `GROUP BY` i logike licznikow tak, aby `recordsTotal` liczyl produkty, a nie scope, gdy nie ma wybranej grupy.
|
||||
- Dodac/metody pomocnicze do pobrania breakdownu dla listy produktow bez N+1 (jedno zapytanie z `IN (...)` + grupowanie po stronie PHP).
|
||||
- Utrzymac zgodnosc sortowania kolumn DataTables dla wiersza glownego.
|
||||
|
||||
Unikac:
|
||||
- SQL sklejanych stringiem bez parametrow.
|
||||
- Dublowania tej samej logiki obliczen metryk w wielu metodach bez wspolnego helpera.
|
||||
</action>
|
||||
<verify>
|
||||
- `php -l autoload/factory/class.Products.php`
|
||||
- Szybki test endpointu `/products/get_products/` dla: (a) ad_group_id="", (b) ad_group_id>0 i porownanie liczby rekordow.
|
||||
- Brak warningow SQL / PHP w logu.
|
||||
</verify>
|
||||
<done>AC-1 i czesc AC-4 spelnione: poprawna agregacja i poprawne liczenie rekordow.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Adaptacja kontrolera odpowiedzi DataTables pod dane glowne + breakdown</name>
|
||||
<files>autoload/controls/class.Products.php</files>
|
||||
<action>
|
||||
W `get_products()` przebudowac format odpowiedzi JSON tak, aby kazdy wiersz glowny produktu zawieral identyfikator i dane potrzebne do renderu rozwijanych podwierszy (np. `row_meta` / `breakdown_rows`).
|
||||
|
||||
Zakres:
|
||||
- Zachowac obecne formatowanie komorek glownych (ROAS bar, warningi, linki, akcje).
|
||||
- Dla podwierszy zwracac pelne metryki, ale bez kontrolnych elementow edycji.
|
||||
- Zachowac kompatybilnosc z istniejacymi handlerami JS, ktore operuja na kolumnach glownych.
|
||||
|
||||
Unikac:
|
||||
- Wykonywania dodatkowego zapytania DB na kazdy pojedynczy wiersz.
|
||||
- Mieszania HTML edycyjnego do danych podwierszy.
|
||||
</action>
|
||||
<verify>
|
||||
- `php -l autoload/controls/class.Products.php`
|
||||
- Odpowiedz endpointu zawiera dane breakdown tylko dla produktow z biezacej strony i nie psuje `recordsTotal`, `recordsFiltered`, `data`.
|
||||
</verify>
|
||||
<done>AC-2 i AC-3 backend-ready: API dostarcza dane do podwierszy i oddziela je od edycji.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: UI DataTable - przycisk rozwin i render podwierszy</name>
|
||||
<files>templates/products/main_view.php</files>
|
||||
<action>
|
||||
Dodac w tabeli produktow obsluge rozwijanych podwierszy:
|
||||
- Przycisk w wierszu glownym (rozwin/zwin) widoczny tylko gdy sa szczegoly.
|
||||
- Render child-row (DataTables child row) z pelnym zestawem metryk dla kampania+grupa.
|
||||
- Podwiersze readonly: bez inputow i bez akcji edycyjnych/usuwania.
|
||||
- Zachowac obecne akcje edycyjne tylko dla glownego wiersza.
|
||||
|
||||
Dodatkowo:
|
||||
- Dodac czytelne style dla child-row (wizualnie odroznione od glownej tabeli).
|
||||
- Nie przekraczac 3 poziomow zagniezdzenia - wydzielic helpery JS do budowy HTML podwierszy i toggla.
|
||||
</action>
|
||||
<verify>
|
||||
- `php -l templates/products/main_view.php`
|
||||
- Manual: rozwin/zwin dziala, brak bledow w konsoli, sortowanie/paginacja dalej dziala.
|
||||
</verify>
|
||||
<done>AC-2, AC-3 i pozostala czesc AC-4 spelnione po stronie UI.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<what-built>
|
||||
Nowy widok listy produktow: glowny agregat per produkt + rozwijane podwiersze kampania/grupa z pelnymi metrykami, bez edycji w podwierszach.
|
||||
</what-built>
|
||||
<how-to-verify>
|
||||
1. Otworz `/products`, wybierz klienta.
|
||||
2. Ustaw kampanie dowolnie, ale pozostaw "Grupa reklam" na "- wszystkie grupy -".
|
||||
3. Potwierdz: kazdy produkt jest raz jako wiersz glowny, metryki sa sumaryczne.
|
||||
4. Kliknij przycisk rozwin przy kilku produktach.
|
||||
5. Potwierdz: podwiersze pokazuja pelne metryki per kampania/grupa, bez inputow i bez akcji.
|
||||
6. Zweryfikuj, ze edycja (`min_roas`, `CL1`, `CL4`) dziala tylko w wierszu glownym.
|
||||
7. Sprawdz sortowanie, paginacje i filtry tekstowe.
|
||||
8. Sprawdz konsolę przegladarki i logi PHP: brak nowych bledow.
|
||||
</how-to-verify>
|
||||
<resume-signal>Wpisz "approved" aby przejsc do UNIFY, albo opisz odchylenia do poprawy.</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## DO NOT CHANGE
|
||||
- Migracje bazy i schemat tabel.
|
||||
- Logika niezalezna od /products (inne kontrolery/widoki).
|
||||
- Mechanika CSRF/sesji i routing aplikacji.
|
||||
|
||||
## SCOPE LIMITS
|
||||
- Tylko zakladka `/products`.
|
||||
- Tylko zachowanie listy i sposob prezentacji danych.
|
||||
- Bez nowych funkcji biznesowych poza agregacja + podwiersze.
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
Before declaring plan complete:
|
||||
- [ ] `php -l autoload/factory/class.Products.php`
|
||||
- [ ] `php -l autoload/controls/class.Products.php`
|
||||
- [ ] `php -l templates/products/main_view.php`
|
||||
- [ ] Manualna weryfikacja AC-1..AC-4
|
||||
- [ ] Brak nowych bledow JS/PHP/SQL
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Widok przy pustym `ad_group_id` pokazuje agregat 1 produkt = 1 wiersz glowny.
|
||||
- Podwiersze sa rozwijane przyciskiem i pokazuja pelne metryki per kampania/grupa.
|
||||
- Edycja pozostaje tylko w wierszu glownym.
|
||||
- Brak regresji filtrow, sortowania i paginacji.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.paul/phases/04-products-aggregate-breakdown/04-01-SUMMARY.md`.
|
||||
</output>
|
||||
138
.paul/phases/04-products-aggregate-breakdown/04-01-SUMMARY.md
Normal file
138
.paul/phases/04-products-aggregate-breakdown/04-01-SUMMARY.md
Normal file
@@ -0,0 +1,138 @@
|
||||
---
|
||||
phase: 04-products-aggregate-breakdown
|
||||
plan: 01
|
||||
subsystem: ui
|
||||
tags: [products, datatables, aggregation, breakdown, php, jquery]
|
||||
|
||||
requires:
|
||||
- phase: 03-products-all-campaigns-view
|
||||
provides: stable products scope filters and all-campaign selector behavior
|
||||
|
||||
provides:
|
||||
- Aggregated product rows (1 product = 1 row when no ad group is selected)
|
||||
- Expandable campaign/ad-group breakdown rows with full metrics
|
||||
- Edit actions kept only on the main product row
|
||||
|
||||
affects:
|
||||
- products table data contract from /products/get_products/
|
||||
- products DataTable row rendering and child-row behavior
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- aggregate main row + readonly scope breakdown in child row
|
||||
|
||||
key-files:
|
||||
created: []
|
||||
modified:
|
||||
- autoload/factory/class.Products.php
|
||||
- autoload/controls/class.Products.php
|
||||
- templates/products/main_view.php
|
||||
|
||||
key-decisions:
|
||||
- "Use one batch breakdown query for page product IDs to avoid N+1"
|
||||
- "Expose breakdown as row_meta in DataTables payload"
|
||||
- "Keep inline edit fields only in parent row"
|
||||
|
||||
patterns-established:
|
||||
- "When scope is not narrowed to ad_group_id, aggregate per product and show scope details on demand"
|
||||
|
||||
duration: ~23min
|
||||
started: 2026-04-25T17:05:05+02:00
|
||||
completed: 2026-04-25T17:28:08+02:00
|
||||
---
|
||||
|
||||
# Phase 4 Plan 01: Products Aggregate Breakdown (Summary)
|
||||
|
||||
**Na /products wdrozono model: glowny agregat per produkt + rozwijane readonly podwiersze kampania/grupa z pelnymi metrykami.**
|
||||
|
||||
## Performance
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Duration | ~23 min |
|
||||
| Started | 2026-04-25T17:05:05+02:00 |
|
||||
| Completed | 2026-04-25T17:28:08+02:00 |
|
||||
| Tasks | 4/4 (3 auto + 1 human-verify) |
|
||||
| Files modified | 3 |
|
||||
|
||||
## Acceptance Criteria Results
|
||||
|
||||
| Criterion | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| AC-1: Agregacja produktu bez wybranej grupy | Pass | Backend grupuje po `p.id`, recordsTotal liczy DISTINCT produktow |
|
||||
| AC-2: Rozwijane podwiersze z pelnym rozbiciem | Pass | Child-row DataTables renderuje pelny zestaw metryk per kampania+grupa |
|
||||
| AC-3: Edycja tylko w wierszu glownym | Pass | Podwiersze sa readonly; inputy i akcje zostaja w parent row |
|
||||
| AC-4: Brak regresji filtrowania/listy | Pass | User checkpoint `approved`; endpoint i lintery bez bledow |
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- Przebudowano zapytania listy produktow tak, aby glowny wynik byl agregowany per produkt.
|
||||
- Dodano zbiorcze pobieranie breakdownu scope dla aktualnej strony (bez N+1).
|
||||
- Rozszerzono payload DataTables o `row_meta.breakdown_rows` i dodano UI rozwin/zwin z tabela szczegolow.
|
||||
|
||||
## Task Commits
|
||||
|
||||
Brak commitu fazowego na tym etapie (working tree zawiera rowniez inne lokalne zmiany projektu).
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
| File | Change | Purpose |
|
||||
|------|--------|---------|
|
||||
| `autoload/factory/class.Products.php` | Modified (+148/-101) | Agregacja per produkt, wspolny filtr, breakdown query, records count |
|
||||
| `autoload/controls/class.Products.php` | Modified (+53/-19) | Dolaczenie breakdownu do odpowiedzi i row_meta dla DataTables |
|
||||
| `templates/products/main_view.php` | Modified (+162/-1) | UI toggle, child-row render, style breakdownu |
|
||||
|
||||
## Decisions Made
|
||||
|
||||
| Decision | Rationale | Impact |
|
||||
|----------|-----------|--------|
|
||||
| Batch breakdown query for page products | Ogranicza obciazenie DB i unika N+1 | Stabilna wydajnosc przy paginacji |
|
||||
| Keep edit controls only in parent row | Zgodnosc z wymaganiem usera | Jasny podzial: edycja vs analiza |
|
||||
| Render breakdown in DataTables child row | Minimalny blast radius dla istniejacego ukladu kolumn | Niska regresyjnosc UI |
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Summary
|
||||
|
||||
| Type | Count | Impact |
|
||||
|------|-------|--------|
|
||||
| Auto-fixed | 1 | Niski, bez zmiany zakresu |
|
||||
| Scope additions | 0 | Brak |
|
||||
| Deferred | 0 | Brak |
|
||||
|
||||
**Total impact:** Plan zrealizowany zgodnie z zakresem; jedna techniczna korekta podczas implementacji.
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
1. Tymczasowe uszkodzenie pliku kontrolera podczas edycji skryptowej
|
||||
- Found during: Task 2
|
||||
- Issue: `autoload/controls/class.Products.php` zostal chwilowo uszkodzony (parse error)
|
||||
- Fix: przywrocenie pliku z HEAD i ponowne, punktowe patche
|
||||
- Verification: `php -l autoload/controls/class.Products.php` OK
|
||||
|
||||
### Deferred Items
|
||||
|
||||
Brak.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
| Issue | Resolution |
|
||||
|-------|------------|
|
||||
| Konflikty przy automatycznej podmianie wiekszego bloku kontrolera | Zmiana strategii na male, precyzyjne patche i ponowna walidacja |
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
**Ready:**
|
||||
- Kontrakt endpointu `/products/get_products/` obsluguje agregat + breakdown.
|
||||
- UI listy wspiera drill-down bez naruszania obecnych akcji.
|
||||
|
||||
**Concerns:**
|
||||
- W working tree sa tez niezalezne lokalne zmiany (`.vscode/ftp-kr.sync.cache.json`).
|
||||
|
||||
**Blockers:**
|
||||
- Brak.
|
||||
|
||||
---
|
||||
*Phase: 04-products-aggregate-breakdown, Plan: 01*
|
||||
*Completed: 2026-04-25*
|
||||
34
.vscode/ftp-kr.sync.cache.json
vendored
34
.vscode/ftp-kr.sync.cache.json
vendored
@@ -751,6 +751,12 @@
|
||||
"size": 796,
|
||||
"lmtime": 1776845275979,
|
||||
"modified": false
|
||||
},
|
||||
"2026-04-24.md": {
|
||||
"type": "-",
|
||||
"size": 742,
|
||||
"lmtime": 1777064721127,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
"docs": {
|
||||
@@ -803,6 +809,12 @@
|
||||
"size": 9033,
|
||||
"lmtime": 1776845292488,
|
||||
"modified": false
|
||||
},
|
||||
"governance_2026-04-24.jsonl": {
|
||||
"type": "-",
|
||||
"size": 1614,
|
||||
"lmtime": 1777064743676,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
"phases": {
|
||||
@@ -833,12 +845,26 @@
|
||||
"lmtime": 1776845256633,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
"03-products-all-campaigns-view": {
|
||||
"03-01-PLAN.md": {
|
||||
"type": "-",
|
||||
"size": 12821,
|
||||
"lmtime": 1777064481097,
|
||||
"modified": false
|
||||
},
|
||||
"03-01-SUMMARY.md": {
|
||||
"type": "-",
|
||||
"size": 6938,
|
||||
"lmtime": 1777064708436,
|
||||
"modified": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"STATE.md": {
|
||||
"type": "-",
|
||||
"size": 1518,
|
||||
"lmtime": 1776845291913,
|
||||
"size": 2531,
|
||||
"lmtime": 1777064743120,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
@@ -897,8 +923,8 @@
|
||||
"products": {
|
||||
"main_view.php": {
|
||||
"type": "-",
|
||||
"size": 83598,
|
||||
"lmtime": 1776810839722,
|
||||
"size": 83538,
|
||||
"lmtime": 1777064546782,
|
||||
"modified": false
|
||||
},
|
||||
"product_history.php": {
|
||||
|
||||
@@ -965,6 +965,10 @@ class Products
|
||||
|
||||
$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, $filter_cl1 );
|
||||
$product_ids = array_values( array_unique( array_filter( array_map( function( $row ) {
|
||||
return (int) ( $row['product_id'] ?? 0 );
|
||||
}, (array) $db_results ) ) ) );
|
||||
$breakdown_map = \factory\Products::get_products_scope_breakdown( $client_id, $product_ids, $campaign_id, $ad_group_id, $search, $filter_cl4, $filter_cl1 );
|
||||
|
||||
// Sredni CR konta — do obliczenia progu klikniec
|
||||
$account_cr = \factory\Products::get_account_conversion_rate( (int) $client_id );
|
||||
@@ -977,11 +981,13 @@ class Products
|
||||
|
||||
foreach ( $db_results as $row )
|
||||
{
|
||||
$product_id = (int) ( $row['product_id'] ?? 0 );
|
||||
$breakdown_rows = (array) ( $breakdown_map[ $product_id ] ?? [] );
|
||||
$custom_class = '';
|
||||
$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' );
|
||||
$product_url = trim( (string) \factory\Products::get_product_data( $row['product_id'], 'product_url' ) );
|
||||
$custom_label_4 = \factory\Products::get_product_data( $product_id, 'custom_label_4' );
|
||||
$custom_label_1 = \factory\Products::get_product_data( $product_id, 'custom_label_1' );
|
||||
$custom_name = \factory\Products::get_product_data( $product_id, 'title' );
|
||||
$product_url = trim( (string) \factory\Products::get_product_data( $product_id, 'product_url' ) );
|
||||
|
||||
if ( $custom_name )
|
||||
{
|
||||
@@ -1074,29 +1080,56 @@ class Products
|
||||
|
||||
$history_campaign_id = (int) ( $row['history_campaign_id'] ?? 0 );
|
||||
$history_ad_group_id = (int) ( $row['history_ad_group_id'] ?? 0 );
|
||||
$breakdown_for_view = [];
|
||||
|
||||
if ( $history_campaign_id <= 0 )
|
||||
if ( $history_campaign_id <= 0 && !empty( $breakdown_rows ) )
|
||||
{
|
||||
$history_campaign_id = (int) ( $row['campaign_id'] ?? 0 );
|
||||
$history_campaign_id = (int) ( $breakdown_rows[0]['campaign_id'] ?? 0 );
|
||||
}
|
||||
|
||||
if ( $history_ad_group_id <= 0 )
|
||||
if ( $history_ad_group_id <= 0 && !empty( $breakdown_rows ) )
|
||||
{
|
||||
$history_ad_group_id = (int) ( $row['ad_group_id'] ?? 0 );
|
||||
$history_ad_group_id = (int) ( $breakdown_rows[0]['ad_group_id'] ?? 0 );
|
||||
}
|
||||
|
||||
foreach ( $breakdown_rows as $breakdown_row )
|
||||
{
|
||||
$breakdown_for_view[] = [
|
||||
'campaign_name' => (string) ( $breakdown_row['campaign_name'] ?? '' ),
|
||||
'ad_group_name' => (string) ( $breakdown_row['ad_group_name'] ?? '' ),
|
||||
'impressions' => (int) ( $breakdown_row['impressions'] ?? 0 ),
|
||||
'impressions_30' => (int) ( $breakdown_row['impressions_30'] ?? 0 ),
|
||||
'clicks' => (int) ( $breakdown_row['clicks'] ?? 0 ),
|
||||
'clicks_30' => (int) ( $breakdown_row['clicks_30'] ?? 0 ),
|
||||
'ctr' => (float) ( $breakdown_row['ctr'] ?? 0 ),
|
||||
'cost' => (float) ( $breakdown_row['cost'] ?? 0 ),
|
||||
'cpc' => (float) ( $breakdown_row['cpc'] ?? 0 ),
|
||||
'conversions' => (float) ( $breakdown_row['conversions'] ?? 0 ),
|
||||
'conversions_value' => (float) ( $breakdown_row['conversions_value'] ?? 0 ),
|
||||
'roas' => (float) ( $breakdown_row['roas'] ?? 0 ),
|
||||
'min_roas' => (float) ( $row['min_roas'] ?? 0 ),
|
||||
'custom_label_1' => (string) $custom_label_1,
|
||||
'custom_label_4' => (string) $custom_label_4
|
||||
];
|
||||
}
|
||||
|
||||
$row_meta = [
|
||||
'can_expand' => count( $breakdown_for_view ) > 0,
|
||||
'breakdown_rows' => $breakdown_for_view
|
||||
];
|
||||
|
||||
$data['data'][] = [
|
||||
'', // checkbox column
|
||||
$row['product_id'],
|
||||
$product_id,
|
||||
$row['offer_id'],
|
||||
htmlspecialchars( (string) ( $row['campaign_name'] ?? '' ) ),
|
||||
htmlspecialchars( (string) ( $row['ad_group_name'] ?? '' ) ),
|
||||
$product_url_html,
|
||||
'<div class="table-product-title" product_id="' . $row['product_id'] . '">
|
||||
<a href="/products/product_history/client_id=' . $client_id . '&product_id=' . $row['product_id'] . '&campaign_id=' . $history_campaign_id . '&ad_group_id=' . $history_ad_group_id . '" target="_blank" class="' . $custom_class . '">
|
||||
'<div class="table-product-title" product_id="' . $product_id . '">
|
||||
<a href="/products/product_history/client_id=' . $client_id . '&product_id=' . $product_id . '&campaign_id=' . $history_campaign_id . '&ad_group_id=' . $history_ad_group_id . '" target="_blank" class="' . $custom_class . '">
|
||||
' . $row['name'] . '
|
||||
</a>
|
||||
<span class="edit-product-title" product_id="' . $row['product_id'] . '">
|
||||
<span class="edit-product-title" product_id="' . $product_id . '">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</span>
|
||||
</div>',
|
||||
@@ -1111,14 +1144,15 @@ class Products
|
||||
round( $row['conversions'], 2 ),
|
||||
\S::number_display( $row['conversions_value'] ),
|
||||
$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 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 min_roas" product_id="' . $product_id . '" value="' . $row['min_roas'] . '" style="width: 100px;">',
|
||||
'<input type="text" class="form-control custom_label_1" product_id="' . $product_id . '" value="' . $custom_label_1 . '" style="' . $custom_label_1_color . '">',
|
||||
'<input type="text" class="form-control custom_label_4" product_id="' . $product_id . '" value="' . $custom_label_4 . '" style="' . $custom_label_4_color . '">',
|
||||
'<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-secondary view-merchant-logs" product_id="' . $row['product_id'] . '" title="Pokaż logi synchronizacji Merchant"><i class="fa-solid fa-clock-rotate-left"></i></button>'
|
||||
. '<button type="button" class="btn btn-danger delete-product" product_id="' . $row['product_id'] . '" title="Usuń produkt"><i class="fa-solid fa-trash"></i></button>'
|
||||
. '</div>'
|
||||
. '<button type="button" class="btn btn-primary assign-product-scope" product_id="' . $product_id . '" title="Dodaj produkt do kampanii/grupy"><i class="fa-solid fa-diagram-project"></i></button>'
|
||||
. '<button type="button" class="btn btn-secondary view-merchant-logs" product_id="' . $product_id . '" title="Pokaż logi synchronizacji Merchant"><i class="fa-solid fa-clock-rotate-left"></i></button>'
|
||||
. '<button type="button" class="btn btn-danger delete-product" product_id="' . $product_id . '" title="Usuń produkt"><i class="fa-solid fa-trash"></i></button>'
|
||||
. '</div>',
|
||||
$row_meta
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -487,6 +487,39 @@ class Products
|
||||
$sql .= ' AND ag.status = \'active\'';
|
||||
}
|
||||
|
||||
static private function build_products_filters( &$sql, &$params, $search, $custom_label_4, $custom_label_1 )
|
||||
{
|
||||
$search = trim( (string) $search );
|
||||
$custom_label_4 = trim( (string) $custom_label_4 );
|
||||
$custom_label_1 = trim( (string) $custom_label_1 );
|
||||
|
||||
if ( $search !== '' )
|
||||
{
|
||||
$sql .= ' AND (
|
||||
p.name LIKE :search
|
||||
OR p.title LIKE :search
|
||||
OR p.offer_id LIKE :search
|
||||
OR p.custom_label_4 LIKE :search
|
||||
OR p.custom_label_1 LIKE :search
|
||||
OR c.campaign_name LIKE :search
|
||||
OR ag.ad_group_name LIKE :search
|
||||
)';
|
||||
$params[':search'] = '%' . $search . '%';
|
||||
}
|
||||
|
||||
if ( $custom_label_4 !== '' )
|
||||
{
|
||||
$sql .= ' AND p.custom_label_4 LIKE :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 . '%';
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -522,15 +555,17 @@ class Products
|
||||
p.offer_id,
|
||||
p.min_roas,
|
||||
COALESCE( NULLIF( TRIM( p.custom_label_1 ), \'\' ), \'\' ) AS custom_label_1,
|
||||
pa.campaign_id AS campaign_id,
|
||||
COALESCE( NULLIF( TRIM( c.campaign_name ), \'\' ), \'--- brak kampanii ---\' ) AS campaign_name,
|
||||
CASE
|
||||
WHEN pa.ad_group_id = 0 THEN \'PMax (bez grup reklam)\'
|
||||
ELSE COALESCE( NULLIF( TRIM( ag.ad_group_name ), \'\' ), \'--- brak grupy reklam ---\' )
|
||||
WHEN COUNT( DISTINCT pa.campaign_id ) > 1 THEN CONCAT( \'Wiele kampanii (\', COUNT( DISTINCT pa.campaign_id ), \')\' )
|
||||
ELSE COALESCE( NULLIF( TRIM( MAX( c.campaign_name ) ), \'\' ), \'--- brak kampanii ---\' )
|
||||
END AS campaign_name,
|
||||
CASE
|
||||
WHEN COUNT( DISTINCT CONCAT( pa.campaign_id, \':\', pa.ad_group_id ) ) > 1 THEN CONCAT( \'Wiele grup (\', COUNT( DISTINCT CONCAT( pa.campaign_id, \':\', pa.ad_group_id ) ), \')\' )
|
||||
WHEN MIN( pa.ad_group_id ) = 0 THEN \'PMax (bez grup reklam)\'
|
||||
ELSE COALESCE( NULLIF( TRIM( MAX( ag.ad_group_name ) ), \'\' ), \'--- brak grupy reklam ---\' )
|
||||
END AS ad_group_name,
|
||||
pa.ad_group_id AS ad_group_id,
|
||||
pa.campaign_id AS history_campaign_id,
|
||||
pa.ad_group_id AS history_ad_group_id,
|
||||
MIN( pa.campaign_id ) AS history_campaign_id,
|
||||
MIN( pa.ad_group_id ) AS history_ad_group_id,
|
||||
COALESCE( NULLIF( TRIM( p.title ), \'\' ), NULLIF( TRIM( p.name ), \'\' ), p.offer_id ) AS name,
|
||||
SUM( pa.impressions_all_time ) AS impressions,
|
||||
SUM( pa.impressions_30 ) AS impressions_30,
|
||||
@@ -558,34 +593,9 @@ class Products
|
||||
WHERE p.client_id = :client_id';
|
||||
|
||||
self::build_scope_filters( $sql, $params, $campaign_id, $ad_group_id );
|
||||
self::build_products_filters( $sql, $params, $search, $custom_label_4, $custom_label_1 );
|
||||
|
||||
if ( $search )
|
||||
{
|
||||
$sql .= ' AND (
|
||||
p.name LIKE :search
|
||||
OR p.title LIKE :search
|
||||
OR p.offer_id LIKE :search
|
||||
OR p.custom_label_4 LIKE :search
|
||||
OR p.custom_label_1 LIKE :search
|
||||
OR c.campaign_name LIKE :search
|
||||
OR ag.ad_group_name LIKE :search
|
||||
)';
|
||||
$params[':search'] = '%' . $search . '%';
|
||||
}
|
||||
|
||||
if ( $custom_label_4 !== '' )
|
||||
{
|
||||
$sql .= ' AND p.custom_label_4 LIKE :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, 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 .= ' GROUP BY p.id, p.offer_id, p.min_roas, p.custom_label_1, p.name, p.title';
|
||||
$sql .= ' ORDER BY ' . $order_sql . ' ' . $order_dir . ', product_id DESC LIMIT ' . $start . ', ' . $limit;
|
||||
|
||||
return $mdb -> query( $sql, $params ) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
@@ -597,47 +607,30 @@ class Products
|
||||
|
||||
$params = [ ':client_id' => $client_id ];
|
||||
|
||||
$sql = 'SELECT MIN( p.min_roas ) AS min_roas,
|
||||
MAX(
|
||||
$sql = 'SELECT
|
||||
MIN( t.min_roas ) AS min_roas,
|
||||
MAX( t.roas ) AS max_roas
|
||||
FROM (
|
||||
SELECT
|
||||
p.id AS product_id,
|
||||
p.min_roas AS min_roas,
|
||||
CASE
|
||||
WHEN COALESCE( pa.cost_all_time, 0 ) > 0 THEN ROUND( COALESCE( pa.conversion_value_all_time, 0 ) / pa.cost_all_time * 100, 2 )
|
||||
WHEN SUM( pa.cost_all_time ) > 0 THEN ROUND( SUM( pa.conversion_value_all_time ) / SUM( pa.cost_all_time ) * 100, 2 )
|
||||
ELSE 0
|
||||
END
|
||||
) AS max_roas
|
||||
END AS roas,
|
||||
SUM( pa.conversions_all_time ) AS conversions
|
||||
FROM products_aggregate AS pa
|
||||
INNER JOIN products AS p ON p.id = pa.product_id
|
||||
LEFT JOIN campaigns AS c ON c.id = pa.campaign_id
|
||||
LEFT JOIN campaign_ad_groups AS ag ON ag.id = pa.ad_group_id
|
||||
WHERE p.client_id = :client_id
|
||||
AND pa.conversions_all_time > 10';
|
||||
WHERE p.client_id = :client_id';
|
||||
|
||||
self::build_scope_filters( $sql, $params, $campaign_id, $ad_group_id );
|
||||
self::build_products_filters( $sql, $params, $search, $custom_label_4, $custom_label_1 );
|
||||
|
||||
if ( $search )
|
||||
{
|
||||
$sql .= ' AND (
|
||||
p.name LIKE :search
|
||||
OR p.title LIKE :search
|
||||
OR p.offer_id LIKE :search
|
||||
OR p.custom_label_4 LIKE :search
|
||||
OR p.custom_label_1 LIKE :search
|
||||
OR c.campaign_name LIKE :search
|
||||
OR ag.ad_group_name LIKE :search
|
||||
)';
|
||||
$params[':search'] = '%' . $search . '%';
|
||||
}
|
||||
|
||||
if ( $custom_label_4 !== '' )
|
||||
{
|
||||
$sql .= ' AND p.custom_label_4 LIKE :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, p.min_roas
|
||||
HAVING SUM( pa.conversions_all_time ) > 10
|
||||
) AS t';
|
||||
|
||||
$row = $mdb -> query( $sql, $params ) -> fetch( \PDO::FETCH_ASSOC );
|
||||
|
||||
@@ -676,9 +669,7 @@ class Products
|
||||
global $mdb;
|
||||
|
||||
$params = [ ':client_id' => (int) $client_id ];
|
||||
$sql = 'SELECT COUNT(0)
|
||||
FROM (
|
||||
SELECT p.id
|
||||
$sql = 'SELECT COUNT( DISTINCT p.id )
|
||||
FROM products_aggregate AS pa
|
||||
INNER JOIN products AS p ON p.id = pa.product_id
|
||||
LEFT JOIN campaigns AS c ON c.id = pa.campaign_id
|
||||
@@ -686,39 +677,95 @@ class Products
|
||||
WHERE p.client_id = :client_id';
|
||||
|
||||
self::build_scope_filters( $sql, $params, $campaign_id, $ad_group_id );
|
||||
|
||||
if ( $search )
|
||||
{
|
||||
$sql .= ' AND (
|
||||
p.name LIKE :search
|
||||
OR p.title LIKE :search
|
||||
OR p.offer_id LIKE :search
|
||||
OR p.custom_label_4 LIKE :search
|
||||
OR p.custom_label_1 LIKE :search
|
||||
OR c.campaign_name LIKE :search
|
||||
OR ag.ad_group_name LIKE :search
|
||||
)';
|
||||
$params[':search'] = '%' . $search . '%';
|
||||
}
|
||||
|
||||
if ( $custom_label_4 !== '' )
|
||||
{
|
||||
$sql .= ' AND p.custom_label_4 LIKE :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
|
||||
) AS grouped_rows';
|
||||
self::build_products_filters( $sql, $params, $search, $custom_label_4, $custom_label_1 );
|
||||
|
||||
return $mdb -> query( $sql, $params ) -> fetchColumn();
|
||||
}
|
||||
|
||||
static public function get_products_scope_breakdown( $client_id, $product_ids, $campaign_id = 0, $ad_group_id = 0, $search = '', $custom_label_4 = '', $custom_label_1 = '' )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$product_ids = array_values( array_filter( array_map( 'intval', (array) $product_ids ) ) );
|
||||
|
||||
if ( empty( $product_ids ) )
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
$params = [ ':client_id' => (int) $client_id ];
|
||||
$in_placeholders = [];
|
||||
|
||||
foreach ( $product_ids as $idx => $product_id )
|
||||
{
|
||||
$key = ':product_id_' . $idx;
|
||||
$in_placeholders[] = $key;
|
||||
$params[ $key ] = $product_id;
|
||||
}
|
||||
|
||||
$sql = 'SELECT
|
||||
pa.product_id,
|
||||
pa.campaign_id,
|
||||
pa.ad_group_id,
|
||||
COALESCE( NULLIF( TRIM( c.campaign_name ), \'\' ), \'--- brak kampanii ---\' ) AS campaign_name,
|
||||
CASE
|
||||
WHEN pa.ad_group_id = 0 THEN \'PMax (bez grup reklam)\'
|
||||
ELSE COALESCE( NULLIF( TRIM( ag.ad_group_name ), \'\' ), \'--- brak grupy reklam ---\' )
|
||||
END AS ad_group_name,
|
||||
SUM( pa.impressions_all_time ) AS impressions,
|
||||
SUM( pa.impressions_30 ) AS impressions_30,
|
||||
SUM( pa.clicks_all_time ) AS clicks,
|
||||
SUM( pa.clicks_30 ) AS clicks_30,
|
||||
CASE
|
||||
WHEN SUM( pa.impressions_all_time ) > 0 THEN ROUND( SUM( pa.clicks_all_time ) / SUM( pa.impressions_all_time ) * 100, 2 )
|
||||
ELSE 0
|
||||
END AS ctr,
|
||||
SUM( pa.cost_all_time ) AS cost,
|
||||
CASE
|
||||
WHEN SUM( pa.clicks_all_time ) > 0 THEN ROUND( SUM( pa.cost_all_time ) / SUM( pa.clicks_all_time ), 6 )
|
||||
ELSE 0
|
||||
END AS cpc,
|
||||
SUM( pa.conversions_all_time ) AS conversions,
|
||||
SUM( pa.conversion_value_all_time ) AS conversions_value,
|
||||
CASE
|
||||
WHEN SUM( pa.cost_all_time ) > 0 THEN ROUND( SUM( pa.conversion_value_all_time ) / SUM( pa.cost_all_time ) * 100, 2 )
|
||||
ELSE 0
|
||||
END AS roas
|
||||
FROM products_aggregate AS pa
|
||||
INNER JOIN products AS p ON p.id = pa.product_id
|
||||
LEFT JOIN campaigns AS c ON c.id = pa.campaign_id
|
||||
LEFT JOIN campaign_ad_groups AS ag ON ag.id = pa.ad_group_id
|
||||
WHERE p.client_id = :client_id
|
||||
AND pa.product_id IN (' . implode( ', ', $in_placeholders ) . ')';
|
||||
|
||||
self::build_scope_filters( $sql, $params, $campaign_id, $ad_group_id );
|
||||
self::build_products_filters( $sql, $params, $search, $custom_label_4, $custom_label_1 );
|
||||
|
||||
$sql .= ' GROUP BY pa.product_id, pa.campaign_id, pa.ad_group_id, c.campaign_name, ag.ad_group_name
|
||||
ORDER BY campaign_name ASC, ad_group_name ASC';
|
||||
|
||||
$rows = $mdb -> query( $sql, $params ) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
$map = [];
|
||||
|
||||
foreach ( $rows as $row )
|
||||
{
|
||||
$product_id = (int) ( $row['product_id'] ?? 0 );
|
||||
if ( $product_id <= 0 )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( !isset( $map[ $product_id ] ) )
|
||||
{
|
||||
$map[ $product_id ] = [];
|
||||
}
|
||||
|
||||
$map[ $product_id ][] = $row;
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
static public function get_product_full_context( $product_id )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
@@ -187,6 +187,55 @@
|
||||
background-color: #6690f4;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.products-page .products-id-cell {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.products-page .products-breakdown-toggle {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
color: #374151;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.products-page .products-breakdown-wrap {
|
||||
padding: 10px 14px;
|
||||
background: #f8fafc;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.products-page .products-breakdown-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.products-page .products-breakdown-table th,
|
||||
.products-page .products-breakdown-table td {
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 6px 8px;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.products-page .products-breakdown-table th:nth-child(1),
|
||||
.products-page .products-breakdown-table th:nth-child(2),
|
||||
.products-page .products-breakdown-table td:nth-child(1),
|
||||
.products-page .products-breakdown-table td:nth-child(2),
|
||||
.products-page .products-breakdown-table td:nth-child(15),
|
||||
.products-page .products-breakdown-table td:nth-child(16) {
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
|
||||
<?php
|
||||
@@ -528,6 +577,83 @@ function init_products_scope_select_search()
|
||||
} );
|
||||
}
|
||||
|
||||
function products_breakdown_number( value, digits )
|
||||
{
|
||||
var num = Number( value || 0 );
|
||||
if ( !isFinite( num ) )
|
||||
{
|
||||
num = 0;
|
||||
}
|
||||
|
||||
return num.toLocaleString( 'pl-PL', {
|
||||
minimumFractionDigits: digits,
|
||||
maximumFractionDigits: digits
|
||||
} );
|
||||
}
|
||||
|
||||
function products_breakdown_meta( row )
|
||||
{
|
||||
if ( Array.isArray( row ) && row.length > 22 && row[22] && typeof row[22] === 'object' )
|
||||
{
|
||||
return row[22];
|
||||
}
|
||||
|
||||
return { can_expand: false, breakdown_rows: [] };
|
||||
}
|
||||
|
||||
function products_build_breakdown_html( row_meta )
|
||||
{
|
||||
var rows = Array.isArray( row_meta.breakdown_rows ) ? row_meta.breakdown_rows : [];
|
||||
|
||||
if ( !rows.length )
|
||||
{
|
||||
return '<div class="products-breakdown-wrap">Brak szczegolow dla tego produktu.</div>';
|
||||
}
|
||||
|
||||
var html = '<div class="products-breakdown-wrap"><table class="products-breakdown-table">';
|
||||
html += '<thead><tr>' +
|
||||
'<th>Kampania</th>' +
|
||||
'<th>Grupa reklam</th>' +
|
||||
'<th>Wysw.</th>' +
|
||||
'<th>Wysw. (30d)</th>' +
|
||||
'<th>Klik.</th>' +
|
||||
'<th>Klik. (30d)</th>' +
|
||||
'<th>CTR</th>' +
|
||||
'<th>Koszt</th>' +
|
||||
'<th>CPC</th>' +
|
||||
'<th>Konw.</th>' +
|
||||
'<th>Wart. konw.</th>' +
|
||||
'<th>ROAS</th>' +
|
||||
'<th>Min. ROAS</th>' +
|
||||
'<th>CL1</th>' +
|
||||
'<th>CL4</th>' +
|
||||
'</tr></thead><tbody>';
|
||||
|
||||
rows.forEach( function( entry ) {
|
||||
html += '<tr>' +
|
||||
'<td>' + escape_html( entry.campaign_name || '' ) + '</td>' +
|
||||
'<td>' + escape_html( entry.ad_group_name || '' ) + '</td>' +
|
||||
'<td>' + products_breakdown_number( entry.impressions, 0 ) + '</td>' +
|
||||
'<td>' + products_breakdown_number( entry.impressions_30, 0 ) + '</td>' +
|
||||
'<td>' + products_breakdown_number( entry.clicks, 0 ) + '</td>' +
|
||||
'<td>' + products_breakdown_number( entry.clicks_30, 0 ) + '</td>' +
|
||||
'<td>' + products_breakdown_number( entry.ctr, 2 ) + '%</td>' +
|
||||
'<td>' + products_breakdown_number( entry.cost, 2 ) + '</td>' +
|
||||
'<td>' + products_breakdown_number( entry.cpc, 2 ) + '</td>' +
|
||||
'<td>' + products_breakdown_number( entry.conversions, 2 ) + '</td>' +
|
||||
'<td>' + products_breakdown_number( entry.conversions_value, 2 ) + '</td>' +
|
||||
'<td>' + products_breakdown_number( entry.roas, 0 ) + '</td>' +
|
||||
'<td>' + products_breakdown_number( entry.min_roas, 2 ) + '</td>' +
|
||||
'<td>' + escape_html( entry.custom_label_1 || '' ) + '</td>' +
|
||||
'<td>' + escape_html( entry.custom_label_4 || '' ) + '</td>' +
|
||||
'</tr>';
|
||||
} );
|
||||
|
||||
html += '</tbody></table></div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
$( function()
|
||||
{
|
||||
init_products_scope_select_search();
|
||||
@@ -557,7 +683,18 @@ $( function()
|
||||
return '<input type="checkbox" class="product-checkbox" value="' + row[1] + '" />';
|
||||
}
|
||||
},
|
||||
{ width: '50px', orderable: false },
|
||||
{ width: '70px', orderable: false, render: function( data, type, row ) {
|
||||
var row_meta = products_breakdown_meta( row );
|
||||
var toggle = '';
|
||||
|
||||
if ( row_meta.can_expand )
|
||||
{
|
||||
toggle = '<button type="button" class="products-breakdown-toggle js-products-breakdown-toggle" aria-expanded="false" title="Pokaz rozbicie"><i class="fa-solid fa-chevron-right"></i></button>';
|
||||
}
|
||||
|
||||
return '<div class="products-id-cell">' + toggle + '<span>' + escape_html( data ) + '</span></div>';
|
||||
}
|
||||
},
|
||||
{ width: '80px', name: 'offer_id' },
|
||||
{ width: '200px', name: 'campaign_name' },
|
||||
{ width: '200px', name: 'ad_group_name' },
|
||||
@@ -603,6 +740,30 @@ $( function()
|
||||
products_apply_saved_columns_visibility( products_table );
|
||||
products_render_columns_picker( products_table );
|
||||
|
||||
$( '#products tbody' ).on( 'click', '.js-products-breakdown-toggle', function( e ) {
|
||||
e.preventDefault();
|
||||
|
||||
var $btn = $( this );
|
||||
var $tr = $btn.closest( 'tr' );
|
||||
var dt_row = products_table.row( $tr );
|
||||
var row_data = dt_row.data() || [];
|
||||
var row_meta = products_breakdown_meta( row_data );
|
||||
|
||||
if ( dt_row.child.isShown() )
|
||||
{
|
||||
dt_row.child.hide();
|
||||
$tr.removeClass( 'products-breakdown-open' );
|
||||
$btn.attr( 'aria-expanded', 'false' );
|
||||
$btn.find( 'i' ).removeClass( 'fa-chevron-down' ).addClass( 'fa-chevron-right' );
|
||||
return;
|
||||
}
|
||||
|
||||
dt_row.child( products_build_breakdown_html( row_meta ) ).show();
|
||||
$tr.addClass( 'products-breakdown-open' );
|
||||
$btn.attr( 'aria-expanded', 'true' );
|
||||
$btn.find( 'i' ).removeClass( 'fa-chevron-right' ).addClass( 'fa-chevron-down' );
|
||||
} );
|
||||
|
||||
function reload_products_table()
|
||||
{
|
||||
products_table.ajax.reload( null, false );
|
||||
|
||||
Reference in New Issue
Block a user