feat: Add Supplemental Feeds feature with UI and backend support
- Implemented the main view for Supplemental Feeds, displaying clients with Merchant Account IDs and their associated feed files. - Added styling for the feeds page and its components, including headers, empty states, and dropdown menus for syncing actions. - Created backend logic to generate supplemental feeds for clients, including file handling and data sanitization. - Integrated new routes and views for managing feeds, ensuring proper data retrieval and display. - Updated navigation to include the new Supplemental Feeds section. - Added necessary documentation for CRON job management related to feed generation.
This commit is contained in:
@@ -8,13 +8,16 @@ RewriteRule ^(.*)$ https://%1/$1 [R=301,L]
|
||||
RewriteCond %{SERVER_PORT} !=443
|
||||
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=permanent]
|
||||
|
||||
# Pliki TSV z feeds/ - serwuj statycznie
|
||||
RewriteCond %{REQUEST_URI} ^/feeds/.+\.tsv$ [NC]
|
||||
RewriteRule ^ - [L]
|
||||
|
||||
# Statyczne zasoby - pomijaj
|
||||
RewriteCond %{REQUEST_URI} ^/(libraries|layout|upload|temp)/ [NC]
|
||||
RewriteRule ^ - [L]
|
||||
|
||||
# Istniejące pliki/katalogi - pomijaj
|
||||
RewriteCond %{REQUEST_FILENAME} -f [OR]
|
||||
RewriteCond %{REQUEST_FILENAME} -d
|
||||
# Istniejące pliki - pomijaj (katalogi NIE, zeby /feeds trafialo do index.php)
|
||||
RewriteCond %{REQUEST_FILENAME} -f
|
||||
RewriteRule ^ - [L]
|
||||
|
||||
# Wszystko inne → index.php
|
||||
|
||||
299
.vscode/ftp-kr.sync.cache.json
vendored
299
.vscode/ftp-kr.sync.cache.json
vendored
@@ -89,20 +89,20 @@
|
||||
},
|
||||
"class.CampaignTerms.php": {
|
||||
"type": "-",
|
||||
"size": 21563,
|
||||
"lmtime": 1771441276763,
|
||||
"modified": true
|
||||
"size": 34205,
|
||||
"lmtime": 1771966770529,
|
||||
"modified": false
|
||||
},
|
||||
"class.Clients.php": {
|
||||
"type": "-",
|
||||
"size": 12653,
|
||||
"lmtime": 1771619242656,
|
||||
"modified": true
|
||||
"size": 13909,
|
||||
"lmtime": 1772117575587,
|
||||
"modified": false
|
||||
},
|
||||
"class.Cron.php": {
|
||||
"type": "-",
|
||||
"size": 182635,
|
||||
"lmtime": 1771851157171,
|
||||
"size": 185030,
|
||||
"lmtime": 1772117310268,
|
||||
"modified": false
|
||||
},
|
||||
"class.FacebookAds.php": {
|
||||
@@ -111,6 +111,12 @@
|
||||
"lmtime": 1771619366367,
|
||||
"modified": false
|
||||
},
|
||||
"class.Feeds.php": {
|
||||
"type": "-",
|
||||
"size": 1768,
|
||||
"lmtime": 1772117999910,
|
||||
"modified": false
|
||||
},
|
||||
"class.Logs.php": {
|
||||
"type": "-",
|
||||
"size": 4821,
|
||||
@@ -119,8 +125,8 @@
|
||||
},
|
||||
"class.Products.php": {
|
||||
"type": "-",
|
||||
"size": 45248,
|
||||
"lmtime": 1771757540415,
|
||||
"size": 47110,
|
||||
"lmtime": 1772116324270,
|
||||
"modified": false
|
||||
},
|
||||
"class.Site.php": {
|
||||
@@ -131,9 +137,9 @@
|
||||
},
|
||||
"class.Users.php": {
|
||||
"type": "-",
|
||||
"size": 20039,
|
||||
"lmtime": 1771617570626,
|
||||
"modified": true
|
||||
"size": 21234,
|
||||
"lmtime": 1772117354114,
|
||||
"modified": false
|
||||
},
|
||||
"class.XmlFiles.php": {
|
||||
"type": "-",
|
||||
@@ -151,8 +157,8 @@
|
||||
},
|
||||
"class.Campaigns.php": {
|
||||
"type": "-",
|
||||
"size": 11208,
|
||||
"lmtime": 1771755241179,
|
||||
"size": 13229,
|
||||
"lmtime": 1771966790175,
|
||||
"modified": false
|
||||
},
|
||||
"class.Clients.php": {
|
||||
@@ -181,9 +187,9 @@
|
||||
},
|
||||
"class.Products.php": {
|
||||
"type": "-",
|
||||
"size": 33709,
|
||||
"size": 34441,
|
||||
"lmtime": 1771757529304,
|
||||
"modified": false
|
||||
"modified": true
|
||||
},
|
||||
"class.Users.php": {
|
||||
"type": "-",
|
||||
@@ -196,6 +202,12 @@
|
||||
"size": 1453,
|
||||
"lmtime": 0,
|
||||
"modified": true
|
||||
},
|
||||
"class.CronQueue.php": {
|
||||
"type": "-",
|
||||
"size": 2858,
|
||||
"lmtime": 1772116301682,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
@@ -219,8 +231,8 @@
|
||||
},
|
||||
"class.GoogleAdsApi.php": {
|
||||
"type": "-",
|
||||
"size": 122118,
|
||||
"lmtime": 1771954979121,
|
||||
"size": 125232,
|
||||
"lmtime": 1771966884611,
|
||||
"modified": false
|
||||
},
|
||||
"class.OpenAiApi.php": {
|
||||
@@ -228,6 +240,12 @@
|
||||
"size": 29408,
|
||||
"lmtime": 1771171891986,
|
||||
"modified": true
|
||||
},
|
||||
"class.SupplementalFeed.php": {
|
||||
"type": "-",
|
||||
"size": 3860,
|
||||
"lmtime": 1772118229909,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
"view": {
|
||||
@@ -243,6 +261,12 @@
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"class.Logs.php": {
|
||||
"type": "-",
|
||||
"size": 211,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"class.Site.php": {
|
||||
"type": "-",
|
||||
"size": 649,
|
||||
@@ -254,6 +278,12 @@
|
||||
"size": 415,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"class.Feeds.php": {
|
||||
"type": "-",
|
||||
"size": 187,
|
||||
"lmtime": 1772117805998,
|
||||
"modified": false
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -266,10 +296,22 @@
|
||||
"cron.php": {
|
||||
"type": "-",
|
||||
"size": 1977,
|
||||
"lmtime": 0,
|
||||
"modified": true
|
||||
"lmtime": 1772116852209,
|
||||
"modified": false
|
||||
},
|
||||
"docs": {
|
||||
"class-methods.md": {
|
||||
"type": "-",
|
||||
"size": 58706,
|
||||
"lmtime": 1771954009821,
|
||||
"modified": false
|
||||
},
|
||||
"CRON_QUEUE.md": {
|
||||
"type": "-",
|
||||
"size": 1192,
|
||||
"lmtime": 1771955129000,
|
||||
"modified": false
|
||||
},
|
||||
"database.sql": {
|
||||
"type": "-",
|
||||
"size": 9123,
|
||||
@@ -294,10 +336,84 @@
|
||||
"lmtime": 1771496247126,
|
||||
"modified": true
|
||||
},
|
||||
"class-methods.md": {
|
||||
"todo.md": {
|
||||
"type": "-",
|
||||
"size": 58706,
|
||||
"lmtime": 1771954009821,
|
||||
"size": 657,
|
||||
"lmtime": 1772115859276,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
"feeds": {
|
||||
".gitkeep": {
|
||||
"type": "-",
|
||||
"size": 0,
|
||||
"lmtime": 1772116360653,
|
||||
"modified": false
|
||||
},
|
||||
".htaccess": {
|
||||
"type": "-",
|
||||
"size": 96,
|
||||
"lmtime": 1772116360000,
|
||||
"modified": false
|
||||
},
|
||||
"supplemental_10.tsv": {
|
||||
"type": "-",
|
||||
"size": 573,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"supplemental_1.tsv": {
|
||||
"type": "-",
|
||||
"size": 45,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"supplemental_2.tsv": {
|
||||
"type": "-",
|
||||
"size": 1331,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"supplemental_3.tsv": {
|
||||
"type": "-",
|
||||
"size": 377,
|
||||
"lmtime": 1772117280000,
|
||||
"modified": false
|
||||
},
|
||||
"supplemental_4.tsv": {
|
||||
"type": "-",
|
||||
"size": 385,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"supplemental_5.tsv": {
|
||||
"type": "-",
|
||||
"size": 436,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"supplemental_6.tsv": {
|
||||
"type": "-",
|
||||
"size": 45,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"supplemental_7.tsv": {
|
||||
"type": "-",
|
||||
"size": 297,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"supplemental_8.tsv": {
|
||||
"type": "-",
|
||||
"size": 449,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"supplemental_9.tsv": {
|
||||
"type": "-",
|
||||
"size": 45,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
@@ -309,15 +425,15 @@
|
||||
},
|
||||
".htaccess": {
|
||||
"type": "-",
|
||||
"size": 601,
|
||||
"lmtime": 0,
|
||||
"modified": true
|
||||
"size": 716,
|
||||
"lmtime": 1772117932512,
|
||||
"modified": false
|
||||
},
|
||||
"index.php": {
|
||||
"type": "-",
|
||||
"size": 4210,
|
||||
"lmtime": 1771198110809,
|
||||
"modified": true
|
||||
"size": 4267,
|
||||
"lmtime": 1772117836613,
|
||||
"modified": false
|
||||
},
|
||||
"install.php": {
|
||||
"type": "-",
|
||||
@@ -334,14 +450,14 @@
|
||||
},
|
||||
"style.css": {
|
||||
"type": "-",
|
||||
"size": 58603,
|
||||
"lmtime": 1771955179296,
|
||||
"size": 60877,
|
||||
"lmtime": 1772117956039,
|
||||
"modified": false
|
||||
},
|
||||
"style.css.map": {
|
||||
"type": "-",
|
||||
"size": 156429,
|
||||
"lmtime": 1771955179296,
|
||||
"size": 162739,
|
||||
"lmtime": 1772117956039,
|
||||
"modified": false
|
||||
},
|
||||
"style-old.css": {
|
||||
@@ -358,8 +474,8 @@
|
||||
},
|
||||
"style.scss": {
|
||||
"type": "-",
|
||||
"size": 68201,
|
||||
"lmtime": 1771955178718,
|
||||
"size": 70931,
|
||||
"lmtime": 1772117955600,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
@@ -573,16 +689,28 @@
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"024_campaign_ad_groups_status.sql": {
|
||||
"type": "-",
|
||||
"size": 131,
|
||||
"lmtime": 1771755182086,
|
||||
"modified": false
|
||||
},
|
||||
"025_campaign_keywords_status.sql": {
|
||||
"type": "-",
|
||||
"size": 115,
|
||||
"lmtime": 1771966738564,
|
||||
"modified": false
|
||||
},
|
||||
"demo_data.sql": {
|
||||
"type": "-",
|
||||
"size": 21146,
|
||||
"lmtime": 0,
|
||||
"modified": true
|
||||
},
|
||||
"024_campaign_ad_groups_status.sql": {
|
||||
"026_cron_queue.sql": {
|
||||
"type": "-",
|
||||
"size": 131,
|
||||
"lmtime": 1771755182086,
|
||||
"size": 1851,
|
||||
"lmtime": 1772116290944,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
@@ -593,6 +721,50 @@
|
||||
"modified": false
|
||||
},
|
||||
"templates": {
|
||||
"allegro": {},
|
||||
"campaign_alerts": {},
|
||||
"campaigns": {
|
||||
"main_view.php": {
|
||||
"type": "-",
|
||||
"size": 21607,
|
||||
"lmtime": 1771717394441,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
"campaign_terms": {
|
||||
"main_view.php": {
|
||||
"type": "-",
|
||||
"size": 98481,
|
||||
"lmtime": 1771967157143,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
"clients": {
|
||||
"main_view.php": {
|
||||
"type": "-",
|
||||
"size": 16977,
|
||||
"lmtime": 1772117630298,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
"cron": {},
|
||||
"facebook_ads": {
|
||||
"main_view.php": {
|
||||
"type": "-",
|
||||
"size": 10806,
|
||||
"lmtime": 1771717403326,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
"html": {},
|
||||
"logs": {
|
||||
"main_view.php": {
|
||||
"type": "-",
|
||||
"size": 5948,
|
||||
"lmtime": 1771717396318,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
"products": {
|
||||
"main_view.php": {
|
||||
"type": "-",
|
||||
@@ -610,31 +782,23 @@
|
||||
"site": {
|
||||
"layout-cron.php": {
|
||||
"type": "-",
|
||||
"size": 5764,
|
||||
"size": 5763,
|
||||
"lmtime": 1771367592957,
|
||||
"modified": false
|
||||
"modified": true
|
||||
},
|
||||
"layout-logged.php": {
|
||||
"type": "-",
|
||||
"size": 7746,
|
||||
"lmtime": 1771367592216,
|
||||
"size": 10230,
|
||||
"lmtime": 1772117855095,
|
||||
"modified": false
|
||||
},
|
||||
"layout-unlogged.php": {
|
||||
"type": "-",
|
||||
"size": 2024,
|
||||
"size": 2023,
|
||||
"lmtime": 0,
|
||||
"modified": true
|
||||
}
|
||||
},
|
||||
"campaigns": {
|
||||
"main_view.php": {
|
||||
"type": "-",
|
||||
"size": 21607,
|
||||
"lmtime": 1771717394441,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
"users": {
|
||||
"login-form.php": {
|
||||
"type": "-",
|
||||
@@ -649,35 +813,12 @@
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
"campaign_terms": {
|
||||
"xml_files": {},
|
||||
"feeds": {
|
||||
"main_view.php": {
|
||||
"type": "-",
|
||||
"size": 94858,
|
||||
"lmtime": 1771954474677,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
"clients": {
|
||||
"main_view.php": {
|
||||
"type": "-",
|
||||
"size": 9867,
|
||||
"lmtime": 1771494851645,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
"facebook_ads": {
|
||||
"main_view.php": {
|
||||
"type": "-",
|
||||
"size": 10806,
|
||||
"lmtime": 1771717403326,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
"logs": {
|
||||
"main_view.php": {
|
||||
"type": "-",
|
||||
"size": 5948,
|
||||
"lmtime": 1771717396318,
|
||||
"size": 3231,
|
||||
"lmtime": 1772117831024,
|
||||
"modified": false
|
||||
}
|
||||
}
|
||||
|
||||
46
AGENTS.md
46
AGENTS.md
@@ -1,46 +0,0 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Struktura projektu i organizacja modułów
|
||||
To repozytorium zawiera aplikację PHP w lekkiej architekturze MVC do zarządzania reklamami.
|
||||
|
||||
- Punkty wejścia: `index.php`, `ajax.php`, `api.php`, `cron.php`, `install.php`
|
||||
- Kontrolery: `autoload/controls/class.*.php` (`\controls`)
|
||||
- Fabryki (dostęp do danych): `autoload/factory/class.*.php` (`\factory`)
|
||||
- Warstwa widoku: `autoload/view/class.*.php` (`\view`)
|
||||
- Integracje zewnętrzne (Google Ads, OpenAI, Claude, Meta): `autoload/services/`
|
||||
- Szablony: `templates/<moduł>/`
|
||||
- Style: `layout/style.scss` -> `layout/style.css`
|
||||
- Zmiany schematu bazy: `migrations/*.sql` (numerowane, przyrostowe)
|
||||
- Dokumentacja techniczna i notatki: `docs/`
|
||||
|
||||
## Komendy build/test/development
|
||||
- `php -S localhost:8000` - uruchamia lokalny serwer PHP z katalogu repozytorium.
|
||||
- `php install.php` - wykonuje oczekujące migracje bazy danych.
|
||||
- `php install.php --with_demo` - wykonuje migracje i ładuje dane demonstracyjne.
|
||||
- `php install.php --force` - wymusza ponowne uruchomienie wszystkich migracji (ostrożnie).
|
||||
- `php -l ścieżka\do\pliku.php` - sprawdza składnię pliku PHP.
|
||||
|
||||
Projekt nie wymaga standardowo pipeline `Node`/`Composer` do uruchomienia. SCSS kompiluje się zwykle przez VS Code Live Sass Compiler podczas edycji `layout/style.scss`.
|
||||
|
||||
## Styl kodu i konwencje nazewnictwa
|
||||
- Zachowuj obecny wzorzec nazw plików/klas: `class.Moduł.php`, namespace małymi literami (np. `\controls`).
|
||||
- Nazwy klas: `PascalCase`; metody, zmienne i kolumny DB: `snake_case`.
|
||||
- Utrzymuj styl formatowania zgodny z edytowanym plikiem (historyczny styl nawiasów i wcięć).
|
||||
- Preferuj małe, statyczne metody w kontrolerach/fabrykach oraz jawne tablice danych do szablonów/JSON.
|
||||
|
||||
## Wytyczne testowania
|
||||
W repozytorium nie ma obecnie skonfigurowanego automatycznego zestawu testów (`phpunit` nie jest skonfigurowany).
|
||||
|
||||
- Wykonuj ręczne testy smoke dla zmienionych tras, np. `/campaigns/main_view`, `/users/settings`.
|
||||
- Dla zmian w API/AJAX sprawdzaj odpowiedzi JSON zarówno dla sukcesu, jak i błędów.
|
||||
- Przed PR uruchamiaj `php -l` dla każdego modyfikowanego pliku PHP.
|
||||
|
||||
## Zasady commitów i pull requestów
|
||||
- Stosuj krótkie, rzeczowe komunikaty commitów. Historia preferuje prefiksy Conventional Commits, szczególnie `feat:` (oraz `fix:`, `refactor:`, `chore:`).
|
||||
- Jeden commit powinien obejmować jeden spójny zakres zmian (schema, backend, UI).
|
||||
- PR powinien zawierać: cel zmian, listę zmienionych modułów/ścieżek, wpływ na migracje, kroki testowe i zrzuty ekranu dla zmian UI.
|
||||
- Dodawaj powiązane ID zadania/issue, jeśli istnieją.
|
||||
|
||||
## Bezpieczeństwo i konfiguracja
|
||||
- Traktuj `config.php` i klucze API jako dane wrażliwe; nie commituj prawdziwych sekretów.
|
||||
- Tymczasowe pliki debugowe z `tmp/` nie powinny trafiać do commitów produkcyjnych, chyba że jest to celowe.
|
||||
144
CLAUDE.md
144
CLAUDE.md
@@ -4,100 +4,114 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## Project Overview
|
||||
|
||||
adsPRO is a PHP SaaS application for managing Google Ads campaigns, products, and clients. It integrates with Google Ads API, OpenAI, and Claude AI to provide AI-powered ad optimization. UI language is Polish.
|
||||
adsPRO is a PHP web application for managing Google Ads, Facebook Ads, and Google Merchant Center campaigns. It tracks campaign performance (ROAS, budgets, conversions), manages product feeds, and provides AI-powered suggestions for product titles/descriptions. The UI is in Polish.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **PHP** (no framework, custom MVC-like architecture)
|
||||
- **MySQL** via Medoo ORM (`$mdb` global) and RedBeanPHP (`\R::`) in legacy endpoints
|
||||
- **Frontend**: jQuery, DataTables, Highcharts, Select2, Bootstrap 5, Font Awesome 6
|
||||
- **SCSS** for styling (`layout/style.scss` → `layout/style.css`)
|
||||
- **Google Ads REST API** v23 (direct cURL, no SDK)
|
||||
- **Facebook Ads Graph API** (direct cURL)
|
||||
- **AI services**: OpenAI, Claude, Gemini APIs for product text suggestions
|
||||
|
||||
## Architecture
|
||||
|
||||
Custom lightweight MVC framework with three layers:
|
||||
### Request Flow
|
||||
|
||||
- **Controllers** (`autoload/controls/class.*.php`, namespace `\controls`) - handle requests, return rendered views or JSON
|
||||
- **Factories** (`autoload/factory/class.*.php`, namespace `\factory`) - data access layer, all static methods, use Medoo ORM
|
||||
- **Views** (`autoload/view/class.*.php`, namespace `\view`) - compose templates with data
|
||||
- **Services** (`autoload/services/class.*.php`, namespace `\services`) - external API integrations (GoogleAdsApi, ClaudeApi, OpenAiApi)
|
||||
- **Templates** (`templates/`) - PHP files, variables accessed via `$this->varName`
|
||||
`index.php` is the single entry point. `.htaccess` rewrites all non-static requests to it. The router uses a `$route_aliases` map and fallback URL segment parsing (`/module/action`) to set `$_GET['module']` and `$_GET['action']`. `\controls\Site::route()` instantiates `\controls\{Module}` and calls the action method.
|
||||
|
||||
### Autoloading
|
||||
Other entry points:
|
||||
- `ajax.php` — AJAX requests (same autoloader, session-based auth with IP binding)
|
||||
- `api.php` — external API endpoints (domain tester, Open Page Rank)
|
||||
- `cron.php` — legacy cron (task reminders, recurring tasks)
|
||||
|
||||
PSR-0-like: `\controls\Campaigns` resolves to `autoload/controls/class.Campaigns.php`.
|
||||
### Autoloader & Namespace Convention
|
||||
|
||||
### Routing
|
||||
`spl_autoload_register` maps namespaces to filesystem paths:
|
||||
- `\controls\Campaigns` → `autoload/controls/class.Campaigns.php`
|
||||
- `\factory\Clients` → `autoload/factory/class.Clients.php`
|
||||
- `\services\GoogleAdsApi` → `autoload/services/class.GoogleAdsApi.php`
|
||||
- `\view\Site` → `autoload/view/class.Site.php`
|
||||
|
||||
Entry point: `index.php`. URL `/module/action/key=value` maps to `\controls\Module::action()`. Route aliases defined in `$route_aliases` array. Default route: `campaigns/main_view`.
|
||||
File naming: `class.{ClassName}.php`. Root-level classes (no namespace): `autoload/class.{Name}.php`.
|
||||
|
||||
### Database
|
||||
### Layer Responsibilities
|
||||
|
||||
MySQL via Medoo ORM (`global $mdb`). Common patterns:
|
||||
```php
|
||||
$mdb->select('table', '*', ['field' => $value]);
|
||||
$mdb->get('table', '*', ['id' => $id]);
|
||||
$mdb->insert('table', ['field' => $value]);
|
||||
$mdb->update('table', ['field' => $value], ['id' => $id]);
|
||||
$mdb->query($sql, [':param' => $value])->fetchAll(\PDO::FETCH_ASSOC);
|
||||
```
|
||||
- **`autoload/controls/`** — Controllers. Handle request params via `\S::get()`, call factory/services, return HTML (via `\Tpl::view()`) or `echo json_encode()` + `exit` for AJAX.
|
||||
- **`autoload/factory/`** — Data access layer. Static methods wrapping `$mdb` queries. Named after domain entities (Clients, Campaigns, Products, etc.).
|
||||
- **`autoload/services/`** — External API integrations (GoogleAdsApi, FacebookAdsApi, ClaudeApi, OpenAiApi, GeminiApi, SupplementalFeed).
|
||||
- **`autoload/view/`** — View helpers. Thin wrappers that call `\Tpl::view()` with template path and data.
|
||||
- **`templates/{module}/`** — PHP template files rendered by `Tpl::view()`. Templates access data via `$this->varName`.
|
||||
|
||||
### Key Utility Classes
|
||||
### Key Base Classes
|
||||
|
||||
- `\S` - static helpers: `\S::get('param')` (POST/GET), `\S::get_session()`, `\S::set_session()`, `\S::alert()`, `\S::send_email()`
|
||||
- `\Tpl::view('path/template', ['var' => $data])` - template rendering
|
||||
- `\Html::input()`, `\Html::select()`, etc. - form component builders
|
||||
- `\Cache::store()`, `\Cache::fetch()` - file-based caching
|
||||
- **`\S`** — Static utility: `get()` reads POST/GET, `get_session()`/`set_session()` for sessions, `alert()` for flash messages, `send_email()` for SMTP.
|
||||
- **`\Tpl`** — Template engine. `Tpl::view('module/template', [...])` renders `templates/module/template.php`. Looks in `templates_user/` first (override), then `templates/`.
|
||||
- **`\DbModel`** — Active Record base class. Subclasses set `$table`; provides `save()`, `delete()`.
|
||||
- **`\Cache`** — File-based cache in `temp/` directory. `Cache::store($key, $data, $ttl)` / `Cache::fetch($key)`.
|
||||
|
||||
### Authentication
|
||||
### Settings Storage
|
||||
|
||||
Session-based with cookie auto-login. User stored in `$_SESSION['user']`. Public paths whitelisted in `index.php`. IP validated per session.
|
||||
App settings (API keys, cron state, feature flags) are stored in a `settings` DB table as key-value pairs. Access via `\services\GoogleAdsApi::get_setting($key)` / `set_setting($key, $value)` — these are used globally, not just for Google Ads.
|
||||
|
||||
## Commands
|
||||
## Database Migrations
|
||||
|
||||
### Database Migrations
|
||||
Numbered SQL files in `migrations/` (e.g., `001_google_ads_settings.sql`). Run via:
|
||||
|
||||
```bash
|
||||
# Run migrations (via browser or CLI)
|
||||
php install.php
|
||||
|
||||
# With demo data
|
||||
php install.php --with_demo
|
||||
|
||||
# Force re-run all
|
||||
php install.php --force
|
||||
php install.php # apply pending migrations
|
||||
php install.php --force # re-apply all migrations
|
||||
php install.php --with_demo # include demo_data.sql
|
||||
```
|
||||
|
||||
Migration files in `migrations/` follow pattern `NNN_description.sql`. Tracked in `schema_migrations` table (idempotent).
|
||||
Tracks applied migrations in `schema_migrations` table.
|
||||
|
||||
### SASS Compilation
|
||||
## Cron System
|
||||
|
||||
VS Code Live Sass Compiler watches `layout/style.scss` and compiles to `layout/style.css` (compressed).
|
||||
Multiple cron endpoints called externally (e.g., every 1-5 minutes):
|
||||
|
||||
### Deployment
|
||||
| Endpoint | Purpose |
|
||||
|---|---|
|
||||
| `/cron.php` | Legacy: task reminders, recurring tasks |
|
||||
| `/cron/cron_universal` | Google Ads campaigns + products sync (backfill window) |
|
||||
| `/cron/cron_campaigns_product_alerts_merchant` | Product alerts from Merchant Center |
|
||||
| `/cron/cron_products_urls` | Fetch product URLs from Merchant Center |
|
||||
| `/cron/cron_facebook_ads` | Facebook Ads sync (30-day window) |
|
||||
|
||||
Files auto-upload to remote server via VS Code FTP-Kr extension (`.vscode/ftp-kr.json`). No build step required.
|
||||
Progress is tracked in `cron_sync_status` table with phases (pending → fetch → aggregate_30 → done). Dashboard at `/settings` shows real-time progress with ETA calculations.
|
||||
|
||||
## Code Conventions
|
||||
## Supplemental Feeds
|
||||
|
||||
- **PHP style**: Spaces inside parentheses `if ( $x )`, braces on new line, 2-space indent in templates, 4-space in classes
|
||||
- **Naming**: Classes PascalCase, methods/variables/columns snake_case, namespaces lowercase
|
||||
- **Static methods**: Controllers and factories use `static public function`
|
||||
- **JSON endpoints**: `echo json_encode([...]); exit;`
|
||||
- **Template variables**: passed as array to `\Tpl::view()`, accessed as `$this->varName`
|
||||
`\services\SupplementalFeed::generate_for_client($id)` generates TSV files in `feeds/` directory (e.g., `feeds/supplemental_1.tsv`) for Google Merchant Center. Managed via `/feeds` UI.
|
||||
|
||||
## Frontend Stack
|
||||
## Coding Conventions
|
||||
|
||||
jQuery 3.6, DataTables 2.1, Bootstrap 4, Select2 4.1, Highcharts, Font Awesome 6.5, jquery-confirm for modals. All loaded via CDN or from `libraries/`.
|
||||
- All classes use static methods extensively
|
||||
- Database access uses the global `$mdb` (Medoo instance) — never instantiate a new connection
|
||||
- AJAX endpoints: `echo json_encode([...])` then `exit`
|
||||
- Page actions: return HTML string from `\Tpl::view()`
|
||||
- POST/GET params: always use `\S::get('param_name')`, never `$_POST`/`$_GET` directly
|
||||
- Flash messages: `\S::alert('message')` then `header('Location: ...')` + `exit`
|
||||
- Timezone: `Europe/Warsaw`
|
||||
- Currency display: `\S::number_display($value)` formats as "1 234,56 zł"
|
||||
- All user-facing strings are in Polish
|
||||
|
||||
## Entry Points
|
||||
## Adding a New Module
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `index.php` | Main app (routing + auth) |
|
||||
| `ajax.php` | AJAX requests (authenticated) |
|
||||
| `api.php` | Public API |
|
||||
| `cron.php` | Background jobs |
|
||||
| `install.php` | Database migration runner |
|
||||
| `config.php` | DB and email credentials |
|
||||
1. Create controller: `autoload/controls/class.{ModuleName}.php` (namespace `controls`)
|
||||
2. Create factory: `autoload/factory/class.{ModuleName}.php` (namespace `factory`)
|
||||
3. Optionally create view helper: `autoload/view/class.{ModuleName}.php` (namespace `view`)
|
||||
4. Create templates: `templates/{module_name}/main_view.php` etc.
|
||||
5. Add route aliases in `index.php` `$route_aliases` array if clean URLs are needed
|
||||
6. Add sidebar link in `templates/site/layout-logged.php`
|
||||
|
||||
## API Settings Storage
|
||||
## Frontend Libraries (in `libraries/`)
|
||||
|
||||
Google Ads, Claude, and OpenAI API keys are stored in the `settings` table (key-value) and managed via the Settings page (`\controls\Users::settings`).
|
||||
|
||||
## Project Memory
|
||||
|
||||
Plik `docs/memory.md` zawiera trwala pamiec projektu - decyzje, ustalenia i wzorce potwierdzone w trakcie pracy. Czytaj go na poczatku sesji i aktualizuj gdy zapadna nowe istotne decyzje.
|
||||
- `medoo/` — Medoo PHP database framework (SQL builder)
|
||||
- `rb.php` — RedBeanPHP ORM (used in legacy `api.php`, `cron.php`)
|
||||
- `phpmailer/` — Email sending
|
||||
- `framework/` — jQuery UI, Bootstrap, various jQuery plugins
|
||||
- `functions.js` — Shared JS utilities
|
||||
- `adspro-dialog.js/.css` — Custom modal dialog component
|
||||
|
||||
4
MEMORY.md
Normal file
4
MEMORY.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# Google Ads Project Memory
|
||||
|
||||
## Zasady pracy
|
||||
- Zawsze pisz do mnie po polsku
|
||||
@@ -315,6 +315,20 @@ class Clients
|
||||
// Tabele Facebook Ads mogly nie byc jeszcze zainstalowane.
|
||||
}
|
||||
|
||||
// Supplemental Feed: binarny per klient (wygenerowany dzisiaj = 1/1, nie = 0/1)
|
||||
$feed_today = date( 'Y-m-d' );
|
||||
$feed_last_date = (string) \services\GoogleAdsApi::get_setting( 'cron_supplemental_feed_last_date' );
|
||||
$feed_done_today = ( $feed_last_date === $feed_today );
|
||||
|
||||
if ( !empty( $merchant_clients_ids ) )
|
||||
{
|
||||
foreach ( $merchant_clients_ids as $cid )
|
||||
{
|
||||
$cid = (int) $cid;
|
||||
$data[ $cid ]['feed'] = [ $feed_done_today ? 1 : 0, 1 ];
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode( [ 'status' => 'ok', 'data' => $data ] );
|
||||
exit;
|
||||
}
|
||||
@@ -396,6 +410,27 @@ class Clients
|
||||
\services\FacebookAdsApi::set_setting( 'cron_facebook_ads_force_client_id', (string) $id );
|
||||
\services\FacebookAdsApi::set_setting( 'cron_facebook_ads_force_requested_at', date( 'Y-m-d H:i:s' ) );
|
||||
}
|
||||
else if ( $pipeline === 'supplemental_feed' )
|
||||
{
|
||||
$has_merchant_id = trim( (string) ( $client['google_merchant_account_id'] ?? '' ) ) !== '';
|
||||
if ( !$has_merchant_id )
|
||||
{
|
||||
echo json_encode( [ 'success' => false, 'message' => 'Klient nie ma ustawionego Merchant Account ID.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
\services\SupplementalFeed::generate_for_client( $id );
|
||||
echo json_encode( [ 'success' => true, 'pipeline' => 'supplemental_feed', 'immediate' => true ] );
|
||||
exit;
|
||||
}
|
||||
catch ( \Throwable $e )
|
||||
{
|
||||
echo json_encode( [ 'success' => false, 'message' => 'Blad generowania feedu: ' . $e -> getMessage() ] );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Domyslny reset (wszystkie pipeline oparte o cron_sync_status).
|
||||
|
||||
@@ -246,6 +246,61 @@ class Cron
|
||||
] );
|
||||
}
|
||||
|
||||
// --- Supplemental Feed: generuj raz dziennie gdy pipeline'y ukonczone ---
|
||||
$today = date( 'Y-m-d' );
|
||||
$feed_last_date = (string) self::get_setting_value( 'cron_supplemental_feed_last_date', '' );
|
||||
|
||||
if ( $feed_last_date !== $today )
|
||||
{
|
||||
$feed_clients = $mdb -> query(
|
||||
"SELECT id, name, google_merchant_account_id FROM clients
|
||||
WHERE " . $clients_not_deleted_sql . "
|
||||
AND COALESCE(active, 0) = 1
|
||||
AND google_merchant_account_id IS NOT NULL
|
||||
AND google_merchant_account_id <> ''
|
||||
ORDER BY id ASC"
|
||||
) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
|
||||
$feed_generated = 0;
|
||||
$feed_products_total = 0;
|
||||
$feed_errors = [];
|
||||
$feed_details = [];
|
||||
|
||||
foreach ( $feed_clients as $fc )
|
||||
{
|
||||
try
|
||||
{
|
||||
$feed_result = \services\SupplementalFeed::generate_for_client( (int) $fc['id'] );
|
||||
$feed_generated++;
|
||||
$feed_products_total += (int) $feed_result['products_written'];
|
||||
$feed_details[] = [
|
||||
'client_id' => (int) $fc['id'],
|
||||
'client_name' => (string) $fc['name'],
|
||||
'products_written' => (int) $feed_result['products_written'],
|
||||
'file' => (string) $feed_result['file']
|
||||
];
|
||||
}
|
||||
catch ( \Throwable $e )
|
||||
{
|
||||
$feed_errors[] = 'Feed klient #' . $fc['id'] . ': ' . $e -> getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
self::set_setting_value( 'cron_supplemental_feed_last_date', $today );
|
||||
self::set_setting_value( 'cron_supplemental_feed_last_count', (string) $feed_products_total );
|
||||
self::set_setting_value( 'cron_supplemental_feed_files', (string) $feed_generated );
|
||||
|
||||
self::output_cron_response( [
|
||||
'result' => 'Supplemental feed: ' . $feed_generated . ' plikow, ' . $feed_products_total . ' produktow.',
|
||||
'supplemental_feed' => 1,
|
||||
'feed_generated' => $feed_generated,
|
||||
'feed_products_total' => $feed_products_total,
|
||||
'feed_details' => $feed_details,
|
||||
'active_date' => $sync_date,
|
||||
'errors' => $feed_errors
|
||||
] );
|
||||
}
|
||||
|
||||
self::output_cron_response( [
|
||||
'result' => 'Wszyscy aktywni klienci zostali przetworzeni dla calego okna dat.',
|
||||
'active_date' => $sync_date,
|
||||
@@ -4991,6 +5046,7 @@ class Cron
|
||||
return $label;
|
||||
}
|
||||
|
||||
|
||||
// ===========================
|
||||
// FRAZY - history 30
|
||||
// ===========================
|
||||
|
||||
57
autoload/controls/class.Feeds.php
Normal file
57
autoload/controls/class.Feeds.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
namespace controls;
|
||||
|
||||
class Feeds
|
||||
{
|
||||
static public function main_view()
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
// Sprawdz czy kolumna deleted istnieje
|
||||
$has_deleted = false;
|
||||
try
|
||||
{
|
||||
$stmt = $mdb -> pdo -> prepare( "SHOW COLUMNS FROM clients LIKE 'deleted'" );
|
||||
$stmt -> execute();
|
||||
$has_deleted = (bool) $stmt -> fetch( \PDO::FETCH_ASSOC );
|
||||
}
|
||||
catch ( \Throwable $e ) {}
|
||||
|
||||
$not_deleted_sql = $has_deleted ? 'COALESCE( deleted, 0 ) = 0' : '1=1';
|
||||
|
||||
$clients = $mdb -> query(
|
||||
"SELECT id, name, google_merchant_account_id
|
||||
FROM clients
|
||||
WHERE " . $not_deleted_sql . "
|
||||
AND google_merchant_account_id IS NOT NULL
|
||||
AND google_merchant_account_id <> ''
|
||||
ORDER BY name ASC"
|
||||
) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
|
||||
$base_url = ( !empty( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http' )
|
||||
. '://' . $_SERVER['HTTP_HOST'];
|
||||
|
||||
$feeds_dir = __DIR__ . '/../../feeds';
|
||||
$items = [];
|
||||
|
||||
foreach ( $clients as $client )
|
||||
{
|
||||
$filename = 'supplemental_' . $client['id'] . '.tsv';
|
||||
$file_path = $feeds_dir . '/' . $filename;
|
||||
$exists = file_exists( $file_path );
|
||||
|
||||
$items[] = [
|
||||
'client_id' => (int) $client['id'],
|
||||
'client_name' => (string) $client['name'],
|
||||
'merchant_id' => (string) $client['google_merchant_account_id'],
|
||||
'filename' => $filename,
|
||||
'url' => $base_url . '/feeds/' . $filename,
|
||||
'exists' => $exists,
|
||||
'modified_at' => $exists ? date( 'Y-m-d H:i:s', filemtime( $file_path ) ) : null,
|
||||
'size' => $exists ? filesize( $file_path ) : 0,
|
||||
];
|
||||
}
|
||||
|
||||
return \view\Feeds::main_view( $items );
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ class Products
|
||||
return [ 'status' => 'skipped', 'message' => 'Brak zmian do synchronizacji.' ];
|
||||
}
|
||||
|
||||
$supported_fields = [ 'title', 'description', 'google_product_category', 'custom_label_4' ];
|
||||
$supported_fields = [ 'title', 'description', 'google_product_category' ];
|
||||
$normalized_changes = [];
|
||||
|
||||
foreach ( $changed_fields as $field => $change )
|
||||
@@ -660,6 +660,22 @@ class Products
|
||||
exit;
|
||||
}
|
||||
|
||||
static public function delete_product_merchant_sync_log()
|
||||
{
|
||||
$log_id = (int) \S::get( 'log_id' );
|
||||
|
||||
if ( $log_id <= 0 )
|
||||
{
|
||||
echo json_encode( [ 'status' => 'error', 'message' => 'Nieprawidłowe ID logu.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
$result = \factory\Products::delete_product_merchant_sync_log( $log_id );
|
||||
|
||||
echo json_encode( [ 'status' => $result ? 'ok' : 'error' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
static public function ai_suggest()
|
||||
{
|
||||
$product_id = \S::get( 'product_id' );
|
||||
@@ -1055,17 +1071,9 @@ class Products
|
||||
{
|
||||
$product_id = \S::get( 'product_id' );
|
||||
$custom_label_4 = \S::get( 'custom_label_4' );
|
||||
$old_custom_label_4 = (string) \factory\Products::get_product_data( $product_id, 'custom_label_4' );
|
||||
|
||||
if ( \factory\Products::set_product_data( $product_id, 'custom_label_4', $custom_label_4 ) )
|
||||
{
|
||||
self::sync_product_fields_to_merchant( $product_id, [
|
||||
'custom_label_4' => [
|
||||
'old' => $old_custom_label_4,
|
||||
'new' => (string) $custom_label_4
|
||||
]
|
||||
], 'products_ui' );
|
||||
|
||||
\factory\Products::add_product_comment( $product_id, 'Zmiana etykiety 4 na: ' . $custom_label_4 );
|
||||
echo json_encode( [ 'status' => 'ok' ] );
|
||||
}
|
||||
@@ -1247,9 +1255,30 @@ class Products
|
||||
|
||||
\factory\Products::set_product_data( $product_id, 'product_url', $product_url ?: '' );
|
||||
|
||||
if ( !empty( $changed_for_merchant ) )
|
||||
foreach ( $changed_for_merchant as $field => $change )
|
||||
{
|
||||
self::sync_product_fields_to_merchant( $product_id, $changed_for_merchant, 'products_ui' );
|
||||
if ( trim( $change['old'] ) === trim( $change['new'] ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$log_old = $change['old'];
|
||||
$log_new = $change['new'];
|
||||
|
||||
if ( $field === 'description' )
|
||||
{
|
||||
$log_old = $log_old !== '' ? '(zmieniono)' : '';
|
||||
$log_new = '(zmieniono)';
|
||||
}
|
||||
|
||||
\factory\Products::add_product_merchant_sync_log( [
|
||||
'product_id' => $product_id,
|
||||
'field_name' => $field,
|
||||
'old_value' => $log_old,
|
||||
'new_value' => $log_new,
|
||||
'sync_status' => 'local',
|
||||
'sync_source' => 'products_ui'
|
||||
] );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -343,12 +343,29 @@ class Users
|
||||
$facebook_meta .= ', ' . $facebook_eta_meta;
|
||||
}
|
||||
|
||||
// --- Supplemental Feed --- (postep = wygenerowany dzisiaj, binarny jak Facebook)
|
||||
$feed_today = date( 'Y-m-d' );
|
||||
$feed_last_date = (string) \services\GoogleAdsApi::get_setting( 'cron_supplemental_feed_last_date' );
|
||||
$feed_done_today = ( $feed_last_date === $feed_today );
|
||||
|
||||
$feed_processed = $feed_done_today ? $merchant_clients_total : 0;
|
||||
$feed_total = max( 1, $merchant_clients_total );
|
||||
|
||||
$feed_last_count = (int) \services\GoogleAdsApi::get_setting( 'cron_supplemental_feed_last_count' );
|
||||
$feed_files = (int) \services\GoogleAdsApi::get_setting( 'cron_supplemental_feed_files' );
|
||||
|
||||
$feed_meta = 'Klienci z Merchant ID: ' . $merchant_clients_total
|
||||
. ', ostatnia generacja: ' . ( $feed_last_date ?: 'nigdy' )
|
||||
. ', pliki: ' . $feed_files
|
||||
. ', produkty: ' . $feed_last_count
|
||||
. ', dzisiaj: ' . ( $feed_done_today ? 'tak' : 'nie' );
|
||||
|
||||
$cron_schedule = [];
|
||||
|
||||
// --- Endpointy CRON ---
|
||||
$cron_endpoints = [
|
||||
[ 'name' => 'Legacy CRON', 'path' => '/cron.php', 'action' => 'cron_legacy', 'plan' => '' ],
|
||||
[ 'name' => 'Cron uniwersalny (Google Ads)', 'path' => '/cron/cron_universal', 'action' => 'cron_universal', 'plan' => 'Co 1 min: kampanie (wczoraj) + frazy/produkty (7 dni wstecz) + Merchant URL' ],
|
||||
[ 'name' => 'Cron uniwersalny (Google Ads)', 'path' => '/cron/cron_universal', 'action' => 'cron_universal', 'plan' => 'Co 1 min: kampanie (wczoraj) + frazy/produkty (7 dni wstecz) + Merchant URL + supplemental feed (raz dziennie)' ],
|
||||
[ 'name' => 'Cron alertow kampanii (Merchant)', 'path' => '/cron/cron_campaigns_product_alerts_merchant', 'action' => 'cron_campaigns_product_alerts_merchant', 'plan' => 'Co 15 min: alerty produktowe z Google Merchant' ],
|
||||
[ 'name' => 'Cron URL produktów (Merchant)', 'path' => '/cron/cron_products_urls', 'action' => 'cron_products_urls', 'plan' => '' ],
|
||||
[ 'name' => 'Cron Facebook Ads', 'path' => '/cron/cron_facebook_ads', 'action' => 'cron_facebook_ads', 'plan' => 'Co 5 min: 30 dni wstecz od wczoraj, blokada ponownego pobrania w tym samym dniu' ],
|
||||
@@ -399,6 +416,13 @@ class Users
|
||||
'percent' => self::progress_percent( $facebook_processed, $facebook_total ),
|
||||
'meta' => $facebook_meta
|
||||
],
|
||||
[
|
||||
'name' => 'Supplemental Feed',
|
||||
'processed' => $feed_processed,
|
||||
'total' => $feed_total,
|
||||
'percent' => self::progress_percent( $feed_processed, $feed_total ),
|
||||
'meta' => $feed_meta
|
||||
],
|
||||
],
|
||||
'schedule' => $cron_schedule,
|
||||
'urls' => $urls
|
||||
|
||||
@@ -597,6 +597,22 @@ class Products
|
||||
) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
}
|
||||
|
||||
static public function delete_product_merchant_sync_log( $log_id )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$log_id = (int) $log_id;
|
||||
|
||||
if ( $log_id <= 0 )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$pdo = $mdb -> delete( 'products_merchant_sync_log', [ 'id' => $log_id ] );
|
||||
|
||||
return $pdo -> rowCount() > 0;
|
||||
}
|
||||
|
||||
static public function get_product_ads_keyword_context( $product_id )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
119
autoload/services/class.SupplementalFeed.php
Normal file
119
autoload/services/class.SupplementalFeed.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
namespace services;
|
||||
|
||||
class SupplementalFeed
|
||||
{
|
||||
/**
|
||||
* Generuje supplemental feed TSV dla klienta.
|
||||
* Zwraca tablice ze statystykami: products_total, products_written, file.
|
||||
*/
|
||||
static public function generate_for_client( $client_id )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$client_id = (int) $client_id;
|
||||
|
||||
$products = $mdb -> query(
|
||||
"SELECT p.offer_id, p.title, p.description, p.google_product_category
|
||||
FROM products p
|
||||
WHERE p.client_id = :client_id
|
||||
AND p.offer_id IS NOT NULL
|
||||
AND p.offer_id <> ''
|
||||
AND ( p.title IS NOT NULL OR p.description IS NOT NULL OR p.google_product_category IS NOT NULL )",
|
||||
[ ':client_id' => $client_id ]
|
||||
) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
|
||||
$feeds_dir = __DIR__ . '/../../feeds';
|
||||
if ( !is_dir( $feeds_dir ) )
|
||||
{
|
||||
mkdir( $feeds_dir, 0755, true );
|
||||
}
|
||||
|
||||
$filename = 'supplemental_' . $client_id . '.tsv';
|
||||
$file_path = $feeds_dir . '/' . $filename;
|
||||
$fp = fopen( $file_path, 'w' );
|
||||
|
||||
if ( $fp === false )
|
||||
{
|
||||
throw new \RuntimeException( 'Nie mozna otworzyc pliku: ' . $file_path );
|
||||
}
|
||||
|
||||
fwrite( $fp, "id\ttitle\tdescription\tgoogle_product_category\n" );
|
||||
|
||||
$written = 0;
|
||||
foreach ( $products as $row )
|
||||
{
|
||||
$title = self::sanitize_for_tsv( $row['title'] ?? '' );
|
||||
$description = self::sanitize_for_tsv( $row['description'] ?? '' );
|
||||
$category = trim( (string) ( $row['google_product_category'] ?? '' ) );
|
||||
|
||||
if ( $title === '' && $description === '' && $category === '' )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
fwrite( $fp, implode( "\t", [
|
||||
$row['offer_id'],
|
||||
$title,
|
||||
$description,
|
||||
$category
|
||||
] ) . "\n" );
|
||||
|
||||
$written++;
|
||||
}
|
||||
|
||||
fclose( $fp );
|
||||
|
||||
return [
|
||||
'products_total' => count( $products ),
|
||||
'products_written' => $written,
|
||||
'file' => $filename
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Czyści HTML z tekstu na potrzeby TSV.
|
||||
* Zachowuje strukturę (akapity, listy) jako escaped \n.
|
||||
* GMC interpretuje literalny \n jako nowa linia w opisie.
|
||||
*/
|
||||
static private function sanitize_for_tsv( $value )
|
||||
{
|
||||
$value = (string) $value;
|
||||
if ( $value === '' ) return '';
|
||||
|
||||
// <li> → newline + punktor
|
||||
$value = preg_replace( '#<li[^>]*>#i', "\n- ", $value );
|
||||
|
||||
// Blokowe tagi zamykajace → newline (akapity, divy, listy, naglowki)
|
||||
$value = preg_replace( '#</(?:p|div|h[1-6]|tr|ul|ol)>#i', "\n", $value );
|
||||
$value = preg_replace( '#<br\s*/?>#i', "\n", $value );
|
||||
|
||||
// Usun pozostale tagi
|
||||
$value = strip_tags( $value );
|
||||
|
||||
// Dekoduj encje HTML
|
||||
$value = html_entity_decode( $value, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
|
||||
|
||||
// Taby → spacja (tab lamie TSV)
|
||||
$value = str_replace( "\t", ' ', $value );
|
||||
|
||||
// Normalizuj newline
|
||||
$value = str_replace( "\r\n", "\n", $value );
|
||||
$value = str_replace( "\r", "\n", $value );
|
||||
|
||||
// Wielokrotne puste linie → max 1
|
||||
$value = preg_replace( "/\n{3,}/", "\n\n", $value );
|
||||
|
||||
// Spacje wielokrotne w ramach linii → jedna
|
||||
$value = preg_replace( '/ {2,}/', ' ', $value );
|
||||
|
||||
// Trim kazdej linii
|
||||
$value = implode( "\n", array_map( 'trim', explode( "\n", $value ) ) );
|
||||
$value = trim( $value );
|
||||
|
||||
// Escape newline jako literalny \n dla TSV
|
||||
$value = str_replace( "\n", '\\n', $value );
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
12
autoload/view/class.Feeds.php
Normal file
12
autoload/view/class.Feeds.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace view;
|
||||
|
||||
class Feeds
|
||||
{
|
||||
static public function main_view( $items = [] )
|
||||
{
|
||||
return \Tpl::view( 'feeds/main_view', [
|
||||
'items' => $items,
|
||||
] );
|
||||
}
|
||||
}
|
||||
27
docs/CRON_QUEUE.md
Normal file
27
docs/CRON_QUEUE.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Kolejka Cron (DB)
|
||||
|
||||
## Cel
|
||||
- Zadania cron sa zapisywane w bazie (`cron_jobs`) i planowane przez harmonogram (`cron_schedules`).
|
||||
- Aktualnie domyslnie dziala zadanie: `product_links_health_check` (co 7 dni).
|
||||
|
||||
## Tabele
|
||||
- `cron_jobs` - kolejka zadan z priorytetem, retry i backoff.
|
||||
- `cron_schedules` - definicje cyklicznych zadan.
|
||||
- `product_link_alerts` - alerty dla nieistniejacych powiazan produktu.
|
||||
|
||||
## Uruchamianie
|
||||
- Jednorazowo: `php bin/cron.php`
|
||||
- Z limitem batcha: `php bin/cron.php --limit=50`
|
||||
- Z panelu (`Ustawienia -> Cron`) mozna wlaczyc uruchamianie workera podczas requestow HTTP.
|
||||
|
||||
## Zalecenie dla systemowego crona
|
||||
- Uruchamiaj `php /sciezka/do/orderPRO/bin/cron.php` co 1-5 minut.
|
||||
- Harmonogram 7-dniowy jest liczony przez `cron_schedules.next_run_at`, wiec sam worker powinien byc uruchamiany regularnie.
|
||||
|
||||
## Jak dziala `product_links_health_check`
|
||||
1. Pobiera aktywne integracje `shoppro` z API key.
|
||||
2. Odswieza cache ofert (`channel_offers`) przez import API.
|
||||
3. Czyści nieaktualne rekordy ofert z cache.
|
||||
4. Weryfikuje aktywne powiazania `product_channel_map`.
|
||||
5. Dla brakujacych powiazan ustawia alert `missing_remote_link`.
|
||||
6. Dla przywroconych powiazan zamyka alert.
|
||||
2
docs/todo.md
Normal file
2
docs/todo.md
Normal file
@@ -0,0 +1,2 @@
|
||||
1. Zmiany w tytuła, opisie i kategorii google jednak nie powinn być synchronizowane przez API z Google Merchant Center (także to jest do wyłączenia) a powinny być jedynie zapisywanie w tabeli products, następnie trzeba stworzyć zadanie CRON, który będzie dla każdego z Klientów tworzył suplemental feed. Zadanie ma to się uruchamiać co 4h.
|
||||
2. Cron ma działać na podstawie bazy danych. Czyli musi być jakieś narzędzie, które będzie dodawało zadania do bazy danych w odpowiednich odstępach czasu. Schemat działania jest mniej więcej opisany w pliku CRON_QUEUE.md (to jest plik z innego projektu więc wymaga dopasownia klas i metod)
|
||||
0
feeds/.gitkeep
Normal file
0
feeds/.gitkeep
Normal file
5
feeds/.htaccess
Normal file
5
feeds/.htaccess
Normal file
@@ -0,0 +1,5 @@
|
||||
Allow from all
|
||||
|
||||
<IfModule mod_mime.c>
|
||||
AddType text/tab-separated-values .tsv
|
||||
</IfModule>
|
||||
1
feeds/supplemental_1.tsv
Normal file
1
feeds/supplemental_1.tsv
Normal file
@@ -0,0 +1 @@
|
||||
id title description google_product_category
|
||||
|
6
feeds/supplemental_10.tsv
Normal file
6
feeds/supplemental_10.tsv
Normal file
@@ -0,0 +1,6 @@
|
||||
id title description google_product_category
|
||||
shopify_pl_8476788261204_46803770409300 GS żarówka LED E27 7W ciepłobiała zestaw 10 sztuk
|
||||
shopify_pl_8454714753364_46732986483028 GS żarówka LED 7W E27 barwa ciepła biała
|
||||
shopify_pl_8454759842132_46733043466580 Kobi Light żarówka LED E27 7W barwa neutralna biała 4000K 600lm 2425
|
||||
shopify_pl_8459153473876_46746440270164 Kobi Light żarówka LED świecowa E14 1,5W 150 lm zimnobiała 6000K 2425
|
||||
shopify_pl_8476790358356_46803775684948 Kobi Light żarówki LED E27 7W 4000K neutralne białe zestaw 10 sztuk 2425
|
||||
|
13
feeds/supplemental_2.tsv
Normal file
13
feeds/supplemental_2.tsv
Normal file
@@ -0,0 +1,13 @@
|
||||
id title description google_product_category
|
||||
1778 Pudełko na pieniądze z życzeniami na Chrzest Święty Dłonie UV drewniane 14,5 × 14,5 cm
|
||||
944 Drewniana zakładka do książki z grawerem Piesek, sklejka brzozowa 18 x 5 cm
|
||||
1536 Tabliczka do zdjęć serduszko z napisem Miłość naszego życia, sklejka brzozowa 11 x 10 cm
|
||||
2016 Pudełko na pieniądze z życzeniami na Chrzest Święty z nadrukiem UV, aniołek dziewczynka, drewniane białe 14,5 x 14,5 cm
|
||||
1928 Magnes podziękowanie na wieczór panieński na plastrze brzozy Szampan z woreczkiem z organzy 12 x 15 cm
|
||||
1720 Prośba o zostanie Matką Chrzestną z akrylu lustrzanego złotego Dłonie 14 x 10 cm 2 mm
|
||||
1345 Zawieszka do wózka, przypinka z agrafką i czerwoną kokardką, różowo-fioletowa, personalizowana drewniana
|
||||
1841 Magnes podziękowanie dla gości na chrzest Stopa ze sklejki brzozowej 12 x 8 cm, woreczek z organzy 12 x 15 cm
|
||||
1961 Akrylowe podziękowanie na chrzest dla Ojca Chrzestnego ze zdjęciem, wzór 1, biały tekst
|
||||
1470 Drewniana prośba o bycie ojcem chrzestnym Misiek, serduszko czerwone 12 x 12 cm, sklejka brzozowa 3 mm
|
||||
1719 Prośba o zostanie Ojcem Chrzestnym z akrylu lustrzanego złotego 14 x 10 cm, wzór Dłonie
|
||||
1599 Pudełko na pieniądze z życzeniami na Chrzest Święty, drewniane, Gołąbek 14,5 x 14,5 x 2,5 cm
|
||||
|
5
feeds/supplemental_3.tsv
Normal file
5
feeds/supplemental_3.tsv
Normal file
@@ -0,0 +1,5 @@
|
||||
id title description google_product_category
|
||||
4302 Gen Factor Personal Care Cinnamic krem przeciwstarzeniowy i depigmentacyjny 6262
|
||||
7792 Aurumaris serum do twarzy z mikroigłami Total Remake Smart Serum 30ml 6262
|
||||
3510 Gen Factor Przeciwzmarszczkowy krem do twarzy z proteinami Personal Care Red 6262
|
||||
114 Podopharm Onygen krem na onycholizę do regeneracji paznokci 20ml 478
|
||||
|
4
feeds/supplemental_4.tsv
Normal file
4
feeds/supplemental_4.tsv
Normal file
@@ -0,0 +1,4 @@
|
||||
id title description google_product_category
|
||||
shopify_zz_15830679224669_62928228286813 IBRA Makeup Kępki rzęs ślubne Bride Style MIX 8-12mm profil C 2761
|
||||
shopify_zz_15440624222557_57759508234589 IBRA Makeup Kępki rzęs Insta Style MIX długości 10-14 mm skręt C 2761
|
||||
shopify_zz_15085459505501_56180531462493 Ibra Makeup Kępki rzęs syntetyczne 12mm skręt C Extra Double 2761
|
||||
|
6
feeds/supplemental_5.tsv
Normal file
6
feeds/supplemental_5.tsv
Normal file
@@ -0,0 +1,6 @@
|
||||
id title description google_product_category
|
||||
1123 AAA cążki do skórek IL-10 5 mm ze stali japońskiej
|
||||
3410 Ibra Makeup błyszczący cień do powiek Brown Sugar różowe złoto i brąz
|
||||
285 Karaja podkład liftingujący Skin Velvet nr 6 do cery suchej i mieszanej
|
||||
3274 IBRA Makeup wygładzający puder transparentny No More Pore Pro Makeup Academy
|
||||
120 Karaja wodoodporna mikro kredka do brwi Micro Browliner nr 1 jasny brąz
|
||||
|
1
feeds/supplemental_6.tsv
Normal file
1
feeds/supplemental_6.tsv
Normal file
@@ -0,0 +1 @@
|
||||
id title description google_product_category
|
||||
|
5
feeds/supplemental_7.tsv
Normal file
5
feeds/supplemental_7.tsv
Normal file
@@ -0,0 +1,5 @@
|
||||
id title description google_product_category
|
||||
6358 Elpar przewód ziemny YKY 5x16 RE drut 1KV
|
||||
641 NKT Przewód elektryczny YKY 5x10 mm² ziemny drut miedziany
|
||||
6334 Elpar przewód bezhalogenowy N2XH-J 3x2,5 RE 0,6/1kV B2ca
|
||||
5279 Elpar przewód elektryczny OWY (H05VV-F) 5x4 mm² linka czarny
|
||||
|
6
feeds/supplemental_8.tsv
Normal file
6
feeds/supplemental_8.tsv
Normal file
@@ -0,0 +1,6 @@
|
||||
id title description google_product_category
|
||||
808 Ssawka do odkurzacza mała z welurem 32 mm zamiennik do Zelmer
|
||||
1 Worki do odkurzacza Zelmer Meteor, Admiral 1010, 1020, 1030 papierowe 5 szt. z filtrami
|
||||
387 Zelmer zestaw trzepaków do miksera ręcznego do modeli 181, 281, 371, 381
|
||||
397 Zelmer sprzęgło napędowe ślimaka 86.1203 do maszynki do mielenia mięsa
|
||||
21 Worki papierowe do odkurzacza Amica Sumam, Nortes, Universis, Beris 5 szt.
|
||||
|
1
feeds/supplemental_9.tsv
Normal file
1
feeds/supplemental_9.tsv
Normal file
@@ -0,0 +1 @@
|
||||
id title description google_product_category
|
||||
|
@@ -51,6 +51,7 @@ $route_aliases = [
|
||||
'settings/save_ai_prompts' => ['users', 'settings_save_ai_prompts'],
|
||||
'products/ai_suggest' => ['products', 'ai_suggest'],
|
||||
'clients/save' => ['clients', 'save'],
|
||||
'feeds' => ['feeds', 'main_view'],
|
||||
'logs' => ['logs', 'main_view'],
|
||||
'logs/get_data_table' => ['logs', 'get_logs_data_table'],
|
||||
'logs/get_detail' => ['logs', 'get_detail'],
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1194,6 +1194,72 @@ table {
|
||||
}
|
||||
|
||||
// --- Clients page ---
|
||||
// --- Feeds page ---
|
||||
.feeds-page {
|
||||
.feeds-header {
|
||||
margin-bottom: 20px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: $cTextDark;
|
||||
|
||||
i {
|
||||
color: $cPrimary;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state-box {
|
||||
text-align: center;
|
||||
padding: 50px 20px;
|
||||
color: #A0AEC0;
|
||||
|
||||
i {
|
||||
font-size: 40px;
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.feed-link {
|
||||
color: $cPrimary;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 11px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-icon-copy {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #8899A6;
|
||||
cursor: pointer;
|
||||
padding: 2px 6px;
|
||||
font-size: 13px;
|
||||
margin-left: 6px;
|
||||
|
||||
&:hover {
|
||||
color: $cPrimary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.clients-page {
|
||||
.clients-header {
|
||||
display: flex;
|
||||
@@ -1351,6 +1417,59 @@ table {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sync-dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
.sync-dropdown-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 100%;
|
||||
z-index: 100;
|
||||
background: #fff;
|
||||
border: 1px solid #E2E8F0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||
padding: 4px 0;
|
||||
min-width: 200px;
|
||||
white-space: nowrap;
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 8px 14px;
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 13px;
|
||||
color: $cTextDark;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
background: #F7FAFC;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
i {
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
color: #8899A6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-open .sync-dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 50px 20px !important;
|
||||
|
||||
@@ -68,24 +68,34 @@
|
||||
<button type="button" class="btn-icon btn-icon-sync" onclick="toggleClientActive(<?= $client['id']; ?>, this)" title="<?= $is_client_active ? 'Dezaktywuj klienta' : 'Aktywuj klienta'; ?>">
|
||||
<i class="fa-solid <?= $is_client_active ? 'fa-toggle-on' : 'fa-toggle-off'; ?>"></i>
|
||||
</button>
|
||||
<?php if ( $client['google_ads_customer_id'] ): ?>
|
||||
<button type="button" class="btn-icon btn-icon-sync client-sync-action" onclick="syncClient(<?= $client['id']; ?>, 'campaigns', this)" title="Odswiez kampanie" <?= $is_client_active ? '' : 'disabled'; ?>>
|
||||
<i class="fa-solid fa-bullhorn"></i>
|
||||
<div class="sync-dropdown" data-client-id="<?= $client['id']; ?>">
|
||||
<button type="button" class="btn-icon btn-icon-sync client-sync-action" onclick="toggleSyncMenu(this)" title="Odswiez dane" <?= $is_client_active ? '' : 'disabled'; ?>>
|
||||
<i class="fa-solid fa-arrows-rotate"></i>
|
||||
</button>
|
||||
<button type="button" class="btn-icon btn-icon-sync client-sync-action" onclick="syncClient(<?= $client['id']; ?>, 'products', this)" title="Odswiez produkty" <?= $is_client_active ? '' : 'disabled'; ?>>
|
||||
<i class="fa-solid fa-box-open"></i>
|
||||
</button>
|
||||
<?php if ( !empty( $client['google_merchant_account_id'] ) ): ?>
|
||||
<button type="button" class="btn-icon btn-icon-sync client-sync-action" onclick="syncClient(<?= $client['id']; ?>, 'campaigns_product_alerts_merchant', this)" title="Odswiez walidacje Merchant" <?= $is_client_active ? '' : 'disabled'; ?>>
|
||||
<i class="fa-solid fa-store"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
<?php if ( !empty( $client['facebook_ads_account_id'] ) ): ?>
|
||||
<button type="button" class="btn-icon btn-icon-sync client-sync-action" onclick="syncClient(<?= $client['id']; ?>, 'facebook_ads', this)" title="Odswiez Facebook Ads" <?= $is_client_active ? '' : 'disabled'; ?>>
|
||||
<i class="fa-brands fa-facebook-f"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<div class="sync-dropdown-menu">
|
||||
<?php if ( $client['google_ads_customer_id'] ): ?>
|
||||
<button type="button" onclick="syncFromMenu(<?= $client['id']; ?>, 'campaigns', this)">
|
||||
<i class="fa-solid fa-bullhorn"></i> Kampanie
|
||||
</button>
|
||||
<button type="button" onclick="syncFromMenu(<?= $client['id']; ?>, 'products', this)">
|
||||
<i class="fa-solid fa-box-open"></i> Produkty
|
||||
</button>
|
||||
<?php if ( !empty( $client['google_merchant_account_id'] ) ): ?>
|
||||
<button type="button" onclick="syncFromMenu(<?= $client['id']; ?>, 'campaigns_product_alerts_merchant', this)">
|
||||
<i class="fa-solid fa-store"></i> Walidacja Merchant
|
||||
</button>
|
||||
<button type="button" onclick="syncFromMenu(<?= $client['id']; ?>, 'supplemental_feed', this)">
|
||||
<i class="fa-solid fa-file-csv"></i> Supplemental Feed
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
<?php if ( !empty( $client['facebook_ads_account_id'] ) ): ?>
|
||||
<button type="button" onclick="syncFromMenu(<?= $client['id']; ?>, 'facebook_ads', this)">
|
||||
<i class="fa-brands fa-facebook-f"></i> Facebook Ads
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn-icon btn-icon-edit" onclick="editClient(<?= $client['id']; ?>)" title="Edytuj">
|
||||
<i class="fa-solid fa-pen"></i>
|
||||
</button>
|
||||
@@ -253,8 +263,25 @@ function toggleClientActive( id, btn )
|
||||
} );
|
||||
}
|
||||
|
||||
function syncClient( id, pipeline, btn )
|
||||
// --- Sync dropdown menu ---
|
||||
function toggleSyncMenu( btn )
|
||||
{
|
||||
var $dropdown = $( btn ).closest( '.sync-dropdown' );
|
||||
var wasOpen = $dropdown.hasClass( 'is-open' );
|
||||
$( '.sync-dropdown.is-open' ).removeClass( 'is-open' );
|
||||
if ( !wasOpen ) $dropdown.addClass( 'is-open' );
|
||||
}
|
||||
|
||||
$( document ).on( 'click', function( e ) {
|
||||
if ( !$( e.target ).closest( '.sync-dropdown' ).length )
|
||||
{
|
||||
$( '.sync-dropdown.is-open' ).removeClass( 'is-open' );
|
||||
}
|
||||
});
|
||||
|
||||
function syncFromMenu( id, pipeline, btn )
|
||||
{
|
||||
$( '.sync-dropdown.is-open' ).removeClass( 'is-open' );
|
||||
var $btn = $( btn );
|
||||
var $icon = $btn.find( 'i' );
|
||||
var origClass = $icon.attr( 'class' );
|
||||
@@ -266,6 +293,7 @@ function syncClient( id, pipeline, btn )
|
||||
campaigns: 'kampanii',
|
||||
products: 'produktow',
|
||||
campaigns_product_alerts_merchant: 'walidacji Merchant',
|
||||
supplemental_feed: 'supplemental feed',
|
||||
facebook_ads: 'Facebook Ads'
|
||||
};
|
||||
|
||||
@@ -278,21 +306,18 @@ function syncClient( id, pipeline, btn )
|
||||
|
||||
if ( data.success )
|
||||
{
|
||||
$btn.addClass( 'is-queued' );
|
||||
|
||||
var cron_hint = pipeline === 'facebook_ads'
|
||||
? ' Dane zostana pobrane przy najblizszym uruchomieniu /cron/cron_facebook_ads.'
|
||||
: ' Dane zostana pobrane przy najblizszym uruchomieniu CRON.';
|
||||
var refresh_hint = pipeline === 'facebook_ads'
|
||||
? ' Wymuszenie Facebook Ads nadpisuje dane dla okresu z config.php i pobiera tylko aktywne kampanie/zestawy/reklamy.'
|
||||
: '';
|
||||
var msg = data.immediate
|
||||
? 'Supplemental feed wygenerowany pomyslnie.'
|
||||
: 'Synchronizacja ' + labels[ pipeline ] + ' zostala zakolejkowana. Dane zostana pobrane przy najblizszym uruchomieniu CRON.';
|
||||
|
||||
$.alert({
|
||||
title: 'Zakolejkowano',
|
||||
content: 'Synchronizacja ' + labels[ pipeline ] + ' zostala zakolejkowana.' + cron_hint + refresh_hint,
|
||||
title: data.immediate ? 'Gotowe' : 'Zakolejkowano',
|
||||
content: msg,
|
||||
type: 'green',
|
||||
autoClose: 'ok|3000'
|
||||
});
|
||||
|
||||
loadSyncStatus();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -390,6 +415,7 @@ function loadSyncStatus()
|
||||
if ( info.campaigns ) html += renderSyncBar( 'K:', info.campaigns[0], info.campaigns[1] );
|
||||
if ( info.products ) html += renderSyncBar( 'P:', info.products[0], info.products[1] );
|
||||
if ( info.merchant ) html += renderSyncBar( 'M:', info.merchant[0], info.merchant[1] );
|
||||
if ( info.feed ) html += renderSyncBar( 'F:', info.feed[0], info.feed[1] );
|
||||
if ( info.facebook_ads ) html += renderSyncBar( 'FB:', info.facebook_ads[0], info.facebook_ads[1] );
|
||||
html += '</div>';
|
||||
|
||||
|
||||
85
templates/feeds/main_view.php
Normal file
85
templates/feeds/main_view.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<div class="feeds-page">
|
||||
<div class="feeds-header">
|
||||
<h2><i class="fa-solid fa-file-csv"></i> Supplemental Feeds</h2>
|
||||
</div>
|
||||
|
||||
<?php if ( empty( $this -> items ) ): ?>
|
||||
<div class="empty-state-box">
|
||||
<i class="fa-solid fa-file-csv"></i>
|
||||
<p>Brak klientow z ustawionym Merchant Account ID.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="feeds-table-wrap">
|
||||
<table class="table" id="feeds-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 60px;">#ID</th>
|
||||
<th>Klient</th>
|
||||
<th>Merchant ID</th>
|
||||
<th>Plik</th>
|
||||
<th style="width: 130px;">Status</th>
|
||||
<th style="width: 160px;">Ostatnia aktualizacja</th>
|
||||
<th style="width: 90px;">Rozmiar</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ( $this -> items as $item ): ?>
|
||||
<tr>
|
||||
<td><?= $item['client_id']; ?></td>
|
||||
<td><?= htmlspecialchars( $item['client_name'] ); ?></td>
|
||||
<td><span class="badge-id"><?= htmlspecialchars( $item['merchant_id'] ); ?></span></td>
|
||||
<td>
|
||||
<?php if ( $item['exists'] ): ?>
|
||||
<a href="<?= htmlspecialchars( $item['url'] ); ?>" target="_blank" class="feed-link">
|
||||
<i class="fa-solid fa-arrow-up-right-from-square"></i>
|
||||
<?= htmlspecialchars( $item['filename'] ); ?>
|
||||
</a>
|
||||
<button type="button" class="btn-icon btn-icon-copy" onclick="copyFeedUrl(this, '<?= htmlspecialchars( $item['url'] ); ?>')" title="Kopiuj URL">
|
||||
<i class="fa-regular fa-copy"></i>
|
||||
</button>
|
||||
<?php else: ?>
|
||||
<span class="text-muted"><?= htmlspecialchars( $item['filename'] ); ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ( $item['exists'] ): ?>
|
||||
<span class="badge badge-success">Wygenerowany</span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-secondary">Oczekuje</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ( $item['modified_at'] ): ?>
|
||||
<?= $item['modified_at']; ?>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">—</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ( $item['exists'] ): ?>
|
||||
<?= number_format( $item['size'] / 1024, 1 ); ?> KB
|
||||
<?php else: ?>
|
||||
<span class="text-muted">—</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
function copyFeedUrl( btn, url )
|
||||
{
|
||||
navigator.clipboard.writeText( url ).then( function() {
|
||||
var $btn = $( btn );
|
||||
var $icon = $btn.find( 'i' );
|
||||
$icon.attr( 'class', 'fa-solid fa-check' );
|
||||
setTimeout( function() {
|
||||
$icon.attr( 'class', 'fa-regular fa-copy' );
|
||||
}, 1500 );
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -1246,7 +1246,7 @@ $( function()
|
||||
var product_id = $( this ).attr( 'product_id' );
|
||||
|
||||
$.confirm({
|
||||
title: 'Logi synchronizacji Merchant (produkt #' + product_id + ')',
|
||||
title: 'Logi zmian produktu',
|
||||
content: '<div class="merchant-logs-wrap" style="max-height:460px;overflow:auto">Ładowanie logów...</div>',
|
||||
useBootstrap: false,
|
||||
boxWidth: '1100px',
|
||||
@@ -1262,74 +1262,114 @@ $( function()
|
||||
var jc = this;
|
||||
var $wrap = jc.$content.find( '.merchant-logs-wrap' );
|
||||
|
||||
$.ajax({
|
||||
url: '/products/get_product_merchant_sync_logs/',
|
||||
type: 'POST',
|
||||
data: { product_id: product_id, limit: 100 },
|
||||
success: function( response )
|
||||
{
|
||||
var data;
|
||||
function load_logs()
|
||||
{
|
||||
$wrap.html( 'Ładowanie logów...' );
|
||||
|
||||
try
|
||||
$.ajax({
|
||||
url: '/products/get_product_merchant_sync_logs/',
|
||||
type: 'POST',
|
||||
data: { product_id: product_id, limit: 100 },
|
||||
success: function( response )
|
||||
{
|
||||
data = JSON.parse( response );
|
||||
}
|
||||
catch ( err )
|
||||
var data;
|
||||
|
||||
try
|
||||
{
|
||||
data = JSON.parse( response );
|
||||
}
|
||||
catch ( err )
|
||||
{
|
||||
$wrap.html( '<div class="text-danger">Nie udało się odczytać odpowiedzi serwera.</div>' );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( data.status !== 'ok' )
|
||||
{
|
||||
$wrap.html( '<div class="text-danger">' + escape_html( data.message || 'Błąd pobierania logów.' ) + '</div>' );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( !data.logs || !data.logs.length )
|
||||
{
|
||||
$wrap.html( '<div class="text-muted">Brak logów dla tego produktu.</div>' );
|
||||
return;
|
||||
}
|
||||
|
||||
var rows_html = '';
|
||||
$.each( data.logs, function( _, log ) {
|
||||
var status_class = log.sync_status === 'success'
|
||||
? 'text-success'
|
||||
: ( log.sync_status === 'error' ? 'text-danger' : 'text-muted' );
|
||||
|
||||
rows_html += '<tr>' +
|
||||
'<td>' + escape_html( log.date_add || '' ) + '</td>' +
|
||||
'<td>' + escape_html( log.field_name || '' ) + '</td>' +
|
||||
'<td class="' + status_class + '"><b>' + escape_html( log.sync_status || '' ) + '</b></td>' +
|
||||
'<td>' + escape_html( log.sync_source || '' ) + '</td>' +
|
||||
'<td>' + escape_html( log.old_value || '' ) + '</td>' +
|
||||
'<td>' + escape_html( log.new_value || '' ) + '</td>' +
|
||||
'<td class="text-center"><button type="button" class="btn btn-sm btn-danger delete-merchant-log" data-log-id="' + log.id + '" title="Usuń log"><i class="fa fa-trash"></i></button></td>' +
|
||||
'</tr>';
|
||||
} );
|
||||
|
||||
$wrap.html(
|
||||
'<table class="table table-sm table-bordered table-striped" style="font-size:12px;">' +
|
||||
'<thead>' +
|
||||
'<tr>' +
|
||||
'<th style="min-width:140px;">Data</th>' +
|
||||
'<th style="min-width:120px;">Pole</th>' +
|
||||
'<th style="min-width:90px;">Status</th>' +
|
||||
'<th style="min-width:110px;">Źródło</th>' +
|
||||
'<th style="min-width:180px;">Stara wartość</th>' +
|
||||
'<th style="min-width:180px;">Nowa wartość</th>' +
|
||||
'<th style="width:60px;"></th>' +
|
||||
'</tr>' +
|
||||
'</thead>' +
|
||||
'<tbody>' + rows_html + '</tbody>' +
|
||||
'</table>'
|
||||
);
|
||||
},
|
||||
error: function()
|
||||
{
|
||||
$wrap.html( '<div class="text-danger">Nie udało się odczytać odpowiedzi serwera.</div>' );
|
||||
return;
|
||||
$wrap.html( '<div class="text-danger">Nie udało się pobrać logów.</div>' );
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ( data.status !== 'ok' )
|
||||
load_logs();
|
||||
|
||||
$wrap.on( 'click', '.delete-merchant-log', function()
|
||||
{
|
||||
var $btn = $( this );
|
||||
var log_id = $btn.data( 'log-id' );
|
||||
|
||||
$btn.prop( 'disabled', true );
|
||||
|
||||
$.ajax({
|
||||
url: '/products/delete_product_merchant_sync_log/',
|
||||
type: 'POST',
|
||||
data: { log_id: log_id },
|
||||
success: function( response )
|
||||
{
|
||||
$wrap.html( '<div class="text-danger">' + escape_html( data.message || 'Błąd pobierania logów.' ) + '</div>' );
|
||||
return;
|
||||
}
|
||||
var data;
|
||||
|
||||
if ( !data.logs || !data.logs.length )
|
||||
try { data = JSON.parse( response ); } catch( e ) { return; }
|
||||
|
||||
if ( data.status === 'ok' )
|
||||
{
|
||||
load_logs();
|
||||
}
|
||||
else
|
||||
{
|
||||
$btn.prop( 'disabled', false );
|
||||
}
|
||||
},
|
||||
error: function()
|
||||
{
|
||||
$wrap.html( '<div class="text-muted">Brak logów synchronizacji dla tego produktu.</div>' );
|
||||
return;
|
||||
$btn.prop( 'disabled', false );
|
||||
}
|
||||
|
||||
var rows_html = '';
|
||||
$.each( data.logs, function( _, log ) {
|
||||
var status_class = log.sync_status === 'success'
|
||||
? 'text-success'
|
||||
: ( log.sync_status === 'error' ? 'text-danger' : 'text-muted' );
|
||||
|
||||
rows_html += '<tr>' +
|
||||
'<td>' + escape_html( log.date_add || '' ) + '</td>' +
|
||||
'<td>' + escape_html( log.field_name || '' ) + '</td>' +
|
||||
'<td class="' + status_class + '"><b>' + escape_html( log.sync_status || '' ) + '</b></td>' +
|
||||
'<td>' + escape_html( log.sync_source || '' ) + '</td>' +
|
||||
'<td>' + escape_html( log.old_value || '' ) + '</td>' +
|
||||
'<td>' + escape_html( log.new_value || '' ) + '</td>' +
|
||||
'<td>' + escape_html( log.error_message || '' ) + '</td>' +
|
||||
'</tr>';
|
||||
} );
|
||||
|
||||
$wrap.html(
|
||||
'<table class="table table-sm table-bordered table-striped" style="font-size:12px;">' +
|
||||
'<thead>' +
|
||||
'<tr>' +
|
||||
'<th style="min-width:140px;">Data</th>' +
|
||||
'<th style="min-width:120px;">Pole</th>' +
|
||||
'<th style="min-width:90px;">Status</th>' +
|
||||
'<th style="min-width:110px;">Źródło</th>' +
|
||||
'<th style="min-width:180px;">Stara wartość</th>' +
|
||||
'<th style="min-width:180px;">Nowa wartość</th>' +
|
||||
'<th style="min-width:220px;">Błąd</th>' +
|
||||
'</tr>' +
|
||||
'</thead>' +
|
||||
'<tbody>' + rows_html + '</tbody>' +
|
||||
'</table>'
|
||||
);
|
||||
},
|
||||
error: function()
|
||||
{
|
||||
$wrap.html( '<div class="text-danger">Nie udało się pobrać logów synchronizacji.</div>' );
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<body class="logged">
|
||||
<?php
|
||||
$module = $this -> current_module;
|
||||
$google_ads_modules = [ 'campaigns', 'campaign_terms', 'products', 'campaign_alerts', 'clients' ];
|
||||
$google_ads_modules = [ 'campaigns', 'campaign_terms', 'products', 'campaign_alerts', 'clients', 'feeds' ];
|
||||
$is_google_ads_module = in_array( $module, $google_ads_modules, true );
|
||||
$facebook_ads_modules = [ 'facebook_ads' ];
|
||||
$is_facebook_ads_module = in_array( $module, $facebook_ads_modules, true );
|
||||
@@ -95,6 +95,12 @@
|
||||
<span>Klienci</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="<?= $module === 'feeds' ? 'active' : '' ?>">
|
||||
<a href="/feeds">
|
||||
<i class="fa-solid fa-file-csv"></i>
|
||||
<span>Supplemental Feeds</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-group <?= $is_facebook_ads_module ? 'active' : '' ?>">
|
||||
@@ -162,6 +168,7 @@
|
||||
'products' => 'Produkty',
|
||||
'campaign_alerts' => 'Alerty',
|
||||
'clients' => 'Klienci',
|
||||
'feeds' => 'Supplemental Feeds',
|
||||
'facebook_ads' => 'Facebook Ads',
|
||||
'allegro' => 'Allegro import',
|
||||
'logs' => 'Logi',
|
||||
|
||||
Reference in New Issue
Block a user