feat: Implement pagination and filtering for linked offers by integration

- Refactored `listLinkedOffersByIntegration` to `paginateLinkedOffersByIntegration` in `MarketplaceRepository`.
- Added pagination support with `page` and `per_page` filters.
- Introduced sorting options for offers.
- Created `listOfferChannelsByIntegration` method to retrieve distinct sales channels.
- Enhanced SQL queries to support dynamic filtering based on provided parameters.

feat: Add new fields for products and SKU generation

- Introduced new fields: `new_to_date`, `additional_message`, `additional_message_required`, and `additional_message_text` in the `products` table.
- Added `findAllSkus` method in `ProductRepository` to retrieve all SKUs.
- Created `ProductSkuGenerator` class to handle SKU generation based on a configurable format.
- Implemented `nextSku` method to generate the next available SKU.

feat: Enhance product settings management in the UI

- Added new settings page for product SKU format in `SettingsController`.
- Implemented form handling for saving SKU format settings.
- Updated the view to include SKU format configuration options.

feat: Implement cron job for refreshing ShopPro offer titles

- Created `ShopProOfferTitlesRefreshHandler` to handle the cron job for refreshing offer titles.
- Integrated with the `OfferImportService` to import offers from ShopPro.

docs: Update database schema documentation

- Added documentation for new fields in the `products` table and new cron job for offer title refresh.
- Documented the purpose and structure of the `app_settings` table.

migrations: Add necessary migrations for new features

- Created migration to add `products_sku_format` setting in `app_settings`.
- Added migration to introduce new fields in the `products` table.
- Created migration for the new cron job schedule for refreshing ShopPro offer titles.
This commit is contained in:
2026-03-01 22:05:21 +01:00
parent bcf078baac
commit d1576bc4ab
28 changed files with 1503 additions and 104 deletions

View File

@@ -6,9 +6,18 @@
.wysiwyg-wrap .ql-editor { min-height: var(--editor-min-height, 80px); }
</style>
<?php
$integrationEditMode = (bool) ($integrationEditMode ?? false);
$productFormAction = (string) ($productFormAction ?? '/products/update');
$productBackUrl = (string) ($productBackUrl ?? '/products');
?>
<section class="card">
<h1><?= $e($t('products.edit.title', ['id' => (string) ($productId ?? 0)])) ?></h1>
<h1><?= $e((string) ($title ?? $t('products.edit.title', ['id' => (string) ($productId ?? 0)]))) ?></h1>
<p class="muted"><?= $e($t('products.edit.description')) ?></p>
<?php if ($integrationEditMode): ?>
<p class="muted mt-8">Tryb integracyjny: zapis aktualizuje bezposrednio produkt w shopPRO i synchronizuje dane lokalne.</p>
<?php endif; ?>
</section>
<section class="card mt-16">
@@ -21,15 +30,17 @@
<?php endif; ?>
<?php $images = is_array($productImages ?? null) ? $productImages : []; ?>
<form class="product-form mt-16" method="post" action="/products/update" enctype="multipart/form-data">
<form class="product-form mt-16" method="post" action="<?= $e($productFormAction) ?>" enctype="multipart/form-data">
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
<input type="hidden" name="id" value="<?= $e((string) ($productId ?? 0)) ?>">
<input type="hidden" id="product-image-csrf" value="<?= $e($csrfToken ?? '') ?>">
<div class="form-grid">
<label class="form-field">
<div class="form-field">
<span class="field-label">SKU</span>
<input class="form-control" type="text" name="sku" value="<?= $e((string) ($form['sku'] ?? '')) ?>">
</label>
<input class="form-control" type="text" id="product-sku-input" name="sku" value="<?= $e((string) ($form['sku'] ?? '')) ?>">
<button type="button" class="btn btn--secondary mt-12" id="product-generate-sku-btn"><?= $e($t('products.actions.generate_next_sku')) ?></button>
</div>
<label class="form-field">
<span class="field-label">EAN</span>
@@ -214,10 +225,10 @@
<?php endforeach; ?>
</div>
<?php if (!$integrationEditMode): ?>
<section class="card mt-16">
<h3><?= $e($t('products.images.title')) ?></h3>
<p class="muted"><?= $e($t('products.images.description')) ?></p>
<input type="hidden" id="product-image-csrf" value="<?= $e($csrfToken ?? '') ?>">
<div class="product-images-grid mt-12" id="product-images-grid" data-product-id="<?= $e((string) ($productId ?? 0)) ?>">
<?php foreach ($images as $image): ?>
@@ -262,14 +273,68 @@
<p class="muted" id="product-image-upload-status"></p>
<p class="muted"><?= $e($t('products.images.main_hint')) ?></p>
</section>
<?php endif; ?>
<div class="form-actions mt-16">
<button class="btn btn--primary" type="submit"><?= $e($t('products.actions.save')) ?></button>
<a class="btn btn--secondary" href="/products"><?= $e($t('products.actions.back')) ?></a>
<a class="btn btn--secondary" href="<?= $e($productBackUrl) ?>"><?= $e($t('products.actions.back')) ?></a>
</div>
</form>
</section>
<script>
(function() {
var skuInput = document.getElementById('product-sku-input');
var generateSkuBtn = document.getElementById('product-generate-sku-btn');
var tokenInput = document.getElementById('product-image-csrf');
if (!skuInput || !generateSkuBtn || !tokenInput) return;
var csrfToken = tokenInput.value || '';
var errTitle = <?= json_encode((string) $t('products.sku_generator.confirm_title'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
var errDefault = <?= json_encode((string) $t('products.sku_generator.failed'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
function showError(message) {
if (window.OrderProAlerts && typeof window.OrderProAlerts.alert === 'function') {
window.OrderProAlerts.alert({
title: errTitle,
message: message || errDefault,
danger: true
});
return;
}
var uploadStatus = document.getElementById('product-image-upload-status');
if (uploadStatus) {
uploadStatus.textContent = message || errDefault;
}
}
generateSkuBtn.addEventListener('click', async function() {
generateSkuBtn.disabled = true;
try {
var payload = new FormData();
payload.append('_token', csrfToken);
var response = await fetch('/products/next-sku', {
method: 'POST',
body: payload,
credentials: 'same-origin'
});
var result = await response.json();
if (!response.ok || result.ok !== true || !result.sku) {
throw new Error(result.message || errDefault);
}
skuInput.value = String(result.sku);
} catch (error) {
showError((error && error.message) ? error.message : errDefault);
} finally {
generateSkuBtn.disabled = false;
}
});
})();
</script>
<script>
(function() {
var grid = document.getElementById('product-images-grid');
@@ -524,17 +589,15 @@
<script>
(function() {
var initialTab = <?= json_encode((string) ($initialContentTab ?? ''), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
var nav = document.getElementById('content-tabs-nav');
if (!nav) return;
nav.addEventListener('click', function(e) {
var btn = e.target.closest('.content-tab-btn');
function setTab(tabId) {
if (!tabId) return;
var btn = nav.querySelector('.content-tab-btn[data-tab="' + tabId.replace(/"/g, '\\"') + '"]');
if (!btn) return;
var tabId = btn.getAttribute('data-tab');
if (!tabId) return;
// deactivate all
nav.querySelectorAll('.content-tab-btn').forEach(function(b) {
b.classList.remove('is-active');
});
@@ -542,10 +605,22 @@
p.classList.remove('is-active');
});
// activate selected
btn.classList.add('is-active');
var panel = document.getElementById('content-tab-' + tabId);
if (panel) panel.classList.add('is-active');
}
nav.addEventListener('click', function(e) {
var btn = e.target.closest('.content-tab-btn');
if (!btn) return;
var tabId = btn.getAttribute('data-tab');
if (!tabId) return;
setTab(tabId);
});
if (initialTab) {
setTab(initialTab);
}
})();
</script>