Files
adsPRO/.paul/phases/05-products-scope-history-delete/05-01-PLAN.md
2026-04-30 01:04:06 +02:00

17 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, delegation
phase plan type wave depends_on files_modified autonomous delegation
05-products-scope-history-delete 01 execute 1
autoload/factory/class.Products.php
autoload/controls/class.Products.php
templates/products/main_view.php
false off
## Goal Dodac w widoku /products w rozwinietym podwierszu breakdown (per kampania+grupa reklam) przycisk usuwania z potwierdzeniem, ktory wycina lokalne wpisy statystyczno-historyczne dla konkretnej trojki product_id + campaign_id + ad_group_id.

Purpose

Pozwala uzytkownikowi rzetelnie wyczyscic statystyki produktu w obrebie jednej grupy reklam (np. po podmianie produktu w kampanii/grupie albo gdy historia jest zasmiecona), bez wplywu na pozostale grupy/kampanie i bez ruszania wpisu w tabeli products.

Output

  • Nowy endpoint AJAX /products/delete_product_scope_history/
  • Nowa metoda factory \factory\Products::delete_product_scope_history($product_id, $campaign_id, $ad_group_id)
  • UI: kolumna "Akcje" w tabeli breakdown z czerwona ikona kosza, dialog potwierdzenia, optymistyczne usuniecie wiersza i odswiezenie wiersza nadrzednego (agregatu) bez resetu paginacji.
- **Zakres usuwania** — Co dokladnie ma usuwac przycisk z podwiersza breakdown? → Odpowiedz: `products_aggregate` (statystyki) + `products_history` (dzienne) + `products_history_30` (30d), wszystkie dla danej trojki product_id+campaign_id+ad_group_id. - **Miejsce UI** — Gdzie umiescic akcje usuwania? → Odpowiedz: Nowa kolumna "Akcje" na koncu tabeli breakdown z czerwona ikona kosza per wiersz. - **Scope API** — Czy usuwanie lokalne czy z Google Ads API? → Odpowiedz: Tylko lokalnie (DB). Bez wywolan do Google Ads API. Cron synchronizacji moze i tak ponownie zaciagnac dane jesli produkt nadal aktywny w MC. - **UI po usun.** — Co po udanym usunieciu? → Odpowiedz: Usun wiersz breakdown + odswiez agregat parent (`ajax.reload(null, false)`), zachowujac paginacje.

Project Context

@.paul/STATE.md @.paul/phases/04-products-aggregate-breakdown/04-01-SUMMARY.md

Source Files

@autoload/factory/class.Products.php @autoload/controls/class.Products.php @templates/products/main_view.php

<acceptance_criteria>

AC-1: Endpoint usuwa lokalne wpisy dla trojki product+campaign+ad_group

Given uzytkownik wysyla POST do /products/delete_product_scope_history/ z parametrami product_id, campaign_id, ad_group_id (wszystkie >= 0, product_id > 0)
When zadanie zostaje przetworzone
Then z products_aggregate znika dokladnie jeden wiersz pasujacy do (product_id, campaign_id, ad_group_id)
  And z products_history znikaja wszystkie wpisy pasujace do tej samej trojki
  And z products_history_30 znikaja wszystkie wpisy pasujace do tej samej trojki
  And zaden wiersz w `products`, `products_aggregate`, `products_history`, `products_history_30` dla *innych* trojek (inne campaign_id lub ad_group_id, ten sam product_id) nie zostaje naruszony
  And odpowiedz to JSON `{"status":"ok"}`

AC-2: Walidacja parametrow i bledow

Given POST do /products/delete_product_scope_history/ bez product_id (lub product_id <= 0)
When zadanie zostaje przetworzone
Then odpowiedz to JSON `{"status":"error","message":"<komunikat PL>"}`
  And zaden wiersz w bazie nie zostaje usuniety

Uwaga: campaign_id lub ad_group_id rowne 0 sa dozwolone (PMax-y maja ad_group_id = 0, breakdown query renderuje takie wiersze).

AC-3: UI kolumny Akcje + potwierdzenie

Given uzytkownik rozwinal breakdown produktu w /products
When patrzy na tabele rozbicia
Then na koncu kazdego wiersza widzi nowa kolumne "Akcje" z czerwona ikona kosza (`fa-solid fa-trash`, `btn btn-sm btn-danger`)
  And po klinieciu w ikone otwiera sie dialog $.confirm z tekstem zawierajacym nazwe kampanii i grupy reklam danego wiersza oraz pytaniem o potwierdzenie
  And dialog ma przyciski "Usun" (akcja) i "Anuluj"
  And dopoki uzytkownik nie potwierdzi, zaden request nie jest wysylany

AC-4: UI po sukcesie aktualizuje sie bez utraty stanu

Given uzytkownik potwierdzil usuniecie i serwer zwrocil status ok
When odpowiedz dotrze do przegladarki
Then wiersz breakdown znika z aktualnie otwartej tabeli rozbicia (bez przeladowania calej strony)
  And glowna tabela `#products` jest odswiezona przez `ajax.reload(null, false)` (zachowana paginacja, sortowanie, filtry)
  And uzytkownik widzi toast/komunikat sukcesu (PL, krotki)
  And przy bledzie z serwera wyswietla sie czerwony dialog z `message` z odpowiedzi (lub fallback PL) i wiersz NIE jest usuwany z UI

</acceptance_criteria>

Task 1: Factory - delete_product_scope_history autoload/factory/class.Products.php Dodaj statyczna metode `delete_product_scope_history( $product_id, $campaign_id, $ad_group_id )` w klasie `\factory\Products`. - Rzutuj wszystkie 3 argumenty na (int). - Walidacja: `$product_id <= 0` -> return false. - Uzyj globalnego `$mdb` (Medoo) zgodnie z konwencja pliku. - Wykonaj 3 osobne DELETE w jednej transakcji `$mdb->pdo->beginTransaction()` / `commit()` / w catch `rollBack()` + `return false`: 1. `$mdb->delete('products_aggregate', ['product_id' => $product_id, 'campaign_id' => $campaign_id, 'ad_group_id' => $ad_group_id])` 2. `$mdb->delete('products_history', ['product_id' => $product_id, 'campaign_id' => $campaign_id, 'ad_group_id' => $ad_group_id])` 3. `$mdb->delete('products_history_30',['product_id' => $product_id, 'campaign_id' => $campaign_id, 'ad_group_id' => $ad_group_id])` - Zwroc `true` po commit. - NIE dotykaj tabeli `products` ani `campaigns`/`campaign_ad_groups` (chronione - viz. boundaries). - Avoid: jakichkolwiek warunkow `LIKE`, surowego SQL z konkatenacja stringow (Medoo dela tu wszystko bezpiecznie). Avoid: usuwania bez `campaign_id`/`ad_group_id` w klauzuli (skasowaloby calego produkta). `php -l autoload/factory/class.Products.php` zwraca "No syntax errors". Manualny test SQL (na kopii lub z dry-runem): przed zmiana zliczyc `SELECT COUNT(*) FROM products_aggregate WHERE product_id=X AND campaign_id=Y AND ad_group_id=Z` (oczekiwane 1), wywolac metode, ponownie zliczyc (oczekiwane 0). Powtorzyc dla `products_history` i `products_history_30`. Sprawdzic, ze inne trojki dla tego samego product_id pozostaly. AC-1 spelnione: usuwanie ograniczone do trojki, AC-2 spelnione w czesci serwerowej (zwrot false dla zlych parametrow). Task 2: Controller - akcja AJAX delete_product_scope_history autoload/controls/class.Products.php Dodaj public static akcje `delete_product_scope_history()` w klasie `\controls\Products` (obok istniejacych `delete_product()` / `delete_products()`). - Czytaj parametry przez `\S::get`: $product_id = (int) \S::get('product_id'); $campaign_id = (int) \S::get('campaign_id'); $ad_group_id = (int) \S::get('ad_group_id'); - Walidacja: `$product_id <= 0` -> `echo json_encode(['status'=>'error','message'=>'Brak identyfikatora produktu.']); exit;` - Wywolaj `\factory\Products::delete_product_scope_history($product_id, $campaign_id, $ad_group_id)`. - Sukces: `echo json_encode(['status'=>'ok']); exit;` - Porazka: `echo json_encode(['status'=>'error','message'=>'Nie udalo sie usunac wpisow historii dla tego zakresu.']); exit;` - Naladuj wzor istniejacych akcji `delete_product()` (linia ~1163) co do stylu (brak naglowkow, `echo json_encode(...)`, `exit`). - Avoid: dodawania nagłowkow `Content-Type: application/json` (reszta kontrolera tego nie robi - jQuery sobie radzi). `php -l autoload/controls/class.Products.php` zwraca "No syntax errors". Recznie z konsoli przegladarki na zalogowanej sesji: `$.post('/products/delete_product_scope_history/', {product_id: 0}, console.log)` -> `{status:'error', message: ...}` `$.post('/products/delete_product_scope_history/', {product_id: , campaign_id: , ad_group_id: }, console.log)` -> `{status:'ok'}` i wpisy znikaja. AC-1, AC-2 spelnione end-to-end od strony serwera. Task 3: UI - kolumna Akcje w breakdown + handler templates/products/main_view.php Modyfikacje wylacznie w obrebie breakdown:
a) `products_build_breakdown_html(row_meta)` (~linia 604):
   - Dodaj `<th>Akcje</th>` na koncu naglowka tabeli (po `<th>CL4</th>`).
   - W petli `rows.forEach` dodaj na koncu `<tr>` jeszcze jedna komorke:
     `'<td class="text-center"><button type="button" class="btn btn-sm btn-danger js-products-breakdown-delete" title="Usun wpisy historii dla tej kampanii+grupy" '
       + 'data-product-id="' + entry.product_id + '" '
       + 'data-campaign-id="' + (entry.campaign_id || 0) + '" '
       + 'data-ad-group-id="' + (entry.ad_group_id || 0) + '" '
       + 'data-campaign-name="' + escape_html(entry.campaign_name || '') + '" '
       + 'data-ad-group-name="' + escape_html(entry.ad_group_name || '') + '">'
       + '<i class="fa-solid fa-trash"></i></button></td>'`
   - WYMAGANE: w tasku 4 (factory/breakdown payload) `entry` musi zawierac `product_id`, `campaign_id`, `ad_group_id`. Sprawdzic istniejacy SELECT (`get_products_scope_breakdown`) - juz je zwraca (linie 707-709). Nie trzeba zmian backendu.

b) Dopisz delegowany handler kliku (np. tuz przy istniejacym handlerze `js-products-breakdown-toggle`, ~linia 743):

   ```js
   $( '#products tbody' ).on( 'click', '.js-products-breakdown-delete', function( e ) {
     e.preventDefault();
     e.stopPropagation();
     var $btn        = $( this );
     var productId   = parseInt( $btn.data( 'product-id' ), 10 ) || 0;
     var campaignId  = parseInt( $btn.data( 'campaign-id' ), 10 ) || 0;
     var adGroupId   = parseInt( $btn.data( 'ad-group-id' ), 10 ) || 0;
     var campaignName= String( $btn.data( 'campaign-name' ) || '' );
     var adGroupName = String( $btn.data( 'ad-group-name' ) || '' );
     if ( productId <= 0 ) { return; }

     $.confirm({
       title: 'Usun wpisy historii',
       content: 'Czy na pewno chcesz usunac wpisy statystyk i historii tego produktu w kampanii <strong>'
         + escape_html(campaignName) + '</strong> / grupie <strong>' + escape_html(adGroupName) + '</strong>?'
         + '<br><br><small>Operacja usuwa wpisy z products_aggregate, products_history oraz products_history_30 dla tej kombinacji. Nie usuwa produktu z tabeli products ani z Google Ads.</small>',
       buttons: {
         confirm: {
           text: 'Usun',
           btnClass: 'btn-danger',
           action: function() {
             var $tr = $btn.closest( 'tr' );
             $btn.prop( 'disabled', true );
             $.ajax({
               url: '/products/delete_product_scope_history/',
               type: 'POST',
               dataType: 'json',
               data: { product_id: productId, campaign_id: campaignId, ad_group_id: adGroupId },
               success: function( res ) {
                 if ( res && res.status === 'ok' ) {
                   $tr.remove();
                   if ( typeof products_table !== 'undefined' && products_table ) {
                     products_table.ajax.reload( null, false );
                   }
                   show_toast( 'Usunieto wpisy historii dla wybranego zakresu.', 'success' );
                 } else {
                   $btn.prop( 'disabled', false );
                   $.alert({ title: 'Blad', content: ( res && res.message ) || 'Nie udalo sie usunac wpisow.' });
                 }
               },
               error: function() {
                 $btn.prop( 'disabled', false );
                 $.alert({ title: 'Blad', content: 'Blad polaczenia z serwerem.' });
               }
             });
           }
         },
         cancel: { text: 'Anuluj' }
       }
     });
   });
   ```

   Uwaga: `show_toast` i `escape_html` istnieja juz w pliku - uzyj ich. Jezeli `products_table` nie jest w scope tego closure (sprawdzic - jest zdefiniowana w glownym `$( function() { ... })`), uzyj `$( '#products' ).DataTable().ajax.reload( null, false )`.

c) CSS: nie wymagany dodatkowy CSS - kolumna "Akcje" przejmuje style domyslne `.products-breakdown-table td`. Ewentualnie w sekcji styli (~linia 217-237) dopisac `.products-breakdown-table td:last-child { width: 56px; text-align: center; }` jezeli kolumna sie rozjedzie.

d) Zaktualizuj komentarz/uwage na gorze tabeli breakdown jezeli istnieje (sprawdzic - prawdopodobnie nie ma).

Avoid: zmian w naglowkach pozostalych kolumn. Avoid: globalnego refresh `location.reload()`. Avoid: ruszania w `products_breakdown_meta` ani `get_products_scope_breakdown` (payload juz ma wymagane pola).
Manual w przegladarce na /products: 1. Zaloguj sie, wybierz klienta, znajdz produkt z breakdownem (kilka kampanii/grup). 2. Rozwin produkt -> widoczna kolumna "Akcje" z koszem na koncu tabeli. 3. Klik kosz -> dialog z nazwa kampanii i grupy. 4. Anuluj -> nic sie nie dzieje. 5. Klik kosz -> Usun -> toast sukcesu, wiersz znika z tabeli rozbicia, glowny wiersz produktu ma zaktualizowane sumy (np. impressions, cost, conversions sa pomniejszone o usuniety zakres). 6. SQL post-check: brak wpisow w `products_aggregate`, `products_history`, `products_history_30` dla skasowanej trojki; pozostale trojki dla tego product_id sa nietkniete. 7. Browser console - brak bledow JS. AC-3, AC-4 spelnione: dialog potwierdzenia + bezpieczne odswiezenie UI bez utraty paginacji. Dodano lokalne usuwanie wpisow statystyczno-historycznych (products_aggregate + products_history + products_history_30) per trojka product_id + campaign_id + ad_group_id z UI breakdown w /products. 1. Otworz https://adspro.projectpro.pl/products i wybierz klienta z aktywnymi kampaniami. 2. Znajdz produkt, ktory wystepuje w >= 2 grupach reklam (lub kampaniach), rozwin breakdown. 3. Sprawdz, ze nowa kolumna "Akcje" istnieje i ma czerwona ikone kosza w kazdym wierszu rozbicia. 4. Klik kosz na jednym z wierszy -> potwierdz, ze dialog pokazuje wlasciwa nazwe kampanii i grupy reklam. 5. Anuluj raz - wiersz nie powinien zniknac. 6. Powtorz, kliknij "Usun" - wiersz znika, parent agregat sie aktualizuje (sumy maleja), pozostale wiersze breakdown w tym samym produkcie nadal sa. 7. Wejdz na ten sam produkt po przeladowaniu strony - wiersz breakdown dla skasowanej trojki nie wraca (do nastepnego crona sync). 8. Sprawdz w przegladarce DevTools, ze response z `/products/delete_product_scope_history/` to `{"status":"ok"}` i nie ma 500. Wpisz "approved" aby zamknac plan, albo opisz problemy do poprawy.

DO NOT CHANGE

  • autoload/factory/class.Products.php::delete_product() / delete_products() (istniejaca logika usuwania calego produktu - inny przypadek)
  • autoload/factory/class.Products.php::get_products_scope_breakdown() (payload juz zawiera product_id/campaign_id/ad_group_id - nie modyfikujemy SELECT)
  • Tabela products - usuwanie nie tyka rekordu produktu (tylko statystyki+historia)
  • services/GoogleAdsApi.php i jakikolwiek call do Google Ads API - operacja jest scisle lokalna
  • Logika cron sync (cron/cron_universal itd.) - nie zmieniamy; po usunieciu cron moze ponownie zaciagnac dane jezeli produkt jest aktywny w MC, to jest oczekiwane

SCOPE LIMITS

  • Brak operacji bulk (usuwanie wybranych wielu wierszy breakdown jednoczesnie) - jeden wiersz = jeden klik = jedno potwierdzenie
  • Brak undo / soft-delete - hard delete z DB
  • Brak zmian w endpoincie /products/get_products/ - payload juz zawiera potrzebne ID
  • Brak telemetrii / audit log - jezeli okaze sie potrzebny, oddzielny plan
Przed zamknieciem planu: - [ ] `php -l autoload/factory/class.Products.php` OK - [ ] `php -l autoload/controls/class.Products.php` OK - [ ] Manual: scenariusz Task 3 verify (kroki 1-7) i checkpoint human-verify (kroki 1-8) - [ ] DevTools: brak bledow JS i 500 z endpointu - [ ] SQL post-check: usunieto dokladnie te trojki, ktore mialy zniknac - [ ] Wszystkie AC-1..AC-4 spelnione

<success_criteria>

  • Wszystkie 3 auto taski + 1 human-verify ukonczone i potwierdzone
  • Brak nowych bledow lintera / parse errors
  • UI breakdown zachowuje sie zgodnie z AC-3 i AC-4
  • Operacja jest atomowa (transakcja) i ograniczona do podanej trojki </success_criteria>
Po ukonczeniu utworz `.paul/phases/05-products-scope-history-delete/05-01-SUMMARY.md` zgodnie z konwencja phase 04 (frontmatter + sekcje Performance / AC Results / Files / Decisions / Deviations / Issues / Next Phase Readiness).