This commit is contained in:
pbao9
2026-02-06 20:52:59 +07:00
parent d7aadf7544
commit e7b9ce7485
17 changed files with 570 additions and 36 deletions
@@ -56,7 +56,7 @@ class ProductCategoryController extends Controller
public function destroy(int $id)
{
$this->productCategoryService->delete($id);
return response()->json(['success' => true]);
return redirect()->route('products.group.index')->with('success', 'Delete Group Success');
}
public function datatable(Request $request)
@@ -48,8 +48,11 @@ class ProductController extends Controller
// Fetch OS versions with their group for the OS selector component
$osVersions = \App\Models\Os::with('osGroup')->where('status', 1)->get();
// Fetch Control Panels for Hosting products
$controlPanels = \App\Models\ControlPanel::active()->ordered()->get();
if ($step === 1) {
return view("administrator.pages.product.create", compact('group', 'type', 'title', 'osVersions'));
return view("administrator.pages.product.create", compact('group', 'type', 'title', 'osVersions', 'controlPanels'));
}
if (!$request->has(['step', 'type'])) {
@@ -97,7 +100,10 @@ class ProductController extends Controller
$view = "administrator.pages.product.edit";
$data = compact('product', 'title', 'group', 'type', 'osVersions');
// Fetch Control Panels for Hosting products
$controlPanels = \App\Models\ControlPanel::active()->ordered()->get();
$data = compact('product', 'title', 'group', 'type', 'osVersions', 'controlPanels');
return view($view, $data);
}
+34 -5
View File
@@ -49,6 +49,9 @@ class ProductRequest extends FormRequest
],
'short_description' => ['nullable', 'string', 'max:255'],
'description' => ['nullable', 'string'],
'thumbnail' => ['nullable', 'string', 'max:255'],
'gallery' => ['nullable', 'array'],
'gallery.*' => ['nullable', 'string', 'max:255'],
'stock' => ['nullable', 'integer', 'min:0'],
'is_unlimited' => ['boolean'],
'cpu' => ['nullable', 'string', 'max:255'],
@@ -64,11 +67,23 @@ class ProductRequest extends FormRequest
'is_active' => ['boolean'],
'sort_order' => ['nullable', 'integer'],
'published_at' => ['nullable', 'date'],
// OS versions (pivot table os_product)
// OS versions (pivot table os_product) - for VPS
'os_ids' => ['nullable', 'array'],
'os_ids.*' => ['exists:os,id'],
// Control Panels (pivot table product_control_panel) - for Hosting
'control_panel_ids' => ['nullable', 'array'],
'control_panel_ids.*' => ['exists:control_panels,id'],
'control_panel_price_modifiers' => ['nullable', 'array'],
'control_panel_price_modifiers.*' => ['nullable', 'numeric', 'min:0'],
'control_panel_default' => ['nullable', 'exists:control_panels,id'],
// Images
'thumbnail' => ['nullable', 'string', 'max:255'],
'gallery' => ['nullable', 'array'],
'gallery.*' => ['nullable', 'string', 'max:255'],
// Pricing cycles (table product_pricings)
'pricing' => ['nullable', 'array'],
];
@@ -127,7 +142,14 @@ class ProductRequest extends FormRequest
'short_description.max' => 'Mô tả ngắn không được vượt quá 255 ký tự.',
'description.string' => 'Mô tả phải là chuỗi ký tự.',
'thumbnail.string' => 'Đường dẫn ảnh đại diện phải là chuỗi ký tự.',
'thumbnail.max' => 'Đường dẫn ảnh đại diện không được vượt quá 255 ký tự.',
'gallery.array' => 'Thư viện ảnh không hợp lệ.',
'gallery.*.string' => 'Đường dẫn ảnh trong thư viện phải là chuỗi ký tự.',
'gallery.*.max' => 'Đường dẫn ảnh trong thư viện không được vượt quá 255 ký tự.',
'stock.integer' => 'Số lượng tồn kho phải là số nguyên.',
'stock.min' => 'Số lượng tồn kho không được âm.',
@@ -168,7 +190,14 @@ class ProductRequest extends FormRequest
'os_ids.array' => 'Danh sách hệ điều hành không hợp lệ.',
'os_ids.*.exists' => 'Hệ điều hành được chọn không tồn tại.',
'control_panel_ids.array' => 'Danh sách Control Panel không hợp lệ.',
'control_panel_ids.*.exists' => 'Control Panel được chọn không tồn tại.',
'control_panel_price_modifiers.array' => 'Danh sách phụ phí Control Panel không hợp lệ.',
'control_panel_price_modifiers.*.numeric' => 'Phụ phí Control Panel phải là số.',
'control_panel_price_modifiers.*.min' => 'Phụ phí Control Panel không được âm.',
'control_panel_default.exists' => 'Control Panel mặc định không tồn tại.',
'pricing.array' => 'Thông tin giá không hợp lệ.',
'pricing.*.price.numeric' => 'Giá phải là số.',
'pricing.*.price.min' => 'Giá không được âm.',
+32 -2
View File
@@ -24,11 +24,16 @@ class ProductService
// Create product
$product = $this->repository->create($productData);
// Sync OS versions if provided
// Sync OS versions if provided (for VPS)
if (!empty($data['os_ids'])) {
$product->osVersions()->sync($data['os_ids']);
}
// Sync Control Panels if provided (for Hosting)
if (!empty($data['control_panel_ids'])) {
$this->saveControlPanels($product, $data);
}
// Save pricing cycles if provided
if (!empty($data['pricing'])) {
$this->savePricings($product, $data['pricing'], $data['currency'] ?? 'USD');
@@ -48,11 +53,16 @@ class ProductService
// Get the product for relationships
$product = $this->repository->find($id);
// Sync OS versions if provided
// Sync OS versions if provided (for VPS)
if (isset($data['os_ids'])) {
$product->osVersions()->sync($data['os_ids']);
}
// Sync Control Panels if provided (for Hosting)
if (isset($data['control_panel_ids'])) {
$this->saveControlPanels($product, $data);
}
// Save pricing cycles if provided
if (!empty($data['pricing'])) {
$this->savePricings($product, $data['pricing'], $data['currency'] ?? 'USD');
@@ -95,6 +105,26 @@ class ProductService
}
}
/**
* Save Control Panels for product (for Hosting)
*/
private function saveControlPanels($product, array $data): void
{
$syncData = [];
foreach ($data['control_panel_ids'] as $panelId) {
$priceModifier = $data['control_panel_price_modifiers'][$panelId] ?? 0;
$isDefault = isset($data['control_panel_default']) && $data['control_panel_default'] == $panelId;
$syncData[$panelId] = [
'price_modifier' => $priceModifier,
'is_default' => $isDefault,
];
}
$product->controlPanels()->sync($syncData);
}
/**
* Prepare data for create/update
*/
+50
View File
@@ -0,0 +1,50 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ControlPanel extends Model
{
use HasFactory;
protected $fillable = [
'name',
'slug',
'logo',
'description',
'is_active',
'sort_order',
];
protected $casts = [
'is_active' => 'boolean',
];
/**
* Relationship: Control Panel có nhiều Products (Hosting)
*/
public function products()
{
return $this->belongsToMany(Product::class, 'product_control_panel')
->withPivot(['price_modifier', 'is_default'])
->withTimestamps();
}
/**
* Scope: Chỉ lấy active panels
*/
public function scopeActive($query)
{
return $query->where('is_active', true);
}
/**
* Scope: Sắp xếp theo sort_order
*/
public function scopeOrdered($query)
{
return $query->orderBy('sort_order', 'asc');
}
}
+13
View File
@@ -19,6 +19,8 @@ class Product extends Model
'slug',
'short_description',
'description',
'thumbnail',
'gallery',
'setup_fee',
'trial_days',
'cpu',
@@ -39,6 +41,7 @@ class Product extends Model
'is_active' => 'boolean',
'is_unlimited' => 'boolean',
'published_at' => 'date',
'gallery' => 'array',
];
/**
@@ -58,6 +61,16 @@ class Product extends Model
->withTimestamps();
}
/**
* Relationship: Product has many Control Panels (for Hosting)
*/
public function controlPanels()
{
return $this->belongsToMany(ControlPanel::class, 'product_control_panel')
->withPivot(['price_modifier', 'is_default'])
->withTimestamps();
}
/**
* Relationship: Product has many pricings
*/
@@ -20,6 +20,8 @@ return new class extends Migration {
$table->string('slug')->unique();
$table->string('short_description')->nullable();
$table->text('description')->nullable();
$table->string('thumbnail')->nullable();
$table->json('gallery')->nullable();
// Trial / stock
$table->integer('stock')->nullable();
@@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('control_panels', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->string('logo')->nullable();
$table->text('description')->nullable();
$table->boolean('is_active')->default(true);
$table->integer('sort_order')->default(0);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('control_panels');
}
};
@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('product_control_panel', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('product_id');
$table->unsignedBigInteger('control_panel_id');
$table->decimal('price_modifier', 15, 2)->default(0)->comment('Phụ phí cho control panel này');
$table->boolean('is_default')->default(false)->comment('Panel mặc định cho sản phẩm');
$table->timestamps();
$table->foreign('product_id')
->references('id')
->on('products')
->onDelete('cascade');
$table->foreign('control_panel_id')
->references('id')
->on('control_panels')
->onDelete('cascade');
$table->unique(['product_id', 'control_panel_id']);
});
}
public function down(): void
{
Schema::dropIfExists('product_control_panel');
}
};
+82
View File
@@ -0,0 +1,82 @@
<?php
namespace Database\Seeders;
use App\Models\ControlPanel;
use Illuminate\Database\Seeder;
use Illuminate\Support\Str;
class ControlPanelSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$panels = [
[
'name' => 'Plesk Panel',
'slug' => 'plesk',
'logo' => '/images/control-panels/plesk.png',
'description' => 'Plesk là control panel thương mại hàng đầu với giao diện thân thiện, hỗ trợ cả Windows và Linux.',
'is_active' => true,
'sort_order' => 1,
],
[
'name' => 'DirectAdmin',
'slug' => 'directadmin',
'logo' => '/images/control-panels/directadmin.png',
'description' => 'DirectAdmin là control panel nhẹ, nhanh và ổn định, phù hợp cho các server có cấu hình thấp.',
'is_active' => true,
'sort_order' => 2,
],
[
'name' => 'aaPanel',
'slug' => 'aapanel',
'logo' => '/images/control-panels/aapanel.png',
'description' => 'aaPanel là control panel miễn phí với giao diện đơn giản, dễ sử dụng, hỗ trợ LNMP/LAMP.',
'is_active' => true,
'sort_order' => 3,
],
[
'name' => 'cPanel',
'slug' => 'cpanel',
'logo' => '/images/control-panels/cpanel.png',
'description' => 'cPanel là control panel phổ biến nhất thế giới với đầy đủ tính năng quản lý hosting.',
'is_active' => true,
'sort_order' => 4,
],
[
'name' => 'CyberPanel',
'slug' => 'cyberpanel',
'logo' => '/images/control-panels/cyberpanel.png',
'description' => 'CyberPanel là control panel miễn phí sử dụng OpenLiteSpeed, tối ưu hiệu suất website.',
'is_active' => true,
'sort_order' => 5,
],
[
'name' => 'HestiaCP',
'slug' => 'hestiacp',
'logo' => '/images/control-panels/hestiacp.png',
'description' => 'HestiaCP là control panel mã nguồn mở, nhẹ và miễn phí, fork từ VestaCP.',
'is_active' => true,
'sort_order' => 6,
],
[
'name' => 'Không có Panel',
'slug' => 'no-panel',
'logo' => null,
'description' => 'Không cài đặt control panel, quản lý server thủ công qua SSH.',
'is_active' => true,
'sort_order' => 99,
],
];
foreach ($panels as $panel) {
ControlPanel::updateOrCreate(
['slug' => $panel['slug']],
$panel
);
}
}
}
+1
View File
@@ -30,5 +30,6 @@ class DatabaseSeeder extends Seeder
$this->call(TemplateSeeder::class);
$this->call(Setting::class);
$this->call(MenuSeeder::class);
$this->call(ControlPanelSeeder::class);
}
}
@@ -21,11 +21,13 @@
</div>
</main>
<footer class="modal__footer">
<x-button-crm type="button" variant="secondary" size="sm" icon="x" data-micromodal-close>Cancel</x-button-crm>
<x-button-crm type="button" variant="secondary" size="sm" icon="x"
data-micromodal-close>Cancel</x-button-crm>
<form id="deleteForm" method="POST" class="d-inline">
@csrf
@method('DELETE')
<x-button-crm type="submit" variant="danger" size="sm" icon="trash-2">Confirm Delete</x-button-crm>
<x-button-crm type="submit" variant="danger" size="sm" icon="trash-2">Confirm
Delete</x-button-crm>
</form>
</footer>
</div>
@@ -34,10 +36,10 @@
@push('custom-scripts')
<script>
document.addEventListener('DOMContentLoaded', function () {
document.addEventListener('DOMContentLoaded', function() {
function initializeModal() {
if (typeof ModalManager !== 'undefined') {
window.showDeleteModal = function (id, name) {
window.showDeleteModal = function(id, name) {
// Update modal content
const nameElement = document.getElementById('deleteItemName');
const formElement = document.getElementById('deleteForm');
@@ -60,4 +62,4 @@
setTimeout(initializeModal, 50);
});
</script>
@endpush
@endpush
@@ -1,2 +1,13 @@
<x-button-crm type="button" variant="primary" size="sm" route="products.group.edit" :params="['id' => $row->id]" icon="pencil">Edit</x-button-crm>
<x-button-crm type="button" variant="danger" size="sm" route="products.group.destroy" :params="['id' => $row->id]" icon="trash">Delete</x-button-crm>
<div class="d-flex gap-2">
<x-button-crm type="button" variant="primary" size="sm" route="products.group.edit" :params="['id' => $row->id]" icon="pencil">Edit</x-button-crm>
<x-button-crm type="button" variant="danger" size="sm" icon="trash" data-micromodal-trigger="deleteModal-{{ $row->id }}">Delete</x-button-crm>
</div>
@include('administrator.pages.product.categories.datatable.modal', [
'deleteRoute' => 'products.group.destroy',
'itemLabel' => 'Product Group',
'itemId' => $row->id,
'itemName' => $row->name,
'modalId' => 'deleteModal-' . $row->id
])
@@ -0,0 +1,34 @@
<!-- Delete Confirmation Modal -->
<div class="modal micromodal-slide" id="{{ $modalId ?? 'deleteModal' }}" aria-hidden="true">
<div class="modal__overlay" tabindex="-1" data-micromodal-close>
<div class="modal__container modal--sm" role="dialog" aria-modal="true" aria-labelledby="deleteModalLabel-{{ $modalId ?? 'deleteModal' }}">
<header class="modal__header bg-primary text-white">
<h6 class="modal__title" id="deleteModalLabel-{{ $modalId ?? 'deleteModal' }}">
<i data-lucide="trash-2" class="w-15px h-15px me-2"></i>
Confirm Delete
</h6>
<button class="modal__close" aria-label="Close modal" data-micromodal-close>
<i data-lucide="x"></i>
</button>
</header>
<main class="modal__content">
<div class="text-center">
<i data-lucide="alert-triangle" class="text-warning mb-3" style="width: 48px; height: 48px;"></i>
<h6 class="mb-2">Are you sure?</h6>
<p class="text-muted small mb-1">Delete {{ $itemLabel }}:</p>
<p class="fw-bold mb-2">{{ $itemName ?? '' }}</p>
<p class="text-danger small mb-0">This action cannot be undone!</p>
</div>
</main>
<footer class="modal__footer">
<x-button-crm type="button" variant="secondary" size="sm" icon="x" data-micromodal-close>Cancel</x-button-crm>
<form action="{{ route($deleteRoute, ['id' => $itemId ?? 0]) }}" method="POST" class="d-inline">
@csrf
@method('DELETE')
<x-button-crm type="submit" variant="danger" size="sm" icon="trash-2">Confirm Delete</x-button-crm>
</form>
</footer>
</div>
</div>
</div>
@@ -59,4 +59,40 @@
@foreach (config('assets.datatable.js') as $datatable)
<script src="{{ asset($datatable) }}"></script>
@endforeach
@endpush
@endpush
@push('custom-scripts')
<script>
document.addEventListener('DOMContentLoaded', function () {
// Function to initialize MicroModal
function initMicroModal() {
if (typeof MicroModal !== 'undefined') {
MicroModal.init({
onShow: modal => console.info(`${modal.id} is shown`),
onClose: modal => console.info(`${modal.id} is hidden`),
openTrigger: 'data-micromodal-trigger',
closeTrigger: 'data-micromodal-close',
disableScroll: true,
disableFocus: false,
awaitOpenAnimation: false,
awaitCloseAnimation: false,
debugMode: true
});
}
}
// Initialize on page load
initMicroModal();
// Re-initialize after DataTable draws (when action column renders)
$('#productTable').on('draw.dt', function() {
setTimeout(function() {
initMicroModal();
// Re-initialize Lucide icons for modal content
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
}, 100);
});
});
</script>
@endpush
@@ -136,8 +136,7 @@
<div class="input-group input-group-sm">
<span class="input-group-text">$</span>
<input type="number" step="0.01" class="form-control"
name="pricing[{{ $cycle->value }}][price_sale]"
placeholder="0.00">
name="pricing[{{ $cycle->value }}][price_sale]" placeholder="0.00">
</div>
</td>
</tr>
@@ -221,20 +220,6 @@
@endforeach
</select>
</div>
{{-- Operating System --}}
<div class="form-group mb-3">
<label for="os_ids" class="mb-2">Operating System</label>
<select class="form-control select2" name="os_ids[]" id="os_ids" multiple data-placeholder="-- Select OS versions --">
@foreach ($osVersions as $osVersion)
<option value="{{ $osVersion->id }}" {{ in_array($osVersion->id, old('os_ids', [])) ? 'selected' : '' }}>
{{ $osVersion->osGroup->name ?? 'Unknown' }} - {{ $osVersion->version }}
</option>
@endforeach
</select>
<small class="text-muted">Select multiple OS versions supported by this product</small>
</div>
{{-- Product Type --}}
<div class="form-group mb-3">
<label for="product_type" class="mb-2">Product Type</label>
@@ -248,6 +233,59 @@
@endforeach
</select>
</div>
{{-- Operating System (for VPS) --}}
<div class="form-group mb-3" id="os-section">
<label for="os_ids" class="mb-2">Operating System</label>
<select class="form-control select2" name="os_ids[]" id="os_ids" multiple
data-placeholder="-- Select OS versions --">
@foreach ($osVersions as $osVersion)
<option value="{{ $osVersion->id }}"
{{ in_array($osVersion->id, old('os_ids', [])) ? 'selected' : '' }}>
{{ $osVersion->osGroup->name ?? 'Unknown' }} - {{ $osVersion->version }}
</option>
@endforeach
</select>
<small class="text-muted">Select multiple OS versions supported by this product</small>
</div>
{{-- Control Panels (for Hosting) --}}
<div class="form-group mb-3" id="control-panel-section">
<label for="control_panel_ids" class="mb-2">Control Panels</label>
<select class="form-control select2" name="control_panel_ids[]" id="control_panel_ids"
multiple data-placeholder="-- Select Control Panels --">
@foreach ($controlPanels as $panel)
<option value="{{ $panel->id }}"
{{ in_array($panel->id, old('control_panel_ids', [])) ? 'selected' : '' }}>
{{ $panel->name }}
</option>
@endforeach
</select>
<small class="text-muted">Select control panels available for this hosting product</small>
{{-- Price modifiers for each panel --}}
<div id="panel-price-modifiers" class="mt-3">
@foreach ($controlPanels as $panel)
<div class="panel-price-row mb-2" id="panel-price-{{ $panel->id }}"
style="display: none;">
<label class="form-label">{{ $panel->name }} - Phụ phí (VNĐ)</label>
<div class="input-group">
<input type="number" class="form-control"
name="control_panel_price_modifiers[{{ $panel->id }}]" placeholder="0"
min="0" step="1000"
value="{{ old('control_panel_price_modifiers.' . $panel->id, 0) }}">
<div class="input-group-append">
<div class="input-group-text">
<input type="radio" name="control_panel_default"
value="{{ $panel->id }}"
{{ old('control_panel_default') == $panel->id ? 'checked' : '' }}>
<span class="ms-1">Mặc định</span>
</div>
</div>
</div>
</div>
@endforeach
</div>
</div>
{{-- Status --}}
<div class="form-group mb-3">
@@ -316,6 +354,51 @@
if ($(".select2").length) {
$(".select2").select2();
}
// Toggle OS/Control Panel sections based on Product Type
function toggleProductSections() {
const productType = $('#product_type').val();
if (productType === 'hosting') {
$('#os-section').hide();
$('#control-panel-section').show();
} else if (productType === 'vps') {
$('#os-section').show();
$('#control-panel-section').hide();
} else {
// For addon, domain, or empty
$('#os-section').hide();
$('#control-panel-section').hide();
}
}
// Show/hide price modifier inputs when panels are selected
$('#control_panel_ids').on('change', function() {
const selectedPanels = $(this).val() || [];
$('.panel-price-row').hide();
selectedPanels.forEach(function(panelId) {
$('#panel-price-' + panelId).show();
});
// Clear unselected panels
$('.panel-price-row').each(function() {
const panelId = $(this).attr('id').replace('panel-price-', '');
if (!selectedPanels.includes(panelId)) {
$(this).find('input[type="number"]').val(0);
$(this).find('input[type="radio"]').prop('checked', false);
}
});
});
// Initial toggle on page load
toggleProductSections();
// Toggle when product type changes
$('#product_type').on('change', toggleProductSections);
// Trigger initial panel price display
$('#control_panel_ids').trigger('change');
});
</script>
<script src="{{ asset('lib/ckeditor/ckeditor.js') }}"></script>
@@ -220,8 +220,8 @@
</select>
</div>
{{-- Operating System --}}
<div class="form-group mb-3">
{{-- Operating System (for VPS) --}}
<div class="form-group mb-3" id="os-section">
<label for="os_ids" class="mb-2">Operating System</label>
@php
$selectedOsIds = old('os_ids', $product->osVersions->pluck('id')->toArray());
@@ -237,6 +237,54 @@
</select>
<small class="text-muted">Select multiple OS versions supported by this product</small>
</div>
{{-- Control Panels (for Hosting) --}}
<div class="form-group mb-3" id="control-panel-section" style="display: none;">
<label for="control_panel_ids" class="mb-2">Control Panels</label>
@php
$selectedPanelIds = old('control_panel_ids', $product->controlPanels->pluck('id')->toArray());
$panelPriceModifiers = old('control_panel_price_modifiers',
$product->controlPanels->pluck('pivot.price_modifier', 'id')->toArray());
$defaultPanel = old('control_panel_default',
$product->controlPanels->where('pivot.is_default', true)->first()?->id);
@endphp
<select class="form-control select2" name="control_panel_ids[]" id="control_panel_ids" multiple data-placeholder="-- Select Control Panels --">
@foreach ($controlPanels as $panel)
<option value="{{ $panel->id }}" {{ in_array($panel->id, $selectedPanelIds) ? 'selected' : '' }}>
{{ $panel->name }}
</option>
@endforeach
</select>
<small class="text-muted">Select control panels available for this hosting product</small>
{{-- Price modifiers for each panel --}}
<div id="panel-price-modifiers" class="mt-3">
@foreach ($controlPanels as $panel)
<div class="panel-price-row mb-2" id="panel-price-{{ $panel->id }}"
style="display: {{ in_array($panel->id, $selectedPanelIds) ? 'block' : 'none' }};">
<label class="form-label">{{ $panel->name }} - Phụ phí (VNĐ)</label>
<div class="input-group">
<input type="number"
class="form-control"
name="control_panel_price_modifiers[{{ $panel->id }}]"
placeholder="0"
min="0"
step="1000"
value="{{ $panelPriceModifiers[$panel->id] ?? 0 }}">
<div class="input-group-append">
<div class="input-group-text">
<input type="radio"
name="control_panel_default"
value="{{ $panel->id }}"
{{ $defaultPanel == $panel->id ? 'checked' : '' }}>
<span class="ms-1">Mặc định</span>
</div>
</div>
</div>
</div>
@endforeach
</div>
</div>
{{-- Product Type --}}
<div class="form-group mb-3">
@@ -320,6 +368,51 @@
if ($(".select2").length) {
$(".select2").select2();
}
// Toggle OS/Control Panel sections based on Product Type
function toggleProductSections() {
const productType = $('#product_type').val();
if (productType === 'hosting') {
$('#os-section').hide();
$('#control-panel-section').show();
} else if (productType === 'vps') {
$('#os-section').show();
$('#control-panel-section').hide();
} else {
// For addon, domain, or empty
$('#os-section').hide();
$('#control-panel-section').hide();
}
}
// Show/hide price modifier inputs when panels are selected
$('#control_panel_ids').on('change', function() {
const selectedPanels = $(this).val() || [];
$('.panel-price-row').hide();
selectedPanels.forEach(function(panelId) {
$('#panel-price-' + panelId).show();
});
// Clear unselected panels
$('.panel-price-row').each(function() {
const panelId = $(this).attr('id').replace('panel-price-', '');
if (!selectedPanels.includes(panelId)) {
$(this).find('input[type="number"]').val(0);
$(this).find('input[type="radio"]').prop('checked', false);
}
});
});
// Initial toggle on page load
toggleProductSections();
// Toggle when product type changes
$('#product_type').on('change', toggleProductSections);
// Trigger initial panel price display
$('#control_panel_ids').trigger('change');
});
</script>
<script src="{{ asset('lib/ckeditor/ckeditor.js') }}"></script>