feat: Add IntegrationRepository and ShopProClient for managing integrations and fetching products from shopPRO API

This commit is contained in:
2026-02-23 23:28:55 +01:00
parent b312dc56e3
commit 18d0019c28
54 changed files with 10397 additions and 393 deletions

View File

@@ -0,0 +1,139 @@
<section class="card">
<h1><?= $e($t('products.create.title')) ?></h1>
<p class="muted"><?= $e($t('products.create.description')) ?></p>
</section>
<section class="card mt-16">
<?php if (!empty($errors)): ?>
<div class="alert alert--danger" role="alert">
<?php foreach ((array) $errors as $error): ?>
<div><?= $e((string) $error) ?></div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<form class="product-form mt-16" method="post" action="/products">
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
<div class="form-grid">
<label class="form-field">
<span class="field-label"><?= $e($t('products.fields.name')) ?></span>
<input class="form-control" type="text" name="name" required value="<?= $e((string) ($form['name'] ?? '')) ?>">
</label>
<label class="form-field">
<span class="field-label">SKU</span>
<input class="form-control" type="text" name="sku" value="<?= $e((string) ($form['sku'] ?? '')) ?>">
</label>
<label class="form-field">
<span class="field-label">EAN</span>
<input class="form-control" type="text" name="ean" value="<?= $e((string) ($form['ean'] ?? '')) ?>">
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('products.fields.type')) ?></span>
<select class="form-control" name="type">
<option value="simple"<?= (string) ($form['type'] ?? '') === 'simple' ? ' selected' : '' ?>><?= $e($t('products.type.simple')) ?></option>
<option value="variant_parent"<?= (string) ($form['type'] ?? '') === 'variant_parent' ? ' selected' : '' ?>><?= $e($t('products.type.variant_parent')) ?></option>
</select>
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('products.fields.status')) ?></span>
<select class="form-control" name="status">
<option value="1"<?= (string) ($form['status'] ?? '1') === '1' ? ' selected' : '' ?>><?= $e($t('products.status.active')) ?></option>
<option value="0"<?= (string) ($form['status'] ?? '1') === '0' ? ' selected' : '' ?>><?= $e($t('products.status.inactive')) ?></option>
</select>
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('products.fields.promoted')) ?></span>
<select class="form-control" name="promoted">
<option value="0"<?= (string) ($form['promoted'] ?? '0') === '0' ? ' selected' : '' ?>><?= $e($t('products.promoted.no')) ?></option>
<option value="1"<?= (string) ($form['promoted'] ?? '0') === '1' ? ' selected' : '' ?>><?= $e($t('products.promoted.yes')) ?></option>
</select>
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('products.fields.vat')) ?></span>
<input class="form-control" type="number" step="0.01" min="0" max="100" name="vat" value="<?= $e((string) ($form['vat'] ?? '')) ?>">
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('products.fields.quantity')) ?></span>
<input class="form-control" type="number" step="0.001" min="0" name="quantity" value="<?= $e((string) ($form['quantity'] ?? '0')) ?>">
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('products.fields.weight')) ?></span>
<input class="form-control" type="number" step="0.001" min="0" name="weight" value="<?= $e((string) ($form['weight'] ?? '')) ?>">
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('products.fields.price_input_mode')) ?></span>
<select class="form-control" name="price_input_mode">
<option value="brutto"<?= (string) ($form['price_input_mode'] ?? 'brutto') === 'brutto' ? ' selected' : '' ?>><?= $e($t('products.price_mode.brutto')) ?></option>
<option value="netto"<?= (string) ($form['price_input_mode'] ?? 'brutto') === 'netto' ? ' selected' : '' ?>><?= $e($t('products.price_mode.netto')) ?></option>
</select>
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('products.fields.price_brutto')) ?></span>
<input class="form-control" type="number" step="0.01" min="0" name="price_brutto" value="<?= $e((string) ($form['price_brutto'] ?? '')) ?>">
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('products.fields.price_netto')) ?></span>
<input class="form-control" type="number" step="0.01" min="0" name="price_netto" value="<?= $e((string) ($form['price_netto'] ?? '')) ?>">
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('products.fields.price_brutto_promo')) ?></span>
<input class="form-control" type="number" step="0.01" min="0" name="price_brutto_promo" value="<?= $e((string) ($form['price_brutto_promo'] ?? '')) ?>">
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('products.fields.price_netto_promo')) ?></span>
<input class="form-control" type="number" step="0.01" min="0" name="price_netto_promo" value="<?= $e((string) ($form['price_netto_promo'] ?? '')) ?>">
</label>
</div>
<label class="form-field mt-16">
<span class="field-label"><?= $e($t('products.fields.short_description')) ?></span>
<textarea class="form-control" name="short_description" rows="3"><?= $e((string) ($form['short_description'] ?? '')) ?></textarea>
</label>
<label class="form-field mt-12">
<span class="field-label"><?= $e($t('products.fields.description')) ?></span>
<textarea class="form-control" name="description" rows="6"><?= $e((string) ($form['description'] ?? '')) ?></textarea>
</label>
<div class="form-grid mt-16">
<label class="form-field">
<span class="field-label"><?= $e($t('products.fields.meta_title')) ?></span>
<input class="form-control" type="text" name="meta_title" value="<?= $e((string) ($form['meta_title'] ?? '')) ?>">
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('products.fields.meta_description')) ?></span>
<input class="form-control" type="text" name="meta_description" value="<?= $e((string) ($form['meta_description'] ?? '')) ?>">
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('products.fields.meta_keywords')) ?></span>
<input class="form-control" type="text" name="meta_keywords" value="<?= $e((string) ($form['meta_keywords'] ?? '')) ?>">
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('products.fields.seo_link')) ?></span>
<input class="form-control" type="text" name="seo_link" value="<?= $e((string) ($form['seo_link'] ?? '')) ?>">
</label>
</div>
<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>
</div>
</form>
</section>

View File

@@ -0,0 +1,383 @@
<section class="card">
<h1><?= $e($t('products.edit.title', ['id' => (string) ($productId ?? 0)])) ?></h1>
<p class="muted"><?= $e($t('products.edit.description')) ?></p>
</section>
<section class="card mt-16">
<?php if (!empty($errors)): ?>
<div class="alert alert--danger" role="alert">
<?php foreach ((array) $errors as $error): ?>
<div><?= $e((string) $error) ?></div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php $images = is_array($productImages ?? null) ? $productImages : []; ?>
<form class="product-form mt-16" method="post" action="/products/update" enctype="multipart/form-data">
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
<input type="hidden" name="id" value="<?= $e((string) ($productId ?? 0)) ?>">
<div class="form-grid">
<label class="form-field">
<span class="field-label"><?= $e($t('products.fields.name')) ?></span>
<input class="form-control" type="text" name="name" required value="<?= $e((string) ($form['name'] ?? '')) ?>">
</label>
<label class="form-field">
<span class="field-label">SKU</span>
<input class="form-control" type="text" name="sku" value="<?= $e((string) ($form['sku'] ?? '')) ?>">
</label>
<label class="form-field">
<span class="field-label">EAN</span>
<input class="form-control" type="text" name="ean" value="<?= $e((string) ($form['ean'] ?? '')) ?>">
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('products.fields.type')) ?></span>
<select class="form-control" name="type">
<option value="simple"<?= (string) ($form['type'] ?? '') === 'simple' ? ' selected' : '' ?>><?= $e($t('products.type.simple')) ?></option>
<option value="variant_parent"<?= (string) ($form['type'] ?? '') === 'variant_parent' ? ' selected' : '' ?>><?= $e($t('products.type.variant_parent')) ?></option>
</select>
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('products.fields.status')) ?></span>
<select class="form-control" name="status">
<option value="1"<?= (string) ($form['status'] ?? '1') === '1' ? ' selected' : '' ?>><?= $e($t('products.status.active')) ?></option>
<option value="0"<?= (string) ($form['status'] ?? '1') === '0' ? ' selected' : '' ?>><?= $e($t('products.status.inactive')) ?></option>
</select>
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('products.fields.promoted')) ?></span>
<select class="form-control" name="promoted">
<option value="0"<?= (string) ($form['promoted'] ?? '0') === '0' ? ' selected' : '' ?>><?= $e($t('products.promoted.no')) ?></option>
<option value="1"<?= (string) ($form['promoted'] ?? '0') === '1' ? ' selected' : '' ?>><?= $e($t('products.promoted.yes')) ?></option>
</select>
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('products.fields.vat')) ?></span>
<input class="form-control" type="number" step="0.01" min="0" max="100" name="vat" value="<?= $e((string) ($form['vat'] ?? '')) ?>">
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('products.fields.quantity')) ?></span>
<input class="form-control" type="number" step="0.001" min="0" name="quantity" value="<?= $e((string) ($form['quantity'] ?? '0')) ?>">
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('products.fields.weight')) ?></span>
<input class="form-control" type="number" step="0.001" min="0" name="weight" value="<?= $e((string) ($form['weight'] ?? '')) ?>">
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('products.fields.price_input_mode')) ?></span>
<select class="form-control" name="price_input_mode">
<option value="brutto"<?= (string) ($form['price_input_mode'] ?? 'brutto') === 'brutto' ? ' selected' : '' ?>><?= $e($t('products.price_mode.brutto')) ?></option>
<option value="netto"<?= (string) ($form['price_input_mode'] ?? 'brutto') === 'netto' ? ' selected' : '' ?>><?= $e($t('products.price_mode.netto')) ?></option>
</select>
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('products.fields.price_brutto')) ?></span>
<input class="form-control" type="number" step="0.01" min="0" name="price_brutto" value="<?= $e((string) ($form['price_brutto'] ?? '')) ?>">
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('products.fields.price_netto')) ?></span>
<input class="form-control" type="number" step="0.01" min="0" name="price_netto" value="<?= $e((string) ($form['price_netto'] ?? '')) ?>">
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('products.fields.price_brutto_promo')) ?></span>
<input class="form-control" type="number" step="0.01" min="0" name="price_brutto_promo" value="<?= $e((string) ($form['price_brutto_promo'] ?? '')) ?>">
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('products.fields.price_netto_promo')) ?></span>
<input class="form-control" type="number" step="0.01" min="0" name="price_netto_promo" value="<?= $e((string) ($form['price_netto_promo'] ?? '')) ?>">
</label>
</div>
<label class="form-field mt-16">
<span class="field-label"><?= $e($t('products.fields.short_description')) ?></span>
<textarea class="form-control" name="short_description" rows="3"><?= $e((string) ($form['short_description'] ?? '')) ?></textarea>
</label>
<label class="form-field mt-12">
<span class="field-label"><?= $e($t('products.fields.description')) ?></span>
<textarea class="form-control" name="description" rows="6"><?= $e((string) ($form['description'] ?? '')) ?></textarea>
</label>
<div class="form-grid mt-16">
<label class="form-field">
<span class="field-label"><?= $e($t('products.fields.meta_title')) ?></span>
<input class="form-control" type="text" name="meta_title" value="<?= $e((string) ($form['meta_title'] ?? '')) ?>">
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('products.fields.meta_description')) ?></span>
<input class="form-control" type="text" name="meta_description" value="<?= $e((string) ($form['meta_description'] ?? '')) ?>">
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('products.fields.meta_keywords')) ?></span>
<input class="form-control" type="text" name="meta_keywords" value="<?= $e((string) ($form['meta_keywords'] ?? '')) ?>">
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('products.fields.seo_link')) ?></span>
<input class="form-control" type="text" name="seo_link" value="<?= $e((string) ($form['seo_link'] ?? '')) ?>">
</label>
</div>
<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): ?>
<?php
$imageId = (int) ($image['id'] ?? 0);
$isMain = (int) ($image['is_main'] ?? 0) === 1;
$publicUrl = (string) ($image['public_url'] ?? '');
?>
<article
class="product-image-card<?= $isMain ? ' is-main' : '' ?>"
data-image-id="<?= $e((string) $imageId) ?>"
data-storage-path="<?= $e((string) ($image['storage_path'] ?? '')) ?>"
>
<div class="product-image-card__thumb-wrap">
<?php if ($publicUrl !== ''): ?>
<img class="product-image-card__thumb" src="<?= $e($publicUrl) ?>" alt="<?= $e((string) ($image['alt'] ?? '')) ?>">
<?php else: ?>
<div class="product-image-card__thumb is-empty">NO IMAGE</div>
<?php endif; ?>
<span class="product-image-card__badge"><?= $e($t('products.images.main')) ?></span>
</div>
<div class="product-image-card__meta"><?= $e((string) ($image['storage_path'] ?? '')) ?></div>
<div class="product-image-card__actions">
<button type="button" class="btn btn--secondary btn-set-main"<?= $isMain ? ' disabled' : '' ?>>
<?= $e($t('products.images.set_main')) ?>
</button>
<button type="button" class="btn btn--danger btn-delete-image">
<?= $e($t('products.images.remove')) ?>
</button>
</div>
</article>
<?php endforeach; ?>
</div>
<p class="muted mt-12" id="product-images-empty"<?= $images === [] ? '' : ' style="display:none;"' ?>>
<?= $e($t('products.images.empty')) ?>
</p>
<label class="form-field mt-16">
<span class="field-label"><?= $e($t('products.images.add_new')) ?></span>
<input class="form-control" type="file" id="product-image-upload" name="new_images[]" accept=".jpg,.jpeg,.png,.webp,.gif,image/*" multiple>
</label>
<p class="muted" id="product-image-upload-status"></p>
<p class="muted"><?= $e($t('products.images.main_hint')) ?></p>
</section>
<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>
</div>
</form>
</section>
<script>
(function() {
var grid = document.getElementById('product-images-grid');
var emptyState = document.getElementById('product-images-empty');
var uploadInput = document.getElementById('product-image-upload');
var uploadStatus = document.getElementById('product-image-upload-status');
var tokenInput = document.getElementById('product-image-csrf');
if (!grid || !uploadInput || !tokenInput) return;
var productId = grid.getAttribute('data-product-id');
var csrfToken = tokenInput.value || '';
var txtSetMain = <?= json_encode((string) $t('products.images.set_main'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
var txtRemove = <?= json_encode((string) $t('products.images.remove'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
var txtMain = <?= json_encode((string) $t('products.images.main'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
var txtUploadPending = <?= json_encode((string) $t('products.images.uploading'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
var txtUploadOk = <?= json_encode((string) $t('products.images.uploaded_ok'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
var txtDeleteConfirm = <?= json_encode((string) $t('products.images.confirm_delete'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
var txtConfirmTitle = <?= json_encode((string) $t('products.images.confirm_title'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
var txtConfirmYes = <?= json_encode((string) $t('products.images.confirm_yes'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
var txtConfirmNo = <?= json_encode((string) $t('products.images.confirm_no'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
function refreshMainState(mainId) {
var cards = grid.querySelectorAll('.product-image-card');
cards.forEach(function(card) {
var cardId = Number(card.getAttribute('data-image-id') || 0);
var isMain = cardId === mainId;
card.classList.toggle('is-main', isMain);
var setMainBtn = card.querySelector('.btn-set-main');
if (setMainBtn) setMainBtn.disabled = isMain;
});
}
function updateEmptyState() {
if (!emptyState) return;
emptyState.style.display = grid.querySelector('.product-image-card') ? 'none' : '';
}
function buildCard(image) {
var article = document.createElement('article');
article.className = 'product-image-card' + (Number(image.is_main) === 1 ? ' is-main' : '');
article.setAttribute('data-image-id', String(image.id));
article.setAttribute('data-storage-path', String(image.storage_path || ''));
var thumbWrap = document.createElement('div');
thumbWrap.className = 'product-image-card__thumb-wrap';
if (image.public_url) {
var img = document.createElement('img');
img.className = 'product-image-card__thumb';
img.src = image.public_url;
img.alt = image.alt || '';
thumbWrap.appendChild(img);
} else {
var noimg = document.createElement('div');
noimg.className = 'product-image-card__thumb is-empty';
noimg.textContent = 'NO IMAGE';
thumbWrap.appendChild(noimg);
}
var badge = document.createElement('span');
badge.className = 'product-image-card__badge';
badge.textContent = txtMain;
thumbWrap.appendChild(badge);
var meta = document.createElement('div');
meta.className = 'product-image-card__meta';
meta.textContent = image.storage_path || '';
var actions = document.createElement('div');
actions.className = 'product-image-card__actions';
var setMainBtn = document.createElement('button');
setMainBtn.type = 'button';
setMainBtn.className = 'btn btn--secondary btn-set-main';
setMainBtn.textContent = txtSetMain;
if (Number(image.is_main) === 1) setMainBtn.disabled = true;
var removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'btn btn--danger btn-delete-image';
removeBtn.textContent = txtRemove;
actions.appendChild(setMainBtn);
actions.appendChild(removeBtn);
article.appendChild(thumbWrap);
article.appendChild(meta);
article.appendChild(actions);
return article;
}
async function postForm(url, data) {
var response = await fetch(url, { method: 'POST', body: data, credentials: 'same-origin' });
var payload = await response.json();
if (!response.ok || payload.ok !== true) {
throw new Error(payload.message || 'Blad operacji.');
}
return payload;
}
uploadInput.addEventListener('change', async function() {
if (!uploadInput.files || uploadInput.files.length === 0) return;
var formData = new FormData();
formData.append('_token', csrfToken);
formData.append('id', String(productId));
Array.prototype.forEach.call(uploadInput.files, function(file) {
formData.append('new_images[]', file);
});
uploadStatus.textContent = txtUploadPending;
uploadInput.disabled = true;
try {
var result = await postForm('/products/images/upload', formData);
(result.images || []).forEach(function(image) {
grid.appendChild(buildCard(image));
if (Number(image.is_main) === 1) refreshMainState(Number(image.id));
});
uploadStatus.textContent = txtUploadOk;
if (result.message) uploadStatus.textContent += ' ' + result.message;
updateEmptyState();
} catch (error) {
uploadStatus.textContent = error.message || 'Blad uploadu.';
} finally {
uploadInput.value = '';
uploadInput.disabled = false;
}
});
grid.addEventListener('click', async function(event) {
var target = event.target;
if (!target || !(target instanceof HTMLElement)) return;
var card = target.closest('.product-image-card');
if (!card) return;
var imageId = Number(card.getAttribute('data-image-id') || 0);
if (imageId <= 0) return;
if (target.classList.contains('btn-set-main')) {
if (target.disabled) return;
var dataMain = new FormData();
dataMain.append('_token', csrfToken);
dataMain.append('id', String(productId));
dataMain.append('image_id', String(imageId));
target.disabled = true;
try {
await postForm('/products/images/set-main', dataMain);
refreshMainState(imageId);
} catch (error) {
target.disabled = false;
if (window.OrderProAlerts && typeof window.OrderProAlerts.alert === 'function') {
window.OrderProAlerts.alert({ title: 'Blad', message: error.message || 'Blad operacji.', danger: true });
} else if (uploadStatus) {
uploadStatus.textContent = error.message || 'Blad operacji.';
}
}
}
if (target.classList.contains('btn-delete-image')) {
var confirmDelete = async function() {
var dataDelete = new FormData();
dataDelete.append('_token', csrfToken);
dataDelete.append('id', String(productId));
dataDelete.append('image_id', String(imageId));
var result = await postForm('/products/images/delete', dataDelete);
card.remove();
if (Number(result.main_image_id || 0) > 0) {
refreshMainState(Number(result.main_image_id));
}
updateEmptyState();
};
if (window.OrderProAlerts && typeof window.OrderProAlerts.confirm === 'function') {
var accepted = await window.OrderProAlerts.confirm({
title: txtConfirmTitle,
message: txtDeleteConfirm,
confirmLabel: txtConfirmYes,
cancelLabel: txtConfirmNo,
danger: true
});
if (!accepted) return;
try { await confirmDelete(); } catch (error) {
window.OrderProAlerts.alert({ title: 'Blad', message: error.message || 'Blad operacji.', danger: true });
}
} else {
try { await confirmDelete(); } catch (error) {
if (uploadStatus) uploadStatus.textContent = error.message || 'Blad operacji.';
}
}
}
});
})();
</script>

View File

@@ -0,0 +1,177 @@
<section class="card">
<div class="page-head">
<div>
<h1><?= $e($t('products.title')) ?></h1>
<p class="muted"><?= $e($t('products.description')) ?></p>
</div>
</div>
</section>
<?php if (!empty($errorMessage)): ?>
<section class="card mt-16">
<div class="alert alert--danger" role="alert">
<?= $e((string) $errorMessage) ?>
</div>
</section>
<?php endif; ?>
<?php if (!empty($successMessage)): ?>
<section class="card mt-16">
<div class="alert alert--success" role="status">
<?= $e((string) $successMessage) ?>
</div>
</section>
<?php endif; ?>
<?php require __DIR__ . '/../components/table-list.php'; ?>
<?php
$integrations = is_array($shopProIntegrations ?? null) ? $shopProIntegrations : [];
?>
<div class="modal-backdrop" data-modal-backdrop="product-image-preview-modal" hidden>
<div class="modal modal--image-preview" role="dialog" aria-modal="true" aria-labelledby="product-image-preview-title">
<div class="modal__header">
<h3 id="product-image-preview-title">Podglad zdjecia</h3>
<button type="button" class="btn btn--secondary" data-close-modal="product-image-preview-modal">Zamknij</button>
</div>
<div class="modal__body">
<img src="" alt="" class="product-image-preview__img" data-product-image-preview-target>
</div>
</div>
</div>
<div class="modal-backdrop" data-modal-backdrop="product-import-modal" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="product-import-modal-title">
<div class="modal__header">
<h3 id="product-import-modal-title"><?= $e($t('products.import.title')) ?></h3>
<button type="button" class="btn btn--secondary" data-close-modal="product-import-modal"><?= $e($t('products.import.close')) ?></button>
</div>
<form action="/products/import/shoppro" method="post" class="modal__body">
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
<label class="form-field">
<span class="field-label"><?= $e($t('products.import.integration')) ?></span>
<select class="form-control" name="integration_id" required>
<option value=""><?= $e($t('products.import.integration_placeholder')) ?></option>
<?php foreach ($integrations as $integration): ?>
<option value="<?= $e((string) ($integration['id'] ?? 0)) ?>">
<?= $e((string) ($integration['name'] ?? '')) ?> (ID: <?= $e((string) ($integration['id'] ?? 0)) ?>)
</option>
<?php endforeach; ?>
</select>
</label>
<?php if (empty($integrations)): ?>
<p class="muted"><?= $e($t('products.import.no_integrations')) ?></p>
<?php endif; ?>
<div class="form-field">
<span class="field-label"><?= $e($t('products.import.mode')) ?></span>
<label class="field-inline">
<input type="radio" name="import_mode" value="all" checked>
<?= $e($t('products.import.mode_all')) ?>
</label>
<label class="field-inline">
<input type="radio" name="import_mode" value="single">
<?= $e($t('products.import.mode_single')) ?>
</label>
</div>
<label class="form-field" data-single-id-wrap hidden>
<span class="field-label"><?= $e($t('products.import.external_id')) ?></span>
<input class="form-control" type="number" min="1" name="external_product_id" data-single-id-input>
</label>
<label class="form-field">
<span class="field-label">
<input type="checkbox" name="import_variants" value="1">
<?= $e($t('products.import.with_variants')) ?>
</span>
<small class="muted"><?= $e($t('products.import.with_variants_hint')) ?></small>
</label>
<div class="form-actions mt-16">
<button type="submit" class="btn btn--primary"<?= empty($integrations) ? ' disabled' : '' ?>><?= $e($t('products.import.submit')) ?></button>
</div>
</form>
</div>
</div>
<script>
(function() {
var openBtn = document.querySelector('[data-open-modal="product-import-modal"]');
var backdrop = document.querySelector('[data-modal-backdrop="product-import-modal"]');
var closeBtn = document.querySelector('[data-close-modal="product-import-modal"]');
if (!openBtn || !backdrop || !closeBtn) return;
var modeInputs = backdrop.querySelectorAll('input[name="import_mode"]');
var singleIdWrap = backdrop.querySelector('[data-single-id-wrap]');
var singleIdInput = backdrop.querySelector('[data-single-id-input]');
function syncMode() {
var mode = 'all';
modeInputs.forEach(function(input) {
if (input.checked) mode = input.value;
});
var isSingle = mode === 'single';
if (singleIdWrap) singleIdWrap.hidden = !isSingle;
if (singleIdInput) {
singleIdInput.required = isSingle;
if (!isSingle) singleIdInput.value = '';
}
}
openBtn.addEventListener('click', function() {
backdrop.hidden = false;
syncMode();
});
closeBtn.addEventListener('click', function() {
backdrop.hidden = true;
});
backdrop.addEventListener('click', function(event) {
if (event.target === backdrop) {
backdrop.hidden = true;
}
});
modeInputs.forEach(function(input) {
input.addEventListener('change', syncMode);
});
})();
(function() {
var previewBackdrop = document.querySelector('[data-modal-backdrop="product-image-preview-modal"]');
if (!previewBackdrop) return;
var previewImage = previewBackdrop.querySelector('[data-product-image-preview-target]');
var closeBtn = previewBackdrop.querySelector('[data-close-modal="product-image-preview-modal"]');
if (!previewImage || !closeBtn) return;
function closePreview() {
previewBackdrop.hidden = true;
previewImage.setAttribute('src', '');
}
document.addEventListener('click', function(event) {
var trigger = event.target.closest('[data-product-image-preview]');
if (!trigger) return;
var imageUrl = trigger.getAttribute('data-product-image-preview') || '';
if (imageUrl === '') return;
previewImage.setAttribute('src', imageUrl);
previewBackdrop.hidden = false;
});
closeBtn.addEventListener('click', closePreview);
previewBackdrop.addEventListener('click', function(event) {
if (event.target === previewBackdrop) {
closePreview();
}
});
})();
</script>

View File

@@ -0,0 +1,251 @@
<?php $item = is_array($product ?? null) ? $product : []; ?>
<?php $links = is_array($productLinks ?? null) ? $productLinks : []; ?>
<?php $integrations = is_array($linkIntegrations ?? null) ? $linkIntegrations : []; ?>
<?php $offers = is_array($linkOffers ?? null) ? $linkOffers : []; ?>
<?php $eventsByMap = is_array($productLinkEventsByMap ?? null) ? $productLinkEventsByMap : []; ?>
<?php $selectedIntegrationId = (int) ($selectedLinksIntegrationId ?? 0); ?>
<?php $linksQueryValue = (string) ($linksQuery ?? ''); ?>
<?php $productIdValue = (int) ($productId ?? 0); ?>
<section class="card">
<h1><?= $e($t('products.links.page_title', ['id' => (string) ($productId ?? 0)])) ?></h1>
<p class="muted"><?= $e($t('products.links.description')) ?></p>
</section>
<section class="card mt-16">
<div class="product-tabs-nav">
<a class="btn btn--secondary" href="/products/<?= $e((string) $productIdValue) ?>"><?= $e($t('products.tabs.details')) ?></a>
<span class="btn btn--primary"><?= $e($t('products.tabs.links')) ?></span>
</div>
</section>
<section class="card mt-16">
<div class="product-links-head">
<div>
<strong><?= $e($t('products.fields.name')) ?>:</strong>
<?= $e((string) ($item['name'] ?? '')) ?>
</div>
<div>
<strong>SKU:</strong>
<?= $e((string) ($item['sku'] ?? '')) ?>
</div>
<div>
<strong>EAN:</strong>
<?= $e((string) ($item['ean'] ?? '')) ?>
</div>
</div>
</section>
<section class="card mt-16">
<h3><?= $e($t('products.links.title')) ?></h3>
<?php if (!empty($linksErrorMessage)): ?>
<div class="alert alert--danger mt-12" role="alert"><?= $e((string) $linksErrorMessage) ?></div>
<?php endif; ?>
<?php if (!empty($linksSuccessMessage)): ?>
<div class="alert alert--success mt-12" role="status"><?= $e((string) $linksSuccessMessage) ?></div>
<?php endif; ?>
<h4 class="section-title mt-16"><?= $e($t('products.links.current_links')) ?></h4>
<?php if ($links === []): ?>
<p class="muted mt-12"><?= $e($t('products.links.empty_links')) ?></p>
<?php else: ?>
<div class="table-wrap mt-12">
<table class="table">
<thead>
<tr>
<th><?= $e($t('products.links.fields.integration')) ?></th>
<th><?= $e($t('products.links.fields.channel')) ?></th>
<th><?= $e($t('products.links.fields.external_product_id')) ?></th>
<th><?= $e($t('products.links.fields.external_variant_id')) ?></th>
<th><?= $e($t('products.links.fields.link_type')) ?></th>
<th><?= $e($t('products.links.fields.confidence')) ?></th>
<th><?= $e($t('products.links.fields.link_status')) ?></th>
<th><?= $e($t('products.links.fields.updated_at')) ?></th>
<th><?= $e($t('products.links.fields.history')) ?></th>
<th><?= $e($t('products.links.fields.actions')) ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($links as $link): ?>
<?php
$mapId = (int) ($link['id'] ?? 0);
$linkStatus = (string) ($link['link_status'] ?? '');
$isActive = $linkStatus === 'active';
$confidence = $link['confidence'] ?? null;
$lastChangeAt = trim((string) ($link['updated_at'] ?? ''));
if ($lastChangeAt === '') {
$lastChangeAt = trim((string) ($link['linked_at'] ?? ''));
}
?>
<tr>
<td><?= $e((string) (($link['integration_name'] ?? '') !== '' ? $link['integration_name'] : ('#' . (string) ($link['integration_id'] ?? 0)))) ?></td>
<td><?= $e((string) ($link['channel_name'] ?? '')) ?></td>
<td><?= $e((string) ($link['external_product_id'] ?? '')) ?></td>
<td><?= $e((string) ($link['external_variant_id'] ?? '')) ?></td>
<td><?= $e((string) ($link['link_type'] ?? '')) ?></td>
<td><?= $e($confidence === null ? '-' : ((string) $confidence . '%')) ?></td>
<td>
<span class="status-pill<?= $isActive ? ' is-active' : '' ?>">
<?= $e($linkStatus) ?>
</span>
</td>
<td><?= $e($lastChangeAt === '' ? '-' : $lastChangeAt) ?></td>
<td>
<?php $events = is_array($eventsByMap[$mapId] ?? null) ? $eventsByMap[$mapId] : []; ?>
<?php if ($events === []): ?>
<span class="muted">-</span>
<?php else: ?>
<ul class="product-link-events-list">
<?php foreach ($events as $event): ?>
<li>
<span class="product-link-events-type"><?= $e((string) ($event['event_type'] ?? '')) ?></span>
<span class="product-link-events-date"><?= $e((string) ($event['created_at'] ?? '')) ?></span>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</td>
<td>
<div class="product-links-actions-row">
<form action="/products/<?= $e((string) $productIdValue) ?>/links/<?= $e((string) $mapId) ?>/relink" method="post" class="product-links-inline-form product-links-relink-form">
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
<input type="hidden" name="product_id" value="<?= $e((string) ($productId ?? 0)) ?>">
<input type="hidden" name="map_id" value="<?= $e((string) $mapId) ?>">
<input type="hidden" name="integration_id" value="<?= $e((string) ((int) ($link['integration_id'] ?? 0))) ?>">
<input class="form-control" type="text" name="external_product_id" required value="<?= $e((string) ($link['external_product_id'] ?? '')) ?>">
<input class="form-control" type="text" name="external_variant_id" value="<?= $e((string) ($link['external_variant_id'] ?? '')) ?>" placeholder="<?= $e($t('products.links.fields.external_variant_id_optional')) ?>">
<button type="submit" class="btn btn--secondary" data-links-action="relink"><?= $e($t('products.links.actions.relink')) ?></button>
</form>
<form action="/products/<?= $e((string) $productIdValue) ?>/links/<?= $e((string) $mapId) ?>/unlink" method="post" class="product-links-unlink-form">
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
<input type="hidden" name="product_id" value="<?= $e((string) ($productId ?? 0)) ?>">
<input type="hidden" name="map_id" value="<?= $e((string) $mapId) ?>">
<button type="submit" class="btn btn--danger" data-links-action="unlink"><?= $e($t('products.links.actions.unlink')) ?></button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<h4 class="section-title mt-16"><?= $e($t('products.links.search_title')) ?></h4>
<form class="product-links-search-form mt-12" action="/products/<?= $e((string) $productIdValue) ?>/links" method="get">
<input type="hidden" name="id" value="<?= $e((string) ($productId ?? 0)) ?>">
<label class="form-field">
<span class="field-label"><?= $e($t('products.links.fields.integration')) ?></span>
<select class="form-control" name="links_integration_id" required>
<option value="0"><?= $e($t('products.links.integration_placeholder')) ?></option>
<?php foreach ($integrations as $integration): ?>
<?php $integrationId = (int) ($integration['id'] ?? 0); ?>
<option value="<?= $e((string) $integrationId) ?>"<?= $integrationId === $selectedIntegrationId ? ' selected' : '' ?>>
<?= $e((string) ($integration['name'] ?? '')) ?>
</option>
<?php endforeach; ?>
</select>
</label>
<label class="form-field">
<span class="field-label"><?= $e($t('products.links.fields.search')) ?></span>
<input class="form-control" type="text" name="links_query" value="<?= $e($linksQueryValue) ?>" placeholder="<?= $e($t('products.links.search_placeholder')) ?>">
</label>
<button type="submit" class="btn btn--primary"><?= $e($t('products.links.actions.search')) ?></button>
</form>
<?php if ($offers === []): ?>
<p class="muted mt-12"><?= $e($t('products.links.empty_offers')) ?></p>
<?php else: ?>
<div class="table-wrap mt-12">
<table class="table">
<thead>
<tr>
<th><?= $e($t('products.links.fields.offer_name')) ?></th>
<th>SKU</th>
<th>EAN</th>
<th><?= $e($t('products.links.fields.external_product_id')) ?></th>
<th><?= $e($t('products.links.fields.external_variant_id')) ?></th>
<th><?= $e($t('products.links.fields.match_hint')) ?></th>
<th><?= $e($t('products.links.fields.confidence')) ?></th>
<th><?= $e($t('products.links.fields.actions')) ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($offers as $offer): ?>
<tr>
<td><?= $e((string) ($offer['name'] ?? '')) ?></td>
<td><?= $e((string) ($offer['sku'] ?? '')) ?></td>
<td><?= $e((string) ($offer['ean'] ?? '')) ?></td>
<td><?= $e((string) ($offer['external_product_id'] ?? '')) ?></td>
<td><?= $e((string) ($offer['external_variant_id'] ?? '')) ?></td>
<td><?= $e((string) ($offer['match_hint'] ?? '')) ?></td>
<td><?= $e((string) ((int) ($offer['match_confidence'] ?? 0)) . '%') ?></td>
<td>
<form action="/products/<?= $e((string) $productIdValue) ?>/links" method="post">
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
<input type="hidden" name="product_id" value="<?= $e((string) ($productId ?? 0)) ?>">
<input type="hidden" name="integration_id" value="<?= $e((string) ($offer['integration_id'] ?? 0)) ?>">
<input type="hidden" name="external_product_id" value="<?= $e((string) ($offer['external_product_id'] ?? '')) ?>">
<input type="hidden" name="external_variant_id" value="<?= $e((string) ($offer['external_variant_id'] ?? '')) ?>">
<button type="submit" class="btn btn--primary"><?= $e($t('products.links.actions.link')) ?></button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</section>
<section class="card mt-16">
<a class="btn btn--secondary" href="/products"><?= $e($t('products.actions.back')) ?></a>
<a class="btn btn--secondary" href="/products/<?= $e((string) $productIdValue) ?>"><?= $e($t('products.actions.preview')) ?></a>
<a class="btn btn--primary" href="/products/edit?id=<?= $e((string) ($productId ?? 0)) ?>"><?= $e($t('products.actions.edit')) ?></a>
</section>
<script>
(function () {
var unlinkForms = document.querySelectorAll('.product-links-unlink-form');
var relinkForms = document.querySelectorAll('.product-links-relink-form');
var unlinkMessage = <?= json_encode((string) $t('products.links.confirm.unlink_message'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
var relinkMessage = <?= json_encode((string) $t('products.links.confirm.relink_message'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
var confirmTitle = <?= json_encode((string) $t('products.links.confirm.title'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
var confirmYes = <?= json_encode((string) $t('products.links.confirm.yes'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
var confirmNo = <?= json_encode((string) $t('products.links.confirm.no'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
async function handleConfirmSubmit(event, message, danger) {
if (!window.OrderProAlerts || typeof window.OrderProAlerts.confirm !== 'function') {
return;
}
event.preventDefault();
var accepted = await window.OrderProAlerts.confirm({
title: confirmTitle,
message: message,
confirmLabel: confirmYes,
cancelLabel: confirmNo,
danger: danger === true
});
if (!accepted) {
return;
}
event.target.submit();
}
unlinkForms.forEach(function (form) {
form.addEventListener('submit', function (event) {
handleConfirmSubmit(event, unlinkMessage, true);
});
});
relinkForms.forEach(function (form) {
form.addEventListener('submit', function (event) {
handleConfirmSubmit(event, relinkMessage, false);
});
});
})();
</script>

View File

@@ -0,0 +1,135 @@
<section class="card">
<h1><?= $e($t('products.show.title', ['id' => (string) ($productId ?? 0)])) ?></h1>
<p class="muted"><?= $e($t('products.show.description')) ?></p>
</section>
<?php $item = is_array($product ?? null) ? $product : []; ?>
<?php $images = is_array($productImages ?? null) ? $productImages : []; ?>
<?php $variants = is_array($productVariants ?? null) ? $productVariants : []; ?>
<?php $importWarning = is_array($productImportWarning ?? null) ? $productImportWarning : null; ?>
<?php $productIdValue = (int) ($productId ?? 0); ?>
<section class="card mt-16">
<div class="product-tabs-nav">
<span class="btn btn--primary"><?= $e($t('products.tabs.details')) ?></span>
<a class="btn btn--secondary" href="/products/<?= $e((string) $productIdValue) ?>/links"><?= $e($t('products.tabs.links')) ?></a>
</div>
</section>
<section class="card mt-16">
<?php if ($importWarning !== null && !empty($importWarning['messages'])): ?>
<div class="alert alert--danger" role="alert">
<div><strong><?= $e($t('products.variants.import_warning_title')) ?></strong></div>
<?php foreach ((array) ($importWarning['messages'] ?? []) as $warning): ?>
<div><?= $e((string) $warning) ?></div>
<?php endforeach; ?>
<?php if (!empty($importWarning['created_at'])): ?>
<div class="muted mt-8"><?= $e($t('products.variants.import_warning_date')) ?>: <?= $e((string) $importWarning['created_at']) ?></div>
<?php endif; ?>
</div>
<?php endif; ?>
<h3><?= $e($t('products.show.details')) ?></h3>
<table class="table mt-12">
<tbody>
<tr><th>ID</th><td><?= $e((string) ($item['id'] ?? '')) ?></td></tr>
<tr><th><?= $e($t('products.fields.name')) ?></th><td><?= $e((string) ($item['name'] ?? '')) ?></td></tr>
<tr><th>SKU</th><td><?= $e((string) ($item['sku'] ?? '')) ?></td></tr>
<tr><th>EAN</th><td><?= $e((string) ($item['ean'] ?? '')) ?></td></tr>
<tr><th><?= $e($t('products.fields.type')) ?></th><td><?= $e((string) ($item['type'] ?? '')) ?></td></tr>
<tr><th><?= $e($t('products.fields.status')) ?></th><td><?= $e((string) ($item['status'] ?? '')) ?></td></tr>
<tr><th><?= $e($t('products.fields.promoted')) ?></th><td><?= $e((string) ($item['promoted'] ?? '')) ?></td></tr>
<tr><th><?= $e($t('products.fields.vat')) ?></th><td><?= $e((string) ($item['vat'] ?? '')) ?></td></tr>
<tr><th><?= $e($t('products.fields.weight')) ?></th><td><?= $e((string) ($item['weight'] ?? '')) ?></td></tr>
<tr><th><?= $e($t('products.fields.quantity')) ?></th><td><?= $e((string) ($item['quantity'] ?? '')) ?></td></tr>
<tr><th><?= $e($t('products.fields.price_brutto')) ?></th><td><?= $e((string) ($item['price_brutto'] ?? '')) ?></td></tr>
<tr><th><?= $e($t('products.fields.price_netto')) ?></th><td><?= $e((string) ($item['price_netto'] ?? '')) ?></td></tr>
<tr><th><?= $e($t('products.fields.price_brutto_promo')) ?></th><td><?= $e((string) ($item['price_brutto_promo'] ?? '')) ?></td></tr>
<tr><th><?= $e($t('products.fields.price_netto_promo')) ?></th><td><?= $e((string) ($item['price_netto_promo'] ?? '')) ?></td></tr>
<tr><th><?= $e($t('products.fields.short_description')) ?></th><td><?= $e((string) ($item['short_description'] ?? '')) ?></td></tr>
<tr><th><?= $e($t('products.fields.description')) ?></th><td><?= $e((string) ($item['description'] ?? '')) ?></td></tr>
<tr><th><?= $e($t('products.fields.meta_title')) ?></th><td><?= $e((string) ($item['meta_title'] ?? '')) ?></td></tr>
<tr><th><?= $e($t('products.fields.meta_description')) ?></th><td><?= $e((string) ($item['meta_description'] ?? '')) ?></td></tr>
<tr><th><?= $e($t('products.fields.meta_keywords')) ?></th><td><?= $e((string) ($item['meta_keywords'] ?? '')) ?></td></tr>
<tr><th><?= $e($t('products.fields.seo_link')) ?></th><td><?= $e((string) ($item['seo_link'] ?? '')) ?></td></tr>
<tr><th><?= $e($t('products.fields.updated_at')) ?></th><td><?= $e((string) ($item['updated_at'] ?? '')) ?></td></tr>
</tbody>
</table>
</section>
<section class="card mt-16">
<h3><?= $e($t('products.variants.title')) ?></h3>
<?php if ($variants === []): ?>
<p class="muted"><?= $e($t('products.variants.empty')) ?></p>
<?php else: ?>
<div class="table-wrap mt-12">
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>SKU</th>
<th>EAN</th>
<th><?= $e($t('products.fields.price_brutto')) ?></th>
<th><?= $e($t('products.fields.price_netto')) ?></th>
<th><?= $e($t('products.fields.weight')) ?></th>
<th><?= $e($t('products.fields.status')) ?></th>
<th><?= $e($t('products.variants.attributes')) ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($variants as $variant): ?>
<?php
$attributes = is_array($variant['attributes'] ?? null) ? $variant['attributes'] : [];
$attributeText = [];
foreach ($attributes as $attribute) {
$attributeName = (string) ($attribute['attribute_name'] ?? '');
$valueName = (string) ($attribute['value_name'] ?? '');
if ($attributeName === '' || $valueName === '') {
continue;
}
$attributeText[] = $attributeName . ': ' . $valueName;
}
?>
<tr>
<td><?= $e((string) ($variant['id'] ?? 0)) ?></td>
<td><?= $e((string) ($variant['sku'] ?? '')) ?></td>
<td><?= $e((string) ($variant['ean'] ?? '')) ?></td>
<td><?= $e((string) ($variant['price_brutto'] ?? '')) ?></td>
<td><?= $e((string) ($variant['price_netto'] ?? '')) ?></td>
<td><?= $e((string) ($variant['weight'] ?? '')) ?></td>
<td><?= $e(((int) ($variant['status'] ?? 0)) === 1 ? $t('products.status.active') : $t('products.status.inactive')) ?></td>
<td><?= $e($attributeText !== [] ? implode(', ', $attributeText) : '-') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</section>
<section class="card mt-16">
<h3><?= $e($t('products.images.title')) ?></h3>
<?php if ($images === []): ?>
<p class="muted"><?= $e($t('products.images.empty')) ?></p>
<?php else: ?>
<div class="product-show-images-grid mt-12">
<?php foreach ($images as $image): ?>
<div class="product-show-image-card">
<div><strong>ID:</strong> <?= $e((string) ($image['id'] ?? 0)) ?><?= ((int) ($image['is_main'] ?? 0) === 1) ? ' | <strong>' . $e($t('products.images.main')) . '</strong>' : '' ?></div>
<div class="muted"><?= $e((string) ($image['storage_path'] ?? '')) ?></div>
<?php if ((string) ($image['public_url'] ?? '') !== ''): ?>
<div class="mt-12">
<img src="<?= $e((string) $image['public_url']) ?>" alt="<?= $e((string) ($image['alt'] ?? '')) ?>" class="product-show-image">
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</section>
<section class="card mt-16">
<a class="btn btn--secondary" href="/products"><?= $e($t('products.actions.back')) ?></a>
<a class="btn btn--secondary" href="/products/<?= $e((string) $productIdValue) ?>/links"><?= $e($t('products.actions.links')) ?></a>
<a class="btn btn--primary" href="/products/edit?id=<?= $e((string) $productIdValue) ?>"><?= $e($t('products.actions.edit')) ?></a>
</section>