feat: Implement addon functionality with schema updates, seeder, and administrator UI adjustments, including new favicons and a migration snapshot.

This commit is contained in:
pbao9
2026-02-13 10:07:24 +07:00
parent 64c3182b00
commit b127fa7c66
13 changed files with 4441 additions and 525 deletions
+3841
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -12,6 +12,7 @@ class Addon extends Model
protected $table = 'product_addon';
protected $fillable = [
'type_id',
'code',
'name',
'price',
@@ -19,6 +20,7 @@ class Addon extends Model
'max',
'step',
'prefix',
'status'
'status',
'sort_order'
];
}
@@ -14,6 +14,7 @@ return new class extends Migration {
{
Schema::create('product_addon', function (Blueprint $table) {
$table->id();
$table->string('type_id', 50)->default('vps')->index();
$table->string('code', 50)->unique();
$table->string('name', 100);
$table->string('prefix', 50)->nullable();
@@ -24,6 +25,7 @@ return new class extends Migration {
$table->enum('input_type', ['number', 'slider', 'select'])
->default('number');
$table->boolean('status')->default(true);
$table->integer('sort_order')->default(0)->index();
$table->timestamps();
});
}
+6
View File
@@ -11,6 +11,7 @@ class AddonSeeder extends Seeder
{
$addons = [
[
'type_id' => 'vps',
'code' => 'cpu',
'name' => 'CPU',
'price' => 100000,
@@ -19,8 +20,10 @@ class AddonSeeder extends Seeder
'step' => 1,
'prefix' => 'vCore',
'status' => true,
'sort_order' => 0,
],
[
'type_id' => 'vps',
'code' => 'ram',
'name' => 'RAM',
'price' => 50000,
@@ -29,8 +32,10 @@ class AddonSeeder extends Seeder
'step' => 1,
'prefix' => 'GB',
'status' => true,
'sort_order' => 1,
],
[
'type_id' => 'vps',
'code' => 'disk',
'name' => 'SSD',
'price' => 4000,
@@ -39,6 +44,7 @@ class AddonSeeder extends Seeder
'step' => 10,
'prefix' => 'GB',
'status' => true,
'sort_order' => 2,
],
];
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 82 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 82 KiB

@@ -30,7 +30,7 @@
<!-- CSRF Token -->
<meta name="_token" content="{{ csrf_token() }}">
<link rel="shortcut icon" href="{{ asset('/favicon.ico') }}">
<link rel="shortcut icon" href="{{ asset('/images/favi.svg') }}">
<!-- plugin css -->
@foreach (config('assets.static.css') as $css)
@@ -51,7 +51,7 @@
<div class="page-wrapper">
@include('administrator.layout.partials.header')
<div class="page-content">
<div class="card mb-4 bg-white shadow-sm">
<div class="card mb-4 bg-white rounded-4 shadow-none">
<div class="card-body p-3">
@yield('breadcrumb')
@yield('tab')
@@ -43,16 +43,16 @@
@endsection
@section('content')
@if(auth()->guard('admins')->user()->can("manager module vcenter"))
<div class="card card-primary card-outline">
<div class="card rounded-4 shadow-none">
<div class="card-header d-flex justify-content-end gap-2">
@include("administrator.pages.block.ModuleVCenter")
</div>
<div class="card-body">
<div class="card-body p-0">
<x-datatable id="vcenterTable" ajax="{{ route('module.vcenter.datatable') }}" :columns="[
['key' => 'host', 'label' => 'Host'],
['key' => 'user', 'label' => 'User'],
['key' => 'status', 'label' => 'Status'],
['key' => 'action', 'label' => 'Action', 'searchable' => false],
['key' => 'action', 'label' => 'Action', 'searchable' => false],
]" />
</div>
</div>
@@ -39,7 +39,7 @@
@csrf
<div class="row">
<div class="col-md-8">
<div class="card card-primary card-outline">
<div class="card card-primary card-outline rounded-4 shadow-none">
<div class="card-header">
<h5 class="card-title m-0">Basic Information</h5>
</div>
@@ -63,7 +63,8 @@
@if(isset($categories))
@foreach($categories as $category)
<option value="{{ $category->id }}" {{ old('parent_id') == $category->id ? 'selected' : '' }}>
{{ $category->name }}</option>
{{ $category->name }}
</option>
@endforeach
@endif
</select>
@@ -73,7 +74,7 @@
</div>
<div class="col-md-4">
<div class="card card-secondary card-outline">
<div class="card card-secondary card-outline shadow-none rounded-4">
<div class="card-header">
<h5 class="card-title m-0">Classification</h5>
</div>
@@ -100,9 +101,10 @@
</div>
<x-fixed-action-bar>
<x-button-crm type="button" variant="secondary" size="sm" icon="arrow-left"
route="products.group.index">Cancel</x-button-crm>
<x-button-crm type="submit" variant="primary" size="sm" icon="plus-circle">Create Category</x-button-crm>
<x-button-crm type="button" variant="outline-info" size="sm" icon="arrow-left"
route="products.group.index">Back</x-button-crm>
<x-button-crm type="submit" variant="outline-primary" size="sm" icon="plus-circle">Create
Category</x-button-crm>
</x-fixed-action-bar>
</form>
@endsection
@@ -7,46 +7,46 @@
@section('breadcrumb')
@php
$breadcrumbItems = [
['label' => 'Product'],
['label' => 'Product Group'],
['label' => 'Product Group List', 'active' => true]
];
$breadcrumbItems = [
['label' => 'Product'],
['label' => 'Product Group'],
['label' => 'Product Group 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 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">
<div class="card-body p-0">
<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'],
['key' => 'action', 'label' => 'Action', 'searchable' => false],
]" />
['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'],
['key' => 'action', 'label' => 'Action', 'searchable' => false],
]" />
</div>
</div>
@@ -164,4 +164,4 @@ $tabs = [
@endpush
{{-- Render all modals outside datatable wrapper --}}
@stack('modals')
@stack('modals')
@@ -38,8 +38,8 @@
<div class="row align-items-start">
{{-- Left Column: Basic Info & Pricing --}}
<div class="col-md-8">
<div class="card card-primary card-outline">
<div class="card-header">
<div class="card card-primary card-outline rounded-4 shadow-none">
<div class="card-header rounded-top-4">
<h5 class="card-title m-0">Basic Information</h5>
</div>
<div class="card-body">
@@ -195,8 +195,8 @@
{{-- Right Column: Classification & Settings --}}
<div class="col-md-4 sticky-top top-10 z-0">
<div class="card card-secondary card-outline mb-3">
<div class="card-header">
<div class="card card-secondary card-outline mb-3 rounded-4 shadow-none">
<div class="card-header rounded-top-4">
<h5 class="card-title m-0">Classification</h5>
</div>
<div class="card-body">
@@ -305,8 +305,8 @@
</div>
</div>
<div class="card card-secondary card-outline">
<div class="card-header">
<div class="card card-secondary card-outline rounded-4 shadow-none">
<div class="card-header rounded-top-4">
<h5 class="card-title m-0">Inventory & Limits</h5>
</div>
<div class="card-body">
@@ -1,58 +1,58 @@
@php
$tag = 'button';
$href = null;
$buttonType = $type;
if ($type === 'delete' || $confirm) {
$tag = 'button';
$buttonType = 'button';
$attributes = $attributes->merge([
'onclick' => "confirmDelete('$confirmUrl', '$confirmName')",
]);
} elseif ($route) {
$href = route($route, $params);
$tag = 'a';
} elseif ($url) {
$href = $url;
$tag = 'a';
} elseif ($attributes->has('href')) {
$href = $attributes->get('href');
$tag = 'a';
}
$href = null;
$buttonType = $type;
$variants = [
'primary' => 'btn-primary',
'secondary' => 'btn-secondary',
'outline' => 'btn-outline-primary',
'outline-primary' => 'btn-outline-primary',
'outline-secondary' => 'btn-outline-secondary',
'outline-success' => 'btn-outline-success',
'outline-danger' => 'btn-outline-danger',
'outline-warning' => 'btn-outline-warning',
'outline-info' => 'btn-outline-info',
'ghost' => 'btn-light',
'link' => 'btn-link',
'danger' => 'btn-danger',
'destructive' => 'btn-danger',
'success' => 'btn-success',
'warning' => 'btn-warning',
'info' => 'btn-info',
'light' => 'btn-light',
'dark' => 'btn-dark',
];
if ($type === 'delete' || $confirm) {
$tag = 'button';
$buttonType = 'button';
$attributes = $attributes->merge([
'onclick' => "confirmDelete('$confirmUrl', '$confirmName')",
]);
} elseif ($route) {
$href = route($route, $params);
$tag = 'a';
} elseif ($url) {
$href = $url;
$tag = 'a';
} elseif ($attributes->has('href')) {
$href = $attributes->get('href');
$tag = 'a';
}
$sizes = [
'default' => '',
'sm' => 'btn-sm',
'lg' => 'btn-lg',
'xs' => 'btn-xs',
'icon' => 'btn-icon',
];
$variants = [
'primary' => 'btn-primary',
'secondary' => 'btn-secondary',
'outline' => 'btn-outline-primary',
'outline-primary' => 'btn-outline-primary',
'outline-secondary' => 'btn-outline-secondary',
'outline-success' => 'btn-outline-success',
'outline-danger' => 'btn-outline-danger',
'outline-warning' => 'btn-outline-warning',
'outline-info' => 'btn-outline-info',
'ghost' => 'btn-light',
'link' => 'btn-link',
'danger' => 'btn-danger',
'destructive' => 'btn-danger',
'success' => 'btn-success',
'warning' => 'btn-warning',
'info' => 'btn-info',
'light' => 'btn-light',
'dark' => 'btn-dark',
];
$variantClass = $variants[$variant ?? 'primary'] ?? $variants['primary'];
$sizeClass = $sizes[$size ?? 'default'] ?? '';
$sizes = [
'default' => '',
'sm' => 'btn-sm',
'lg' => 'btn-lg',
'xs' => 'btn-xs',
'icon' => 'btn-icon',
];
$classes = "btn d-flex align-items-center {$variantClass} {$sizeClass}";
$variantClass = $variants[$variant ?? 'primary'] ?? $variants['primary'];
$sizeClass = $sizes[$size ?? 'default'] ?? '';
$classes = "btn d-flex align-items-center rounded-4 border {$variantClass} {$sizeClass}";
@endphp
@if($tag === 'a')
<a href="{{ $href }}" {{ $attributes->merge(['class' => $classes]) }}>
+476 -431
View File
@@ -1,17 +1,17 @@
@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"
@@ -22,8 +22,8 @@ $dtColumns = collect($columns)
<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">
<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>
@@ -58,14 +58,14 @@ $dtColumns = collect($columns)
</div>
</div>
@endif
<div class="data-table-wrapper rounded-3 border-light position-relative overflow-hidden border">
<div class="data-table-wrapper position-relative overflow-hidden shadow-none">
{{-- Loading Process Indicator --}}
<div class="loading-process" id="{{ $id }}-loading" style="display: none;">
<div class="loading-bar"></div>
<div class="loading-overlay"></div>
</div>
<table id="{{ $id }}" class="table-hover mb-0 table bg-white align-middle"
<table id="{{ $id }}" class="table-hover mb-0 table bg-white shadow-none align-middle"
style="width:100%; border-collapse: separate; border-spacing: 0;">
<thead class="bg-light">
<tr>
@@ -102,8 +102,7 @@ $dtColumns = collect($columns)
</span>
<input type="text"
class="form-control form-control-sm border-start-0 column-search border-light bg-white ps-0 shadow-none"
placeholder="Filter..." data-column="{{ $loop->index }}"
style="font-size: 0.8125rem;">
placeholder="Filter..." data-column="{{ $loop->index }}" style="font-size: 0.8125rem;">
</div>
@endif
</th>
@@ -116,473 +115,519 @@ $dtColumns = collect($columns)
</table>
</div>
@push('custom-scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
$.fn.DataTable.ext.pager.numbers_length = 7;
<script>
document.addEventListener('DOMContentLoaded', function () {
$.fn.DataTable.ext.pager.numbers_length = 7;
var table = $('#{{ $id }}').DataTable({
processing: true,
serverSide: true,
ajax: '{{ $ajax }}',
columns: @json($dtColumns),
orderCellsTop: true,
fixedHeader: false,
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...",
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",
paginate: {
first: '<span class="d-flex align-items-center justify-content-center"><i data-lucide="chevrons-left" width="16" height="16"></i></span>',
last: '<span class="d-flex align-items-center justify-content-center"><i data-lucide="chevrons-right" width="16" height="16"></i></span>',
next: '<span class="d-flex align-items-center justify-content-center"><i data-lucide="chevron-right" width="16" height="16"></i></span>',
previous: '<span class="d-flex align-items-center justify-content-center"><i data-lucide="chevron-left" width="16" height="16"></i></span>'
},
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-3'<'col-sm-12'tr>>" +
"<'row px-3 pb-3 pt-1 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>>",
initComplete: function() {
// Initialize select2 with custom styling
$('.dataTables_length select').select2({
minimumResultsForSearch: Infinity,
width: '80px',
dropdownCssClass: 'small-select-dropdown',
selectionCssClass: 'small-select-selection'
});
var table = $('#{{ $id }}').DataTable({
processing: true,
serverSide: true,
ajax: '{{ $ajax }}',
columns: @json($dtColumns),
orderCellsTop: true,
fixedHeader: false,
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...",
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",
paginate: {
first: '<span class="d-flex align-items-center justify-content-center"><i data-lucide="chevrons-left" width="16" height="16"></i></span>',
last: '<span class="d-flex align-items-center justify-content-center"><i data-lucide="chevrons-right" width="16" height="16"></i></span>',
next: '<span class="d-flex align-items-center justify-content-center"><i data-lucide="chevron-right" width="16" height="16"></i></span>',
previous: '<span class="d-flex align-items-center justify-content-center"><i data-lucide="chevron-left" width="16" height="16"></i></span>'
},
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>>",
initComplete: function () {
// Initialize select2 with custom styling
$('.dataTables_length select').select2({
minimumResultsForSearch: Infinity,
width: '80px',
dropdownCssClass: 'small-select-dropdown',
selectionCssClass: 'small-select-selection'
});
// Debounced search for column inputs (ERP Standard: Reduce server load)
let searchTimeouts = {};
this.api().columns().every(function() {
var that = this;
$('input', this.header()).on('keyup change clear', function() {
const colIdx = that.index();
const value = this.value;
// Debounced search for column inputs (ERP Standard: Reduce server load)
let searchTimeouts = {};
this.api().columns().every(function () {
var that = this;
$('input', this.header()).on('keyup change clear', function () {
const colIdx = that.index();
const value = this.value;
clearTimeout(searchTimeouts[colIdx]);
searchTimeouts[colIdx] = setTimeout(function() {
if (that.search() !== value) {
that.search(value).draw();
}
}, 400); // 400ms delay
});
});
},
drawCallback: function() {
// Re-initialize Select2 on draw (if lost)
if ($('.dataTables_length select').data('select2')) {
$('.dataTables_length select').select2('destroy');
}
$('.dataTables_length select').select2({
minimumResultsForSearch: Infinity,
width: '80px'
});
clearTimeout(searchTimeouts[colIdx]);
searchTimeouts[colIdx] = setTimeout(function () {
if (that.search() !== value) {
that.search(value).draw();
}
}, 400); // 400ms delay
});
});
},
drawCallback: function () {
// Re-initialize Select2 on draw (if lost)
if ($('.dataTables_length select').data('select2')) {
$('.dataTables_length select').select2('destroy');
}
$('.dataTables_length select').select2({
minimumResultsForSearch: Infinity,
width: '80px'
});
// Re-initialize icons with slight delay to ensuring DOM is ready
setTimeout(function() {
if (window.lucide) {
lucide.createIcons();
}
}, 50);
}
});
// Re-initialize icons with slight delay to ensuring DOM is ready
setTimeout(function () {
if (window.lucide) {
lucide.createIcons();
}
}, 50);
}
});
// 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');
}
});
// 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) {
loadingEl.fadeIn(100);
} else {
loadingEl.fadeOut(200);
}
});
// Custom Loading Process Logic
table.on('processing.dt', function (e, settings, processing) {
const loadingEl = $('#{{ $id }}-loading');
if (processing) {
loadingEl.fadeIn(100);
} else {
loadingEl.fadeOut(200);
}
});
// Prevent sorting when clicking on the input
$('#{{ $id }} thead tr:eq(1) th').on('click', function(e) {
e.stopPropagation();
});
// Prevent sorting when clicking on the input
$('#{{ $id }} thead tr:eq(1) th').on('click', 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();
});
});
// 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)
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 = [];
@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 = [];
// Handle Master Checkbox
$(document).on('change', '#dt-master-checkbox', function() {
const isChecked = $(this).is(':checked');
$('.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');
});
// 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);
}
// 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 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;
// 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);
}
});
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);
});
// 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;
// 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);
openerBtn.on('click', function () {
if (selectedIds.length === 0) return;
countDisplay.text(selectedIds.length);
if (!bsModal) {
bsModal = new bootstrap.Modal(modalEl);
}
bsModal.show();
});
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 = [];
bulkActions.hide();
// 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 = [];
bulkActions.hide();
// Show success toast
if (typeof FuiToast !== 'undefined') {
FuiToast.success(response.message ||
'Selected items have been deleted successfully.');
}
// Show success toast
if (typeof FuiToast !== 'undefined') {
FuiToast.success(response.message ||
'Selected items have been deleted successfully.');
}
// 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');
}
});
});
});
@endif
</script>
// 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');
}
});
});
});
@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;
position: relative;
}
<style>
/* Custom SAAS DataTable Styles */
.data-table-wrapper {
background: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
overflow-y: auto;
position: relative;
}
.pagination button.page-link {
height: 100%;
}
.pagination button.page-link {
height: 100%;
}
.data-table-wrapper .table thead th {
border-color: #f1f5f9 !important;
font-family: 'Inter', sans-serif;
vertical-align: middle;
position: sticky;
top: 0;
z-index: 3; {{-- Lowered from 10 --}}
background-color: #f8f9fa;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.data-table-wrapper .table thead th {
border-color: #f1f5f9 !important;
font-family: 'Inter', sans-serif;
vertical-align: middle;
position: sticky;
top: 0;
z-index: 3;
/* 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 --}}
border-left: 1px solid #f1f5f9;
box-shadow: -2px 0 5px rgba(0, 0, 0, 0.02);
}
{
{
-- Lowered from 10 --
}
}
.table thead th:last-child {
background: #f8f9fa;
z-index: 4; {{-- Lowered from 11 --}}
}
background-color: #f8f9fa;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
/* 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 --}}
border-right: 1px solid #f1f5f9;
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.02);
}
/* 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;
.table thead th:first-child {
background: #f8f9fa;
z-index: 4; {{-- Lowered from 11 --}}
}
{
{
-- Lowered from 5 --
}
}
.table tbody tr:hover td {
background-color: #f8fafc !important;
cursor: pointer;
}
border-left: 1px solid #f1f5f9;
box-shadow: -2px 0 5px rgba(0, 0, 0, 0.02);
}
.data-table-wrapper .table td:not(:last-child) {
border-right: 1px solid #f1f5f9;
}
.table thead th:last-child {
background: #f8f9fa;
z-index: 4;
/* Sticky for second row (filter inputs) if present */
.data-table-wrapper .table thead tr:nth-child(2) th {
top: 45px;
/* Adjust based on typical header height */
z-index: 4;
}
{
{
-- Lowered from 11 --
}
}
}
.data-table-wrapper .input-group-text {
color: #94a3b8;
/* Slate 400 */
}
/* Sticky Checkbox Column */
.table thead th:first-child,
.table tbody td:first-child {
position: sticky;
left: 0;
background: #fff;
z-index: 2;
.data-table-wrapper .form-control::placeholder {
color: #cbd5e1;
/* Slate 300 */
}
{
{
-- Lowered from 5 --
}
}
.dataTables_wrapper .pagination .page-item .page-link {
border: 1px solid transparent;
border-radius: 6px;
margin: 0 2px;
color: #64748b;
padding: 0;
font-size: 0.875rem;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
height: 38px;
min-width: 38px;
}
border-right: 1px solid #f1f5f9;
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.02);
}
.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);
border-color: #0f172a;
}
.table thead th:first-child {
background: #f8f9fa;
z-index: 4;
.dataTables_wrapper .pagination .page-item.disabled .page-link {
color: #cbd5e1;
background-color: transparent;
}
{
{
-- Lowered from 11 --
}
}
}
.dataTables_wrapper .pagination .page-item:not(.active) .page-link:hover {
background-color: #f8fafc;
color: #0f172a;
border-color: #e2e8f0;
}
.table tbody tr:hover td {
background-color: #f8fafc !important;
cursor: pointer;
}
/* Select2 Customization for Table Length */
.dataTables_length {
margin-right: 1rem;
}
.data-table-wrapper .table td:not(:last-child) {
border-right: 1px solid #f1f5f9;
}
.select2-container--default .select2-selection--single {
border: 1px solid #e2e8f0;
border-radius: 6px;
height: 32px;
display: flex;
align-items: center;
outline: none;
}
/* Sticky for second row (filter inputs) if present */
.data-table-wrapper .table thead tr:nth-child(2) th {
top: 45px;
/* Adjust based on typical header height */
z-index: 4;
}
.select2-container--default .select2-selection--single:focus {
border-color: #94a3b8;
}
.data-table-wrapper .input-group-text {
color: #94a3b8;
/* Slate 400 */
}
.select2-container--default .select2-selection--single .select2-selection__arrow {
height: 30px;
}
.data-table-wrapper .form-control::placeholder {
color: #cbd5e1;
/* Slate 300 */
}
.select2-container--default .select2-selection--single .select2-selection__rendered {
color: #475569;
font-size: 0.8125rem;
padding-left: 10px;
line-height: normal;
}
.dataTables_wrapper .pagination .page-item .page-link {
border: 1px solid transparent;
border-radius: 6px;
margin: 0 2px;
color: #64748b;
padding: 0;
font-size: 0.875rem;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
height: 38px;
min-width: 38px;
}
/* Loading Process Styles */
.loading-process {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 5; {{-- Lowered from 1000 --}}
pointer-events: none;
}
.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);
border-color: #0f172a;
}
.loading-bar {
position: absolute;
top: 0;
left: 0;
height: 3px;
width: 100%;
background: #f1f5f9;
overflow: hidden;
z-index: 6; {{-- Lowered from 1001 --}}
}
.dataTables_wrapper .pagination .page-item.disabled .page-link {
color: #cbd5e1;
background-color: transparent;
}
.loading-bar::after {
content: '';
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 50%;
background: linear-gradient(90deg, transparent, #0f172a, transparent);
animation: loading-process-info 1.5s infinite linear;
}
.dataTables_wrapper .pagination .page-item:not(.active) .page-link:hover {
background-color: #f8fafc;
color: #0f172a;
border-color: #e2e8f0;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(1px);
}
/* Select2 Customization for Table Length */
.dataTables_length {
margin-right: 1rem;
}
@keyframes loading-process-info {
0% {
transform: translateX(-100%);
}
.select2-container--default .select2-selection--single {
border: 1px solid #e2e8f0;
border-radius: 6px;
height: 32px;
display: flex;
align-items: center;
outline: none;
}
100% {
transform: translateX(200%);
}
}
.select2-container--default .select2-selection--single:focus {
border-color: #94a3b8;
}
/* Custom Sort Icons Styles */
.sort-icons {
opacity: 0.15;
transition: opacity 0.2s;
}
.select2-container--default .select2-selection--single .select2-selection__arrow {
height: 30px;
}
th:hover .sort-icons {
opacity: 0.5;
}
.select2-container--default .select2-selection--single .select2-selection__rendered {
color: #475569;
font-size: 0.8125rem;
padding-left: 10px;
line-height: normal;
}
th.sorting_asc .sort-icons {
opacity: 1;
}
/* Loading Process Styles */
.loading-process {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 5;
th.sorting_asc .sort-icon-desc {
display: none !important;
}
{
{
-- Lowered from 1000 --
}
}
th.sorting_desc .sort-icons {
opacity: 1;
}
pointer-events: none;
}
th.sorting_desc .sort-icon-asc {
display: none !important;
}
.loading-bar {
position: absolute;
top: 0;
left: 0;
height: 3px;
width: 100%;
background: #f1f5f9;
overflow: hidden;
z-index: 6;
/* Hide default DataTables icons */
table.dataTable thead .sorting:before,
table.dataTable thead .sorting:after,
table.dataTable thead .sorting_asc:before,
table.dataTable thead .sorting_asc:after,
table.dataTable thead .sorting_desc:before,
table.dataTable thead .sorting_desc:after {
display: none !important;
}
{
{
-- Lowered from 1001 --
}
}
}
/* Processing Indicator with improved look - Centered */
div.dataTables_processing {
display: none !important;
/* Hide default processing */
}
</style>
@endpush
.loading-bar::after {
content: '';
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 50%;
background: linear-gradient(90deg, transparent, #0f172a, transparent);
animation: loading-process-info 1.5s infinite linear;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(1px);
}
@keyframes loading-process-info {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(200%);
}
}
/* Custom Sort Icons Styles */
.sort-icons {
opacity: 0.15;
transition: opacity 0.2s;
}
th:hover .sort-icons {
opacity: 0.5;
}
th.sorting_asc .sort-icons {
opacity: 1;
}
th.sorting_asc .sort-icon-desc {
display: none !important;
}
th.sorting_desc .sort-icons {
opacity: 1;
}
th.sorting_desc .sort-icon-asc {
display: none !important;
}
/* Hide default DataTables icons */
table.dataTable thead .sorting:before,
table.dataTable thead .sorting:after,
table.dataTable thead .sorting_asc:before,
table.dataTable thead .sorting_asc:after,
table.dataTable thead .sorting_desc:before,
table.dataTable thead .sorting_desc:after {
display: none !important;
}
/* Processing Indicator with improved look - Centered */
div.dataTables_processing {
display: none !important;
/* Hide default processing */
}
</style>
@endpush