feat: Add XML file management functionality

- Created XmlFiles control class for handling XML file views and regeneration.
- Implemented method to retrieve clients with XML feeds in the factory class.
- Added database migration to include google_merchant_account_id in clients table.
- Created migrations for products_keyword_planner_terms and products_merchant_sync_log tables.
- Added campaign_keywords table migration for managing campaign keyword data.
- Developed main view template for displaying XML files and their statuses.
- Introduced a debug script for analyzing product URLs and their statuses.
This commit is contained in:
2026-02-18 21:23:53 +01:00
parent 3dc06d505a
commit efbdcce08a
36 changed files with 8778 additions and 2615 deletions

View File

@@ -56,6 +56,7 @@
<th>Id oferty</th>
<th>Kampania</th>
<th>Grupa reklam</th>
<th>URL</th>
<th>Nazwa produktu</th>
<th>Wyśw.</th>
<th>Wyśw. (30d)</th>
@@ -83,63 +84,11 @@
$openai_enabled = \services\GoogleAdsApi::get_setting( 'openai_enabled' ) !== '0';
$claude_enabled = \services\GoogleAdsApi::get_setting( 'claude_enabled' ) !== '0';
?>
<style>
.products-page .products-filters .filter-group.filter-group-columns {
min-width: 240px;
}
.products-columns-control {
border: 1px solid #E2E8F0;
border-radius: 6px;
background: #FFFFFF;
overflow: hidden;
}
.products-columns-control summary {
cursor: pointer;
padding: 8px 10px;
font-size: 12px;
font-weight: 600;
color: #334155;
list-style: none;
}
.products-columns-control summary::-webkit-details-marker {
display: none;
}
.products-columns-control summary::after {
content: '\25BC';
float: right;
font-size: 10px;
color: #64748B;
margin-top: 2px;
}
.products-columns-control[open] summary::after {
content: '\25B2';
}
.products-columns-list {
border-top: 1px solid #EEF2F7;
padding: 8px 10px;
max-height: 220px;
overflow-y: auto;
}
.products-columns-list .products-col-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: #334155;
margin-bottom: 6px;
}
.products-columns-list .products-col-item:last-child {
margin-bottom: 0;
}
.products-columns-list .products-col-item input[type=checkbox] {
margin: 0;
}
</style>
<script type="text/javascript">
var AI_OPENAI_ENABLED = <?= $openai_enabled ? 'true' : 'false'; ?>;
var AI_CLAUDE_ENABLED = <?= $claude_enabled ? 'true' : 'false'; ?>;
var PRODUCTS_COLUMNS_STORAGE_KEY = 'products.columns.visibility';
var PRODUCTS_LOCKED_COLUMNS = [ 0, 19 ];
var PRODUCTS_LOCKED_COLUMNS = [ 0, 20 ];
function show_toast( message, type )
{
@@ -160,6 +109,11 @@ function show_toast( message, type )
}, 3000 );
}
function escape_html( value )
{
return $( '<div>' ).text( value == null ? '' : String( value ) ).html();
}
var GOOGLE_TAXONOMY_ENDPOINT = '/tools/google-taxonomy.php';
var googleCategories = [];
@@ -317,7 +271,8 @@ function products_render_columns_picker( table_instance )
}
var id = 'products-col-toggle-' + i;
var th = $( '#products thead th' ).eq( i );
var header_node = table_instance.column( i ).header();
var th = header_node ? $( header_node ) : $();
var title = $.trim( th.find( '.dt-column-title' ).first().text() || th.text() ) || ( 'Kolumna ' + i );
var checked = table_instance.column( i ).visible() ? ' checked' : '';
@@ -356,6 +311,7 @@ $( function()
{ width: '80px', name: 'offer_id' },
{ width: '200px', name: 'campaign_name' },
{ width: '200px', name: 'ad_group_name' },
{ width: '120px', orderable: false, searchable: false },
{ name: 'name' },
{ width: '50px', name: 'impressions' },
{ width: '80px', name: 'impressions_30' },
@@ -370,12 +326,12 @@ $( function()
{ width: '70px', name: 'min_roas' },
{ width: '50px', name: 'cl3', orderable: false },
{ width: '120px', orderable: false },
{ width: '50px', orderable: false, className: 'dt-center' }
{ width: '190px', orderable: false, className: 'dt-center' }
],
order: [ [ 8, 'desc' ] ],
order: [ [ 9, 'desc' ] ],
language: {
processing: '£adowanie...',
emptyTable: 'Brak produktów do wyœwietlenia',
processing: 'Ładowanie...',
emptyTable: 'Brak produktów do wyświetlenia',
info: 'Produkty _START_ - _END_ z _TOTAL_',
infoEmpty: '',
paginate: {
@@ -466,6 +422,308 @@ $( function()
} );
}
$( 'body' ).on( 'click', '.assign-product-scope', function( e )
{
e.preventDefault();
var product_id = $( this ).attr( 'product_id' );
var client_id = $( '#client_id' ).val() || '';
if ( !client_id )
{
$.alert({ title: 'Brak klienta', content: 'Najpierw wybierz klienta, aby przypisać produkt do kampanii i grupy reklam.', type: 'orange' });
return;
}
$.confirm({
title: 'Dodaj produkt do kampanii/grupy',
content: '' +
'<form class="assign-product-form">' +
'<div class="assign-step assign-step-1">' +
'<h4 style="margin-top:0">Krok 1 z 2: Kampania</h4>' +
'<div class="form-group">' +
'<label style="display:block"><input type="radio" name="campaign_mode" value="existing" checked> Istniejąca kampania</label>' +
'<select class="form-control assign-campaign-id" style="margin-top:8px"><option value="">— wybierz kampanię —</option></select>' +
'</div>' +
'<div class="form-group">' +
'<label style="display:block"><input type="radio" name="campaign_mode" value="new"> Nowa kampania</label>' +
'<input type="text" class="form-control assign-campaign-name" placeholder="Nazwa nowej kampanii" style="margin-top:8px;display:none">' +
'<div class="assign-new-campaign-options" style="display:none;margin-top:8px">' +
'<div style="display:flex;gap:8px">' +
'<input type="number" min="1" step="0.01" class="form-control assign-campaign-budget" value="50.00" placeholder="Budżet dzienny (np. 50.00 PLN)">' +
'<input type="number" min="0.1" step="0.01" class="form-control assign-default-cpc" value="1.00" placeholder="Domyślne CPC (np. 1.00 PLN)">' +
'</div>' +
'<small class="text-muted">Dotyczy tylko tworzenia nowej kampanii Standard Shopping.</small>' +
'</div>' +
'</div>' +
'</div>' +
'<div class="assign-step assign-step-2" style="display:none">' +
'<h4 style="margin-top:0">Krok 2 z 2: Grupa reklam</h4>' +
'<div class="form-group">' +
'<label style="display:block"><input type="radio" name="ad_group_mode" value="existing" checked> Istniejąca grupa reklam</label>' +
'<select class="form-control assign-ad-group-id" style="margin-top:8px"><option value="">— wybierz grupę reklam —</option></select>' +
'</div>' +
'<div class="form-group">' +
'<label style="display:block"><input type="radio" name="ad_group_mode" value="new"> Nowa grupa reklam</label>' +
'<input type="text" class="form-control assign-ad-group-name" placeholder="Nazwa nowej grupy reklam" style="margin-top:8px;display:none">' +
'</div>' +
'</div>' +
'</form>',
useBootstrap: false,
boxWidth: '720px',
theme: 'modern',
buttons: {
back: {
text: 'Wstecz',
isHidden: true,
action: function() {
this.$content.find( '.assign-step-1' ).show();
this.$content.find( '.assign-step-2' ).hide();
this.$$back.hide();
this.$$next.show();
this.$$save.hide();
return false;
}
},
next: {
text: 'Dalej',
btnClass: 'btn-blue',
action: function() {
var $content = this.$content;
var campaign_mode = $content.find( 'input[name="campaign_mode"]:checked' ).val();
var campaign_id = $content.find( '.assign-campaign-id' ).val();
var campaign_name = $.trim( $content.find( '.assign-campaign-name' ).val() );
var campaign_daily_budget = parseFloat( $content.find( '.assign-campaign-budget' ).val() || '0' );
var default_cpc = parseFloat( $content.find( '.assign-default-cpc' ).val() || '0' );
if ( campaign_mode === 'existing' && !campaign_id )
{
$.alert( 'Wybierz kampanię.' );
return false;
}
if ( campaign_mode === 'new' && !campaign_name )
{
$.alert( 'Podaj nazwę nowej kampanii.' );
return false;
}
if ( campaign_mode === 'new' && ( isNaN( campaign_daily_budget ) || campaign_daily_budget <= 0 ) )
{
$.alert( 'Podaj poprawny budżet dzienny (większy od 0).' );
return false;
}
if ( campaign_mode === 'new' && ( isNaN( default_cpc ) || default_cpc <= 0 ) )
{
$.alert( 'Podaj poprawne domyślne CPC (większe od 0).' );
return false;
}
this.$content.find( '.assign-step-1' ).hide();
this.$content.find( '.assign-step-2' ).show();
this.$$back.show();
this.$$next.hide();
this.$$save.show();
return false;
}
},
save: {
text: 'Zapisz',
btnClass: 'btn-green',
isHidden: true,
action: function() {
var jc = this;
var $content = jc.$content;
var campaign_mode = $content.find( 'input[name="campaign_mode"]:checked' ).val();
var campaign_id = $content.find( '.assign-campaign-id' ).val() || '';
var campaign_name = $.trim( $content.find( '.assign-campaign-name' ).val() );
var campaign_daily_budget = parseFloat( $content.find( '.assign-campaign-budget' ).val() || '0' );
var default_cpc = parseFloat( $content.find( '.assign-default-cpc' ).val() || '0' );
var ad_group_mode = $content.find( 'input[name="ad_group_mode"]:checked' ).val();
var ad_group_id = $content.find( '.assign-ad-group-id' ).val() || '';
var ad_group_name = $.trim( $content.find( '.assign-ad-group-name' ).val() );
if ( campaign_mode === 'existing' && !campaign_id )
{
$.alert( 'Wybierz kampanię.' );
return false;
}
if ( campaign_mode === 'new' && !campaign_name )
{
$.alert( 'Podaj nazwę nowej kampanii.' );
return false;
}
if ( campaign_mode === 'new' && ( isNaN( campaign_daily_budget ) || campaign_daily_budget <= 0 ) )
{
$.alert( 'Podaj poprawny budżet dzienny (większy od 0).' );
return false;
}
if ( campaign_mode === 'new' && ( isNaN( default_cpc ) || default_cpc <= 0 ) )
{
$.alert( 'Podaj poprawne domyślne CPC (większe od 0).' );
return false;
}
if ( ad_group_mode === 'existing' && !ad_group_id )
{
$.alert( 'Wybierz grupę reklam.' );
return false;
}
if ( ad_group_mode === 'new' && !ad_group_name )
{
$.alert( 'Podaj nazwę nowej grupy reklam.' );
return false;
}
jc.showLoading( true );
$.ajax({
url: '/products/assign_product_scope/',
type: 'POST',
dataType: 'json',
data: {
product_id: product_id,
campaign_mode: campaign_mode,
campaign_id: campaign_id,
campaign_name: campaign_name,
campaign_daily_budget: campaign_daily_budget,
default_cpc: default_cpc,
ad_group_mode: ad_group_mode,
ad_group_id: ad_group_id,
ad_group_name: ad_group_name
},
success: function( res ) {
jc.hideLoading();
if ( res && res.status === 'ok' )
{
jc.close();
reload_products_table();
show_toast( 'Produkt został przypisany do kampanii i grupy reklam.', 'success' );
}
else
{
show_toast( ( res && res.message ) ? res.message : 'Nie udało się zapisać przypisania.', 'error' );
}
},
error: function() {
jc.hideLoading();
show_toast( 'Błąd połączenia podczas przypisywania produktu.', 'error' );
}
});
return false;
}
},
cancel: {
text: 'Anuluj'
}
},
onContentReady: function() {
var jc = this;
var $content = jc.$content;
var $campaignModeInputs = $content.find( 'input[name="campaign_mode"]' );
var $campaignSelect = $content.find( '.assign-campaign-id' );
var $campaignName = $content.find( '.assign-campaign-name' );
var $newCampaignOptions = $content.find( '.assign-new-campaign-options' );
var $adGroupModeInputs = $content.find( 'input[name="ad_group_mode"]' );
var $adGroupSelect = $content.find( '.assign-ad-group-id' );
var $adGroupName = $content.find( '.assign-ad-group-name' );
function loadCampaignsForStep()
{
$campaignSelect.empty().append( '<option value="">— wybierz kampanię —</option>' );
return $.ajax({
url: '/products/get_campaigns_list/client_id=' + client_id,
type: 'GET',
dataType: 'json'
}).done( function( res ) {
( res.campaigns || [] ).forEach( function( row ) {
$campaignSelect.append( '<option value="' + row.id + '">' + escape_html( row.campaign_name || '' ) + '</option>' );
} );
} );
}
function loadAdGroupsForStep( campaign_id )
{
$adGroupSelect.empty().append( '<option value="">— wybierz grupę reklam —</option>' );
if ( !campaign_id )
{
return;
}
$.ajax({
url: '/products/get_campaign_ad_groups/campaign_id=' + campaign_id,
type: 'GET',
dataType: 'json',
success: function( res ) {
( res.ad_groups || [] ).forEach( function( row ) {
$adGroupSelect.append( '<option value="' + row.id + '">' + escape_html( row.ad_group_name || '' ) + '</option>' );
} );
}
});
}
function toggleCampaignMode()
{
var mode = $campaignModeInputs.filter( ':checked' ).val();
var isExisting = mode === 'existing';
$campaignSelect.toggle( isExisting );
$campaignName.toggle( !isExisting );
$newCampaignOptions.toggle( !isExisting );
if ( isExisting )
{
$adGroupModeInputs.filter( '[value="existing"]' ).prop( 'disabled', false );
loadAdGroupsForStep( $campaignSelect.val() || '' );
}
else
{
$adGroupModeInputs.filter( '[value="existing"]' ).prop( 'disabled', true );
$adGroupModeInputs.filter( '[value="new"]' ).prop( 'checked', true );
$adGroupSelect.empty().append( '<option value="">— wybierz grupę reklam —</option>' );
}
toggleAdGroupMode();
}
function toggleAdGroupMode()
{
var mode = $adGroupModeInputs.filter( ':checked' ).val();
var isExisting = mode === 'existing';
$adGroupSelect.toggle( isExisting );
$adGroupName.toggle( !isExisting );
}
$campaignModeInputs.on( 'change', toggleCampaignMode );
$adGroupModeInputs.on( 'change', toggleAdGroupMode );
$campaignSelect.on( 'change', function() {
if ( $campaignModeInputs.filter( ':checked' ).val() === 'existing' )
{
loadAdGroupsForStep( $( this ).val() || '' );
}
} );
loadCampaignsForStep().always( function() {
toggleCampaignMode();
} );
}
});
});
$( 'body' ).on( 'change', '#client_id', function()
{
var client_id = $( this ).val() || '';
@@ -516,6 +774,102 @@ $( function()
} );
});
$( 'body' ).on( 'click', '.view-merchant-logs', function( e )
{
e.preventDefault();
var product_id = $( this ).attr( 'product_id' );
$.confirm({
title: 'Logi synchronizacji Merchant (produkt #' + product_id + ')',
content: '<div class="merchant-logs-wrap" style="max-height:460px;overflow:auto">Ładowanie logów...</div>',
useBootstrap: false,
boxWidth: '1100px',
theme: 'modern',
buttons: {
close: {
text: 'Zamknij',
btnClass: 'btn-blue'
}
},
onContentReady: 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;
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 synchronizacji 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>' + 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>' );
}
});
}
});
});
// Usuwanie produktu
$( 'body' ).on( 'click', '.delete-product', function( e )
{
@@ -584,7 +938,34 @@ $( function()
$.ajax({
url: '/products/save_custom_label_4/',
type: 'POST',
data: { product_id: product_id, custom_label_4: custom_label_4 }
data: { product_id: product_id, custom_label_4: custom_label_4 },
success: function( response )
{
var data;
try
{
data = JSON.parse( response );
}
catch ( e )
{
show_toast( 'Custom Label 4: nieprawidłowa odpowiedź serwera.', 'error' );
return;
}
if ( data.status === 'ok' )
{
show_toast( 'Custom Label 4 zapisany.', 'success' );
}
else
{
show_toast( 'Custom Label 4: zapis nie powiódł się.', 'error' );
}
},
error: function()
{
show_toast( 'Custom Label 4: błąd połączenia podczas zapisu.', 'error' );
}
});
});
@@ -673,6 +1054,7 @@ $( function()
jc.hideLoading();
if ( data.status == 'ok' ) {
jc.close();
reload_products_table();
show_toast( 'Dane produktu zostały zapisane.', 'success' );
} else {
show_toast( 'Błąd: ' + response, 'error' );
@@ -789,10 +1171,18 @@ $( function()
}
if ( data.warning ) {
show_toast( providerLabel + ': ' + data.warning, 'error' );
} else if ( data.page_fetched ) {
show_toast( providerLabel + ': Sugestia wygenerowana z treÅciÄ… strony produktu', 'success' );
} else {
show_toast( providerLabel + ': Sugestia wygenerowana', 'success' );
var successMessage = data.page_fetched
? providerLabel + ': Sugestia wygenerowana z treścią strony produktu'
: providerLabel + ': Sugestia wygenerowana';
if ( ( field == 'title' || field == 'description' ) && data.keyword_planner_terms_used ) {
var kwCount = parseInt( data.keyword_planner_terms_count || 0, 10 );
var kwLabel = kwCount > 0 ? ' (' + kwCount + ' fraz)' : '';
successMessage += '; użyto fraz z Keyword Planner' + kwLabel;
}
show_toast( successMessage, 'success' );
}
} else {
show_toast( providerLabel + ': ' + ( data.message || 'Wystąpił błąd AI.' ), 'error' );