feat: enhance product datatable with bulk delete functionality and refined category/price columns.

This commit is contained in:
pbao9
2026-02-13 14:59:10 +07:00
parent 549e434627
commit 78b87c10fe
8 changed files with 381 additions and 270 deletions
+213 -228
View File
@@ -1,35 +1,33 @@
@props(['id', 'columns', 'ajax', 'bulkDeleteUrl' => null])
@php
$dtColumns = collect($columns)
->map(function ($c) {
return [
'data' => $c['key'],
'name' => $c['key'],
'orderable' => $c['orderable'] ?? true,
'searchable' => $c['searchable'] ?? true,
'className' => $c['className'] ?? '',
];
})
->values();
$dtColumns = collect($columns)
->map(function ($c) {
return [
'data' => $c['key'],
'name' => $c['key'],
'orderable' => $c['orderable'] ?? true,
'searchable' => $c['searchable'] ?? true,
'className' => $c['className'] ?? '',
];
})
->values();
@endphp
@if ($bulkDeleteUrl)
<div class="d-flex justify-content-start align-items-center mb-3" id="{{ $id }}-bulk-actions"
style="display: none !important;">
<div class="bg-danger-subtle text-danger rounded-3 d-flex align-items-center gap-3 px-4 py-2">
<div class="d-flex align-items-center gap-2">
<i data-lucide="alert-circle" width="16" height="16"></i>
<span class="fw-medium"><span id="{{ $id }}-selected-count">0</span> items selected</span>
<div class="d-flex justify-content-start align-items-center mb-3" id="{{ $id }}-bulk-actions"
style="display: none !important;">
<div class="bg-danger-subtle text-danger rounded-3 d-flex align-items-center gap-3 px-4 py-2">
<div class="d-flex align-items-center gap-2">
<i data-lucide="alert-circle" width="16" height="16"></i>
<span class="fw-medium"><span id="{{ $id }}-selected-count">0</span> items selected</span>
</div>
<div class="vr bg-danger opacity-25"></div>
<x-button-c-r-m type="button" variant="danger" size="sm" icon="trash-2" id="{{ $id }}-bulk-delete-opener-btn"
class="shadow-sm">
Delete Selected
</x-button-c-r-m>
</div>
<div class="vr bg-danger opacity-25"></div>
<x-button-c-r-m type="button" variant="danger" size="sm" icon="trash-2" id="{{ $id }}-bulk-delete-opener-btn"
class="shadow-sm">
Delete Selected
</x-button-c-r-m>
</div>
</div>
{{-- Bulk Delete Confirmation Modal (Bootstrap 5) --}}
<div class="modal fade" id="{{ $id }}-bulk-delete-modal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
@@ -58,8 +56,7 @@
</div>
</div>
@endif
<div class="data-table-wrapper position-relative overflow-hidden shadow-none">
{{-- Loading Process Indicator --}}
<div class="data-table-wrapper position-relative shadow-none">
<div class="loading-process" id="{{ $id }}-loading" style="display: none;">
<div class="loading-bar"></div>
<div class="loading-overlay"></div>
@@ -92,7 +89,7 @@
</th>
@endforeach
</tr>
<tr class="filter-row bg-light">
<tr class="filter-row bg-light" id="{{ $id }}-filter-row" style="display: none;">
@foreach ($columns as $col)
<th class="border-bottom p-2">
@if ($col['searchable'] ?? true)
@@ -110,7 +107,6 @@
</tr>
</thead>
<tbody>
{{-- DataTables will populate this --}}
</tbody>
</table>
</div>
@@ -129,8 +125,8 @@
stateSave: true,
pagingType: 'full_numbers',
language: {
loadingRecords: '<div class="d-flex flex-column align-items-center justify-content-center py-5"><div class="spinner-border text-primary mb-3" role="status"></div><h6 class="text-secondary fw-medium">Đang tải dữ liệu...</h6></div>',
searchPlaceholder: "Tìm kiếm...",
loadingRecords: '<div class="d-flex flex-column align-items-center justify-content-center py-5"><div class="spinner-border text-primary mb-3" role="status"></div><h6 class="text-secondary fw-medium">Loading...</h6></div>',
searchPlaceholder: "Search...",
search: "",
lengthMenu: "_MENU_",
info: "Showing <span class='fw-bold'>_START_</span> to <span class='fw-bold'>_END_</span> of <span class='fw-bold'>_TOTAL_</span> entries",
@@ -143,10 +139,34 @@
zeroRecords: '<div class="d-flex flex-column align-items-center justify-content-center py-5"><div class="text-secondary mb-3"><i data-lucide="inbox" width="28" height="28" stroke-width="1.5"></i></div><h6 class="text-secondary fw-medium">No matching records found</h6></div>',
emptyTable: '<div class="d-flex flex-column align-items-center justify-content-center py-5"><div class="text-secondary mb-3"><i data-lucide="inbox" width="28" height="28" stroke-width="1.5"></i></div><h6 class="text-secondary fw-medium">No data available</h6></div>'
},
dom: "<'row p-0'<'col-sm-12'tr>>" +
"<'row px-3 pb-3 pt-3 align-items-center'<'col-sm-12 col-md-5 text-muted small'i><'col-sm-12 col-md-7 d-flex align-items-center justify-content-end gap-2'lp>>",
dom: "<'row px-3 py-2 align-items-center'<'col-sm-12 col-md-6 d-inline-flex align-items-center gap-2'<'dt-filter-toggle'>f><'col-sm-12 col-md-6 d-inline-flex align-items-center justify-content-end gap-2'l>>" +
"<'row p-0'<'col-sm-12'tr>>" +
"<'row px-3 pb-3 pt-3 align-items-center'<'col-sm-12 col-md-5 text-muted small d-inline-flex align-items-center'i><'col-sm-12 col-md-7 d-inline-flex align-items-center justify-content-end gap-2'p>>",
initComplete: function () {
// Initialize select2 with custom styling
var wrapper = $('#{{ $id }}_wrapper');
wrapper.find('.dt-filter-toggle').html(
'<button type="button" class="btn btn-sm btn-outline-secondary d-flex align-items-center gap-1" id="{{ $id }}-filter-toggle-btn">' +
'<i data-lucide="filter" width="14" height="14"></i>' +
'<span>Filters</span>' +
'</button>'
);
$('#{{ $id }}-filter-toggle-btn').on('click', function () {
var filterRow = $('#{{ $id }}-filter-row');
var btn = $(this);
filterRow.toggle();
btn.toggleClass('btn-outline-secondary btn-primary');
if (!filterRow.is(':visible')) {
filterRow.find('input').val('');
table.columns().search('').draw();
}
});
wrapper.find('.dataTables_filter input')
.addClass('form-control form-control-sm')
.attr('placeholder', 'Search...');
$('.dataTables_length select').select2({
minimumResultsForSearch: Infinity,
width: '80px',
@@ -154,7 +174,6 @@
selectionCssClass: 'small-select-selection'
});
// Debounced search for column inputs (ERP Standard: Reduce server load)
let searchTimeouts = {};
this.api().columns().every(function () {
var that = this;
@@ -167,12 +186,11 @@
if (that.search() !== value) {
that.search(value).draw();
}
}, 400); // 400ms delay
}, 400);
});
});
},
drawCallback: function () {
// Re-initialize Select2 on draw (if lost)
if ($('.dataTables_length select').data('select2')) {
$('.dataTables_length select').select2('destroy');
}
@@ -181,7 +199,6 @@
width: '80px'
});
// Re-initialize icons with slight delay to ensuring DOM is ready
setTimeout(function () {
if (window.lucide) {
lucide.createIcons();
@@ -190,16 +207,13 @@
}
});
// ERP Standard: Double click row to edit
$('#{{ $id }} tbody').on('dblclick', 'tr', function () {
// Try to find an edit link in the row
const editLink = $(this).find('a.btn-outline-primary, a[href*="edit"]').first();
if (editLink.length && editLink.attr('href')) {
window.location.href = editLink.attr('href');
}
});
// Custom Loading Process Logic
table.on('processing.dt', function (e, settings, processing) {
const loadingEl = $('#{{ $id }}-loading');
if (processing) {
@@ -209,146 +223,122 @@
}
});
// Prevent sorting when clicking on the input
$('#{{ $id }} thead tr:eq(1) th').on('click', function (e) {
$('#{{ $id }} thead').on('click', '#{{ $id }}-filter-row th', function (e) {
e.stopPropagation();
});
// External trigger for column search (redundant but kept for manual binding if needed)
$('#{{ $id }} .column-search').on('keyup change', function () {
var columnIndex = $(this).data('column');
table.column(columnIndex).search(this.value).draw();
});
});
@if ($bulkDeleteUrl)
(function () {
const bulkActions = $('#{{ $id }}-bulk-actions');
const selectedCount = $('#{{ $id }}-selected-count');
const masterCheckbox = $('#dt-master-checkbox');
let selectedIds = [];
@if ($bulkDeleteUrl)
document.addEventListener('DOMContentLoaded', function () {
const tableId = '#{{ $id }}';
const bulkActions = $('#{{ $id }}-bulk-actions');
const deleteBtn = $('#{{ $id }}-bulk-delete-btn');
const selectedCount = $('#{{ $id }}-selected-count');
const masterCheckbox = $('#dt-master-checkbox');
let selectedIds = [];
$(document).on('change', '#{{ $id }} #dt-master-checkbox', function () {
const isChecked = $(this).is(':checked');
$('#{{ $id }} .dt-checkbox').prop('checked', isChecked).trigger('change');
});
// Handle Master Checkbox
$(document).on('change', '#dt-master-checkbox', function () {
const isChecked = $(this).is(':checked');
$('.dt-checkbox').prop('checked', isChecked).trigger('change');
});
$(document).on('change', '#{{ $id }} .dt-checkbox', function () {
const id = $(this).val();
if ($(this).is(':checked')) {
if (!selectedIds.includes(id)) selectedIds.push(id);
} else {
selectedIds = selectedIds.filter(item => item !== id);
}
// Handle Individual Checkbox
$(document).on('change', '.dt-checkbox', function () {
const id = $(this).val();
if ($(this).is(':checked')) {
if (!selectedIds.includes(id)) selectedIds.push(id);
} else {
selectedIds = selectedIds.filter(item => item !== id);
}
// Update UI
selectedCount.text(selectedIds.length);
if (selectedIds.length > 0) {
bulkActions.show();
bulkActions.removeClass('d-none').addClass('d-flex');
bulkActions.attr('style', ''); // Clear inline style
} else {
bulkActions.hide();
}
// Update Master Checkbox state
const allCheckboxes = $('.dt-checkbox').length;
const checkedCheckboxes = $('.dt-checkbox:checked').length;
if (allCheckboxes > 0 && allCheckboxes === checkedCheckboxes) {
masterCheckbox.prop('checked', true);
masterCheckbox.prop('indeterminate', false);
} else if (checkedCheckboxes > 0) {
masterCheckbox.prop('checked', false);
masterCheckbox.prop('indeterminate', true);
} else {
masterCheckbox.prop('checked', false);
masterCheckbox.prop('indeterminate', false);
}
});
// Reset selection on table draw
$('#{{ $id }}').on('draw.dt', function () {
selectedIds = [];
bulkActions.hide();
masterCheckbox.prop('checked', false);
masterCheckbox.prop('indeterminate', false);
});
// Handle Bulk Delete Opener
const modalId = '{{ $id }}-bulk-delete-modal';
const modalEl = document.getElementById(modalId);
const openerBtn = $('#{{ $id }}-bulk-delete-opener-btn');
const confirmBtn = $('#{{ $id }}-confirm-bulk-delete-btn');
const countDisplay = $('#{{ $id }}-bulk-delete-count-display');
let bsModal = null;
openerBtn.on('click', function () {
if (selectedIds.length === 0) return;
countDisplay.text(selectedIds.length);
if (!bsModal) {
bsModal = new bootstrap.Modal(modalEl);
}
bsModal.show();
});
// Handle Confirm Delete
confirmBtn.on('click', function () {
$.ajax({
url: '{{ $bulkDeleteUrl }}',
type: 'DELETE',
data: {
ids: selectedIds,
_token: '{{ csrf_token() }}'
},
beforeSend: function () {
confirmBtn.prop('disabled', true).text('Deleting...');
},
success: function (response) {
if (bsModal) bsModal.hide();
$('#{{ $id }}').DataTable().ajax.reload();
selectedIds = [];
selectedCount.text(selectedIds.length);
if (selectedIds.length > 0) {
bulkActions.show();
bulkActions.removeClass('d-none').addClass('d-flex');
bulkActions.attr('style', '');
} else {
bulkActions.hide();
}
// Show success toast
if (typeof FuiToast !== 'undefined') {
FuiToast.success(response.message ||
'Selected items have been deleted successfully.');
}
const allCheckboxes = $('#{{ $id }} .dt-checkbox').length;
const checkedCheckboxes = $('#{{ $id }} .dt-checkbox:checked').length;
// Re-render icons after reload
if (window.lucide) lucide.createIcons();
},
error: function (xhr) {
const message = xhr.responseJSON?.message ||
'Something went wrong.';
if (typeof FuiToast !== 'undefined') {
FuiToast.error(message);
} else {
alert(message);
}
if (bsModal) bsModal.hide();
},
complete: function () {
confirmBtn.prop('disabled', false).text('Delete');
if (allCheckboxes > 0 && allCheckboxes === checkedCheckboxes) {
masterCheckbox.prop('checked', true);
masterCheckbox.prop('indeterminate', false);
} else if (checkedCheckboxes > 0) {
masterCheckbox.prop('checked', false);
masterCheckbox.prop('indeterminate', true);
} else {
masterCheckbox.prop('checked', false);
masterCheckbox.prop('indeterminate', false);
}
});
});
$('#{{ $id }}').on('draw.dt', function () {
selectedIds = [];
bulkActions.hide();
masterCheckbox.prop('checked', false);
masterCheckbox.prop('indeterminate', false);
});
const modalEl = document.getElementById('{{ $id }}-bulk-delete-modal');
const openerBtn = $('#{{ $id }}-bulk-delete-opener-btn');
const confirmBtn = $('#{{ $id }}-confirm-bulk-delete-btn');
const countDisplay = $('#{{ $id }}-bulk-delete-count-display');
let bsModal = null;
openerBtn.on('click', function () {
if (selectedIds.length === 0) return;
countDisplay.text(selectedIds.length);
if (!bsModal) {
bsModal = new bootstrap.Modal(modalEl);
}
bsModal.show();
});
confirmBtn.on('click', function () {
$.ajax({
url: '{{ $bulkDeleteUrl }}',
type: 'DELETE',
data: {
ids: selectedIds,
_token: '{{ csrf_token() }}'
},
beforeSend: function () {
confirmBtn.prop('disabled', true).text('Deleting...');
},
success: function (response) {
if (bsModal) bsModal.hide();
$('#{{ $id }}').DataTable().ajax.reload();
selectedIds = [];
bulkActions.hide();
if (typeof FuiToast !== 'undefined') {
FuiToast.success(response.message || 'Selected items have been deleted successfully.');
}
if (window.lucide) lucide.createIcons();
},
error: function (xhr) {
const message = xhr.responseJSON?.message || 'Something went wrong.';
if (typeof FuiToast !== 'undefined') {
FuiToast.error(message);
} else {
alert(message);
}
if (bsModal) bsModal.hide();
},
complete: function () {
confirmBtn.prop('disabled', false).text('Delete');
}
});
});
})();
@endif
});
@endif
</script>
<style>
/* Custom SAAS DataTable Styles */
.data-table-wrapper {
background: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
overflow-y: auto;
overflow: visible;
position: relative;
}
@@ -362,74 +352,51 @@
vertical-align: middle;
position: sticky;
top: 0;
z-index: 3;
{
{
-- Lowered from 10 --
}
}
z-index: 2;
background-color: #f8f9fa;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
/* Sticky Action Column (ERP Standard for wide tables) */
.table thead th:last-child,
.table tbody td:last-child {
position: sticky;
right: 0;
background: #fff;
z-index: 2;
{
{
-- Lowered from 5 --
}
}
z-index: 1;
border-left: 1px solid #f1f5f9;
box-shadow: -2px 0 5px rgba(0, 0, 0, 0.02);
width: 70px;
min-width: 70px;
max-width: 70px;
}
.table thead th:last-child {
background: #f8f9fa;
z-index: 4;
{
{
-- Lowered from 11 --
}
}
z-index: 3;
}
/* Sticky Checkbox Column */
.table thead th:first-child,
.table tbody td:first-child {
position: sticky;
left: 0;
background: #fff;
z-index: 2;
{
{
-- Lowered from 5 --
}
}
z-index: 1;
border-right: 1px solid #f1f5f9;
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.02);
}
.table thead th:first-child {
background: #f8f9fa;
z-index: 4;
z-index: 3;
}
{
{
-- Lowered from 11 --
}
}
.table tbody tr {
position: relative;
z-index: 0;
}
.table tbody tr:hover {
z-index: 5;
}
.table tbody tr:hover td {
@@ -441,21 +408,17 @@
border-right: 1px solid #f1f5f9;
}
/* Sticky for second row (filter inputs) if present */
.data-table-wrapper .table thead tr:nth-child(2) th {
.data-table-wrapper .table thead tr.filter-row th {
top: 45px;
/* Adjust based on typical header height */
z-index: 4;
z-index: 2;
}
.data-table-wrapper .input-group-text {
color: #94a3b8;
/* Slate 400 */
}
.data-table-wrapper .form-control::placeholder {
color: #cbd5e1;
/* Slate 300 */
}
.dataTables_wrapper .pagination .page-item .page-link {
@@ -475,7 +438,6 @@
.dataTables_wrapper .pagination .page-item.active .page-link {
background-color: #0f172a;
/* Slate 900 or Primary Color */
color: white;
font-weight: 500;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
@@ -493,7 +455,6 @@
border-color: #e2e8f0;
}
/* Select2 Customization for Table Length */
.dataTables_length {
margin-right: 1rem;
}
@@ -522,7 +483,6 @@
line-height: normal;
}
/* Loading Process Styles */
.loading-process {
position: absolute;
top: 0;
@@ -530,13 +490,6 @@
right: 0;
bottom: 0;
z-index: 5;
{
{
-- Lowered from 1000 --
}
}
pointer-events: none;
}
@@ -549,12 +502,6 @@
background: #f1f5f9;
overflow: hidden;
z-index: 6;
{
{
-- Lowered from 1001 --
}
}
}
.loading-bar::after {
@@ -588,7 +535,6 @@
}
}
/* Custom Sort Icons Styles */
.sort-icons {
opacity: 0.15;
transition: opacity 0.2s;
@@ -614,7 +560,6 @@
display: none !important;
}
/* Hide default DataTables icons */
table.dataTable thead .sorting:before,
table.dataTable thead .sorting:after,
table.dataTable thead .sorting_asc:before,
@@ -624,10 +569,50 @@
display: none !important;
}
/* Processing Indicator with improved look - Centered */
div.dataTables_processing {
display: none !important;
/* Hide default processing */
}
.dataTables_filter {
display: flex;
align-items: center;
}
.dataTables_filter label {
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0;
}
.dt-action-dropdown .dropdown-menu {
min-width: 140px;
padding: 0.25rem 0;
font-size: 0.8125rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border: 1px solid #e2e8f0;
z-index: 1050;
position: absolute;
}
.dt-action-dropdown .dropdown-item {
padding: 0.4rem 0.75rem;
display: flex;
align-items: center;
gap: 0.5rem;
transition: all 0.15s ease;
}
.dt-action-dropdown .dropdown-item:hover {
background-color: #f8fafc;
}
.dt-action-dropdown .dropdown-item.text-danger:hover {
background-color: #fef2f2;
}
.dt-action-dropdown .dropdown-divider {
margin: 0.25rem 0;
}
</style>
@endpush