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

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