feat: Enhance user settings with cron URL plan display

- Added a new field to display the cron URL plan in user settings.
- Updated JavaScript to handle the new plan data.

refactor: Unify product model and migrate data

- Migrated product data from `products_data` to `products` table.
- Added new columns to `products` for better data organization.
- Created `products_aggregate` table for storing aggregated product metrics.

chore: Drop deprecated products_data table

- Removed `products_data` table as data is now stored in `products`.

feat: Add merchant URL flags to products

- Introduced flags for tracking merchant URL status in `products` table.
- Normalized product URLs to handle empty or invalid values.

feat: Link campaign alerts to specific products

- Added `product_id` column to `campaign_alerts` table for better tracking.
- Created an index for efficient querying of alerts by product.

chore: Add debug scripts for client data inspection

- Created debug scripts to inspect client data from local and remote databases.
- Included error handling and output formatting for better readability.
This commit is contained in:
2026-02-20 17:50:14 +01:00
parent 0024a25bfb
commit 167ced3573
31 changed files with 5697 additions and 1227 deletions

View File

@@ -1,280 +0,0 @@
# adsPRO - System Zarządzania Reklamami Google ADS & Facebook ADS
## Opis projektu
adsPRO to narzędzie webowe (PHP) do zarządzania i automatyzacji kampanii reklamowych Google ADS (priorytet) oraz Facebook ADS (planowane). System umożliwia monitorowanie kampanii, zarządzanie produktami, analizę wydajności (ROAS, CTR, CPC) oraz automatyczne etykietowanie produktów (bestsellery, zombie itp.).
**URL:** https://adspro.projectpro.pl
**Hosting:** Hostido (shared hosting)
## Stack technologiczny
- **PHP 8.x** - czyste PHP z własną strukturą MVC (bez frameworka)
- **MySQL/MariaDB** - baza danych (Medoo ORM)
- **Google ADS API** - pobieranie danych kampanii i produktów (CRON)
- **Facebook ADS API** - planowane w przyszłości
- **CRON** - automatyczna synchronizacja danych
- **SCSS** - stylowanie (kompilacja do CSS)
- **jQuery 3.6** - interaktywność frontend
- **DataTables 2.1.7** - tabele z sortowaniem, filtrowaniem, paginacją
- **Highcharts** - wykresy wydajności
- **Select2** - zaawansowane selecty
- **Font Awesome** - ikony
## Struktura katalogów (nowa)
```
public_html/
├── index.php # Front controller + nowy router
├── .htaccess # Rewrite rules
├── .env # Konfiguracja (przyszłość - migracja z config.php)
├── config.php # Konfiguracja DB (obecna)
├── ajax.php # Ajax handler
├── api.php # API handler (Google ADS webhook)
├── cron.php # CRON handler
├── robots.txt
├── layout/
│ ├── favicon.png
│ ├── style.scss # Główne style (SCSS)
│ └── style.css # Skompilowane style
├── libraries/ # Biblioteki zewnętrzne
│ ├── medoo/
│ ├── phpmailer/
│ ├── select2/
│ ├── jquery-confirm/
│ ├── functions.js # Globalne funkcje JS
│ └── framework/ # Framework UI (skin, pluginy)
├── autoload/ # Kod PHP (MVC)
│ ├── class.S.php # Helper: sesje, requesty, narzędzia
│ ├── class.Tpl.php # Template engine
│ ├── class.Cache.php # Cache
│ ├── class.DbModel.php # Bazowy model DB
│ ├── class.Html.php # HTML helper
│ ├── controls/ # Kontrolery
│ │ ├── class.Site.php # Router (do przebudowy)
│ │ ├── class.Users.php # Logowanie, ustawienia
│ │ ├── class.Dashboard.php # NOWY - Dashboard główny
│ │ ├── class.Campaigns.php # Kampanie Google ADS
│ │ ├── class.Products.php # Produkty
│ │ ├── class.Allegro.php # Import Allegro
│ │ ├── class.Reports.php # NOWY - Raporty i analityka
│ │ ├── class.Api.php # Endpointy API
│ │ └── class.Cron.php # CRON joby
│ ├── factory/ # Modele danych (DB queries)
│ │ ├── class.Users.php
│ │ ├── class.Campaigns.php
│ │ ├── class.Products.php
│ │ └── class.Cron.php
│ └── view/ # View helpers
│ ├── class.Site.php # Renderer layoutu
│ ├── class.Users.php
│ └── class.Cron.php
├── templates/ # Szablony PHP
│ ├── site/
│ │ ├── layout-logged.php # Layout z sidebar (PRZEBUDOWA)
│ │ └── layout-unlogged.php # Layout logowania (PRZEBUDOWA)
│ ├── auth/
│ │ └── login.php # NOWY ekran logowania
│ ├── dashboard/
│ │ └── index.php # NOWY dashboard
│ ├── campaigns/
│ │ └── main_view.php # Widok kampanii
│ ├── products/
│ │ ├── main_view.php # Lista produktów
│ │ └── product_history.php # Historia produktu
│ ├── allegro/
│ │ └── main_view.php # Import Allegro
│ ├── reports/ # NOWE
│ │ └── index.php # Raporty
│ ├── users/
│ │ ├── login-form.php # Stary login (do usunięcia)
│ │ └── settings.php # Ustawienia użytkownika
│ └── html/ # Komponenty HTML
│ ├── button.php
│ ├── input.php
│ ├── select.php
│ └── ...
├── tools/
│ └── google-taxonomy.php
├── tmp/
└── docs/
└── PLAN.md
```
## Nowy system routingu
### Zasada działania
Zamiast obecnego `?module=X&action=Y` → czyste URLe obsługiwane przez `.htaccess` + nowy router w `class.Site.php`.
### Mapa URL
| URL | Kontroler | Metoda | Opis |
|-----|-----------|--------|------|
| `/login` | Users | login_form | Ekran logowania |
| `/logout` | Users | logout | Wylogowanie |
| `/` | Dashboard | index | Dashboard główny |
| `/campaigns` | Campaigns | main_view | Lista kampanii |
| `/campaigns/history/{id}` | Campaigns | history | Historia kampanii |
| `/products` | Products | main_view | Lista produktów |
| `/products/history/{id}` | Products | product_history | Historia produktu |
| `/allegro` | Allegro | main_view | Import Allegro |
| `/reports` | Reports | index | Raporty |
| `/settings` | Users | settings | Ustawienia konta |
| `/api/*` | Api | * | Endpointy API |
| `/cron/*` | Cron | * | CRON joby |
### Nowy .htaccess
```apache
RewriteEngine On
RewriteBase /
# Statyczne zasoby - pomijaj
RewriteCond %{REQUEST_URI} ^/(libraries|layout|upload|temp)/ [NC]
RewriteRule ^ - [L]
# Wszystko inne → index.php
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [L,QSA]
```
### Nowy router (class.Site.php)
```php
// Parsowanie URL z $_SERVER['REQUEST_URI']
// Mapowanie: /segment1/segment2/segment3 → kontroler/akcja/parametry
// Fallback na dashboard dla zalogowanych, login dla niezalogowanych
```
## Główne funkcje
### 1. Nowy ekran logowania
- Nowoczesny design: podzielony ekran (lewa strona - branding/grafika, prawa - formularz)
- Logo "adsPRO" z subtitlem
- Pola: email + hasło
- Checkbox "Zapamiętaj mnie"
- Walidacja AJAX
- Animacje przejścia
- Responsywność (mobile: tylko formularz)
### 2. Nowy layout z menu bocznym (sidebar)
- **Sidebar (lewa strona, 260px):**
- Logo "adsPRO" na górze
- Menu nawigacyjne z ikonami Font Awesome:
- 📊 Dashboard (`/`)
- 📢 Kampanie (`/campaigns`)
- 📦 Produkty (`/products`)
- 📥 Allegro import (`/allegro`)
- 📈 Raporty (`/reports`)
- ⚙️ Ustawienia (`/settings`)
- Aktywny element podświetlony
- Możliwość zwijania sidebar (collapsed → same ikony, 60px)
- Na dole: info o zalogowanym użytkowniku + przycisk wylogowania
- **Top bar (nad contentem):**
- Przycisk hamburger (toggle sidebar)
- Breadcrumbs (ścieżka nawigacji)
- Szybkie akcje / notyfikacje (przyszłość)
- **Content area:**
- Pełna szerokość minus sidebar
- Padding 25px
- Tło #F4F6F9 (jaśniejsze od obecnego)
### 3. Dashboard (NOWY)
- Kafelki podsumowujące (karty):
- Łączna liczba kampanii
- Łączna liczba produktów
- Średni ROAS (30 dni)
- Łączne wydatki (30 dni)
- Wykres trendu ROAS (ostatnie 30 dni)
- Lista ostatnio zmodyfikowanych kampanii
- Produkty wymagające uwagi (niski ROAS, zombie)
### 4. Zarządzanie kampaniami Google ADS
- Wybór klienta (select)
- Lista kampanii z metrykami (DataTables)
- Historia kampanii z wykresem Highcharts
- Metryki: ROAS, budżet, wydatki, wartość konwersji, strategia bidding
- Usuwanie kampanii i wpisów historii
- Komentarze do kampanii
### 5. Zarządzanie produktami
- Wybór klienta
- Konfiguracja min. ROAS dla bestsellerów
- Tabela produktów z metrykami:
- Wyświetlenia, kliknięcia, CTR, koszt, CPC
- Konwersje, wartość konwersji, ROAS
- Custom labels (bestseller/zombie/deleted/pla/paused)
- Edycja inline (min_roas, custom_label)
- Edycja produktu w modalu (tytuł, opis, kategoria Google)
- Historia produktu z wykresem
- Bulk delete zaznaczonych produktów
### 6. Import Allegro
- Upload pliku CSV
- Automatyczne mapowanie ofert
- Raport importu (dodane, zaktualizowane)
### 7. Raporty (NOWY - przyszłość)
- Raport wydajności kampanii
- Raport produktów (bestsellery vs zombie)
- Eksport do Excel
- Porównanie okresów
### 8. Ustawienia
- Dane konta (email)
- Zmiana hasła
- Konfiguracja Pushover (powiadomienia)
- Klucze API (przyszłość: Google ADS, Facebook ADS)
### 9. CRON - synchronizacja danych
- `cron_products` - synchronizacja produktów z Google ADS
- `cron_products_history_30` - historia 30-dniowa produktów
- `cron_xml` - generowanie XML
- `cron_phrases` - synchronizacja fraz
- `cron_phrases_history_30` - historia 30-dniowa fraz
## Plan implementacji
| Etap | Zakres | Priorytet | Pliki |
|------|--------|-----------|-------|
| **1. Nowy routing** | Przebudowa routera, nowy .htaccess, parsowanie czystych URL | 🔴 Wysoki | `.htaccess`, `index.php`, `controls/class.Site.php` |
| **2. Nowy layout (sidebar)** | Layout z bocznym menu, top bar, responsywność | 🔴 Wysoki | `templates/site/layout-logged.php`, `layout/style.scss` |
| **3. Nowy ekran logowania** | Nowoczesny split-screen login, nowy layout-unlogged | 🔴 Wysoki | `templates/site/layout-unlogged.php`, `templates/auth/login.php`, `layout/style.scss` |
| **4. Dashboard** | Nowa strona startowa z podsumowaniem | 🟡 Średni | `controls/class.Dashboard.php`, `templates/dashboard/index.php` |
| **5. Migracja kampanii** | Dostosowanie widoku kampanii do nowego routingu | 🟡 Średni | `controls/class.Campaigns.php`, `templates/campaigns/*` |
| **6. Migracja produktów** | Dostosowanie widoku produktów do nowego routingu | 🟡 Średni | `controls/class.Products.php`, `templates/products/*` |
| **7. Migracja Allegro** | Dostosowanie importu Allegro | 🟢 Niski | `controls/class.Allegro.php`, `templates/allegro/*` |
| **8. Moduł raportów** | Nowy moduł analityczny | 🟢 Niski | `controls/class.Reports.php`, `templates/reports/*` |
| **9. Facebook ADS** | Integracja z Facebook ADS API | 🔵 Przyszłość | Nowe kontrolery, factory, szablony |
## Kolorystyka i design
### Paleta kolorów
- **Primary (akcent):** `#6690F4` (niebieski - obecny)
- **Sidebar tło:** `#1E2A3A` (ciemny granat)
- **Sidebar tekst:** `#A8B7C7` (jasny szary)
- **Sidebar active:** `#6690F4` (primary)
- **Content tło:** `#F4F6F9` (jasnoszary)
- **Karty:** `#FFFFFF`
- **Tekst:** `#4E5E6A` (obecny)
- **Success:** `#57B951`
- **Danger:** `#CC0000`
- **Warning:** `#FF8C00`
### Typografia
- Font: Open Sans (obecny - zachowany)
- Rozmiar bazowy: 14px (sidebar), 15px (content)
## Bezpieczeństwo
- Hasła hashowane MD5 (obecne) → **TODO: migracja na bcrypt**
- Sesje PHP + cookie "zapamiętaj mnie"
- Prepared statements (Medoo ORM)
- htmlspecialchars() w szablonach
- **TODO: CSRF tokeny w formularzach**
- **TODO: migracja config.php → .env (z .htaccess deny)**
## Przyszłe rozszerzenia
- Facebook ADS API - zarządzanie kampaniami FB
- System powiadomień (Pushover + in-app)
- Wielojęzyczność (PL/EN)
- Role użytkowników (admin, manager, viewer)
- Automatyczne reguły (np. "jeśli ROAS < X → zmień label na zombie")
- Integracja z Google Merchant Center
- API REST do integracji z innymi systemami

View File

@@ -1,44 +1,24 @@
-- --------------------------------------------------------
-- Host: host700513.hostido.net.pl
-- Wersja serwera: 10.11.15-MariaDB-cll-lve - MariaDB Server
-- Serwer OS: Linux
-- HeidiSQL Wersja: 12.6.0.6765
-- --------------------------------------------------------
-- Zrzut struktury tabela host700513_adspro.clients
CREATE TABLE IF NOT EXISTS `clients` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL DEFAULT '0',
`google_ads_customer_id` varchar(20) DEFAULT NULL,
`google_merchant_account_id` varchar(32) DEFAULT NULL,
`google_ads_start_date` date DEFAULT NULL,
`active` int(11) DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=48 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET NAMES utf8 */;
/*!50503 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
-- Zrzut struktury tabela host700513_adspro.campaigns
CREATE TABLE IF NOT EXISTS `campaigns` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`client_id` int(11) NOT NULL DEFAULT 0,
`campaign_id` bigint(20) NOT NULL DEFAULT 0,
`campaign_name` varchar(255) NOT NULL DEFAULT '0',
`advertising_channel_type` varchar(40) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `client_id` (`client_id`),
CONSTRAINT `FK__clients` FOREIGN KEY (`client_id`) REFERENCES `clients` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=123 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Eksport danych został odznaczony.
-- Zrzut struktury tabela host700513_adspro.campaigns_comments
CREATE TABLE IF NOT EXISTS `campaigns_comments` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`campaign_id` int(11) NOT NULL,
`comment` text NOT NULL,
`date_add` date NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`) USING BTREE,
KEY `campaign_id` (`campaign_id`),
CONSTRAINT `FK_campaigns_comments_campaigns` FOREIGN KEY (`campaign_id`) REFERENCES `campaigns` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
-- Eksport danych został odznaczony.
) ENGINE=InnoDB AUTO_INCREMENT=56 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Zrzut struktury tabela host700513_adspro.campaigns_history
CREATE TABLE IF NOT EXISTS `campaigns_history` (
@@ -54,235 +34,7 @@ CREATE TABLE IF NOT EXISTS `campaigns_history` (
PRIMARY KEY (`id`) USING BTREE,
KEY `offer_id` (`campaign_id`) USING BTREE,
CONSTRAINT `FK_campaigns_history_campaigns` FOREIGN KEY (`campaign_id`) REFERENCES `campaigns` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=4400 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
-- Eksport danych został odznaczony.
-- Zrzut struktury tabela host700513_adspro.clients
CREATE TABLE IF NOT EXISTS `clients` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL DEFAULT '0',
`google_ads_customer_id` varchar(20) DEFAULT NULL,
`google_merchant_account_id` varchar(32) DEFAULT NULL,
`google_ads_start_date` date DEFAULT NULL,
`deleted` int(11) DEFAULT 0,
`bestseller_min_roas` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=46 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Eksport danych został odznaczony.
-- Zrzut struktury tabela host700513_adspro.phrases
CREATE TABLE IF NOT EXISTS `phrases` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`client_id` int(11) NOT NULL DEFAULT 0,
`phrase` varchar(255) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `FK_phrases_clients` (`client_id`),
CONSTRAINT `FK_phrases_clients` FOREIGN KEY (`client_id`) REFERENCES `clients` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=5512 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Eksport danych został odznaczony.
-- Zrzut struktury tabela host700513_adspro.phrases_history
CREATE TABLE IF NOT EXISTS `phrases_history` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`phrase_id` int(11) NOT NULL DEFAULT 0,
`impressions` int(11) NOT NULL DEFAULT 0,
`clicks` int(11) NOT NULL DEFAULT 0,
`cost` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversions` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversions_value` decimal(20,6) NOT NULL DEFAULT 0.000000,
`date_add` date NOT NULL DEFAULT '0000-00-00',
`updated` int(11) NOT NULL DEFAULT 0,
`deleted` int(11) DEFAULT 0,
PRIMARY KEY (`id`) USING BTREE,
KEY `offer_id` (`phrase_id`) USING BTREE,
CONSTRAINT `FK_phrases_history_phrases` FOREIGN KEY (`phrase_id`) REFERENCES `phrases` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=13088 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
-- Eksport danych został odznaczony.
-- Zrzut struktury tabela host700513_adspro.phrases_history_30
CREATE TABLE IF NOT EXISTS `phrases_history_30` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`phrase_id` int(11) NOT NULL,
`impressions` int(11) NOT NULL,
`clicks` int(11) NOT NULL,
`cost` decimal(20,6) NOT NULL,
`conversions` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversions_value` decimal(20,6) NOT NULL,
`roas` decimal(20,6) NOT NULL,
`date_add` date NOT NULL DEFAULT '0000-00-00',
`deleted` int(11) DEFAULT 0,
PRIMARY KEY (`id`) USING BTREE,
KEY `offer_id` (`phrase_id`) USING BTREE,
CONSTRAINT `FK_phrases_history_30_phrases` FOREIGN KEY (`phrase_id`) REFERENCES `phrases` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=1795 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
-- Eksport danych został odznaczony.
-- Zrzut struktury tabela host700513_adspro.phrases_temp
CREATE TABLE IF NOT EXISTS `phrases_temp` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`phrase_id` int(11) DEFAULT NULL,
`phrase` varchar(255) DEFAULT NULL,
`impressions` int(11) DEFAULT NULL,
`clicks` int(11) DEFAULT NULL,
`cost` decimal(20,6) DEFAULT NULL,
`conversions` decimal(20,6) DEFAULT NULL,
`conversions_value` decimal(20,6) DEFAULT NULL,
`cpc` decimal(20,6) DEFAULT NULL,
`roas` decimal(20,0) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
KEY `offer_id` (`phrase_id`) USING BTREE,
CONSTRAINT `FK_phrases_temp_phrases` FOREIGN KEY (`phrase_id`) REFERENCES `phrases` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=353973 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
-- Eksport danych został odznaczony.
-- Zrzut struktury tabela host700513_adspro.products
CREATE TABLE IF NOT EXISTS `products` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`client_id` int(11) NOT NULL DEFAULT 0,
`offer_id` varchar(50) NOT NULL DEFAULT '0',
`name` varchar(255) NOT NULL DEFAULT '0',
`min_roas` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `FK_offers_clients` (`client_id`),
CONSTRAINT `FK_offers_clients` FOREIGN KEY (`client_id`) REFERENCES `clients` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=5927 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Eksport danych został odznaczony.
-- Zrzut struktury tabela host700513_adspro.products_comments
CREATE TABLE IF NOT EXISTS `products_comments` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`product_id` int(11) NOT NULL,
`comment` text NOT NULL,
`date_add` date NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
KEY `product_id` (`product_id`) USING BTREE,
CONSTRAINT `FK_products_comments_products` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=118 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Eksport danych został odznaczony.
-- Zrzut struktury tabela host700513_adspro.products_data
CREATE TABLE IF NOT EXISTS `products_data` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`product_id` int(11) DEFAULT NULL,
`custom_label_4` varchar(255) DEFAULT NULL,
`custom_label_3` varchar(255) DEFAULT NULL,
`title` varchar(255) DEFAULT NULL,
`description` text DEFAULT NULL,
`google_product_category` text DEFAULT NULL,
`product_url` varchar(500) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `product_id` (`product_id`) USING BTREE,
CONSTRAINT `FK_products_data_products` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=118 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Eksport danych został odznaczony.
-- Zrzut struktury tabela host700513_adspro.products_history
CREATE TABLE IF NOT EXISTS `products_history` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`product_id` int(11) NOT NULL DEFAULT 0,
`impressions` int(11) NOT NULL DEFAULT 0,
`clicks` int(11) NOT NULL DEFAULT 0,
`ctr` decimal(20,6) NOT NULL DEFAULT 0.000000,
`cost` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversions` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversions_value` decimal(20,6) NOT NULL DEFAULT 0.000000,
`date_add` date NOT NULL DEFAULT '0000-00-00',
`updated` int(11) NOT NULL DEFAULT 0,
`deleted` int(11) DEFAULT 0,
PRIMARY KEY (`id`) USING BTREE,
KEY `product_id` (`product_id`) USING BTREE,
CONSTRAINT `FK_products_history_products` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=63549 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
-- Eksport danych został odznaczony.
-- Zrzut struktury tabela host700513_adspro.products_history_30
CREATE TABLE IF NOT EXISTS `products_history_30` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`product_id` int(11) NOT NULL,
`impressions` int(11) NOT NULL,
`clicks` int(11) NOT NULL,
`ctr` decimal(20,6) NOT NULL DEFAULT 0.000000,
`cost` decimal(20,6) NOT NULL,
`conversions` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversions_value` decimal(20,6) NOT NULL,
`roas` decimal(20,6) NOT NULL,
`roas_all_time` decimal(20,6) NOT NULL,
`date_add` date NOT NULL DEFAULT '0000-00-00',
`deleted` int(11) DEFAULT 0,
PRIMARY KEY (`id`),
KEY `product_id` (`product_id`) USING BTREE,
CONSTRAINT `FK_products_history_30_products` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=27655 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Eksport danych został odznaczony.
-- Zrzut struktury tabela host700513_adspro.products_temp
CREATE TABLE IF NOT EXISTS `products_temp` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`product_id` int(11) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
`impressions` int(11) DEFAULT NULL,
`impressions_30` int(11) DEFAULT NULL,
`clicks` int(11) DEFAULT NULL,
`clicks_30` int(11) DEFAULT NULL,
`ctr` decimal(20,6) DEFAULT NULL,
`cost` decimal(20,6) DEFAULT NULL,
`conversions` decimal(20,6) DEFAULT NULL,
`conversions_value` decimal(20,6) DEFAULT NULL,
`cpc` decimal(20,6) DEFAULT NULL,
`roas` decimal(20,0) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `product_id` (`product_id`) USING BTREE,
CONSTRAINT `FK_products_temp_products` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=298845 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Eksport danych został odznaczony.
-- Zrzut struktury tabela host700513_adspro.settings
CREATE TABLE IF NOT EXISTS `settings` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`setting_key` varchar(100) NOT NULL,
`setting_value` text DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_setting_key` (`setting_key`)
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci;
-- Eksport danych został odznaczony.
-- Zrzut struktury tabela host700513_adspro.users
CREATE TABLE IF NOT EXISTS `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`email` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
`name` varchar(255) DEFAULT NULL,
`surname` varchar(255) DEFAULT NULL,
`default_project` int(11) DEFAULT NULL,
`color` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `email` (`email`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_polish_ci;
-- Eksport danych został odznaczony.
/*!40103 SET TIME_ZONE=IFNULL(@OLD_TIME_ZONE, 'system') */;
/*!40101 SET SQL_MODE=IFNULL(@OLD_SQL_MODE, '') */;
/*!40014 SET FOREIGN_KEY_CHECKS=IFNULL(@OLD_FOREIGN_KEY_CHECKS, 1) */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40111 SET SQL_NOTES=IFNULL(@OLD_SQL_NOTES, 1) */;
-- ================================
-- DODANE: struktury kampanie > grupy/frazy
-- ================================
) ENGINE=InnoDB AUTO_INCREMENT=381 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS `campaign_ad_groups` (
`id` int(11) NOT NULL AUTO_INCREMENT,
@@ -304,67 +56,122 @@ CREATE TABLE IF NOT EXISTS `campaign_ad_groups` (
`date_sync` date DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_campaign_ad_groups_campaign_ad_group` (`campaign_id`,`ad_group_id`),
KEY `idx_campaign_ad_groups_campaign_id` (`campaign_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
KEY `idx_campaign_ad_groups_campaign_id` (`campaign_id`),
CONSTRAINT `FK_campaign_ad_groups_campaigns` FOREIGN KEY (`campaign_id`) REFERENCES `campaigns` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=125 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `campaign_search_terms` (
CREATE TABLE IF NOT EXISTS `campaign_alerts` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`campaign_id` int(11) NOT NULL,
`ad_group_id` int(11) NOT NULL,
`search_term` varchar(255) NOT NULL,
`impressions_30` int(11) NOT NULL DEFAULT 0,
`clicks_30` int(11) NOT NULL DEFAULT 0,
`cost_30` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversions_30` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversion_value_30` decimal(20,6) NOT NULL DEFAULT 0.000000,
`roas_30` decimal(20,6) NOT NULL DEFAULT 0.000000,
`impressions_all_time` int(11) NOT NULL DEFAULT 0,
`clicks_all_time` int(11) NOT NULL DEFAULT 0,
`cost_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversions_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversion_value_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000,
`roas_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000,
`date_sync` date DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_campaign_search_terms` (`campaign_id`,`ad_group_id`,`search_term`),
KEY `idx_campaign_search_terms_campaign_id` (`campaign_id`),
KEY `idx_campaign_search_terms_ad_group_id` (`ad_group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `campaign_keywords` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`campaign_id` int(11) NOT NULL,
`ad_group_id` int(11) NOT NULL,
`keyword_text` varchar(255) NOT NULL,
`match_type` varchar(40) DEFAULT NULL,
`impressions_30` int(11) NOT NULL DEFAULT 0,
`clicks_30` int(11) NOT NULL DEFAULT 0,
`cost_30` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversions_30` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversion_value_30` decimal(20,6) NOT NULL DEFAULT 0.000000,
`roas_30` decimal(20,6) NOT NULL DEFAULT 0.000000,
`impressions_all_time` int(11) NOT NULL DEFAULT 0,
`clicks_all_time` int(11) NOT NULL DEFAULT 0,
`cost_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversions_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversion_value_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000,
`roas_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000,
`date_sync` date DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_campaign_keywords` (`campaign_id`,`ad_group_id`,`keyword_text`(191),`match_type`),
KEY `idx_campaign_keywords_campaign_id` (`campaign_id`),
KEY `idx_campaign_keywords_ad_group_id` (`ad_group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `campaign_negative_keywords` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`campaign_id` int(11) NOT NULL,
`client_id` int(11) NOT NULL,
`campaign_id` int(11) DEFAULT NULL,
`campaign_external_id` bigint(20) DEFAULT NULL,
`ad_group_id` int(11) DEFAULT NULL,
`scope` varchar(20) NOT NULL DEFAULT 'campaign',
`keyword_text` varchar(255) NOT NULL,
`match_type` varchar(40) DEFAULT NULL,
`date_sync` date DEFAULT NULL,
`ad_group_external_id` bigint(20) DEFAULT NULL,
`product_id` int(11) DEFAULT NULL,
`alert_type` varchar(120) NOT NULL,
`message` text NOT NULL,
`meta_json` text DEFAULT NULL,
`date_detected` date NOT NULL,
`date_add` datetime NOT NULL DEFAULT current_timestamp(),
`unseen` tinyint(1) NOT NULL DEFAULT 1,
PRIMARY KEY (`id`),
KEY `idx_campaign_negative_keywords_campaign_id` (`campaign_id`),
KEY `idx_campaign_negative_keywords_ad_group_id` (`ad_group_id`)
UNIQUE KEY `uniq_alert_daily` (`client_id`,`campaign_external_id`,`ad_group_external_id`,`alert_type`,`date_detected`),
KEY `idx_alert_date` (`date_detected`),
KEY `idx_alert_client` (`client_id`),
KEY `idx_alert_campaign` (`campaign_id`),
KEY `idx_alert_ad_group` (`ad_group_id`),
KEY `idx_alert_unseen` (`unseen`),
KEY `idx_alert_product` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
CREATE TABLE IF NOT EXISTS `products` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`client_id` int(11) NOT NULL DEFAULT 0,
`offer_id` varchar(50) NOT NULL DEFAULT '0',
`name` varchar(255) NOT NULL DEFAULT '0',
`min_roas` int(11) DEFAULT NULL,
`custom_label_4` varchar(255) DEFAULT NULL,
`custom_label_3` varchar(255) DEFAULT NULL,
`title` varchar(255) DEFAULT NULL,
`description` text DEFAULT NULL,
`google_product_category` text DEFAULT NULL,
`product_url` varchar(500) DEFAULT NULL,
`merchant_url_not_found` tinyint(1) NOT NULL DEFAULT 0,
`merchant_url_last_check` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `FK_offers_clients` (`client_id`),
CONSTRAINT `FK_offers_clients` FOREIGN KEY (`client_id`) REFERENCES `clients` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=8482 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `products_history` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`product_id` int(11) NOT NULL DEFAULT 0,
`campaign_id` int(11) NOT NULL DEFAULT 0,
`ad_group_id` int(11) NOT NULL DEFAULT 0,
`impressions` int(11) NOT NULL DEFAULT 0,
`clicks` int(11) NOT NULL DEFAULT 0,
`ctr` decimal(20,6) NOT NULL DEFAULT 0.000000,
`cost` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversions` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversions_value` decimal(20,6) NOT NULL DEFAULT 0.000000,
`date_add` date NOT NULL DEFAULT '0000-00-00',
`updated` int(11) NOT NULL DEFAULT 0,
`deleted` int(11) DEFAULT 0,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_products_history_scope_day` (`product_id`,`campaign_id`,`ad_group_id`,`date_add`),
KEY `product_id` (`product_id`) USING BTREE,
KEY `idx_products_history_campaign_id` (`campaign_id`),
KEY `idx_products_history_ad_group_id` (`ad_group_id`),
CONSTRAINT `FK_products_history_products` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=37033 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
CREATE TABLE IF NOT EXISTS `products_history_30` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`product_id` int(11) NOT NULL,
`campaign_id` int(11) NOT NULL DEFAULT 0,
`ad_group_id` int(11) NOT NULL DEFAULT 0,
`impressions` int(11) NOT NULL,
`clicks` int(11) NOT NULL,
`ctr` decimal(20,6) NOT NULL DEFAULT 0.000000,
`cost` decimal(20,6) NOT NULL,
`conversions` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversions_value` decimal(20,6) NOT NULL,
`roas` decimal(20,6) NOT NULL,
`roas_all_time` decimal(20,6) NOT NULL,
`date_add` date NOT NULL DEFAULT '0000-00-00',
`deleted` int(11) DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_products_history_30_scope_day` (`product_id`,`campaign_id`,`ad_group_id`,`date_add`),
KEY `product_id` (`product_id`) USING BTREE,
KEY `idx_products_history_30_campaign_id` (`campaign_id`),
KEY `idx_products_history_30_ad_group_id` (`ad_group_id`),
CONSTRAINT `FK_products_history_30_products` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `products_aggregate` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`product_id` int(11) NOT NULL,
`campaign_id` int(11) NOT NULL DEFAULT 0,
`ad_group_id` int(11) NOT NULL DEFAULT 0,
`impressions_30` int(11) NOT NULL DEFAULT 0,
`clicks_30` int(11) NOT NULL DEFAULT 0,
`ctr_30` decimal(20,6) NOT NULL DEFAULT 0.000000,
`cost_30` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversions_30` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversion_value_30` decimal(20,6) NOT NULL DEFAULT 0.000000,
`roas_30` decimal(20,6) NOT NULL DEFAULT 0.000000,
`impressions_all_time` int(11) NOT NULL DEFAULT 0,
`clicks_all_time` int(11) NOT NULL DEFAULT 0,
`ctr_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000,
`cost_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversions_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversion_value_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000,
`roas_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000,
`date_sync` date NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_products_aggregate_scope` (`product_id`,`campaign_id`,`ad_group_id`),
KEY `idx_products_aggregate_campaign_id` (`campaign_id`),
KEY `idx_products_aggregate_ad_group_id` (`ad_group_id`),
KEY `idx_products_aggregate_date_sync` (`date_sync`),
CONSTRAINT `FK_products_aggregate_products` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@@ -1,55 +1,397 @@
# adsPRO - Pamiec projektu
# 2026-02-20 - Obsluga statusu ACTIVE dla klientow
Ten plik sluzy jako trwala pamiec dla Claude Code. Zapisuj tu wzorce, decyzje i ustalenia potwierdzone w trakcie pracy nad projektem.
## Zmienione pliki
## Architektura
- `autoload/controls/class.Clients.php`
- `save()` zapisuje teraz pole `active` (domyslnie `1`, gdy brak wartosci z formularza).
- Dodana nowa akcja `set_active()` pod endpoint `/clients/set_active` do szybkiej zmiany statusu klienta AJAX-em.
- `force_sync()` ma dodatkowa walidacje:
- nie pozwala kolejkowac synchronizacji dla klienta nieaktywnego (`active != 1`),
- nadal blokuje klienta usunietego (`deleted = 1`) i klienta bez wymaganych ID.
- Kompatybilnosc schematu `clients` bez kolumny `deleted`:
- helpery `clients_has_deleted_column()` i `sql_clients_not_deleted()`,
- `force_sync()` i `sync_status()` nie wywalaja sie, gdy w bazie nie ma kolumny `deleted`.
- Custom MVC: Controllers (`\controls`) -> Factories (`\factory`) -> Medoo ORM (`$mdb`)
- Autoload PSR-0: `\controls\Foo` -> `autoload/controls/class.Foo.php`
- Routing w `index.php`: URL `/module/action/` -> `\controls\Module::action()`
- Szablony w `templates/`, zmienne przez `$this->varName`
- Serwisy API: `\services\GoogleAdsApi`, `\services\ClaudeApi`, `\services\OpenAiApi`
- `templates/clients/main_view.php`
- Tabela klientow ma nowa kolumne `Status` (Aktywny/Nieaktywny).
- Wiersz klienta trzyma `data-active` do obslugi UI i synchronizacji.
- Dodany przycisk toggle (ikona `fa-toggle-on/off`) do natychmiastowej aktywacji/dezaktywacji.
- Przyciski synchronizacji (kampanie/produkty/merchant) sa blokowane (`disabled`) dla nieaktywnego klienta i odblokowywane po aktywacji.
- Formularz Dodaj/Edytuj klienta ma nowe pole `Status klienta` (`active`).
- JS:
- `toggleClientActive()` wysyla POST na `/clients/set_active`,
- `updateClientStatusUI()` odswieza status i stan komorki Sync bez przeladowania strony,
- `loadSyncStatus()` pomija paski postepu dla nieaktywnych klientow i pokazuje `nieaktywny`.
## Styl kodu
## Gdzie to jest wykorzystywane
- Spacje w nawiasach: `if ( $x )`, `function( $a, $b )`
- Klamry w nowej linii
- Wszystkie metody kontrolerow i fabryk: `static public function`
- Endpointy JSON: `echo json_encode([...]); exit;`
- Commity po polsku z prefixem: `feat:`, `fix:`, `update:`
- Zarzadzanie statusem klienta:
- UI listy i formularza: `templates/clients/main_view.php`
- Backend zapisu i toggle: `autoload/controls/class.Clients.php`
- Ograniczenie recznego wymuszenia synchronizacji do klientow aktywnych:
- `autoload/controls/class.Clients.php` (`force_sync()`)
## Frontend
# 2026-02-20 - CRON kampanii (nowy przebieg, stare jako archiwum)
- jQuery 3.6, DataTables 2.1, Bootstrap 4, Select2 4.1, Highcharts
- jquery-confirm do modali/dialogow
- Font Awesome 6.5 do ikon
- SASS: `layout/style.scss` -> auto-kompilacja przez Live Sass Compiler
## Zmienione pliki
## Deployment
- `autoload/controls/class.Cron.php`
- Dodany nowy `cron_campaigns()` jako glowny endpoint pod nowy przeplyw.
- Stary kod zostal zachowany jako archiwum: `cron_campaigns_archive()`.
- Nowy przebieg:
- bierze tylko aktywnych klientow (`active = 1`) z Google Ads Customer ID,
- liczy okno dat na podstawie `google_ads_conversion_window_days` z `config.php` (z fallbackiem),
- konczy okno na `przedwczoraj` (bez pobierania danych dzisiejszych),
- przechodzi po datach dzien po dniu (rosnaco),
- zapisuje/aktualizuje kampanie do `campaigns`,
- zapisuje/aktualizuje historie dzienne do `campaigns_history` (upsert po `campaign_id + date_add`),
- zapisuje grupy reklam / groupy PMAX do `campaign_ad_groups`.
- po zakonczeniu kampanii + ad groups dla klienta, dla calego okna dat pobiera search terms dzienne do `campaign_search_terms_history`,
- po pobraniu historii search terms wykonuje agregacje do `campaign_search_terms` (zanim przejdzie do kolejnego klienta).
- Dodany krok syncu fraz dodanych i wykluczonych:
- tabele docelowe: `campaign_keywords` i `campaign_negative_keywords`,
- uruchamiany raz na cykl klienta (po ostatnim dniu okna), nie x razy dla kazdego dnia.
- Kampanie produktowe / PMAX:
- nie maja fraz dodanych, wiec w `campaign_keywords` moga miec 0 rekordow,
- frazy wykluczone sa dalej synchronizowane do `campaign_negative_keywords`.
- FTP auto-upload przez VS Code FTP-Kr
- Brak kroku budowania - pliki laduja bezposrednio na serwer
- Migracje: `php install.php` (idempotentne, sledzenie w `schema_migrations`)
# 2026-02-20 - Produkty: przygotowanie schematu bazy
## Decyzje projektowe
## Zmienione pliki
- Frazy wyszukiwane dodane do wykluczonych oznaczane czerwonym kolorem (klasa CSS `term-is-negative`)
- Negatywne slowa kluczowe dodawane przez Google Ads API i zapisywane lokalnie w `campaign_negative_keywords`
- Klucze API przechowywane w tabeli `settings` (key-value)
- Frazy z Google Ads Keyword Planner dla URL produktu sa cachowane w `products_keyword_planner_terms` i ponownie uzywane przy generowaniu tytulu AI
- Zmiany produktowe (`title`, `description`, `google_product_category`, `custom_label_4`) sa synchronizowane bezposrednio do Merchant API i logowane per pole w `products_merchant_sync_log`
- CRON dziala w trybie **klient po kliencie** (client-first): konczy WSZYSTKIE daty jednego klienta, potem przechodzi do nastepnego. Dzieki temu paski postepu na `/clients` roznia sie miedzy klientami.
- `cron_products` iteruje po datach per klient (`dates_per_run` z parametru `clients_per_run`), domyslnie `10` (max `100`); faza `aggregate_30` wywoluje `rebuild_products_temp` RAZ per klient
- `cron_campaigns` iteruje po datach per klient (`dates_per_run` z parametru `clients_per_run`), domyslnie `2` (max `20`)
- Helpery: `get_active_client($pipeline)` -> pierwszy klient z niezakonczona praca; `get_pending_dates_for_client()` -> daty do przetworzenia; `determine_client_products_phase()` -> faza per klient
- Stan CRON przechowywany w tabeli `cron_sync_status` (wiersz = klient + pipeline + data + phase), zamiast JSON w `settings` (migracja 012)
- Fazy produktow w `cron_sync_status`: pending -> fetch -> aggregate_30 -> done; kampanie: pending -> done
- Force sync klienta = DELETE z `cron_sync_status` (wiersze odtwarzane przez `ensure_sync_rows` w nastepnym cyklu CRON)
- Nowy klient/usuniety klient obslugiwany naturalnie: `ensure_sync_rows` dodaje brakujace, JOIN z `clients` pomija usunietych
- `cleanup_old_sync_rows(30)` czysci zakonczone wiersze starsze niz 30 dni i wiersze usunietych klientow
- `migrations/016_products_model_unification.sql`
- Dodane kolumny produktowe bezposrednio do `products`:
- `custom_label_4`, `custom_label_3`, `title`, `description`, `google_product_category`, `product_url`.
- Backfill danych z `products_data` -> `products` (tylko gdy pole w `products` jest puste).
- Dodana nowa tabela agregacyjna `products_aggregate`:
- scope: `product_id + campaign_id + ad_group_id` (unikalne),
- metryki `*_30` i `*_all_time`,
- `date_sync` (kiedy agregat byl przeliczony).
## Preferencje uzytkownika
- `docs/database.sql`
- Zaktualizowana definicja `products` o nowe kolumny danych produktu.
- Dodana definicja tabeli `products_aggregate`.
- Komunikacja po polsku
- Zwiezle commity po polsku
- Git push tylko na wyrazna prosbe
## Ustalenie projektowe
- `products` staje sie glowna tabela danych produktu.
- `products_data` zostaje tymczasowo dla kompatybilnosci starego kodu; dane sa migrowane do `products`.
- Agregaty dla widokow `/products` powinny docelowo byc czytane z `products_aggregate` zamiast liczenia w locie.
# 2026-02-20 - Produkty: przepiecie na `products` + agregaty
## Zmienione pliki
- `autoload/factory/class.Products.php`
- `get_product_data()`:
- najpierw czyta pola produktowe z `products` (`custom_label_4`, `custom_label_3`, `title`, `description`, `google_product_category`, `product_url`),
- fallback do `products_data` dla kompatybilnosci.
- `set_product_data()`:
- zapisuje pole glownie do `products`,
- rownolegle mirroruje zapis do `products_data` (kompatybilnosc starego kodu).
- `autoload/controls/class.Cron.php`
- `sync_products_fetch_for_client()`:
- import produktow zapisuje dane produktowe bezposrednio do `products` (w tym `title`, `product_url`),
- usuniete poleganie na `products_data` podczas samego fetchu.
- `aggregate_products_history_30_for_client()`:
- po przeliczeniu `products_history_30` odpala przebudowe agregatow `products_aggregate` dla klienta i dnia.
- Dodana metoda `rebuild_products_aggregate_for_client( $client_id, $date_sync )`:
- liczy metryki `*_30` i `*_all_time` z `products_history`,
- zapisuje scope (`product_id + campaign_id + ad_group_id`) do `products_aggregate`.
- `rebuild_products_temp_for_client()`:
- przestawione z liczenia bezposrednio po `products_history` na odczyt z `products_aggregate`,
- zmniejsza liczenie "w locie" dla widoku `/products`.
- `cron_product_history_30_save()`:
- `products_history_30` przechowuje teraz srednie dzienne wartosci z okna do 30 dni (zamiast sumy okna),
- nadal zapisuje `roas_all_time` dla danego dnia.
- `generate_custom_feed_for_client()`:
- zrodlo danych produktowych przepiete na `products` (bez wymaganego `INNER JOIN products_data`).
- diagnostyka i pobieranie brakujacych URL (`cron_products_urls`):
- logika "ma URL / brak URL" bierze pod uwage `products.product_url` z fallbackiem do `products_data`.
## Gdzie to jest wykorzystywane
- Pipeline produktowy:
- `/cron/cron_products`
- etap `fetch` -> `products_history`,
- etap agregacji -> `products_history_30` + `products_aggregate`,
- etap finalny -> `products_temp` budowane z `products_aggregate`.
- Widok tabeli produktow `/products`:
- dane nadal czytane z `products_temp`, ale `products_temp` jest teraz zasilane agregatami z `products_aggregate`.
- Dodany helper `sync_campaigns_snapshot_for_client()` dla nowego przebiegu kampanii.
- Dodany helper `sync_campaign_terms_backfill_for_client()` dla kroku fraz (history + agregacja).
- Tryb wykonania nowego pipeline kampanii: 1 dzien = 1 wywolanie CRON.
- Na jednym wywolaniu: kampanie + ad groups + search terms history + agregacja search terms dla jednego dnia.
- Kolejne wywolanie przechodzi do kolejnego dnia dla tego samego klienta.
- Tryb debug dla nowego CRON:
- `?debug=true` zwraca czytelny HTML (podsumowanie + pelny payload),
- bez debug zwracany jest standardowy JSON.
- Dodany helper `cleanup_pipeline_rows_outside_window()` aby pipeline kampanii trzymal tylko aktualne okno dat.
- Filtry klientow w nowym CRON kampanii sa odporne na stare dane (`NULL`): `COALESCE(active,0)`, `COALESCE(deleted,0)`, `TRIM(COALESCE(google_ads_customer_id,''))`.
- Dodana kompatybilnosc schematu `clients` bez kolumny `deleted`:
- helpery: `clients_has_column()`, `sql_clients_not_deleted()`, `sql_clients_deleted()`,
- nowy pipeline kampanii (`cron_campaigns`/`cron_universal`) nie wywala sie na bazie bez `deleted`.
- `get_conversion_window_days( $prefer_config = false )` uwzglednia teraz konfiguracje z `config.php`.
- `sync_campaign_ad_groups_for_client()` dostal parametr `as_of_date`.
- `autoload/services/class.GoogleAdsApi.php`
- `get_ad_groups_30_days()` wspiera teraz parametr `as_of_date` i zakres dat `[as_of_date-29, as_of_date]`.
- `get_ad_groups_all_time()` wspiera teraz parametr `as_of_date` (filtr `segments.date <= as_of_date` z fallbackiem).
## Gdzie to jest wykorzystywane
- Głowny CRON kampanii: `/cron/cron_campaigns` -> `\controls\Cron::cron_campaigns()`.
- Uniwersalny CRON pipeline (zalecany endpoint): `/cron/cron_universal` -> `\controls\Cron::cron_universal()` (aktualnie deleguje do kroku kampanii).
- Archiwalny CRON kampanii (stara logika): `/cron/cron_campaigns_archive`.
- Dane do wykresow/tabel kampanii pozostaja pobierane z `campaigns_history`.
# 2026-02-20 - CRON uniwersalny jako glowny endpoint (1 dzien na wywolanie)
## Zmienione pliki
- `autoload/controls/class.Cron.php`
- `cron_universal()` nie deleguje juz do `cron_campaigns()`.
- W jednym wywolaniu realizuje sekwencje:
- `kampanie` (snapshot + ad groups + search terms + agregacja),
- `produkty` (fetch + `products_history_30` + `products_aggregate` + `products_temp`).
- Tryb pracy pozostaje: `1 wywolanie = 1 klient + 1 dzien`.
- Status dnia jest zapisywany do `cron_sync_status` dla obu pipeline:
- `campaigns`,
- `products`.
- Gdy krok kampanii zwroci blad, krok produktow dla tego dnia jest pomijany (`products_sync_skipped_reason=campaigns_failed`).
## Gdzie to jest wykorzystywane
- Docelowy adres CRON:
- `/cron/cron_universal?debug=true`
- Stare endpointy (`/cron/cron_campaigns`, `/cron/cron_products`) pozostaja w kodzie, ale nie sa docelowa sciezka wykonywania.
# 2026-02-20 - Poprawka niezaleznosci pipeline w `cron_universal`
## Problem
- `campaigns` mialo juz 100% (`done`) i `cron_universal` konczyl wykonanie, mimo ze `products` mial jeszcze zalegle daty.
## Zmienione pliki
- `autoload/controls/class.Cron.php`
- `cron_universal()` wybiera teraz aktywnego klienta niezaleznie dla obu pipeline:
- `campaigns`,
- `products`.
- Zakonczenie "wszyscy przetworzeni" następuje dopiero, gdy **oba** pipeline nie maja juz aktywnych pozycji.
- Dodane osobne liczenie pozostalych dat:
- `campaigns_remaining_dates`,
- `products_remaining_dates`.
- Statusy `done/pending` sa zapisywane osobno dla kazdego pipeline; produkty nie sa juz blokowane przez sam fakt, ze kampanie sa skonczone globalnie.
- Ujednolicenie trybu `client_id`:
- kampanie i produkty wykonują sie niezaleznie (w tym samym wywolaniu), a bledy sa laczone tylko w odpowiedzi.
# 2026-02-20 - Usuniecie `products_data`
## Zmienione pliki
- `migrations/017_drop_products_data.sql`
- Dodana migracja usuwajaca tabele `products_data`.
- `autoload/factory/class.Products.php`
- `get_product_data()` czyta dane tylko z `products`.
- `set_product_data()` zapisuje dane tylko do `products`.
- `autoload/controls/class.Cron.php`
- diagnostyka URL i wybieranie produktow bez URL opiera sie juz tylko o `products.product_url`.
- `docs/database.sql`
- usunieta definicja tabeli `products_data`.
- `migrations/demo_data.sql`
- usuniete operacje `INSERT/DELETE` na `products_data`,
- etykiety demo (`custom_label_4`) sa ustawiane bezposrednio w `products`.
## Gdzie to jest wykorzystywane
- Dane produktowe (`title`, `description`, `google_product_category`, `custom_label_3`, `custom_label_4`, `product_url`) sa trzymane tylko w `products`.
# 2026-02-20 - Ostatni krok `cron_universal`: URL z Merchant + alerty brakow
## Zmienione pliki
- `autoload/controls/class.Cron.php`
- Dodany helper `sync_products_urls_and_alerts_for_client()`.
- Na koncu przebiegu `cron_universal` (zarowno tryb automatyczny, jak i `client_id`) wykonywany jest krok:
- pobranie URL produktow z Google Merchant Center dla produktow bez URL,
- zapis URL do `products.product_url`.
- Gdy `offer_id` nie istnieje w Merchant Center, tworzony/aktualizowany jest alert w `campaign_alerts`:
- `alert_type = products_missing_in_merchant_center`,
- scope techniczny: `campaign_external_id = 0`, `ad_group_external_id = 0`,
- `meta_json` zawiera m.in. listy `missing_offer_ids` i `missing_product_ids`.
- Gdy w danym dniu brak brakujacych produktow, dzienny alert tego typu jest czyszczony.
- Do odpowiedzi cron dodane pola diagnostyczne:
- `merchant_urls_checked`,
- `merchant_urls_updated`,
- `merchant_missing_in_mc_count`,
- `merchant_missing_offer_ids`.
- `cron_universal` ma dodatkowy fallback niezalezny od pipeline `campaigns/products`:
- gdy oba pipeline sa zakonczone, ale sa jeszcze produkty bez URL, uruchamia sam krok Merchant URL + alerty (`merchant_only=1`),
- dopiero brak takich produktow daje komunikat "Wszyscy aktywni klienci zostali przetworzeni...".
- Krok Merchant URL nie jest wykonywany dla kazdego dnia okna; dziala jako osobny etap po zakonczeniu `campaigns/products`.
- Do zapytan do GMC trafiaja tylko produkty z `products.product_url IS NULL` i `merchant_url_not_found = 0`.
- Na jedno wywolanie wykonywana jest jedna paczka sprawdzen (limit z `config.php`: `cron_products_urls_limit_per_client`, ustawiony na `100`).
- Produkty, ktorych GMC nie zwraca (brak URL), sa oznaczane:
- `products.merchant_url_not_found = 1`,
- `products.merchant_url_last_check = NOW()`,
- dzieki temu nie sa wysylane ponownie w nieskonczonosc.
- Alert `products_missing_in_merchant_center` jest liczony na podstawie calej aktualnej puli `merchant_url_not_found = 1` (nie tylko bieżącej paczki), wiec nie znika przy `checked_products = 0`.
- Alerty sa per produkt (1 alert = 1 produkt):
- dla kazdego produktu bez URL i z `merchant_url_not_found = 1` tworzony jest osobny wpis w `campaign_alerts`,
- tresc alertu zawiera nazwe produktu (fallback: `name`, dalej `offer_id`) i `offer_id`,
- technicznie: `campaign_external_id = products.id`, co stabilizuje unikalnosc wpisu.
- `migrations/018_products_merchant_url_flags.sql`
- Dodane kolumny w `products`:
- `merchant_url_not_found` (TINYINT, domyslnie 0),
- `merchant_url_last_check` (DATETIME).
- Normalizacja: puste/sztuczne `product_url` (`'', '0', '-', 'null'`) ustawiane na `NULL`.
- `autoload/factory/class.Products.php`
- Przy zapisie `product_url`:
- ustawiany jest `merchant_url_last_check`,
- dla poprawnego URL resetowane jest `merchant_url_not_found = 0`.
# 2026-02-20 - Alerty na stronie `/products` dla klient + kampania
## Zmienione pliki
- `autoload/factory/class.Products.php`
- `get_scope_alerts()` nie wymaga juz wybranej grupy reklam:
- minimalny scope: `client_id + campaign_id`,
- filtr `ad_group_id` jest stosowany tylko opcjonalnie (gdy grupa jest wybrana).
- `templates/products/main_view.php`
- `load_scope_alerts()` pobiera alerty juz dla kombinacji `klient + kampania`.
- Sekcja alertow ma zaktualizowany opis: kampania + opcjonalna grupa reklam.
## Gdzie to jest wykorzystywane
- `/products`
- Panel alertow pod filtrami pokazuje alerty:
- dla calej kampanii (gdy grupa reklam nie jest wybrana),
- lub zawezone do konkretnej grupy (gdy grupa reklam jest wybrana).
# 2026-02-20 - Etykietowanie alertow Merchant (bez falszywej kampanii)
## Zmienione pliki
- `autoload/controls/class.Cron.php`
- Dla alertu `products_missing_in_merchant_center` nie jest juz zapisywany `product_id` w `campaign_external_id`.
- Pola scope kampanii/grupy sa zapisywane jako `0` (alert produktowy, bez przypisania do kampanii).
- `templates/campaign_alerts/main_view.php`
- Dla alertu `products_missing_in_merchant_center` tabela alertow pokazuje:
- Kampania: `Produkt (Merchant Center)`,
- Grupa reklam: `---`.
- Dla pozostalych alertow fallback `Kampania #...` / `Grupa reklam #...` dziala tylko dla dodatnich external_id; dla `0` pokazuje neutralne etykiety.
# 2026-02-20 - Powiazanie `campaign_alerts` z `products`
## Zmienione pliki
- `migrations/019_campaign_alerts_product_id.sql`
- Dodana kolumna `campaign_alerts.product_id` (NULL) oraz indeks `idx_alert_product`.
- `autoload/controls/class.Cron.php`
- Alerty `products_missing_in_merchant_center` zapisuja `product_id` w tabeli `campaign_alerts`.
- Dla zachowania unikalnosci dziennej per produkt, techniczny `campaign_external_id` pozostaje rowny `product_id`.
- `autoload/factory/class.CampaignAlerts.php`
- `get_alerts()` zwraca teraz rowniez pole `product_id`.
- `docs/database.sql`
- Dodana aktualna definicja tabeli `campaign_alerts` z kolumna `product_id`.
# 2026-02-20 - CRON produktow: `title` nie jest uzupelniany automatycznie
## Zmienione pliki
- `autoload/controls/class.Cron.php`
- W syncu produktow do tabeli `products` CRON nie zapisuje juz pola `title`.
- Dla nowych produktow CRON zapisuje tylko `name` (bez `title`).
- Dla istniejacych produktow usunieto automatyczne uzupelnianie pustego `title`.
## Gdzie to jest wykorzystywane
- `/cron/cron_universal`
- automatyczny import produktow nie nadpisuje ani nie uzupelnia `products.title`,
- `title` pozostaje polem do recznej edycji i wysylki do GMC.
# 2026-02-20 - Lista produktow z 0 wyswietlen (30 dni) na `/products`
## Zmienione pliki
- `autoload/factory/class.Products.php`
- Dodana metoda `get_products_without_impressions_30( $client_id, $campaign_id, $limit )`.
- Zwraca produkty z wybranej kampanii, ktore maja sume `impressions_30 = 0` na podstawie `products_aggregate`.
- Dodatkowy filtr `ad_group_id` (opcjonalny), aby lista byla zgodna z aktualnym filtrem grupy reklam na widoku.
- `autoload/controls/class.Products.php`
- Dodany endpoint `get_products_without_impressions_30()`.
- Zwraca JSON: `status`, `products[]`, `count` i przyjmuje opcjonalnie `ad_group_id`.
- `templates/products/main_view.php`
- Dodana sekcja nad tabela produktow:
- "Produkty do sprawdzenia (0 wyswietlen w ostatnich 30 dniach)".
- Sekcja pojawia sie dla wybranego `klient + kampania`.
- Lista odswieza sie przy zmianie klienta/kampanii/grupy oraz po zaladowaniu strony.
## Gdzie to jest wykorzystywane
- `/products`
- pomocnicza lista produktow potencjalnie nieistniejacych / wymagajacych weryfikacji (0 wyswietlen w 30 dni dla wybranej kampanii).
# 2026-02-20 - Ustawienia CRON: poprawka licznika klientow + usuniecie "Krok 1/Krok 2"
## Zmienione pliki
- `autoload/controls/class.Users.php`
- Licznik `Klienci z Google Ads ID` liczy teraz klientow z:
- `COALESCE(active, 0) = 1`,
- `TRIM(COALESCE(google_ads_customer_id, '')) <> ''`.
- Analogicznie poprawione filtry dla klientow Merchant i zapytan pomocniczych (wg `active`).
- Harmonogram krokow (`Krok 1`, `Krok 2`) w danych dashboardu CRON jest pusty.
- `templates/users/settings.php`
- Usunieta sekcja wizualna harmonogramu krokow CRON (`Krok 1` / `Krok 2`).
- Usunieta obsluga renderowania tej sekcji w JS odswiezajacym status CRON.
## Gdzie to jest wykorzystywane
- `/settings?settings_tab=cron`
- licznik klientow z Google Ads ID pokazuje poprawna wartosc na podstawie aktywnych klientow (`active = 1`),
- brak sekcji "Krok 1 / Krok 2".
# 2026-02-20 - `/products` czyta bezposrednio z `products_aggregate`
## Zmienione pliki
- `autoload/factory/class.Products.php`
- Zapytania dla listy produktow i licznikow zostaly przepiete z `products_temp` na `products_aggregate`:
- `get_products()`,
- `get_roas_bounds()`,
- `get_records_total_products()`,
- `get_product_full_context()`.
- Metryki all-time sa liczone z pol:
- `impressions_all_time`, `clicks_all_time`, `cost_all_time`, `conversions_all_time`, `conversion_value_all_time`.
- Metryki 30d sa czytane z:
- `impressions_30`, `clicks_30`.
## Gdzie to jest wykorzystywane
- `/products`
- tabela i liczniki nie zaleza juz od `products_temp`; biora dane bezposrednio z `products_aggregate`.
# 2026-02-20 - `custom_label_4` tylko z tabeli `products`
## Ustalenie
- Etykieta `custom_label_4` jest czytana i zapisywana z tabeli `products`.
- Agregaty (`products_aggregate`) nie sa zrodlem dla pola `custom_label_4`.