feat: Implement nested product category display in admin datatable with expandable rows.
This commit is contained in:
@@ -91,14 +91,17 @@ class ProductCategoryController extends Controller
|
||||
public function datatable(Request $request)
|
||||
{
|
||||
$query = $this->productCategoryRepository->filter(
|
||||
$request->all(),
|
||||
[],
|
||||
array_merge($request->all(), ['parent_id' => null]),
|
||||
['children'],
|
||||
[],
|
||||
['*'],
|
||||
true
|
||||
);
|
||||
|
||||
return DataTables::eloquent($query)
|
||||
->addColumn('details_control', function ($d) {
|
||||
return view('administrator.pages.product.categories.datatable.details_control', ['row' => $d])->render();
|
||||
})
|
||||
->addColumn(
|
||||
'action',
|
||||
function ($d) use ($request) {
|
||||
@@ -120,7 +123,7 @@ class ProductCategoryController extends Controller
|
||||
return '<div class="form-check d-flex align-items-center justify-content-center"><input class="form-check-input dt-checkbox" type="checkbox" value="' . $d->id . '"></div>';
|
||||
}
|
||||
)
|
||||
->rawColumns(['action', 'is_active', 'checkbox'])
|
||||
->rawColumns(['details_control', 'action', 'is_active', 'checkbox'])
|
||||
->make(true);
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ class ProductCategories extends Model
|
||||
|
||||
public function children()
|
||||
{
|
||||
return $this->hasMany(ProductCategories::class, 'parent_id');
|
||||
return $this->hasMany(ProductCategories::class, 'parent_id')->with('children');
|
||||
}
|
||||
|
||||
public function products()
|
||||
|
||||
@@ -44,9 +44,9 @@ class ProductCategoryRepository extends BaseRepository implements ProductCategor
|
||||
$query->onlyTrashed();
|
||||
}
|
||||
|
||||
// if (!empty($data['vcenter_id'])) {
|
||||
// $query->where('vcenter_id', $data['vcenter_id']);
|
||||
// }
|
||||
if (array_key_exists('parent_id', $data)) {
|
||||
$query->where('parent_id', $data['parent_id']);
|
||||
}
|
||||
|
||||
if (!empty($withCount)) {
|
||||
$query->withCount($withCount);
|
||||
|
||||
+3
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user