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:
2026-02-26 20:17:13 +01:00
parent 651d925b20
commit fd0db9b145
35 changed files with 1120 additions and 296 deletions

View File

@@ -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>' );
}
});
});
}
});