feat: bulk delete in product archive (v0.327)
- Add bulk_delete_permanent() endpoint (POST ids[], returns JSON) - Checkbox column + bulk action bar with count label - Select-all in table header, confirmation dialog before delete - 2 new tests for bulk_delete_permanent method signature Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,26 @@
|
|||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
|
.bulk-action-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background: #fff3cd;
|
||||||
|
border: 1px solid #ffc107;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-action-bar__info {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-col-bulk-check {
|
||||||
|
width: 36px;
|
||||||
|
padding-left: 10px !important;
|
||||||
|
padding-right: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
.product-archive-thumb-wrap {
|
.product-archive-thumb-wrap {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
@@ -96,5 +118,119 @@
|
|||||||
$popup.removeClass('is-visible');
|
$popup.removeClass('is-visible');
|
||||||
$popupImage.attr('src', '');
|
$popupImage.attr('src', '');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Bulk select ---
|
||||||
|
var $table = $('.table-list-table');
|
||||||
|
var $bar = $('#js-bulk-action-bar');
|
||||||
|
var $label = $bar.find('.js-bulk-count-label');
|
||||||
|
|
||||||
|
// Inject select-all checkbox into _checkbox column header
|
||||||
|
$table.find('thead th.table-col-bulk-check').html(
|
||||||
|
'<input type="checkbox" id="js-bulk-select-all" title="Zaznacz wszystkie">'
|
||||||
|
);
|
||||||
|
|
||||||
|
function updateBar() {
|
||||||
|
var count = $table.find('.js-bulk-check:checked').length;
|
||||||
|
if (count > 0) {
|
||||||
|
$label.text('Zaznaczono: ' + count);
|
||||||
|
$bar.show();
|
||||||
|
} else {
|
||||||
|
$bar.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).on('change.bulkSelect', '#js-bulk-select-all', function() {
|
||||||
|
var checked = $(this).is(':checked');
|
||||||
|
$table.find('.js-bulk-check').prop('checked', checked);
|
||||||
|
updateBar();
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('change.bulkSelect', '.js-bulk-check', function() {
|
||||||
|
var total = $table.find('.js-bulk-check').length;
|
||||||
|
var checked = $table.find('.js-bulk-check:checked').length;
|
||||||
|
$('#js-bulk-select-all').prop('indeterminate', checked > 0 && checked < total);
|
||||||
|
$('#js-bulk-select-all').prop('checked', checked === total && total > 0);
|
||||||
|
updateBar();
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click.bulkDelete', '.js-bulk-delete-btn', function() {
|
||||||
|
var ids = [];
|
||||||
|
$table.find('.js-bulk-check:checked').each(function() {
|
||||||
|
ids.push($(this).val());
|
||||||
|
});
|
||||||
|
if (ids.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var confirmMsg = 'UWAGA! Operacja nieodwracalna!\n\n'
|
||||||
|
+ 'Wybrane produkty (' + ids.length + ' szt.) zostaną trwale usunięte razem ze wszystkimi zdjęciami i załącznikami z serwera.\n\n'
|
||||||
|
+ 'Czy na pewno chcesz usunąć zaznaczone produkty?';
|
||||||
|
|
||||||
|
var doDelete = function() {
|
||||||
|
var $btn = $('.js-bulk-delete-btn');
|
||||||
|
$btn.prop('disabled', true).text('Usuwanie…');
|
||||||
|
|
||||||
|
var formData = [];
|
||||||
|
for (var i = 0; i < ids.length; i++) {
|
||||||
|
formData.push('ids%5B%5D=' + encodeURIComponent(ids[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/admin/product_archive/bulk_delete_permanent/',
|
||||||
|
type: 'POST',
|
||||||
|
data: formData.join('&'),
|
||||||
|
contentType: 'application/x-www-form-urlencoded',
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(resp) {
|
||||||
|
if (resp && resp.deleted > 0) {
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Nie udało się usunąć produktów. Spróbuj ponownie.');
|
||||||
|
$btn.prop('disabled', false).html('<i class="fa fa-trash-o"></i> Usuń zaznaczone trwale');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
alert('Błąd podczas usuwania produktów. Spróbuj ponownie.');
|
||||||
|
$btn.prop('disabled', false).html('<i class="fa fa-trash-o"></i> Usuń zaznaczone trwale');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof $.confirm === 'function') {
|
||||||
|
$.confirm({
|
||||||
|
title: 'Potwierdzenie',
|
||||||
|
content: confirmMsg,
|
||||||
|
type: 'red',
|
||||||
|
boxWidth: '560px',
|
||||||
|
useBootstrap: false,
|
||||||
|
animation: 'scale',
|
||||||
|
closeAnimation: 'scale',
|
||||||
|
backgroundDismissAnimation: 'shake',
|
||||||
|
container: 'body',
|
||||||
|
theme: 'modern',
|
||||||
|
columnClass: '',
|
||||||
|
typeAnimated: true,
|
||||||
|
lazyOpen: false,
|
||||||
|
draggable: false,
|
||||||
|
closeIcon: true,
|
||||||
|
containerFluid: true,
|
||||||
|
escapeKey: true,
|
||||||
|
backgroundDismiss: true,
|
||||||
|
buttons: {
|
||||||
|
cancel: {
|
||||||
|
text: 'Anuluj',
|
||||||
|
btnClass: 'btn-default'
|
||||||
|
},
|
||||||
|
confirm: {
|
||||||
|
text: 'Tak, usuń trwale',
|
||||||
|
btnClass: 'btn-danger',
|
||||||
|
action: doDelete
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (window.confirm(confirmMsg)) {
|
||||||
|
doDelete();
|
||||||
|
}
|
||||||
|
});
|
||||||
})(window.jQuery);
|
})(window.jQuery);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
<div id="js-bulk-action-bar" class="bulk-action-bar" style="display:none;">
|
||||||
|
<span class="bulk-action-bar__info js-bulk-count-label">Zaznaczono: 0</span>
|
||||||
|
<button type="button" class="btn btn-danger btn-sm js-bulk-delete-btn">
|
||||||
|
<i class="fa fa-trash-o"></i> Usuń zaznaczone trwale
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<?= \Shared\Tpl\Tpl::view('components/table-list', ['list' => $this->viewModel]); ?>
|
<?= \Shared\Tpl\Tpl::view('components/table-list', ['list' => $this->viewModel]); ?>
|
||||||
|
|
||||||
<?php if (!empty($this->viewModel->customScriptView)): ?>
|
<?php if (!empty($this->viewModel->customScriptView)): ?>
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ class ProductArchiveController
|
|||||||
. $skuEanHtml;
|
. $skuEanHtml;
|
||||||
|
|
||||||
$rows[] = [
|
$rows[] = [
|
||||||
|
'_checkbox' => '<input type="checkbox" class="js-bulk-check" value="' . $id . '" aria-label="Zaznacz produkt">',
|
||||||
'lp' => $lp++ . '.',
|
'lp' => $lp++ . '.',
|
||||||
'product' => $productCell,
|
'product' => $productCell,
|
||||||
'price_brutto' => $priceBrutto !== '' ? $priceBrutto : '-',
|
'price_brutto' => $priceBrutto !== '' ? $priceBrutto : '-',
|
||||||
@@ -123,6 +124,7 @@ class ProductArchiveController
|
|||||||
|
|
||||||
$viewModel = new \admin\ViewModels\Common\PaginatedTableViewModel(
|
$viewModel = new \admin\ViewModels\Common\PaginatedTableViewModel(
|
||||||
[
|
[
|
||||||
|
['key' => '_checkbox', 'label' => '', 'class' => 'text-center table-col-bulk-check', 'sortable' => false, 'raw' => true],
|
||||||
['key' => 'lp', 'label' => 'Lp.', 'class' => 'text-center', 'sortable' => false],
|
['key' => 'lp', 'label' => 'Lp.', 'class' => 'text-center', 'sortable' => false],
|
||||||
['key' => 'product', 'sort_key' => 'name', 'label' => 'Nazwa', 'sortable' => true, 'raw' => true],
|
['key' => 'product', 'sort_key' => 'name', 'label' => 'Nazwa', 'sortable' => true, 'raw' => true],
|
||||||
['key' => 'price_brutto', 'sort_key' => 'price_brutto', 'label' => 'Cena', 'class' => 'text-center', 'sortable' => true],
|
['key' => 'price_brutto', 'sort_key' => 'price_brutto', 'label' => 'Cena', 'class' => 'text-center', 'sortable' => true],
|
||||||
@@ -190,4 +192,40 @@ class ProductArchiveController
|
|||||||
header( 'Location: /admin/product_archive/list/' );
|
header( 'Location: /admin/product_archive/list/' );
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function bulk_delete_permanent(): void
|
||||||
|
{
|
||||||
|
header( 'Content-Type: application/json; charset=utf-8' );
|
||||||
|
|
||||||
|
$rawIds = isset( $_POST['ids'] ) && is_array( $_POST['ids'] ) ? $_POST['ids'] : [];
|
||||||
|
$ids = [];
|
||||||
|
foreach ( $rawIds as $raw ) {
|
||||||
|
$id = (int) $raw;
|
||||||
|
if ( $id > 0 ) {
|
||||||
|
$ids[] = $id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( empty( $ids ) ) {
|
||||||
|
echo json_encode( ['success' => false, 'message' => 'Nie wybrano żadnych produktów.'] );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$deleted = 0;
|
||||||
|
$errors = [];
|
||||||
|
foreach ( $ids as $id ) {
|
||||||
|
if ( $this->productRepository->delete( $id ) ) {
|
||||||
|
$deleted++;
|
||||||
|
} else {
|
||||||
|
$errors[] = $id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode( [
|
||||||
|
'success' => empty( $errors ),
|
||||||
|
'deleted' => $deleted,
|
||||||
|
'errors' => $errors,
|
||||||
|
] );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,15 @@ Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## ver. 0.327 (2026-02-27) - Masowe usuwanie w archiwum produktów
|
||||||
|
|
||||||
|
- **NEW**: `ProductArchiveController::bulk_delete_permanent()` — endpoint POST `product_archive/bulk_delete_permanent/`, przyjmuje `ids[]`, usuwa każdy produkt przez `ProductRepository::delete()`, zwraca JSON `{success, deleted, errors[]}`
|
||||||
|
- **UX**: Kolumna checkboxów w liście archiwum produktów + pasek akcji masowych z licznikiem zaznaczonych
|
||||||
|
- **UX**: "Zaznacz wszystkie" w nagłówku tabeli (wstrzyknięty via JS), dialog potwierdzenia przed masowym usunięciem
|
||||||
|
- **TEST**: 2 nowe testy w `ProductArchiveControllerTest` — weryfikacja istnienia i sygnatury `bulk_delete_permanent`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## ver. 0.326 (2026-02-27) - API: endpoint categories/list
|
## ver. 0.326 (2026-02-27) - API: endpoint categories/list
|
||||||
|
|
||||||
- **NEW**: `api\Controllers\CategoriesApiController` — nowy kontroler API z akcją `list`
|
- **NEW**: `api\Controllers\CategoriesApiController` — nowy kontroler API z akcją `list`
|
||||||
|
|||||||
@@ -53,4 +53,15 @@ class ProductArchiveControllerTest extends TestCase
|
|||||||
$this->assertCount(1, $params);
|
$this->assertCount(1, $params);
|
||||||
$this->assertEquals('Domain\Product\ProductRepository', $params[0]->getType()->getName());
|
$this->assertEquals('Domain\Product\ProductRepository', $params[0]->getType()->getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testHasBulkDeletePermanentMethod(): void
|
||||||
|
{
|
||||||
|
$this->assertTrue(method_exists($this->controller, 'bulk_delete_permanent'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testBulkDeletePermanentMethodReturnType(): void
|
||||||
|
{
|
||||||
|
$reflection = new \ReflectionClass($this->controller);
|
||||||
|
$this->assertEquals('void', (string)$reflection->getMethod('bulk_delete_permanent')->getReturnType());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user