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

@@ -1,6 +1,28 @@
<?php $integrationData = is_array($integration ?? null) ? $integration : []; ?>
<?php $rows = is_array($offers ?? null) ? $offers : []; ?>
<?php $integrationId = (int) ($integrationData['id'] ?? 0); ?>
<?php $filters = is_array($filters ?? null) ? $filters : []; ?>
<?php $channelOptions = is_array($channelOptions ?? null) ? $channelOptions : []; ?>
<?php $pagination = is_array($pagination ?? null) ? $pagination : []; ?>
<?php
$currentSort = (string) ($filters['sort'] ?? 'updated_at');
$currentDir = strtoupper((string) ($filters['sort_dir'] ?? 'DESC')) === 'ASC' ? 'ASC' : 'DESC';
$page = max(1, (int) ($pagination['page'] ?? 1));
$totalPages = max(1, (int) ($pagination['total_pages'] ?? 1));
$total = max(0, (int) ($pagination['total'] ?? count($rows)));
$perPage = max(1, (int) ($pagination['per_page'] ?? 20));
$buildUrl = static function (array $params = []) use ($integrationId, $filters): string {
$merged = array_merge($filters, $params);
foreach ($merged as $key => $value) {
if ($value === '' || $value === null) {
unset($merged[$key]);
}
}
$query = http_build_query($merged);
$base = '/marketplace/' . $integrationId;
return $query !== '' ? ($base . '?' . $query) : $base;
};
?>
<section class="card">
<h1><?= $e($t('marketplace.offers_title', ['name' => (string) ($integrationData['name'] ?? '')])) ?></h1>
@@ -13,6 +35,44 @@
<?php if (!empty($errorMessage)): ?>
<div class="alert alert--danger mt-12" role="alert"><?= $e((string) $errorMessage) ?></div>
<?php endif; ?>
<?php if (!empty($successMessage)): ?>
<div class="alert alert--success mt-12" role="status"><?= $e((string) $successMessage) ?></div>
<?php endif; ?>
<form method="get" action="/marketplace/<?= $e((string) $integrationId) ?>" class="table-list-filters mt-12">
<label class="form-field">
<span class="field-label"><?= $e($t('products.filters.search')) ?></span>
<input class="form-control" type="text" name="search" value="<?= $e((string) ($filters['search'] ?? '')) ?>" placeholder="Oferta, SKU, EAN, external ID">
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('marketplace.fields.channel')) ?></span>
<select class="form-control" name="channel">
<option value=""><?= $e($t('products.filters.any')) ?></option>
<?php foreach ($channelOptions as $channelName): ?>
<option value="<?= $e((string) $channelName) ?>"<?= (string) ($filters['channel'] ?? '') === (string) $channelName ? ' selected' : '' ?>>
<?= $e((string) $channelName) ?>
</option>
<?php endforeach; ?>
</select>
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('products.filters.per_page')) ?></span>
<select class="form-control" name="per_page">
<?php foreach ([10, 20, 50, 100] as $opt): ?>
<option value="<?= $e((string) $opt) ?>"<?= $perPage === $opt ? ' selected' : '' ?>><?= $e((string) $opt) ?></option>
<?php endforeach; ?>
</select>
</label>
<input type="hidden" name="sort" value="<?= $e((string) ($filters['sort'] ?? 'updated_at')) ?>">
<input type="hidden" name="sort_dir" value="<?= $e((string) ($filters['sort_dir'] ?? 'DESC')) ?>">
<input type="hidden" name="page" value="1">
<div class="filters-actions">
<button class="btn btn--primary" type="submit"><?= $e($t('products.actions.filter')) ?></button>
<a class="btn btn--secondary" href="/marketplace/<?= $e((string) $integrationId) ?>"><?= $e($t('products.actions.reset')) ?></a>
</div>
</form>
<p class="muted mt-12"><?= $e($t('products.pagination.summary', ['total' => (string) $total])) ?></p>
<?php if ($rows === []): ?>
<p class="muted mt-12"><?= $e($t('marketplace.empty_offers')) ?></p>
@@ -21,23 +81,61 @@
<table class="table">
<thead>
<tr>
<th><?= $e($t('marketplace.fields.offer_name')) ?></th>
<th><?= $e($t('marketplace.fields.external_product_id')) ?></th>
<th><?= $e($t('marketplace.fields.external_variant_id')) ?></th>
<th><?= $e($t('marketplace.fields.external_offer_id')) ?></th>
<th><?= $e($t('marketplace.fields.channel')) ?></th>
<th><?= $e($t('marketplace.fields.product')) ?></th>
<th>SKU</th>
<th>EAN</th>
<th><?= $e($t('marketplace.fields.updated_at')) ?></th>
<th>
<a href="<?= $e($buildUrl(['sort' => 'offer_name', 'sort_dir' => ($currentSort === 'offer_name' && $currentDir === 'ASC') ? 'DESC' : 'ASC', 'page' => 1])) ?>" class="table-sort-link">
<?= $e($t('marketplace.fields.offer_name')) ?><?= $currentSort === 'offer_name' ? ($currentDir === 'ASC' ? ' &uarr;' : ' &darr;') : '' ?>
</a>
</th>
<th>
<a href="<?= $e($buildUrl(['sort' => 'external_product_id', 'sort_dir' => ($currentSort === 'external_product_id' && $currentDir === 'ASC') ? 'DESC' : 'ASC', 'page' => 1])) ?>" class="table-sort-link">
<?= $e($t('marketplace.fields.external_product_id')) ?><?= $currentSort === 'external_product_id' ? ($currentDir === 'ASC' ? ' &uarr;' : ' &darr;') : '' ?>
</a>
</th>
<th>
<a href="<?= $e($buildUrl(['sort' => 'external_variant_id', 'sort_dir' => ($currentSort === 'external_variant_id' && $currentDir === 'ASC') ? 'DESC' : 'ASC', 'page' => 1])) ?>" class="table-sort-link">
<?= $e($t('marketplace.fields.external_variant_id')) ?><?= $currentSort === 'external_variant_id' ? ($currentDir === 'ASC' ? ' &uarr;' : ' &darr;') : '' ?>
</a>
</th>
<th>
<a href="<?= $e($buildUrl(['sort' => 'external_offer_id', 'sort_dir' => ($currentSort === 'external_offer_id' && $currentDir === 'ASC') ? 'DESC' : 'ASC', 'page' => 1])) ?>" class="table-sort-link">
<?= $e($t('marketplace.fields.external_offer_id')) ?><?= $currentSort === 'external_offer_id' ? ($currentDir === 'ASC' ? ' &uarr;' : ' &darr;') : '' ?>
</a>
</th>
<th>
<a href="<?= $e($buildUrl(['sort' => 'channel_name', 'sort_dir' => ($currentSort === 'channel_name' && $currentDir === 'ASC') ? 'DESC' : 'ASC', 'page' => 1])) ?>" class="table-sort-link">
<?= $e($t('marketplace.fields.channel')) ?><?= $currentSort === 'channel_name' ? ($currentDir === 'ASC' ? ' &uarr;' : ' &darr;') : '' ?>
</a>
</th>
<th>
<a href="<?= $e($buildUrl(['sort' => 'product_name', 'sort_dir' => ($currentSort === 'product_name' && $currentDir === 'ASC') ? 'DESC' : 'ASC', 'page' => 1])) ?>" class="table-sort-link">
<?= $e($t('marketplace.fields.product')) ?><?= $currentSort === 'product_name' ? ($currentDir === 'ASC' ? ' &uarr;' : ' &darr;') : '' ?>
</a>
</th>
<th>
<a href="<?= $e($buildUrl(['sort' => 'product_sku', 'sort_dir' => ($currentSort === 'product_sku' && $currentDir === 'ASC') ? 'DESC' : 'ASC', 'page' => 1])) ?>" class="table-sort-link">
SKU<?= $currentSort === 'product_sku' ? ($currentDir === 'ASC' ? ' &uarr;' : ' &darr;') : '' ?>
</a>
</th>
<th>
<a href="<?= $e($buildUrl(['sort' => 'product_ean', 'sort_dir' => ($currentSort === 'product_ean' && $currentDir === 'ASC') ? 'DESC' : 'ASC', 'page' => 1])) ?>" class="table-sort-link">
EAN<?= $currentSort === 'product_ean' ? ($currentDir === 'ASC' ? ' &uarr;' : ' &darr;') : '' ?>
</a>
</th>
<th>
<a href="<?= $e($buildUrl(['sort' => 'updated_at', 'sort_dir' => ($currentSort === 'updated_at' && $currentDir === 'ASC') ? 'DESC' : 'ASC', 'page' => 1])) ?>" class="table-sort-link">
<?= $e($t('marketplace.fields.updated_at')) ?><?= $currentSort === 'updated_at' ? ($currentDir === 'ASC' ? ' &uarr;' : ' &darr;') : '' ?>
</a>
</th>
<th><?= $e($t('marketplace.fields.actions')) ?></th>
<th>Kategorie</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $row): ?>
<?php $productId = (int) ($row['product_id'] ?? 0); ?>
<?php $externalProductId = (int) ($row['external_product_id'] ?? 0); ?>
<tr>
<td><?= $e((string) ($row['offer_name'] ?? '')) ?></td>
<td><?= $e(trim((string) ($row['offer_name'] ?? '')) !== '' ? (string) ($row['offer_name'] ?? '') : '-') ?></td>
<td><?= $e((string) ($row['external_product_id'] ?? '')) ?></td>
<td><?= $e((string) ($row['external_variant_id'] ?? '')) ?></td>
<td><?= $e((string) ($row['external_offer_id'] ?? '')) ?></td>
@@ -50,6 +148,16 @@
<td><?= $e((string) ($row['product_sku'] ?? '')) ?></td>
<td><?= $e((string) ($row['product_ean'] ?? '')) ?></td>
<td><?= $e((string) ($row['updated_at'] ?? '')) ?></td>
<td>
<?php if ($externalProductId > 0): ?>
<a
class="btn btn--secondary btn--sm"
href="/marketplace/<?= $e((string) $integrationId) ?>/product/<?= $e((string) $externalProductId) ?>/edit"
><?= $e($t('marketplace.actions.edit_offer')) ?></a>
<?php else: ?>
<span class="muted">-</span>
<?php endif; ?>
</td>
<td>
<button
type="button"
@@ -63,6 +171,25 @@
</tbody>
</table>
</div>
<div class="table-list__footer mt-12">
<div class="pagination">
<?php $startPage = max(1, $page - 2); ?>
<?php $endPage = min($totalPages, $page + 2); ?>
<a class="pagination__item<?= $page <= 1 ? ' is-disabled' : '' ?>" href="<?= $e($buildUrl(['page' => 1])) ?>">&laquo;</a>
<a class="pagination__item<?= $page <= 1 ? ' is-disabled' : '' ?>" href="<?= $e($buildUrl(['page' => max(1, $page - 1)])) ?>">&lsaquo;</a>
<?php for ($i = $startPage; $i <= $endPage; $i++): ?>
<a class="pagination__item<?= $i === $page ? ' is-active' : '' ?>" href="<?= $e($buildUrl(['page' => $i])) ?>">
<?= $e((string) $i) ?>
</a>
<?php endfor; ?>
<a class="pagination__item<?= $page >= $totalPages ? ' is-disabled' : '' ?>" href="<?= $e($buildUrl(['page' => min($totalPages, $page + 1)])) ?>">&rsaquo;</a>
<a class="pagination__item<?= $page >= $totalPages ? ' is-disabled' : '' ?>" href="<?= $e($buildUrl(['page' => $totalPages])) ?>">&raquo;</a>
</div>
</div>
<?php endif; ?>
</section>