ver. 0.295: Admin order product editing — add/remove/modify products, AJAX search, stock adjustment
- Order product CRUD in admin panel (add, delete, edit quantity/prices) - AJAX product search endpoint for order edit form - Automatic stock adjustment when editing order products - Transport cost recalculation based on free delivery threshold - Fix: promo price = 0 when equal to base price (no real promotion) - Clean up stale temp/ build artifacts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
|
||||
// =========================================================================
|
||||
// InPost paczkomat
|
||||
// =========================================================================
|
||||
|
||||
function toggleInpostField() {
|
||||
if ($('#transport_id').val() != '2') {
|
||||
$('#inpost_paczkomat').closest('.row').hide();
|
||||
@@ -12,6 +17,7 @@ $(function() {
|
||||
|
||||
$('body').on('change', '#transport_id', function() {
|
||||
toggleInpostField();
|
||||
recalculateTotals();
|
||||
});
|
||||
|
||||
$('body').on('click', '.btn-paczkomat', function() {
|
||||
@@ -30,9 +36,209 @@ $(function() {
|
||||
$('.inpost-map-container').show();
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// Order save
|
||||
// =========================================================================
|
||||
|
||||
$('body').on('click', '#order-save', function(e) {
|
||||
e.preventDefault();
|
||||
$('#fg-order-details').attr('method', 'POST').attr('action', '/admin/shop_order/order_save/').submit();
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// Product row index counter
|
||||
// =========================================================================
|
||||
|
||||
var productIndex = $('#order-products-body .order-product-row').length;
|
||||
|
||||
// =========================================================================
|
||||
// Remove product
|
||||
// =========================================================================
|
||||
|
||||
$('body').on('click', '.btn-remove-product', function() {
|
||||
var $row = $(this).closest('.order-product-row');
|
||||
var orderProductId = $row.find('input[name$="[order_product_id]"]').val();
|
||||
|
||||
if (orderProductId && orderProductId !== '0') {
|
||||
// Istniejący produkt — ukryj i oznacz do usunięcia
|
||||
$row.hide();
|
||||
$row.append('<input type="hidden" name="products[' + $row.data('index') + '][delete]" value="1">');
|
||||
} else {
|
||||
// Nowy produkt — po prostu usuń z DOM
|
||||
$row.remove();
|
||||
}
|
||||
|
||||
recalculateTotals();
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// Quantity and price change — recalculate
|
||||
// =========================================================================
|
||||
|
||||
$('body').on('change input', '.product-qty, .product-price, .product-price-promo', function() {
|
||||
recalculateTotals();
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// Product search (AJAX)
|
||||
// =========================================================================
|
||||
|
||||
var searchTimer = null;
|
||||
var $searchInput = $('#product-search-input');
|
||||
var $searchResults = $('#product-search-results');
|
||||
|
||||
$searchInput.on('keyup', function() {
|
||||
var query = $(this).val().trim();
|
||||
|
||||
clearTimeout(searchTimer);
|
||||
|
||||
if (query.length < 2) {
|
||||
$searchResults.hide().empty();
|
||||
return;
|
||||
}
|
||||
|
||||
searchTimer = setTimeout(function() {
|
||||
$.ajax({
|
||||
url: '/admin/shop_order/search_products_ajax/',
|
||||
type: 'GET',
|
||||
data: { query: query },
|
||||
dataType: 'json',
|
||||
success: function(data) {
|
||||
$searchResults.empty();
|
||||
|
||||
if (!data || !data.products || data.products.length === 0) {
|
||||
$searchResults.append('<div style="padding:10px;color:#999">Brak wyników</div>');
|
||||
$searchResults.show();
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < data.products.length; i++) {
|
||||
var p = data.products[i];
|
||||
var priceDisplay = p.price_brutto_promo > 0
|
||||
? '<span style="text-decoration:line-through;color:#999">' + parseFloat(p.price_brutto).toFixed(2) + ' zł</span> <strong>' + parseFloat(p.price_brutto_promo).toFixed(2) + ' zł</strong>'
|
||||
: '<strong>' + parseFloat(p.price_brutto).toFixed(2) + ' zł</strong>';
|
||||
|
||||
var stockWarning = p.quantity <= 0
|
||||
? ' <span style="color:#d9534f;font-size:11px">(brak na stanie)</span>'
|
||||
: ' <span style="color:#5cb85c;font-size:11px">(stan: ' + p.quantity + ')</span>';
|
||||
|
||||
var imgHtml = p.image
|
||||
? '<img src="' + p.image + '" style="width:35px;height:35px;object-fit:cover;margin-right:8px;border-radius:3px">'
|
||||
: '<span style="display:inline-block;width:35px;height:35px;margin-right:8px;background:#eee;border-radius:3px"></span>';
|
||||
|
||||
var $item = $('<div class="product-search-item" style="padding:8px 10px;cursor:pointer;display:flex;align-items:center;border-bottom:1px solid #f0f0f0"></div>');
|
||||
$item.html(imgHtml + '<div><div>' + $('<span>').text(p.name).html() + (p.sku ? ' <span style="color:#999;font-size:11px">(' + $('<span>').text(p.sku).html() + ')</span>' : '') + stockWarning + '</div><div>' + priceDisplay + '</div></div>');
|
||||
$item.data('product', p);
|
||||
$searchResults.append($item);
|
||||
}
|
||||
|
||||
$searchResults.show();
|
||||
}
|
||||
});
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Kliknięcie w wynik wyszukiwania — dodaj produkt
|
||||
$('body').on('click', '.product-search-item', function() {
|
||||
var p = $(this).data('product');
|
||||
|
||||
if (p.quantity <= 0) {
|
||||
if (!confirm('Produkt "' + p.name + '" nie jest dostępny na stanie. Czy na pewno chcesz go dodać?')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
addProductRow(p);
|
||||
$searchInput.val('');
|
||||
$searchResults.hide().empty();
|
||||
});
|
||||
|
||||
// Ukryj wyniki przy kliknięciu poza
|
||||
$(document).on('click', function(e) {
|
||||
if (!$(e.target).closest('#add-product-section').length) {
|
||||
$searchResults.hide();
|
||||
}
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// Add product row
|
||||
// =========================================================================
|
||||
|
||||
function addProductRow(product) {
|
||||
var idx = productIndex++;
|
||||
var imgHtml = product.image
|
||||
? '<img src="' + product.image + '" style="max-width:50px;max-height:50px">'
|
||||
: '';
|
||||
|
||||
var effectivePrice = product.price_brutto_promo > 0 ? product.price_brutto_promo : product.price_brutto;
|
||||
|
||||
var $row = $('<tr class="order-product-row" data-index="' + idx + '"></tr>');
|
||||
$row.html(
|
||||
'<input type="hidden" name="products[' + idx + '][order_product_id]" value="0">' +
|
||||
'<input type="hidden" name="products[' + idx + '][product_id]" value="' + product.product_id + '">' +
|
||||
'<input type="hidden" name="products[' + idx + '][parent_product_id]" value="' + (product.parent_product_id || product.product_id) + '">' +
|
||||
'<input type="hidden" name="products[' + idx + '][name]" value="' + $('<span>').text(product.name).html() + '">' +
|
||||
'<input type="hidden" name="products[' + idx + '][vat]" value="' + product.vat + '">' +
|
||||
'<td class="product-image">' + imgHtml + '</td>' +
|
||||
'<td>' + $('<span>').text(product.name).html() +
|
||||
(product.sku ? ' <span class="small text-muted">(' + $('<span>').text(product.sku).html() + ')</span>' : '') +
|
||||
'</td>' +
|
||||
'<td class="tab-center"><input type="number" name="products[' + idx + '][quantity]" value="1" min="1" class="form-control form-control-sm text-center product-qty" style="width:70px"></td>' +
|
||||
'<td class="tab-right"><input type="number" name="products[' + idx + '][price_brutto]" value="' + parseFloat(product.price_brutto).toFixed(2) + '" step="0.01" min="0" class="form-control form-control-sm text-right product-price" style="width:110px"></td>' +
|
||||
'<td class="tab-right"><input type="number" name="products[' + idx + '][price_brutto_promo]" value="' + parseFloat(product.price_brutto_promo).toFixed(2) + '" step="0.01" min="0" class="form-control form-control-sm text-right product-price-promo" style="width:110px"></td>' +
|
||||
'<td class="tab-right product-row-total">' + parseFloat(effectivePrice).toFixed(2).replace('.', ',') + ' zł</td>' +
|
||||
'<td class="text-center"><button type="button" class="btn btn-xs btn-danger btn-remove-product" title="Usuń produkt"><i class="fa fa-times"></i></button></td>'
|
||||
);
|
||||
|
||||
$('#order-products-body').append($row);
|
||||
recalculateTotals();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Recalculate totals
|
||||
// =========================================================================
|
||||
|
||||
function recalculateTotals() {
|
||||
var productsTotal = 0;
|
||||
|
||||
$('#order-products-body .order-product-row:visible').each(function() {
|
||||
var qty = parseFloat($(this).find('.product-qty').val()) || 0;
|
||||
var price = parseFloat($(this).find('.product-price').val()) || 0;
|
||||
var pricePromo = parseFloat($(this).find('.product-price-promo').val()) || 0;
|
||||
|
||||
var effectivePrice = pricePromo > 0 ? pricePromo : price;
|
||||
var rowTotal = effectivePrice * qty;
|
||||
|
||||
$(this).find('.product-row-total').text(rowTotal.toFixed(2).replace('.', ',') + ' zł');
|
||||
productsTotal += rowTotal;
|
||||
});
|
||||
|
||||
// Transport cost (z uwzględnieniem progu darmowej dostawy)
|
||||
var transportId = parseInt($('#transport_id').val()) || 0;
|
||||
var transportCost = 0;
|
||||
var transports = orderEditConfig.transports || [];
|
||||
|
||||
for (var i = 0; i < transports.length; i++) {
|
||||
if (transports[i].id === transportId) {
|
||||
var t = transports[i];
|
||||
if (t.delivery_free === 1 && orderEditConfig.freeDelivery > 0 && productsTotal >= orderEditConfig.freeDelivery) {
|
||||
transportCost = 0;
|
||||
} else {
|
||||
transportCost = t.cost;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var orderTotal = productsTotal + transportCost;
|
||||
|
||||
$('#products-total-display').text(productsTotal.toFixed(2).replace('.', ','));
|
||||
$('#transport-cost-display').text(transportCost.toFixed(2).replace('.', ','));
|
||||
$('#order-summary-display').text(orderTotal.toFixed(2).replace('.', ',') + ' zł');
|
||||
}
|
||||
|
||||
// Inicjalizacja sum przy załadowaniu strony
|
||||
recalculateTotals();
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -84,7 +84,12 @@ $orderId = (int)($this -> order['id'] ?? 0);
|
||||
<? endif;?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div>Kwota zamówienia <b><?= $this -> order[ 'summary' ];?> zł</b></div>
|
||||
<div>Kwota zamówienia <b id="order-summary-display"><?= $this -> order[ 'summary' ];?> zł</b></div>
|
||||
<div class="mt5">
|
||||
<span>Produkty: <b id="products-total-display">0,00</b> zł</span>
|
||||
|
|
||||
<span>Dostawa: <b id="transport-cost-display"><?= number_format((float)($this -> order['transport_cost'] ?? 0), 2, ',', '');?></b> zł</span>
|
||||
</div>
|
||||
<br>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
@@ -160,46 +165,75 @@ $orderId = (int)($this -> order['id'] ?? 0);
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="text-big mb5">Produkty zamówienia:</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<table class="table" id="order-products-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Zdjęcie</th>
|
||||
<th style="width:60px">Zdjęcie</th>
|
||||
<th scope="col">Nazwa</th>
|
||||
<th scope="col" class="tab-center">Ilość</th>
|
||||
<th scope="col" class="tab-right">Cena / szt:</th>
|
||||
<th scope="col" class="tab-right">Cena / szt (po rabacie):</th>
|
||||
<th scope="col" class="tab-right">Suma (po rabacie):</th>
|
||||
<th scope="col" class="tab-center" style="width:80px">Ilość</th>
|
||||
<th scope="col" class="tab-right" style="width:130px">Cena / szt:</th>
|
||||
<th scope="col" class="tab-right" style="width:130px">Cena promo:</th>
|
||||
<th scope="col" class="tab-right" style="width:110px">Suma:</th>
|
||||
<th scope="col" style="width:40px"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<? if ( is_array( $this -> order[ 'products' ] ) ): foreach ( $this -> order[ 'products' ] as $product ):?>
|
||||
<tr class="order-product-details">
|
||||
<tbody id="order-products-body">
|
||||
<? $productRepo = new \Domain\Product\ProductRepository( $GLOBALS['mdb'] ); ?>
|
||||
<? if ( is_array( $this -> order[ 'products' ] ) ): foreach ( $this -> order[ 'products' ] as $i => $product ):?>
|
||||
<tr class="order-product-row" data-index="<?= $i;?>">
|
||||
<input type="hidden" name="products[<?= $i;?>][order_product_id]" value="<?= (int)$product['id'];?>">
|
||||
<input type="hidden" name="products[<?= $i;?>][product_id]" value="<?= (int)$product['product_id'];?>">
|
||||
<input type="hidden" name="products[<?= $i;?>][parent_product_id]" value="<?= (int)($product['parent_product_id'] ?? $product['product_id']);?>">
|
||||
<input type="hidden" name="products[<?= $i;?>][name]" value="<?= htmlspecialchars((string)$product['name'], ENT_QUOTES, 'UTF-8');?>">
|
||||
<input type="hidden" name="products[<?= $i;?>][vat]" value="<?= (float)($product['vat'] ?? 0);?>">
|
||||
<td class="product-image">
|
||||
<? if ( $product['product_id'] ):?>
|
||||
<img src="<?= ( new \Domain\Product\ProductRepository( $GLOBALS['mdb'] ) )->getProductImg( (int)$product['product_id'] );?>">
|
||||
<img src="<?= $productRepo->getProductImg( (int)$product['product_id'] );?>" style="max-width:50px;max-height:50px;">
|
||||
<? endif;?>
|
||||
</td>
|
||||
<td>
|
||||
<a href="<?= ( new \Domain\Product\ProductRepository( $GLOBALS['mdb'] ) )->getProductUrl( (int)$product['product_id'] );?>" target="_blank"><?= $product[ 'name' ];?></a>
|
||||
<br />
|
||||
<div class="atributes">
|
||||
<?= $product[ 'attributes' ];?>
|
||||
</div>
|
||||
<br />
|
||||
<div class="product-message">
|
||||
<?= $product[ 'message' ] != '' ? '<strong>Wiadomość:</strong> ' . $product['message'] : '';?>
|
||||
</div>
|
||||
<a href="<?= $productRepo->getProductUrl( (int)$product['product_id'] );?>" target="_blank"><?= $product[ 'name' ];?></a>
|
||||
<? if ( $product['attributes'] ):?>
|
||||
<div class="atributes small text-muted"><?= $product[ 'attributes' ];?></div>
|
||||
<? endif;?>
|
||||
<? if ( $product[ 'message' ] != '' ):?>
|
||||
<div class="product-message small"><strong>Wiadomość:</strong> <?= $product['message'];?></div>
|
||||
<? endif;?>
|
||||
</td>
|
||||
<td class="tab-center">
|
||||
<input type="number" name="products[<?= $i;?>][quantity]" value="<?= (int)$product['quantity'];?>" min="1" class="form-control form-control-sm text-center product-qty" style="width:70px">
|
||||
</td>
|
||||
<td class="tab-right">
|
||||
<input type="number" name="products[<?= $i;?>][price_brutto]" value="<?= number_format((float)$product['price_brutto'], 2, '.', '');?>" step="0.01" min="0" class="form-control form-control-sm text-right product-price" style="width:110px">
|
||||
</td>
|
||||
<td class="tab-right">
|
||||
<input type="number" name="products[<?= $i;?>][price_brutto_promo]" value="<?= number_format((float)$product['price_brutto_promo'], 2, '.', '');?>" step="0.01" min="0" class="form-control form-control-sm text-right product-price-promo" style="width:110px">
|
||||
</td>
|
||||
<td class="tab-right product-row-total">
|
||||
<?= number_format(((float)$product['price_brutto_promo'] > 0 ? (float)$product['price_brutto_promo'] : (float)$product['price_brutto']) * (int)$product['quantity'], 2, ',', '');?> zł
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<button type="button" class="btn btn-xs btn-danger btn-remove-product" title="Usuń produkt"><i class="fa fa-times"></i></button>
|
||||
</td>
|
||||
<td class="tab-center"><?= $product[ 'quantity' ];?></td>
|
||||
<td class="tab-right"><?= $product[ 'price_brutto' ];?> zł</td>
|
||||
<td class="tab-right"><?= $product[ 'price_brutto_promo' ];?> zł</td>
|
||||
<td class="tab-right"><?= $product[ 'price_brutto_promo' ] * $product[ 'quantity' ];?> zł</td>
|
||||
</tr>
|
||||
<? endforeach; endif;?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb15" id="add-product-section">
|
||||
<div class="text-big mb5">Dodaj produkt:</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div style="position:relative">
|
||||
<input type="text" id="product-search-input" class="form-control" placeholder="Wpisz nazwę produktu..." autocomplete="off">
|
||||
<div id="product-search-results" style="display:none;position:absolute;z-index:1000;width:100%;max-height:300px;overflow-y:auto;background:#fff;border:1px solid #ddd;border-top:none;box-shadow:0 2px 8px rgba(0,0,0,0.15)"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -222,4 +256,11 @@ $orderId = (int)($this -> order['id'] ?? 0);
|
||||
</div>
|
||||
<link class="footer" rel="stylesheet" type="text/css" href="https://geowidget.easypack24.net/css/easypack.css">
|
||||
<script class="footer" type="text/javascript" src="https://geowidget.easypack24.net/js/sdk-for-javascript.js"></script>
|
||||
<script type="text/javascript">
|
||||
var orderEditConfig = {
|
||||
transports: <?= $this -> transports_json ?? '[]';?>,
|
||||
freeDelivery: <?= (float)($this -> free_delivery ?? 0);?>,
|
||||
currentTransportCost: <?= (float)($this -> order['transport_cost'] ?? 0);?>
|
||||
};
|
||||
</script>
|
||||
<?= \Shared\Tpl\Tpl::view('shop-order/order-edit-custom-script');?>
|
||||
Reference in New Issue
Block a user