feat: Implement campaign synchronization feature with dropdown UI
- Updated SCSS styles for new campaign sync buttons and dropdowns. - Refactored main_view.php to replace the single select for campaigns with a multi-select dropdown. - Added JavaScript functions to handle dropdown interactions and sync status updates. - Introduced sync status bars for clients in main_view.php. - Created new database migrations for client sync flags and cron sync status tracking.
This commit is contained in:
@@ -14,7 +14,8 @@
|
||||
"Bash(cd:*)",
|
||||
"Bash(git push:*)",
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)"
|
||||
"Bash(git commit:*)",
|
||||
"Bash(php:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,4 +69,56 @@ class Clients
|
||||
echo json_encode( $client ?: [] );
|
||||
exit;
|
||||
}
|
||||
|
||||
static public function sync_status()
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
// Kampanie: 1 work unit per row (pending=0, done=1)
|
||||
$campaigns_raw = $mdb->query(
|
||||
"SELECT client_id, COUNT(*) as total,
|
||||
SUM(CASE WHEN phase='done' THEN 1 ELSE 0 END) as done
|
||||
FROM cron_sync_status WHERE pipeline='campaigns' GROUP BY client_id"
|
||||
)->fetchAll( \PDO::FETCH_ASSOC );
|
||||
|
||||
// Produkty: 3 work units per row (pending=0, fetch=1, aggregate_30=2, done=3)
|
||||
$products_raw = $mdb->query(
|
||||
"SELECT client_id, COUNT(*) * 3 as total,
|
||||
SUM(CASE phase WHEN 'fetch' THEN 1 WHEN 'aggregate_30' THEN 2 WHEN 'done' THEN 3 ELSE 0 END) as done
|
||||
FROM cron_sync_status WHERE pipeline='products' GROUP BY client_id"
|
||||
)->fetchAll( \PDO::FETCH_ASSOC );
|
||||
|
||||
$data = [];
|
||||
|
||||
foreach ( $campaigns_raw as $row )
|
||||
{
|
||||
$data[ $row['client_id'] ]['campaigns'] = [ (int) $row['done'], (int) $row['total'] ];
|
||||
}
|
||||
|
||||
foreach ( $products_raw as $row )
|
||||
{
|
||||
$data[ $row['client_id'] ]['products'] = [ (int) $row['done'], (int) $row['total'] ];
|
||||
}
|
||||
|
||||
echo json_encode( [ 'status' => 'ok', 'data' => $data ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
static public function force_sync()
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$id = (int) \S::get( 'id' );
|
||||
|
||||
if ( !$id )
|
||||
{
|
||||
echo json_encode( [ 'success' => false, 'message' => 'Brak ID klienta.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
$mdb -> delete( 'cron_sync_status', [ 'client_id' => $id ] );
|
||||
|
||||
echo json_encode( [ 'success' => true ] );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -156,24 +156,20 @@ class Users
|
||||
$base_url = self::get_base_url();
|
||||
$clients_total = (int) $mdb -> query( "SELECT COUNT(*) FROM clients WHERE deleted = 0 AND google_ads_customer_id IS NOT NULL AND google_ads_customer_id <> ''" ) -> fetchColumn();
|
||||
|
||||
$campaign_window_state = self::get_setting_json( 'cron_campaigns_window_state' );
|
||||
$campaign_daily_state = self::get_setting_json( 'cron_campaigns_state' );
|
||||
$campaign_dates = self::normalize_dates( $campaign_window_state['sync_dates'] ?? [] );
|
||||
$campaign_dates_count = count( $campaign_dates );
|
||||
if ( $campaign_dates_count < 1 )
|
||||
{
|
||||
$campaign_dates = [ date( 'Y-m-d' ) ];
|
||||
$campaign_dates_count = 1;
|
||||
}
|
||||
// --- Kampanie ---
|
||||
$campaign_stats = $mdb -> query(
|
||||
"SELECT COUNT(*) as total,
|
||||
SUM(CASE WHEN phase = 'done' THEN 1 ELSE 0 END) as done,
|
||||
COUNT(DISTINCT sync_date) as dates_count,
|
||||
MIN(CASE WHEN phase != 'done' THEN sync_date END) as active_date
|
||||
FROM cron_sync_status WHERE pipeline = 'campaigns'"
|
||||
) -> fetch( \PDO::FETCH_ASSOC );
|
||||
|
||||
$campaign_current_date_index = (int) ( $campaign_window_state['current_date_index'] ?? 0 );
|
||||
$campaign_current_date_index = max( 0, min( $campaign_dates_count - 1, $campaign_current_date_index ) );
|
||||
$campaign_processed_today = count( self::normalize_ids( $campaign_daily_state['processed_ids'] ?? [] ) );
|
||||
$campaign_processed_today = min( $clients_total, $campaign_processed_today );
|
||||
$campaign_total = $clients_total * $campaign_dates_count;
|
||||
$campaign_processed = min( $campaign_total, ( $campaign_current_date_index * $clients_total ) + $campaign_processed_today );
|
||||
$campaign_total = (int) ( $campaign_stats['total'] ?? 0 );
|
||||
$campaign_processed = (int) ( $campaign_stats['done'] ?? 0 );
|
||||
$campaign_dates_count = max( 1, (int) ( $campaign_stats['dates_count'] ?? 1 ) );
|
||||
$campaign_active_date = $campaign_stats['active_date'] ?? '';
|
||||
$campaign_remaining = max( 0, $campaign_total - $campaign_processed );
|
||||
$campaign_active_date = $campaign_window_state['sync_date'] ?? ( $campaign_dates[ $campaign_current_date_index ] ?? '' );
|
||||
$campaign_meta = 'Aktywny dzień: ' . ( $campaign_active_date ?: '-' ) . ', okno dni: ' . $campaign_dates_count;
|
||||
$campaign_eta_meta = self::build_eta_meta( 'cron_campaigns', $campaign_remaining );
|
||||
if ( $campaign_eta_meta !== '' )
|
||||
@@ -181,64 +177,44 @@ class Users
|
||||
$campaign_meta .= ', ' . $campaign_eta_meta;
|
||||
}
|
||||
|
||||
$products_state = self::get_setting_json( 'cron_products_pipeline_state' );
|
||||
$products_dates = self::normalize_dates( $products_state['import_dates'] ?? [] );
|
||||
$products_dates_count = count( $products_dates );
|
||||
if ( $products_dates_count < 1 )
|
||||
{
|
||||
$products_dates = [ date( 'Y-m-d' ) ];
|
||||
$products_dates_count = 1;
|
||||
}
|
||||
// --- Produkty (3 work units per row: fetch, aggregate_30, aggregate_temp) ---
|
||||
$products_stats = $mdb -> query(
|
||||
"SELECT COUNT(*) as total,
|
||||
SUM(CASE phase
|
||||
WHEN 'fetch' THEN 1
|
||||
WHEN 'aggregate_30' THEN 2
|
||||
WHEN 'done' THEN 3
|
||||
ELSE 0
|
||||
END) as work_done,
|
||||
COUNT(DISTINCT sync_date) as dates_count,
|
||||
MIN(CASE WHEN phase != 'done' THEN sync_date END) as active_date
|
||||
FROM cron_sync_status WHERE pipeline = 'products'"
|
||||
) -> fetch( \PDO::FETCH_ASSOC );
|
||||
|
||||
$products_current_date_index = (int) ( $products_state['current_date_index'] ?? 0 );
|
||||
$products_current_date_index = max( 0, min( $products_dates_count - 1, $products_current_date_index ) );
|
||||
$products_phase = (string) ( $products_state['phase'] ?? 'fetch' );
|
||||
|
||||
$products_fetch_done = count( self::normalize_ids( $products_state['fetch_done_ids'] ?? [] ) );
|
||||
$products_aggregate_30_done = count( self::normalize_ids( $products_state['aggregate_30_done_ids'] ?? [] ) );
|
||||
$products_aggregate_temp_done = count( self::normalize_ids( $products_state['aggregate_temp_done_ids'] ?? [] ) );
|
||||
|
||||
$products_fetch_done = min( $clients_total, $products_fetch_done );
|
||||
$products_aggregate_30_done = min( $clients_total, $products_aggregate_30_done );
|
||||
$products_aggregate_temp_done = min( $clients_total, $products_aggregate_temp_done );
|
||||
|
||||
$products_per_day_total = $clients_total * 3;
|
||||
$products_total = $products_per_day_total * $products_dates_count;
|
||||
|
||||
$products_done_in_day = 0;
|
||||
if ( $products_phase === 'aggregate_30' )
|
||||
{
|
||||
$products_done_in_day = $clients_total + $products_aggregate_30_done;
|
||||
}
|
||||
else if ( $products_phase === 'aggregate_temp' )
|
||||
{
|
||||
$products_done_in_day = ( $clients_total * 2 ) + $products_aggregate_temp_done;
|
||||
}
|
||||
else if ( $products_phase === 'done' )
|
||||
{
|
||||
$products_done_in_day = $products_per_day_total;
|
||||
}
|
||||
else
|
||||
{
|
||||
$products_done_in_day = $products_fetch_done;
|
||||
}
|
||||
|
||||
$products_done_in_day = min( $products_per_day_total, $products_done_in_day );
|
||||
$products_processed = min( $products_total, ( $products_current_date_index * $products_per_day_total ) + $products_done_in_day );
|
||||
if ( $products_phase === 'done' )
|
||||
{
|
||||
$products_processed = $products_total;
|
||||
}
|
||||
$products_row_count = (int) ( $products_stats['total'] ?? 0 );
|
||||
$products_work_done = (int) ( $products_stats['work_done'] ?? 0 );
|
||||
$products_total = $products_row_count * 3;
|
||||
$products_processed = min( $products_total, $products_work_done );
|
||||
$products_dates_count = max( 1, (int) ( $products_stats['dates_count'] ?? 1 ) );
|
||||
$products_active_date = $products_stats['active_date'] ?? '';
|
||||
$products_remaining = max( 0, $products_total - $products_processed );
|
||||
|
||||
$products_phase_labels = [
|
||||
'fetch' => 'Pobieranie',
|
||||
'aggregate_30' => 'Agregacja 30 dni',
|
||||
'aggregate_temp' => 'Agregacja temp',
|
||||
'done' => 'Zakończono'
|
||||
];
|
||||
$products_phase_label = $products_phase_labels[ $products_phase ] ?? $products_phase;
|
||||
$products_active_date = $products_state['import_date'] ?? ( $products_dates[ $products_current_date_index ] ?? '' );
|
||||
$products_phase_label = 'Zakończono';
|
||||
if ( $products_active_date )
|
||||
{
|
||||
$current_phase = $mdb -> query(
|
||||
"SELECT phase FROM cron_sync_status cs INNER JOIN clients c ON cs.client_id = c.id AND c.deleted = 0 WHERE cs.pipeline = 'products' AND cs.sync_date = :sync_date AND cs.phase != 'done' ORDER BY FIELD(cs.phase, 'pending', 'fetch', 'aggregate_30') ASC LIMIT 1",
|
||||
[ ':sync_date' => $products_active_date ]
|
||||
) -> fetchColumn();
|
||||
|
||||
$phase_labels = [
|
||||
'pending' => 'Pobieranie',
|
||||
'fetch' => 'Agregacja 30 dni',
|
||||
'aggregate_30' => 'Agregacja temp'
|
||||
];
|
||||
$products_phase_label = $phase_labels[ $current_phase ] ?? 'Zakończono';
|
||||
}
|
||||
|
||||
$products_meta = 'Faza: ' . $products_phase_label . ', aktywny dzień: ' . ( $products_active_date ?: '-' ) . ', okno dni: ' . $products_dates_count;
|
||||
$products_eta_meta = self::build_eta_meta( 'cron_products', $products_remaining );
|
||||
if ( $products_eta_meta !== '' )
|
||||
@@ -246,6 +222,7 @@ class Users
|
||||
$products_meta .= ', ' . $products_eta_meta;
|
||||
}
|
||||
|
||||
// --- Endpointy CRON ---
|
||||
$cron_endpoints = [
|
||||
[ 'name' => 'Legacy CRON', 'path' => '/cron.php', 'action' => 'cron_legacy' ],
|
||||
[ 'name' => 'Cron kampanii', 'path' => '/cron/cron_campaigns', 'action' => 'cron_campaigns' ],
|
||||
@@ -291,49 +268,6 @@ class Users
|
||||
];
|
||||
}
|
||||
|
||||
private static function get_setting_json( $setting_key )
|
||||
{
|
||||
$raw = \services\GoogleAdsApi::get_setting( $setting_key );
|
||||
if ( !$raw )
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
$decoded = json_decode( (string) $raw, true );
|
||||
return is_array( $decoded ) ? $decoded : [];
|
||||
}
|
||||
|
||||
private static function normalize_ids( $items )
|
||||
{
|
||||
$result = [];
|
||||
foreach ( (array) $items as $item )
|
||||
{
|
||||
$id = (int) $item;
|
||||
if ( $id > 0 )
|
||||
{
|
||||
$result[] = $id;
|
||||
}
|
||||
}
|
||||
return array_values( array_unique( $result ) );
|
||||
}
|
||||
|
||||
private static function normalize_dates( $items )
|
||||
{
|
||||
$result = [];
|
||||
foreach ( (array) $items as $item )
|
||||
{
|
||||
$timestamp = strtotime( (string) $item );
|
||||
if ( !$timestamp )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
$result[] = date( 'Y-m-d', $timestamp );
|
||||
}
|
||||
$result = array_values( array_unique( $result ) );
|
||||
sort( $result );
|
||||
return $result;
|
||||
}
|
||||
|
||||
private static function progress_percent( $processed, $total )
|
||||
{
|
||||
$processed = (int) $processed;
|
||||
|
||||
@@ -13,3 +13,4 @@ $settings['email_password'] = 'ProjectPro2025!';
|
||||
$settings['cron_products_clients_per_run'] = 1;
|
||||
$settings['cron_campaigns_clients_per_run'] = 1;
|
||||
$settings['cron_products_urls_limit_per_client'] = 10;
|
||||
$settings['google_ads_conversion_window_days'] = 7;
|
||||
|
||||
@@ -38,10 +38,15 @@ Ten plik sluzy jako trwala pamiec dla Claude Code. Zapisuj tu wzorce, decyzje i
|
||||
- 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_products` dziala batchowo po klientach (`clients_per_run`), domyslnie `10` (max `100`), aby ograniczyc liczbe wywolan; odpowiedz zawiera `estimated_calls_remaining_in_phase`
|
||||
- `cron_campaigns` dziala batchowo po klientach (`clients_per_run`), domyslnie `2` (max `20`), a odpowiedz zawiera `estimated_calls_remaining_today`
|
||||
- Zmiana listy klientow (np. reaktywacja klienta) nie powinna resetowac postepu `cron_products`; pipeline zachowuje przetworzonych i dopina nowych klientow
|
||||
- W `cron_campaigns` stan `processed_ids` jest normalizowany do aktualnej listy aktywnych klientow, aby uniknac rozjazdow postepu po zmianach aktywnosci
|
||||
- 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
|
||||
|
||||
## Preferencje uzytkownika
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -38,7 +38,7 @@ body {
|
||||
color: $cText;
|
||||
background: $cContentBg;
|
||||
max-width: 100vw;
|
||||
overflow: hidden;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.hide {
|
||||
@@ -1168,6 +1168,78 @@ table {
|
||||
color: $cWhite;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-icon-sync {
|
||||
background: #F0FDF4;
|
||||
color: #16a34a;
|
||||
|
||||
&:hover {
|
||||
background: #16a34a;
|
||||
color: $cWhite;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
&.is-queued {
|
||||
background: #FEF3C7;
|
||||
color: #D97706;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.client-sync-bars {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.client-sync-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.client-sync-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #8899A6;
|
||||
width: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.client-sync-track {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
border-radius: 999px;
|
||||
background: #E9EEF5;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.client-sync-fill {
|
||||
height: 100%;
|
||||
border-radius: 999px;
|
||||
background: #CBD5E0;
|
||||
transition: width 0.4s ease;
|
||||
|
||||
&.is-active {
|
||||
background: linear-gradient(90deg, #5A9BFF 0%, #2E6BDF 100%);
|
||||
}
|
||||
|
||||
&.is-done {
|
||||
background: $cSuccess;
|
||||
}
|
||||
}
|
||||
|
||||
.client-sync-pct {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #8899A6;
|
||||
width: 32px;
|
||||
text-align: right;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
@@ -1279,6 +1351,7 @@ table {
|
||||
|
||||
.filter-with-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.form-control {
|
||||
@@ -1310,6 +1383,121 @@ table {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-group-campaign-multi {
|
||||
flex: 2 !important;
|
||||
}
|
||||
|
||||
.campaign-dropdown {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.campaign-dropdown-trigger {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
padding-right: 32px;
|
||||
border: 1px solid $cBorder;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: $cTextDark;
|
||||
background: $cWhite;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: border-color 0.2s;
|
||||
position: relative;
|
||||
min-height: 42px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.campaign-dropdown-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&.is-placeholder {
|
||||
color: #8899A6;
|
||||
}
|
||||
}
|
||||
|
||||
.campaign-dropdown-arrow {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
font-size: 10px;
|
||||
color: #8899A6;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
.campaign-dropdown.is-open {
|
||||
.campaign-dropdown-trigger {
|
||||
border-color: $cPrimary;
|
||||
box-shadow: 0 0 0 3px rgba($cPrimary, 0.1);
|
||||
}
|
||||
|
||||
.campaign-dropdown-arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.campaign-dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.campaign-dropdown-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
max-height: 280px;
|
||||
overflow-y: auto;
|
||||
background: $cWhite;
|
||||
border: 1px solid $cBorder;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.campaign-dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: $cTextDark;
|
||||
margin: 0;
|
||||
transition: background 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: #F8FAFC;
|
||||
}
|
||||
|
||||
&.is-checked {
|
||||
background: #EEF2FF;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
accent-color: $cPrimary;
|
||||
}
|
||||
|
||||
span {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.campaigns-list-panel {
|
||||
|
||||
2
migrations/011_clients_force_sync.sql
Normal file
2
migrations/011_clients_force_sync.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `clients` ADD COLUMN `force_sync_campaigns` TINYINT(1) NOT NULL DEFAULT 0 AFTER `deleted`;
|
||||
ALTER TABLE `clients` ADD COLUMN `force_sync_products` TINYINT(1) NOT NULL DEFAULT 0 AFTER `force_sync_campaigns`;
|
||||
57
migrations/012_cron_sync_status.sql
Normal file
57
migrations/012_cron_sync_status.sql
Normal file
@@ -0,0 +1,57 @@
|
||||
-- Migracja: tabela cron_sync_status zamiast JSON w settings
|
||||
-- Data: 2026-02-19
|
||||
-- Opis: dedykowana tabela do sledzenia postepu pipeline CRON (kampanie, produkty)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `cron_sync_status` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`client_id` INT NOT NULL,
|
||||
`pipeline` ENUM('campaigns','products') NOT NULL,
|
||||
`sync_date` DATE NOT NULL,
|
||||
`phase` ENUM('pending','fetch','aggregate_30','aggregate_temp','done') NOT NULL DEFAULT 'pending',
|
||||
`started_at` DATETIME NULL,
|
||||
`completed_at` DATETIME NULL,
|
||||
`error_message` TEXT NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY `uq_client_pipeline_date` (`client_id`, `pipeline`, `sync_date`),
|
||||
KEY `idx_pipeline_phase` (`pipeline`, `phase`),
|
||||
KEY `idx_client_id` (`client_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
-- Usuniecie kolumny force_sync_campaigns z clients
|
||||
SET @sql = IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'clients'
|
||||
AND COLUMN_NAME = 'force_sync_campaigns'
|
||||
),
|
||||
'ALTER TABLE `clients` DROP COLUMN `force_sync_campaigns`',
|
||||
'DO 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Usuniecie kolumny force_sync_products z clients
|
||||
SET @sql = IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'clients'
|
||||
AND COLUMN_NAME = 'force_sync_products'
|
||||
),
|
||||
'ALTER TABLE `clients` DROP COLUMN `force_sync_products`',
|
||||
'DO 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Usuniecie starych kluczy JSON z settings
|
||||
DELETE FROM `settings` WHERE `setting_key` IN (
|
||||
'cron_campaigns_window_state',
|
||||
'cron_campaigns_state',
|
||||
'cron_products_pipeline_state'
|
||||
);
|
||||
@@ -13,35 +13,24 @@
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="campaign_id"><i class="fa-solid fa-bullhorn"></i> Kampania</label>
|
||||
<div class="filter-group filter-group-campaign-multi">
|
||||
<label><i class="fa-solid fa-bullhorn"></i> Kampania</label>
|
||||
<div class="filter-with-action">
|
||||
<select id="campaign_id" name="campaign_id" class="form-control">
|
||||
<option value="">- wybierz kampanie -</option>
|
||||
</select>
|
||||
<button type="button" id="delete_campaign" class="btn-icon btn-icon-delete" title="Usun kampanie">
|
||||
<select id="campaign_id" name="campaign_id[]" multiple="multiple" style="display:none"></select>
|
||||
<div class="campaign-dropdown" id="campaign_dropdown">
|
||||
<div class="campaign-dropdown-trigger" id="campaign_dropdown_trigger">
|
||||
<span class="campaign-dropdown-text">- wybierz kampanie -</span>
|
||||
<i class="fa-solid fa-chevron-down campaign-dropdown-arrow"></i>
|
||||
</div>
|
||||
<div class="campaign-dropdown-menu" id="campaign_dropdown_menu"></div>
|
||||
</div>
|
||||
<button type="button" id="delete_campaign" class="btn-icon btn-icon-delete" title="Usun zaznaczone kampanie">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="campaigns-list-panel" id="campaigns_list_panel" style="display: none;">
|
||||
<div class="campaigns-list-toolbar">
|
||||
<div class="campaigns-list-toolbar-left">
|
||||
<input type="checkbox" id="campaigns_select_all" title="Zaznacz wszystkie">
|
||||
<label for="campaigns_select_all">Zaznacz wszystkie</label>
|
||||
<span class="campaigns-selected-count">Zaznaczone: <strong id="campaigns_selected_count">0</strong></span>
|
||||
</div>
|
||||
<div class="campaigns-list-toolbar-right">
|
||||
<button type="button" id="campaigns_bulk_delete" class="campaigns-bulk-delete-btn" disabled>
|
||||
<i class="fa-solid fa-trash"></i> Usun zaznaczone
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="campaigns-list-items" id="campaigns_list_items"></div>
|
||||
</div>
|
||||
|
||||
<div class="campaigns-chart-wrap">
|
||||
<div id="container"></div>
|
||||
</div>
|
||||
@@ -95,10 +84,72 @@ function storage_get( key )
|
||||
}
|
||||
}
|
||||
|
||||
function rebuildCampaignDropdown()
|
||||
{
|
||||
var menu = $( '#campaign_dropdown_menu' );
|
||||
menu.empty();
|
||||
|
||||
$( '#campaign_id option' ).each( function()
|
||||
{
|
||||
var val = $( this ).val();
|
||||
var text = $( this ).text();
|
||||
var item = $( '<div class="campaign-dropdown-item"></div>' );
|
||||
var checkbox = $( '<input type="checkbox">' ).val( val );
|
||||
item.append( checkbox ).append( $( '<span></span>' ).text( text ) );
|
||||
menu.append( item );
|
||||
});
|
||||
|
||||
updateCheckboxState();
|
||||
updateDropdownDisplay();
|
||||
}
|
||||
|
||||
function syncDropdownToSelect()
|
||||
{
|
||||
var selected = [];
|
||||
$( '#campaign_dropdown_menu input:checked' ).each( function() {
|
||||
selected.push( $( this ).val() );
|
||||
});
|
||||
$( '#campaign_id' ).val( selected ).trigger( 'change' );
|
||||
updateDropdownDisplay();
|
||||
}
|
||||
|
||||
function updateCheckboxState()
|
||||
{
|
||||
var selected = $( '#campaign_id' ).val() || [];
|
||||
$( '#campaign_dropdown_menu input[type="checkbox"]' ).each( function()
|
||||
{
|
||||
var checked = selected.indexOf( $( this ).val() ) !== -1;
|
||||
$( this ).prop( 'checked', checked );
|
||||
$( this ).closest( '.campaign-dropdown-item' ).toggleClass( 'is-checked', checked );
|
||||
});
|
||||
}
|
||||
|
||||
function updateDropdownDisplay()
|
||||
{
|
||||
var selected = $( '#campaign_id' ).val() || [];
|
||||
var textEl = $( '#campaign_dropdown .campaign-dropdown-text' );
|
||||
|
||||
if ( selected.length === 0 )
|
||||
{
|
||||
textEl.text( '- wybierz kampanie -' ).addClass( 'is-placeholder' );
|
||||
}
|
||||
else if ( selected.length === 1 )
|
||||
{
|
||||
var name = $( '#campaign_id option[value="' + selected[0] + '"]' ).text();
|
||||
textEl.text( name ).removeClass( 'is-placeholder' );
|
||||
}
|
||||
else
|
||||
{
|
||||
var first = $( '#campaign_id option[value="' + selected[0] + '"]' ).text();
|
||||
textEl.text( first + ' (+' + ( selected.length - 1 ) + ')' ).removeClass( 'is-placeholder' );
|
||||
}
|
||||
}
|
||||
|
||||
function reloadChart()
|
||||
{
|
||||
var campaign_id = $( '#campaign_id' ).val();
|
||||
if ( !campaign_id ) return;
|
||||
var vals = $( '#campaign_id' ).val() || [];
|
||||
if ( vals.length !== 1 ) return;
|
||||
var campaign_id = vals[0];
|
||||
|
||||
$.ajax({
|
||||
url: '/campaigns/get_campaign_history_data_table_chart/',
|
||||
@@ -178,111 +229,36 @@ function reloadChart()
|
||||
|
||||
$( function()
|
||||
{
|
||||
function updateCampaignsSelectedCount()
|
||||
$( 'body' ).on( 'click', '#campaign_dropdown_trigger', function( e )
|
||||
{
|
||||
var count = $( '.campaigns-list-item-cb:checked' ).length;
|
||||
$( '#campaigns_selected_count' ).text( count );
|
||||
$( '#campaigns_bulk_delete' ).prop( 'disabled', count === 0 );
|
||||
$( '#campaigns_select_all' ).prop( 'checked', count > 0 && count === $( '.campaigns-list-item-cb' ).length );
|
||||
}
|
||||
|
||||
function buildCampaignsList( campaigns )
|
||||
{
|
||||
var panel = $( '#campaigns_list_panel' );
|
||||
var container = $( '#campaigns_list_items' );
|
||||
container.empty();
|
||||
$( '#campaigns_select_all' ).prop( 'checked', false );
|
||||
updateCampaignsSelectedCount();
|
||||
|
||||
if ( !campaigns.length )
|
||||
{
|
||||
panel.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
campaigns.forEach( function( pair ) {
|
||||
var c = pair[1];
|
||||
var item = $( '<label class="campaigns-list-item">' +
|
||||
'<input type="checkbox" class="campaigns-list-item-cb" value="' + c.id + '" data-name="' + $( '<span>' ).text( c.campaign_name ).html() + '"> ' +
|
||||
'<span class="campaigns-list-item-name">' + $( '<span>' ).text( c.campaign_name ).html() + '</span>' +
|
||||
'</label>' );
|
||||
container.append( item );
|
||||
});
|
||||
|
||||
panel.show();
|
||||
}
|
||||
|
||||
$( 'body' ).on( 'change', '.campaigns-list-item-cb', updateCampaignsSelectedCount );
|
||||
|
||||
$( 'body' ).on( 'change', '#campaigns_select_all', function()
|
||||
{
|
||||
$( '.campaigns-list-item-cb' ).prop( 'checked', $( this ).is( ':checked' ) );
|
||||
updateCampaignsSelectedCount();
|
||||
e.stopPropagation();
|
||||
$( '#campaign_dropdown' ).toggleClass( 'is-open' );
|
||||
});
|
||||
|
||||
$( 'body' ).on( 'click', '#campaigns_bulk_delete', function()
|
||||
$( 'body' ).on( 'change', '#campaign_dropdown_menu input[type="checkbox"]', function()
|
||||
{
|
||||
var checked = $( '.campaigns-list-item-cb:checked' );
|
||||
var count = checked.length;
|
||||
$( this ).closest( '.campaign-dropdown-item' ).toggleClass( 'is-checked', this.checked );
|
||||
syncDropdownToSelect();
|
||||
});
|
||||
|
||||
if ( count === 0 ) return;
|
||||
$( 'body' ).on( 'click', '#campaign_dropdown_menu .campaign-dropdown-item span', function( e )
|
||||
{
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
var val = $( this ).siblings( 'input[type="checkbox"]' ).val();
|
||||
$( '#campaign_dropdown_menu input[type="checkbox"]' ).prop( 'checked', false );
|
||||
$( '#campaign_dropdown_menu .campaign-dropdown-item' ).removeClass( 'is-checked' );
|
||||
$( this ).siblings( 'input[type="checkbox"]' ).prop( 'checked', true );
|
||||
$( this ).closest( '.campaign-dropdown-item' ).addClass( 'is-checked' );
|
||||
$( '#campaign_id' ).val( [ val ] ).trigger( 'change' );
|
||||
updateDropdownDisplay();
|
||||
$( '#campaign_dropdown' ).removeClass( 'is-open' );
|
||||
});
|
||||
|
||||
var names = [];
|
||||
checked.each( function() { names.push( $( this ).data( 'name' ) ); } );
|
||||
var namesList = '<ul style="text-align:left; max-height:200px; overflow:auto; margin-top:10px;">';
|
||||
names.forEach( function( n ) { namesList += '<li>' + n + '</li>'; } );
|
||||
namesList += '</ul>';
|
||||
|
||||
$.confirm({
|
||||
title: 'Potwierdzenie usuniecia',
|
||||
content: 'Czy na pewno chcesz usunac <strong>' + count + '</strong> kampanii?' + namesList + '<br>Ta operacja jest nieodwracalna i usunie rowniez cala historie tych kampanii.',
|
||||
type: 'red',
|
||||
buttons: {
|
||||
confirm: {
|
||||
text: 'Usun (' + count + ')',
|
||||
btnClass: 'btn-red',
|
||||
action: function()
|
||||
{
|
||||
var ids = [];
|
||||
checked.each( function() { ids.push( $( this ).val() ); } );
|
||||
|
||||
$.ajax({
|
||||
url: '/campaigns/delete_campaigns/',
|
||||
type: 'POST',
|
||||
data: { ids: ids },
|
||||
success: function( response )
|
||||
{
|
||||
var data = JSON.parse( response );
|
||||
if ( data.success )
|
||||
{
|
||||
$.alert({
|
||||
title: 'Sukces',
|
||||
content: 'Usunieto ' + data.deleted + ' kampanii.',
|
||||
type: 'green',
|
||||
autoClose: 'ok|2000'
|
||||
});
|
||||
|
||||
var current_campaign = $( '#campaign_id' ).val();
|
||||
if ( ids.indexOf( current_campaign ) !== -1 )
|
||||
storage_set( STORAGE_CAMPAIGN_KEY, '' );
|
||||
|
||||
$( '#client_id' ).trigger( 'change' );
|
||||
}
|
||||
else
|
||||
{
|
||||
$.alert({
|
||||
title: 'Blad',
|
||||
content: data.message || 'Nie udalo sie usunac kampanii.',
|
||||
type: 'red'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
cancel: { text: 'Anuluj' }
|
||||
}
|
||||
});
|
||||
$( document ).on( 'click', function( e )
|
||||
{
|
||||
if ( !$( e.target ).closest( '#campaign_dropdown' ).length )
|
||||
$( '#campaign_dropdown' ).removeClass( 'is-open' );
|
||||
});
|
||||
|
||||
$( 'body' ).on( 'change', '#client_id', function()
|
||||
@@ -295,14 +271,11 @@ $( function()
|
||||
if ( !campaign_to_restore )
|
||||
storage_set( STORAGE_CAMPAIGN_KEY, '' );
|
||||
|
||||
campaigns_select.empty();
|
||||
campaigns_select.append( '<option value="">- wybierz kampanie -</option>' );
|
||||
campaigns_select.empty().trigger( 'change' );
|
||||
rebuildCampaignDropdown();
|
||||
|
||||
if ( !client_id )
|
||||
{
|
||||
$( '#campaigns_list_panel' ).hide();
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '/campaigns/get_campaigns_list/client_id=' + client_id,
|
||||
@@ -324,14 +297,14 @@ $( function()
|
||||
|
||||
campaigns.forEach( function( pair ) {
|
||||
var value = pair[1];
|
||||
campaigns_select.append( '<option value="' + value.id + '">' + value.campaign_name + '</option>' );
|
||||
campaigns_select.append( new Option( value.campaign_name, value.id, false, false ) );
|
||||
});
|
||||
|
||||
buildCampaignsList( campaigns );
|
||||
rebuildCampaignDropdown();
|
||||
|
||||
if ( campaign_to_restore && campaigns_select.find( 'option[value="' + campaign_to_restore + '"]' ).length )
|
||||
if ( campaign_to_restore )
|
||||
{
|
||||
campaigns_select.val( campaign_to_restore ).trigger( 'change' );
|
||||
campaigns_select.val( [ campaign_to_restore ] ).trigger( 'change' );
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -340,9 +313,11 @@ $( function()
|
||||
} ).first();
|
||||
|
||||
if ( account_option.length )
|
||||
campaigns_select.val( account_option.val() ).trigger( 'change' );
|
||||
campaigns_select.val( [ account_option.val() ] ).trigger( 'change' );
|
||||
}
|
||||
|
||||
updateCheckboxState();
|
||||
updateDropdownDisplay();
|
||||
restore_campaign_after_client_load = '';
|
||||
}
|
||||
});
|
||||
@@ -350,10 +325,9 @@ $( function()
|
||||
|
||||
$( 'body' ).on( 'click', '#delete_campaign', function()
|
||||
{
|
||||
var campaign_id = $( '#campaign_id' ).val();
|
||||
var campaign_name = $( '#campaign_id option:selected' ).text();
|
||||
var selected = $( '#campaign_id' ).val() || [];
|
||||
|
||||
if ( !campaign_id )
|
||||
if ( !selected.length )
|
||||
{
|
||||
$.alert({
|
||||
title: 'Uwaga',
|
||||
@@ -363,31 +337,54 @@ $( function()
|
||||
return;
|
||||
}
|
||||
|
||||
var names = [];
|
||||
selected.forEach( function( id ) {
|
||||
var text = $( '#campaign_id option[value="' + id + '"]' ).text();
|
||||
names.push( text );
|
||||
});
|
||||
|
||||
var count = selected.length;
|
||||
var msg = '';
|
||||
|
||||
if ( count === 1 )
|
||||
{
|
||||
msg = 'Czy na pewno chcesz usunac kampanie <strong>' + names[0] + '</strong>?';
|
||||
}
|
||||
else
|
||||
{
|
||||
var namesList = '<ul style="text-align:left; max-height:200px; overflow:auto; margin-top:10px;">';
|
||||
names.forEach( function( n ) { namesList += '<li>' + n + '</li>'; } );
|
||||
namesList += '</ul>';
|
||||
msg = 'Czy na pewno chcesz usunac <strong>' + count + '</strong> kampanii?' + namesList;
|
||||
}
|
||||
|
||||
msg += '<br>Ta operacja jest nieodwracalna i usunie rowniez cala historie kampanii.';
|
||||
|
||||
$.confirm({
|
||||
title: 'Potwierdzenie usuniecia',
|
||||
content: 'Czy na pewno chcesz usunac kampanie <strong>' + campaign_name + '</strong>?<br><br>Ta operacja jest nieodwracalna i usunie rowniez cala historie kampanii.',
|
||||
content: msg,
|
||||
type: 'red',
|
||||
buttons: {
|
||||
confirm: {
|
||||
text: 'Usun',
|
||||
text: count === 1 ? 'Usun' : 'Usun (' + count + ')',
|
||||
btnClass: 'btn-red',
|
||||
keys: ['enter'],
|
||||
action: function()
|
||||
{
|
||||
$.ajax({
|
||||
url: '/campaigns/delete_campaign/campaign_id=' + campaign_id,
|
||||
url: '/campaigns/delete_campaigns/',
|
||||
type: 'POST',
|
||||
data: { ids: selected },
|
||||
success: function( response )
|
||||
{
|
||||
var data = JSON.parse( response );
|
||||
if ( data.success )
|
||||
{
|
||||
if ( storage_get( STORAGE_CAMPAIGN_KEY ) === String( campaign_id ) )
|
||||
storage_set( STORAGE_CAMPAIGN_KEY, '' );
|
||||
storage_set( STORAGE_CAMPAIGN_KEY, '' );
|
||||
|
||||
$.alert({
|
||||
title: 'Sukces',
|
||||
content: 'Kampania zostala usunieta.',
|
||||
content: data.deleted === 1 ? 'Kampania zostala usunieta.' : 'Usunieto ' + data.deleted + ' kampanii.',
|
||||
type: 'green',
|
||||
autoClose: 'ok|2000'
|
||||
});
|
||||
@@ -464,8 +461,7 @@ $( function()
|
||||
|
||||
$( 'body' ).on( 'change', '#campaign_id', function()
|
||||
{
|
||||
var campaign_id = $( this ).val();
|
||||
storage_set( STORAGE_CAMPAIGN_KEY, campaign_id );
|
||||
var vals = $( this ).val() || [];
|
||||
|
||||
if ( $.fn.DataTable.isDataTable( '#products' ) )
|
||||
{
|
||||
@@ -473,46 +469,53 @@ $( function()
|
||||
$( '#products tbody' ).empty();
|
||||
}
|
||||
|
||||
if ( !campaign_id )
|
||||
return;
|
||||
if ( vals.length === 1 )
|
||||
{
|
||||
var campaign_id = vals[0];
|
||||
storage_set( STORAGE_CAMPAIGN_KEY, campaign_id );
|
||||
|
||||
new DataTable( '#products', {
|
||||
ajax: {
|
||||
type: 'POST',
|
||||
url: '/campaigns/get_campaign_history_data_table/campaign_id=' + campaign_id,
|
||||
},
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
searching: false,
|
||||
lengthChange: false,
|
||||
pageLength: 15,
|
||||
columns: [
|
||||
{ width: '130px', name: 'date', orderable: false, className: "nowrap" },
|
||||
{ width: '120px', name: 'roas30', orderable: false, className: "dt-type-numeric" },
|
||||
{ width: '120px', name: 'roas_all_time', orderable: false, className: "dt-type-numeric" },
|
||||
{ width: '180px', name: 'conversion_value', orderable: false, className: "dt-type-numeric" },
|
||||
{ width: '140px', name: 'spend30', orderable: false, className: "dt-type-numeric" },
|
||||
{ width: 'auto', name: 'comment', orderable: false },
|
||||
{ width: 'auto', name: 'bidding_strategy', orderable: false },
|
||||
{ width: '100px', name: 'budget', orderable: false, className: "dt-type-numeric" },
|
||||
{ width: '60px', name: 'actions', orderable: false, className: "dt-center" }
|
||||
],
|
||||
language: {
|
||||
processing: 'Ladowanie...',
|
||||
emptyTable: 'Brak danych do wyswietlenia',
|
||||
info: 'Wpisy _START_ - _END_ z _TOTAL_',
|
||||
infoEmpty: '',
|
||||
lengthMenu: 'Pokaz _MENU_ wpisow',
|
||||
paginate: {
|
||||
first: 'Pierwsza',
|
||||
last: 'Ostatnia',
|
||||
next: 'Dalej',
|
||||
previous: 'Wstecz'
|
||||
new DataTable( '#products', {
|
||||
ajax: {
|
||||
type: 'POST',
|
||||
url: '/campaigns/get_campaign_history_data_table/campaign_id=' + campaign_id,
|
||||
},
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
searching: false,
|
||||
lengthChange: false,
|
||||
pageLength: 15,
|
||||
columns: [
|
||||
{ width: '130px', name: 'date', orderable: false, className: "nowrap" },
|
||||
{ width: '120px', name: 'roas30', orderable: false, className: "dt-type-numeric" },
|
||||
{ width: '120px', name: 'roas_all_time', orderable: false, className: "dt-type-numeric" },
|
||||
{ width: '180px', name: 'conversion_value', orderable: false, className: "dt-type-numeric" },
|
||||
{ width: '140px', name: 'spend30', orderable: false, className: "dt-type-numeric" },
|
||||
{ width: 'auto', name: 'comment', orderable: false },
|
||||
{ width: 'auto', name: 'bidding_strategy', orderable: false },
|
||||
{ width: '100px', name: 'budget', orderable: false, className: "dt-type-numeric" },
|
||||
{ width: '60px', name: 'actions', orderable: false, className: "dt-center" }
|
||||
],
|
||||
language: {
|
||||
processing: 'Ladowanie...',
|
||||
emptyTable: 'Brak danych do wyswietlenia',
|
||||
info: 'Wpisy _START_ - _END_ z _TOTAL_',
|
||||
infoEmpty: '',
|
||||
lengthMenu: 'Pokaz _MENU_ wpisow',
|
||||
paginate: {
|
||||
first: 'Pierwsza',
|
||||
last: 'Ostatnia',
|
||||
next: 'Dalej',
|
||||
previous: 'Wstecz'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
reloadChart();
|
||||
reloadChart();
|
||||
}
|
||||
else
|
||||
{
|
||||
storage_set( STORAGE_CAMPAIGN_KEY, '' );
|
||||
}
|
||||
});
|
||||
|
||||
var saved_client_id = storage_get( STORAGE_CLIENT_KEY );
|
||||
@@ -525,4 +528,3 @@ $( function()
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<th>Google Ads Customer ID</th>
|
||||
<th>Merchant Account ID</th>
|
||||
<th>Dane od</th>
|
||||
<th style="width: 160px;">Sync</th>
|
||||
<th style="width: 120px; text-align: center;">Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -45,7 +46,13 @@
|
||||
<span class="text-muted">— brak —</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="client-sync" data-sync-id="<?= $client['id']; ?>"><span class="text-muted">—</span></td>
|
||||
<td class="actions-cell">
|
||||
<?php if ( $client['google_ads_customer_id'] ): ?>
|
||||
<button type="button" class="btn-icon btn-icon-sync" onclick="syncClient(<?= $client['id']; ?>, this)" title="Pobierz dane z Google Ads">
|
||||
<i class="fa-solid fa-rotate"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<button type="button" class="btn-icon btn-icon-edit" onclick="editClient(<?= $client['id']; ?>)" title="Edytuj">
|
||||
<i class="fa-solid fa-pen"></i>
|
||||
</button>
|
||||
@@ -57,7 +64,7 @@
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="6" class="empty-state">
|
||||
<td colspan="7" class="empty-state">
|
||||
<i class="fa-solid fa-building"></i>
|
||||
<p>Brak klientów. Dodaj pierwszego klienta.</p>
|
||||
</td>
|
||||
@@ -135,6 +142,43 @@ function editClient( id )
|
||||
} );
|
||||
}
|
||||
|
||||
function syncClient( id, btn )
|
||||
{
|
||||
var $btn = $( btn );
|
||||
var $icon = $btn.find( 'i' );
|
||||
|
||||
$btn.prop( 'disabled', true );
|
||||
$icon.removeClass( 'fa-rotate' ).addClass( 'fa-spinner fa-spin' );
|
||||
|
||||
$.post( '/clients/force_sync', { id: id }, function( response )
|
||||
{
|
||||
var data = JSON.parse( response );
|
||||
|
||||
$btn.prop( 'disabled', false );
|
||||
$icon.removeClass( 'fa-spinner fa-spin' ).addClass( 'fa-rotate' );
|
||||
|
||||
if ( data.success )
|
||||
{
|
||||
$btn.addClass( 'is-queued' );
|
||||
|
||||
$.alert({
|
||||
title: 'Zakolejkowano',
|
||||
content: 'Klient zostal oznaczony do ponownej synchronizacji. Przy najblizszym uruchomieniu CRON dane kampanii i produktow zostana pobrane od nowa dla calego okna konwersji.',
|
||||
type: 'green',
|
||||
autoClose: 'ok|3000'
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
$.alert({
|
||||
title: 'Blad',
|
||||
content: data.message || 'Nie udalo sie oznaczyc klienta.',
|
||||
type: 'red'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deleteClient( id, name )
|
||||
{
|
||||
$.confirm( {
|
||||
@@ -177,4 +221,50 @@ $( document ).on( 'keydown', function( e ) {
|
||||
$( '#client-modal' ).on( 'click', function( e ) {
|
||||
if ( e.target === this ) closeClientForm();
|
||||
} );
|
||||
|
||||
// --- Sync status bars ---
|
||||
function renderSyncBar( label, done, total )
|
||||
{
|
||||
var pct = total > 0 ? Math.round( done / total * 100 ) : 0;
|
||||
var cls = pct >= 100 ? 'is-done' : ( pct > 0 ? 'is-active' : '' );
|
||||
|
||||
return '<div class="client-sync-row">' +
|
||||
'<span class="client-sync-label">' + label + '</span>' +
|
||||
'<div class="client-sync-track"><div class="client-sync-fill ' + cls + '" style="width:' + pct + '%"></div></div>' +
|
||||
'<span class="client-sync-pct">' + pct + '%</span>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function loadSyncStatus()
|
||||
{
|
||||
$.getJSON( '/clients/sync_status', function( resp )
|
||||
{
|
||||
if ( resp.status !== 'ok' ) return;
|
||||
|
||||
$( '.client-sync' ).each( function()
|
||||
{
|
||||
var $cell = $( this );
|
||||
var id = $cell.data( 'sync-id' );
|
||||
var info = resp.data[ id ];
|
||||
|
||||
if ( !info )
|
||||
{
|
||||
$cell.html( '<span class="text-muted">—</span>' );
|
||||
return;
|
||||
}
|
||||
|
||||
var html = '<div class="client-sync-bars">';
|
||||
if ( info.campaigns ) html += renderSyncBar( 'K:', info.campaigns[0], info.campaigns[1] );
|
||||
if ( info.products ) html += renderSyncBar( 'P:', info.products[0], info.products[1] );
|
||||
html += '</div>';
|
||||
|
||||
$cell.html( html );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
$( document ).ready( function() {
|
||||
loadSyncStatus();
|
||||
setInterval( loadSyncStatus, 15000 );
|
||||
} );
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user