feat: enhance product datatable with bulk delete functionality and refined category/price columns.
This commit is contained in:
@@ -127,23 +127,80 @@ class ProductController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
public function duplicate($id)
|
||||
{
|
||||
try {
|
||||
$product = $this->productRepository->find($id);
|
||||
|
||||
if (!$product) {
|
||||
return back()->withErrors("Can't find product.");
|
||||
}
|
||||
|
||||
$product->load(['pricings', 'osVersions', 'controlPanels']);
|
||||
|
||||
$newData = $product->only([
|
||||
'short_description', 'description', 'setup_fee', 'trial_days',
|
||||
'cpu', 'ram', 'gpu', 'disk', 'bandwidth', 'stock',
|
||||
'is_unlimited', 'is_active', 'sort_order', 'product_type',
|
||||
'product_categories_id', 'published_at',
|
||||
]);
|
||||
$newData['name'] = $product->name . ' (Copy)';
|
||||
|
||||
$newProduct = $this->productService->store($newData);
|
||||
|
||||
if ($product->osVersions->isNotEmpty()) {
|
||||
$newProduct->osVersions()->sync($product->osVersions->pluck('id'));
|
||||
}
|
||||
|
||||
if ($product->controlPanels->isNotEmpty()) {
|
||||
$syncData = [];
|
||||
foreach ($product->controlPanels as $panel) {
|
||||
$syncData[$panel->id] = [
|
||||
'price_modifier' => $panel->pivot->price_modifier,
|
||||
'is_default' => $panel->pivot->is_default,
|
||||
];
|
||||
}
|
||||
$newProduct->controlPanels()->sync($syncData);
|
||||
}
|
||||
|
||||
if ($product->pricings->isNotEmpty()) {
|
||||
foreach ($product->pricings as $pricing) {
|
||||
$newProduct->pricings()->create([
|
||||
'billing_cycle' => $pricing->billing_cycle,
|
||||
'price' => $pricing->price,
|
||||
'price_sale' => $pricing->price_sale,
|
||||
'currency' => $pricing->currency,
|
||||
'is_active' => $pricing->is_active,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()->route('products.edit', ['id' => $newProduct->id])
|
||||
->with('success', 'Product duplicated successfully');
|
||||
} catch (Exception $e) {
|
||||
return back()->withErrors($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function datatable(Request $request)
|
||||
{
|
||||
$query = $this->productRepository->filter(
|
||||
$request->all(),
|
||||
['category'],
|
||||
['category', 'pricings'],
|
||||
[],
|
||||
['*'],
|
||||
true
|
||||
);
|
||||
|
||||
return DataTables::eloquent($query)
|
||||
->addColumn('category_name', function ($d) {
|
||||
return $d->category?->name ?? '-';
|
||||
->addColumn('checkbox', function ($d) {
|
||||
return '<input type="checkbox" class="form-check-input dt-checkbox" value="' . $d->id . '">';
|
||||
})
|
||||
->addColumn('product_type_label', function ($d) {
|
||||
$type = ProductTypeEnums::tryFrom($d->product_type);
|
||||
return $type ? $type->label() : '-';
|
||||
->addColumn('category', function ($d) {
|
||||
return view('administrator.pages.product.datatable.category', ['row' => $d])->render();
|
||||
})
|
||||
->addColumn('price', function ($d) {
|
||||
return view('administrator.pages.product.datatable.price', ['row' => $d])->render();
|
||||
})
|
||||
->addColumn('specs', function ($d) {
|
||||
return view('administrator.pages.product.datatable.specs', ['row' => $d])->render();
|
||||
@@ -157,7 +214,7 @@ class ProductController extends Controller
|
||||
return view('administrator.pages.product.datatable.action', ['row' => $d])->render();
|
||||
}
|
||||
)
|
||||
->rawColumns(['specs', 'status', 'action'])
|
||||
->rawColumns(['checkbox', 'category', 'price', 'specs', 'status', 'action'])
|
||||
->make(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
.dt-container {
|
||||
&.dt-bootstrap5 {
|
||||
.dt-length {
|
||||
@extend .d-inline-flex;
|
||||
@media(max-width: 767px) {
|
||||
text-align: left;
|
||||
}
|
||||
@@ -13,6 +14,7 @@
|
||||
}
|
||||
}
|
||||
.dt-search {
|
||||
@extend .d-inline-flex;
|
||||
@media(max-width: 767px) {
|
||||
text-align: left;
|
||||
input {
|
||||
|
||||
@@ -1,6 +1,34 @@
|
||||
<div class="d-flex gap-2">
|
||||
<x-button-crm type="button" variant="outline-primary" size="sm" route="products.edit" :params="['id' => $row->id]"
|
||||
icon="pencil">Chỉnh sửa</x-button-crm>
|
||||
<x-button-crm type="button" variant="outline-danger" size="sm" route="products.destroy" :params="['id' => $row->id]"
|
||||
icon="trash">Xóa</x-button-crm>
|
||||
</div>
|
||||
<div class="dt-action-dropdown dropdown">
|
||||
<button class="btn btn-sm btn-light border d-flex align-items-center gap-1" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i data-lucide="more-vertical" width="14" height="14"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ route('products.edit', ['id' => $row->id]) }}">
|
||||
<i data-lucide="pencil" width="14" height="14"></i>
|
||||
Edit
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<form action="{{ route('products.duplicate', ['id' => $row->id]) }}" method="POST" class="d-inline">
|
||||
@csrf
|
||||
<button type="submit" class="dropdown-item">
|
||||
<i data-lucide="copy" width="14" height="14"></i>
|
||||
Duplicate
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<form action="{{ route('products.destroy', ['id' => $row->id]) }}" method="POST" class="d-inline"
|
||||
onsubmit="return confirm('Are you sure you want to delete this product?')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="dropdown-item text-danger">
|
||||
<i data-lucide="trash-2" width="14" height="14"></i>
|
||||
Delete
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
@php
|
||||
$type = \App\Enums\Product\ProductTypeEnums::tryFrom($row->product_type);
|
||||
$categoryName = $row->category?->name;
|
||||
@endphp
|
||||
|
||||
<div class="d-flex flex-column gap-1 small">
|
||||
@if($categoryName)
|
||||
<div class="d-flex align-items-center gap-1">
|
||||
<i data-lucide="folder" width="13" height="13" class="text-warning"></i>
|
||||
<span>{{ $categoryName }}</span>
|
||||
</div>
|
||||
@endif
|
||||
@if($type)
|
||||
<div>
|
||||
<span class="badge bg-light text-dark border" style="font-size: 0.7rem;">{{ $type->label() }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
@php
|
||||
$pricing = $row->pricings->where('is_active', 1)->sortBy('price')->first();
|
||||
@endphp
|
||||
|
||||
@if($pricing)
|
||||
<div class="d-flex flex-column small">
|
||||
@if($pricing->price_sale && $pricing->price_sale < $pricing->price)
|
||||
<span class="text-decoration-line-through text-muted">{{ number_format($pricing->price) }}</span>
|
||||
<span class="fw-semibold text-success">{{ number_format($pricing->price_sale) }}</span>
|
||||
@else
|
||||
<span class="fw-semibold">{{ number_format($pricing->price) }}</span>
|
||||
@endif
|
||||
<span class="text-muted" style="font-size: 0.75rem;">/ {{ $pricing->billing_cycle ?? 'monthly' }}</span>
|
||||
</div>
|
||||
@else
|
||||
<span class="text-muted">-</span>
|
||||
@endif
|
||||
@@ -7,48 +7,51 @@
|
||||
|
||||
@section('breadcrumb')
|
||||
@php
|
||||
$breadcrumbItems = [
|
||||
['label' => 'Product'],
|
||||
['label' => 'Product / Services'],
|
||||
['label' => 'Product List', 'active' => true],
|
||||
];
|
||||
$breadcrumbItems = [
|
||||
['label' => 'Product'],
|
||||
['label' => 'Product / Services'],
|
||||
['label' => 'Product List', 'active' => true],
|
||||
];
|
||||
@endphp
|
||||
<x-admin-breadcrumb :items="$breadcrumbItems" />
|
||||
@endsection
|
||||
@section('tab')
|
||||
@php
|
||||
$tabs = [
|
||||
[
|
||||
'label' => 'Product',
|
||||
'route' => 'products.index',
|
||||
'icon' => 'server',
|
||||
],
|
||||
[
|
||||
'label' => 'Product Group',
|
||||
'route' => 'products.group.index',
|
||||
'icon' => 'server',
|
||||
],
|
||||
];
|
||||
$tabs = [
|
||||
[
|
||||
'label' => 'Product',
|
||||
'route' => 'products.index',
|
||||
'icon' => 'server',
|
||||
],
|
||||
[
|
||||
'label' => 'Product Group',
|
||||
'route' => 'products.group.index',
|
||||
'icon' => 'server',
|
||||
],
|
||||
];
|
||||
@endphp
|
||||
<x-admin-tab :items="$tabs" />
|
||||
@endsection
|
||||
@section('content')
|
||||
@if (auth()->guard('admins')->user()->can('manager product'))
|
||||
<div class="card card-primary card-outline">
|
||||
<div class="card-header d-flex justify-content-end gap-2">
|
||||
<div class="card card-primary card-outline rounded-4 shadow-none">
|
||||
<div class="card-header d-flex justify-content-end gap-2 rounded-top-4">
|
||||
@include('administrator.pages.block.product.btn')
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<x-datatable id="productTable" ajax="{{ route('products.datatable') }}" :columns="[
|
||||
['key' => 'name', 'label' => 'Product Name'],
|
||||
['key' => 'category_name', 'label' => 'Category'],
|
||||
['key' => 'product_type_label', 'label' => 'Type'],
|
||||
['key' => 'specs', 'label' => 'Specifications'],
|
||||
['key' => 'status', 'label' => 'Status'],
|
||||
['key' => 'action', 'label' => 'Action'],
|
||||
]" />
|
||||
<div class="card-body p-0">
|
||||
@php
|
||||
$productColumns = [
|
||||
['key' => 'checkbox', 'label' => '<input type="checkbox" class="form-check-input" id="dt-master-checkbox">', 'orderable' => false, 'searchable' => false, 'className' => 'text-center'],
|
||||
['key' => 'name', 'label' => 'Product Name'],
|
||||
['key' => 'category', 'label' => 'Category / Type', 'searchable' => false],
|
||||
['key' => 'price', 'label' => 'Price', 'orderable' => false, 'searchable' => false],
|
||||
['key' => 'specs', 'label' => 'Specifications', 'orderable' => false, 'searchable' => false],
|
||||
['key' => 'status', 'label' => 'Status', 'searchable' => false],
|
||||
['key' => 'action', 'label' => '', 'orderable' => false, 'searchable' => false, 'className' => 'text-center'],
|
||||
];
|
||||
@endphp
|
||||
<x-datatable id="productTable" ajax="{{ route('products.datatable') }}" :columns="$productColumns" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@else
|
||||
<div class="alert alert-danger alert-dismissible">
|
||||
|
||||
@@ -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
|
||||
@@ -135,6 +135,7 @@ Route::prefix('')->group(function () {
|
||||
Route::get('view-product-item/{id}', 'edit')->name('edit');
|
||||
Route::put('view-product-item/{id}', 'update')->name('update');
|
||||
Route::delete('remove-product-item/{id}', 'destroy')->name('destroy');
|
||||
Route::post('duplicate-product/{id}', 'duplicate')->name('duplicate');
|
||||
Route::get('datatable', 'datatable')->name('datatable');
|
||||
|
||||
Route::controller(ProductCategoryController::class)->prefix('group')->as('group.')->group(function () {
|
||||
|
||||
Reference in New Issue
Block a user