feat: Implement nested product category display in admin datatable with expandable rows.

This commit is contained in:
pbao9
2026-02-11 17:30:20 +07:00
parent 556b2c02e6
commit 99b61bfb51
5 changed files with 108 additions and 15 deletions
@@ -0,0 +1,3 @@
@if($row->children->count() > 0)
<i data-lucide="plus-circle" class="cursor-pointer text-success mx-auto" style="width:15px; height:15px"></i>
@endif
@@ -41,6 +41,7 @@ $tabs = [
<div class="card-body">
<x-datatable id="productTable" ajax="{{ route('products.group.datatable') }}"
bulkDeleteUrl="{{ route('products.group.bulkDelete') }}" :columns="[
['key' => 'details_control', 'label' => '', 'orderable' => false, 'searchable' => false, 'className' => 'details-control text-center cursor-pointer'],
['key' => 'name', 'label' => 'Group'],
['key' => 'slug', 'label' => 'Slug'],
['key' => 'is_active', 'label' => 'Status'],
@@ -61,17 +62,103 @@ $tabs = [
<script src="{{ asset($datatable) }}"></script>
@endforeach
@endpush
@push('custom-styles')
<style>
/* Lucide icon alignment */
td.details-control {
text-align: center;
vertical-align: middle;
cursor: pointer;
}
</style>
@endpush
@push('custom-scripts')
<script>
document.addEventListener('DOMContentLoaded', function () {
// Re-initialize Lucide icons after DataTable draws
$('#productTable').on('draw.dt', function() {
setTimeout(function() {
$('#productTable').on('draw.dt', function () {
setTimeout(function () {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
// Disable double click on rows to prevent unwanted editing
$('#productTable tbody').off('dblclick');
}, 100);
});
// Child Row (Details Control) Handler
var tableId = '#productTable';
$(tableId + ' tbody').on('click', 'td.details-control', function () {
var tr = $(this).closest('tr');
var table = $(tableId).DataTable();
var row = table.row(tr);
var cell = $(this);
if (row.child.isShown()) {
// This row is already open - close it
row.child.hide();
tr.removeClass('shown');
// Reset icon to Plus
cell.html('<i data-lucide="plus-circle" class="cursor-pointer text-success mx-auto" style="width:15px; height:15px"></i>');
} else {
// Open this row
if (row.data().children && row.data().children.length > 0) {
row.child(format(row.data())).show();
tr.addClass('shown');
// Change icon to Minus
cell.html('<i data-lucide="minus-circle" class="cursor-pointer text-danger mx-auto" style="width:15px; height:15px"></i>');
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
}
}
// Re-init icons for the newly added markup (plus/minus)
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
function format(d) {
// `d` is the original data object for the row
if (!d.children || d.children.length === 0) {
return '';
}
var childHtml = '<div class="p-3 bg-light rounded">';
childHtml += '<h6 class="mb-2 text-primary font-weight-bold">Sub Categories</h6>';
childHtml += '<table class="table table-sm table-bordered bg-white mb-0">';
childHtml += '<thead class="thead-light"><tr><th>Group</th><th>Slug</th><th>Status</th><th>Action</th></tr></thead>';
childHtml += '<tbody>';
d.children.forEach(function (child) {
// Re-use logic for status badge if possible, or simple text for now
var statusBadge = child.is_active ?
'<span class="badge bg-success">Active</span>' :
'<span class="badge bg-secondary">Inactive</span>';
// Build Action Buttons (Edit/Delete)
var editUrl = '{{ route("products.group.edit", ":id") }}'.replace(':id', child.id);
var actionButtons = `
<div class="d-flex gap-2">
<a href="${editUrl}" class="btn btn-icon btn-sm btn-outline-primary" title="Edit">
<i data-lucide="edit-2" width="14" height="14"></i>
</a>
</div>
`;
childHtml += '<tr>' +
'<td>' + child.name + '</td>' +
'<td>' + child.slug + '</td>' +
'<td>' + statusBadge + '</td>' +
'<td>' + actionButtons + '</td>' +
'</tr>';
});
childHtml += '</tbody></table></div>';
return childHtml;
}
});
</script>
@endpush
@@ -82,32 +169,32 @@ $tabs = [
.modal-backdrop {
z-index: 9998 !important;
}
.modal {
z-index: 9999 !important;
}
/* Override datatable wrapper overflow to prevent clipping modals */
.data-table-wrapper {
overflow: visible !important;
overflow-x: auto !important;
}
/* Reduce z-index of sticky elements to be below modal */
.data-table-wrapper .table thead th {
z-index: 3 !important;
}
.table thead th:last-child,
.table tbody td:last-child {
z-index: 2 !important;
}
.table thead th:first-child,
.table tbody td:first-child {
z-index: 2 !important;
}
.table thead th:nth-child(2) {
z-index: 4 !important;
}