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:
2026-02-19 12:33:14 +01:00
parent bfbcb1c871
commit 38082c5bac
13 changed files with 1039 additions and 838 deletions

View File

@@ -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

View File

@@ -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;