feat(19-ui-integration): przycisk Drukuj, bulk print, kolejka wydruku

- Przycisk "Drukuj" w prepare.php i show.php z AJAX + duplikat protection
- Bulk print z listy zamówień (checkboxy + header action)
- Kolejka wydruku w Ustawienia > Drukowanie (filtr statusu, retry)
- POST /api/print/jobs/bulk endpoint (package_ids + order_ids)
- ensureLabel() auto-download przez ShipmentProviderRegistry
- Apaczka carrier_id = nazwa usługi, kolumna Przewoznik
- Tab persistence (localStorage), label file_exists check
- Fix use statement ApaczkaApiClient, redirect po utworzeniu przesyłki
- Phase 17 (receipt duplicate guard) + Phase 18 (print queue backend) docs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-22 21:16:54 +01:00
parent d1a1b79247
commit 02d06298ea
33 changed files with 2623 additions and 117 deletions

View File

@@ -50,6 +50,54 @@
popup.style.left = left + 'px';
popup.style.top = top + 'px';
}, true);
// Bulk print labels
var bulkPrintBtn = document.querySelector('.js-bulk-print-labels');
if (bulkPrintBtn) {
bulkPrintBtn.addEventListener('click', function () {
var checked = document.querySelectorAll('.js-table-select-item:checked');
if (checked.length === 0) {
if (window.OrderProAlerts) {
window.OrderProAlerts.show({ message: 'Zaznacz co najmniej jedno zamowienie.', type: 'warning' });
}
return;
}
var orderIds = [];
checked.forEach(function (cb) { orderIds.push(cb.value); });
var csrf = bulkPrintBtn.getAttribute('data-csrf') || '';
bulkPrintBtn.disabled = true;
bulkPrintBtn.textContent = 'Wysylam...';
fetch('/api/print/jobs/bulk', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ order_ids: orderIds, _token: csrf })
})
.then(function (r) { return r.json(); })
.then(function (data) {
var created = (data.created || []).length;
var skipped = (data.skipped || []).length;
var msg = 'Wyslano ' + created + ' zlecen do drukarki.';
if (skipped > 0) {
msg += ' Pominieto ' + skipped + ' (brak etykiety lub juz w kolejce).';
}
if (window.OrderProAlerts) {
window.OrderProAlerts.show({ message: msg, type: 'success' });
}
bulkPrintBtn.disabled = false;
bulkPrintBtn.textContent = 'Drukuj etykiety';
})
.catch(function () {
if (window.OrderProAlerts) {
window.OrderProAlerts.show({ message: 'Blad sieci — sprobuj ponownie.', type: 'error' });
}
bulkPrintBtn.disabled = false;
bulkPrintBtn.textContent = 'Drukuj etykiety';
});
});
}
})();
</script>

View File

@@ -5,6 +5,8 @@ $configsList = is_array($configs ?? null) ? $configs : [];
$sellerData = is_array($seller ?? null) ? $seller : [];
$totalGrossVal = (float) ($totalGross ?? 0);
$orderIdVal = (int) ($orderId ?? 0);
$existingReceiptsList = is_array($existingReceipts ?? null) ? $existingReceipts : [];
$hasExistingReceipts = $existingReceiptsList !== [];
?>
<section class="card">
@@ -18,7 +20,23 @@ $orderIdVal = (int) ($orderId ?? 0);
</div>
</div>
<form method="post" action="/orders/<?= $e((string) $orderIdVal) ?>/receipt/store" class="mt-16">
<?php if ($hasExistingReceipts): ?>
<div class="alert alert--warning mt-12">
<strong>Uwaga!</strong> Do tego zamówienia wystawiono już <?= $e((string) count($existingReceiptsList)) ?> paragon(ów):
<ul class="mt-4">
<?php foreach ($existingReceiptsList as $er): ?>
<li>
<strong><?= $e((string) ($er['receipt_number'] ?? '-')) ?></strong>
— data: <?= $e((string) ($er['issue_date'] ?? '-')) ?>,
kwota: <?= $e(number_format((float) ($er['total_gross'] ?? 0), 2, '.', ' ')) ?> PLN
(<?= $e((string) ($er['config_name'] ?? '-')) ?>)
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<form id="receipt-create-form" method="post" action="/orders/<?= $e((string) $orderIdVal) ?>/receipt/store" class="mt-16">
<input type="hidden" name="_token" value="<?= $e((string) ($csrfToken ?? '')) ?>">
<div class="form-grid-2">
@@ -97,8 +115,25 @@ $orderIdVal = (int) ($orderId ?? 0);
</div>
<div class="mt-16">
<button type="submit" class="btn btn--primary"><?= $e($t('receipts.create.submit')) ?></button>
<?php if ($hasExistingReceipts): ?>
<button type="button" id="receipt-submit-btn" class="btn btn--primary"><?= $e($t('receipts.create.submit')) ?></button>
<?php else: ?>
<button type="submit" class="btn btn--primary"><?= $e($t('receipts.create.submit')) ?></button>
<?php endif; ?>
<a href="/orders/<?= $e((string) $orderIdVal) ?>" class="btn btn--secondary ml-8"><?= $e($t('receipts.create.cancel')) ?></a>
</div>
</form>
</section>
<?php if ($hasExistingReceipts): ?>
<script>
document.getElementById('receipt-submit-btn').addEventListener('click', function() {
window.OrderProAlerts.confirm(
'Do tego zamówienia wystawiono już paragon. Czy na pewno chcesz wystawić kolejny?',
function() {
document.getElementById('receipt-create-form').submit();
}
);
});
</script>
<?php endif; ?>

View File

@@ -355,7 +355,7 @@ foreach ($addressesList as $address) {
<div class="order-tab-panel" data-order-tab-panel="shipments">
<?php if ($packagesList !== []): ?>
<section class="card mt-16">
<h3 class="section-title">Wygenerowane przesylki (WZA)</h3>
<h3 class="section-title">Wygenerowane przesylki</h3>
<div class="table-wrap mt-12">
<table class="table table--details">
<thead>
@@ -369,12 +369,21 @@ foreach ($addressesList as $address) {
</tr>
</thead>
<tbody>
<?php $storageBase = dirname(__DIR__, 3) . '/storage/'; ?>
<?php $pendingPrintIds = is_array($pendingPrintPackageIds ?? null) ? $pendingPrintPackageIds : []; ?>
<?php foreach ($packagesList as $pkg): ?>
<?php
$pkgStatus = (string) ($pkg['status'] ?? 'draft');
$pkgTracking = trim((string) ($pkg['tracking_number'] ?? ''));
$pkgCarrier = trim((string) ($pkg['carrier_id'] ?? ''));
$pkgCarrierId = trim((string) ($pkg['carrier_id'] ?? ''));
$pkgProvider = trim((string) ($pkg['provider'] ?? ''));
$providerLabels = ['apaczka' => 'Apaczka', 'allegro_wza' => 'Allegro', 'inpost' => 'InPost'];
$pkgProviderLabel = $providerLabels[$pkgProvider] ?? $pkgProvider;
$pkgCarrier = $pkgCarrierId !== '' ? ($pkgProviderLabel . ' &rarr; ' . $pkgCarrierId) : $pkgProviderLabel;
$pkgLabelPath = trim((string) ($pkg['label_path'] ?? ''));
if ($pkgLabelPath !== '' && !file_exists($storageBase . $pkgLabelPath)) {
$pkgLabelPath = '';
}
$pkgError = trim((string) ($pkg['error_message'] ?? ''));
?>
<tr>
@@ -388,13 +397,26 @@ foreach ($addressesList as $address) {
<?php endif; ?>
</td>
<td><?= $e($pkgTracking !== '' ? $pkgTracking : '-') ?></td>
<td><?= $e($pkgCarrier !== '' ? $pkgCarrier : '-') ?></td>
<td><?php if ($pkgCarrierId !== ''): ?><?= $e($pkgProviderLabel) ?> &rarr; <?= $e($pkgCarrierId) ?><?php elseif ($pkgProviderLabel !== ''): ?><?= $e($pkgProviderLabel) ?><?php else: ?>-<?php endif; ?></td>
<td>
<?php if ($pkgLabelPath !== ''): ?>
<span style="display:inline-flex;gap:4px;align-items:center">
<?php if ($pkgLabelPath !== '' && $pkgStatus !== 'error'): ?>
<a href="/orders/<?= $e((string) ($orderId ?? 0)) ?>/shipment/prepare" class="btn btn--sm btn--secondary">Pobierz</a>
<?php else: ?>
<?php elseif ($pkgStatus !== 'error'): ?>
-
<?php endif; ?>
<?php if (in_array($pkgStatus, ['label_ready', 'created'], true)): ?>
<?php if (in_array((int) ($pkg['id'] ?? 0), $pendingPrintIds, true)): ?>
<button type="button" class="btn btn--sm btn--danger" disabled style="white-space:nowrap">W kolejce</button>
<?php else: ?>
<button type="button"
class="btn btn--sm btn--secondary btn-print-label"
data-package-id="<?= $e((string) ($pkg['id'] ?? 0)) ?>"
data-order-id="<?= $e((string) ($orderId ?? 0)) ?>"
title="Wyslij do drukarki">Drukuj</button>
<?php endif; ?>
<?php endif; ?>
</span>
</td>
<td class="text-nowrap"><?= $e((string) ($pkg['created_at'] ?? '')) ?></td>
</tr>
@@ -615,14 +637,58 @@ foreach ($addressesList as $address) {
});
}
var storageKey = 'orderDetailTab';
tabButtons.forEach(function (button) {
button.addEventListener('click', function () {
setActiveTab(button.getAttribute('data-order-tab-target') || 'details');
var target = button.getAttribute('data-order-tab-target') || 'details';
setActiveTab(target);
try { localStorage.setItem(storageKey, target); } catch (e) {}
});
});
setActiveTab('details');
var forceTab = <?= json_encode($flashSuccessMsg !== '' && strpos($flashSuccessMsg, 'Przesylka') !== false ? 'shipments' : '') ?>;
var savedTab = null;
try { savedTab = localStorage.getItem(storageKey); } catch (e) {}
setActiveTab(forceTab || savedTab || 'details');
// Print label button handler
document.querySelectorAll('.btn-print-label').forEach(function (btn) {
btn.addEventListener('click', function () {
var packageId = btn.getAttribute('data-package-id');
if (!packageId) return;
btn.disabled = true;
var originalText = btn.innerHTML;
btn.innerHTML = 'Wysylam...';
var csrfInput = document.querySelector('input[name="_token"]');
var csrf = csrfInput ? csrfInput.value : '<?= $e($csrfToken ?? '') ?>';
fetch('/api/print/jobs', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: '_token=' + encodeURIComponent(csrf) + '&package_id=' + encodeURIComponent(packageId)
})
.then(function (r) { return r.json().then(function (d) { return { status: r.status, data: d }; }); })
.then(function (res) {
if (res.status === 201 || res.status === 409) {
btn.innerHTML = 'W kolejce';
btn.disabled = true;
btn.classList.remove('btn--secondary');
btn.classList.add('btn--danger');
} else {
var msg = (res.data && res.data.error) ? res.data.error : 'Nieznany blad';
if (window.OrderProAlerts) { window.OrderProAlerts.show({ message: msg, type: 'error' }); }
btn.innerHTML = originalText;
btn.disabled = false;
}
})
.catch(function () {
if (window.OrderProAlerts) { window.OrderProAlerts.show({ message: 'Blad sieci.', type: 'error' }); }
btn.innerHTML = originalText;
btn.disabled = false;
});
});
});
})();
</script>