feat: Enhance product support in Cart and OrderItem models
- Added new fields 'billing_cycle' and 'product_pricing_id' to Cart and OrderItem models. - Updated product retrieval logic to support both new and legacy systems. - Introduced relationships for Product and ProductPricings in Cart and OrderItem models. - Created migration to add new fields to the carts and order_item tables. - Updated frontend service index to handle new pricing structure and billing cycles. - Improved UI components for better user experience in product selection and pricing display. - Refactored action buttons in various views for consistency and improved styling.
This commit is contained in:
@@ -14,7 +14,10 @@ use App\Models\InvoiceItem;
|
||||
use App\Models\ModuleHosting;
|
||||
use App\Models\ModuleVmware;
|
||||
use App\Models\Order;
|
||||
use App\Models\OrderItem;
|
||||
use App\Models\Os;
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductPricings;
|
||||
use App\Models\VPS;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -36,36 +39,85 @@ class CartController extends Controller
|
||||
public function addCart(Request $request)
|
||||
{
|
||||
$userId = customer()?->id;
|
||||
$months = $request->input('months');
|
||||
$productId = $request->product_id;
|
||||
$typeId = $request->type_id;
|
||||
|
||||
if (!$userId) {
|
||||
return back()->with('error', 'Bạn cần đăng nhập để thêm sản phẩm vào giỏ hàng.');
|
||||
}
|
||||
|
||||
// Validate input data
|
||||
if (!$productId || !$typeId || !$months) {
|
||||
return back()->with('error', 'Dữ liệu không hợp lệ.');
|
||||
// Get request data
|
||||
$productId = $request->product_id;
|
||||
$typeId = $request->type_id; // Can be null for new Product system
|
||||
$months = $request->input('months');
|
||||
$billingCycle = $request->input('billing_cycle'); // New: billing cycle
|
||||
$productPrice = $request->input('product_price'); // Price from form
|
||||
|
||||
// Validate basic input
|
||||
if (!$productId) {
|
||||
return back()->with('error', 'Sản phẩm không hợp lệ.');
|
||||
}
|
||||
|
||||
// Use updateOrCreate instead of delete + create for better performance
|
||||
try {
|
||||
CartModel::updateOrCreate(
|
||||
['customer_id' => $userId], // Only one cart item per user
|
||||
[
|
||||
'product_id' => $productId,
|
||||
'type_product' => $typeId,
|
||||
'months' => $months,
|
||||
]
|
||||
);
|
||||
// Check if this is new Product system or legacy (Hosting/VPS)
|
||||
$product = Product::find($productId);
|
||||
|
||||
return redirect()->route('cart.index')->with('success', 'Sản phẩm đã được thêm vào giỏ hàng!');
|
||||
if ($product) {
|
||||
// NEW FLOW: Using Product table
|
||||
$pricingId = null;
|
||||
$finalPrice = $productPrice;
|
||||
|
||||
// Find the pricing based on billing_cycle
|
||||
if ($billingCycle) {
|
||||
$pricing = ProductPricings::where('product_id', $productId)
|
||||
->where('billing_cycle', $billingCycle)
|
||||
->where('is_active', 1)
|
||||
->first();
|
||||
|
||||
if ($pricing) {
|
||||
$pricingId = $pricing->id;
|
||||
$finalPrice = $pricing->price_sale ?: $pricing->price;
|
||||
}
|
||||
}
|
||||
|
||||
// Add to cart
|
||||
CartModel::updateOrCreate(
|
||||
['customer_id' => $userId],
|
||||
[
|
||||
'product_id' => $productId,
|
||||
'type_product' => null, // New system doesn't use type_product
|
||||
'months' => $months,
|
||||
'billing_cycle' => $billingCycle,
|
||||
'product_pricing_id' => $pricingId,
|
||||
'price' => $finalPrice,
|
||||
'quantity' => 1,
|
||||
]
|
||||
);
|
||||
|
||||
return redirect()->route('cart.index')->with('success', 'Sản phẩm đã được thêm vào giỏ hàng!');
|
||||
} else {
|
||||
// LEGACY FLOW: Using old Hosting/VPS system
|
||||
if (!$typeId || !$months) {
|
||||
return back()->with('error', 'Dữ liệu không hợp lệ.');
|
||||
}
|
||||
|
||||
CartModel::updateOrCreate(
|
||||
['customer_id' => $userId],
|
||||
[
|
||||
'product_id' => $productId,
|
||||
'type_product' => $typeId,
|
||||
'months' => $months,
|
||||
'price' => $productPrice,
|
||||
]
|
||||
);
|
||||
|
||||
return redirect()->route('cart.index')->with('success', 'Sản phẩm đã được thêm vào giỏ hàng!');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Cart add error: ' . $e->getMessage(), [
|
||||
'user_id' => $userId,
|
||||
'product_id' => $productId,
|
||||
'type_id' => $typeId
|
||||
'type_id' => $typeId,
|
||||
'billing_cycle' => $billingCycle ?? null,
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
return back()->with('error', 'Có lỗi xảy ra khi thêm sản phẩm vào giỏ hàng.');
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use App\Models\Hosting;
|
||||
use App\Models\Group;
|
||||
use App\Models\Type;
|
||||
use App\Models\VPS;
|
||||
use App\Models\ProductCategories;
|
||||
use App\Repositories\Addon\AddonRepositoryInterface;
|
||||
use App\Repositories\Domain\DomainRepositoryInterface;
|
||||
use App\Repositories\Group\GroupRepositoryInterface;
|
||||
@@ -18,7 +19,6 @@ use App\Repositories\Hosting\HostingRepositoryInterface;
|
||||
use App\Repositories\Type\TypeRepositoryInterface;
|
||||
use App\Repositories\VPS\VPSRepositoryInterface;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class ShopController extends Controller
|
||||
{
|
||||
@@ -34,24 +34,22 @@ class ShopController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the shop index page with optimized queries
|
||||
* Display the shop index page
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$title = "Dịch vụ của chúng tôi";
|
||||
|
||||
// Cache types with groups for better performance
|
||||
$type = Cache::remember('shop_types_with_groups', 3600, function () {
|
||||
return Type::with(['group' => function ($query) {
|
||||
$query->where('status', 1); // Only active groups
|
||||
}])
|
||||
|
||||
// Get types with groups
|
||||
$type = Type::with(['group' => function ($query) {
|
||||
$query->where('status', 1); // Only active groups
|
||||
}])
|
||||
->whereNotIn('id', [1, 2])
|
||||
->where('status', 1) // Only active types
|
||||
->latest('id')
|
||||
->get();
|
||||
});
|
||||
|
||||
// Handle product filtering with optimized queries
|
||||
// Handle product filtering
|
||||
if ($request->type && $request->group) {
|
||||
$product = $this->getProductsByTypeAndGroup($request->type, $request->group);
|
||||
$group = $this->getGroupsByType($request->type);
|
||||
@@ -84,76 +82,67 @@ class ShopController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* Display products by group slug with pagination and optimization
|
||||
* Display products by category slug (ProductCategories)
|
||||
*/
|
||||
public function show(Request $request, string $slug)
|
||||
{
|
||||
// Cache group data for better performance
|
||||
$page = (int) $request->get('page', 1);
|
||||
$cacheKey = "group_products_{$slug}_page_{$page}";
|
||||
|
||||
$data = Cache::remember($cacheKey, 1800, function () use ($slug, $page) {
|
||||
// Find group with optimized query
|
||||
$group = Group::with(['type'])
|
||||
->where('slug', $slug)
|
||||
->where('status', 1) // Only active groups
|
||||
->first();
|
||||
// Find category by slug
|
||||
$category = ProductCategories::with(['parent', 'children'])
|
||||
->where('slug', $slug)
|
||||
->where('is_active', 1)
|
||||
->first();
|
||||
|
||||
if (!$group) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get products with pagination based on group type
|
||||
$products = $this->getPaginatedProducts($group, $page);
|
||||
|
||||
return [
|
||||
'group' => $group,
|
||||
'products' => $products,
|
||||
];
|
||||
});
|
||||
|
||||
if (!$data) {
|
||||
abort(404, 'Nhóm sản phẩm không tồn tại');
|
||||
if (!$category) {
|
||||
abort(404, 'Danh mục sản phẩm không tồn tại');
|
||||
}
|
||||
|
||||
// Get products with pagination and pricing
|
||||
$products = $category->products()
|
||||
->with([
|
||||
'pricings' => function ($query) {
|
||||
$query->where('is_active', 1)->orderBy('price');
|
||||
},
|
||||
'osVersions.osGroup',
|
||||
'controlPanels'
|
||||
])
|
||||
->where('is_active', 1)
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate(12);
|
||||
|
||||
return view('frontend.service.index', [
|
||||
'group' => $data['group'],
|
||||
'products' => $data['products'],
|
||||
'breadcrumbs' => $this->crums->add(__('Dashboard'))->add('Sản phẩm thuộc ' . $data['group']->name),
|
||||
'category' => $category,
|
||||
'products' => $products,
|
||||
'breadcrumbs' => $this->crums->add(__('Dashboard'))->add('Sản phẩm thuộc ' . $category->name),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get products by type and group with optimization
|
||||
* Get products by type and group
|
||||
*/
|
||||
private function getProductsByTypeAndGroup(int $typeId, int $groupId)
|
||||
{
|
||||
return Cache::remember("products_type_{$typeId}_group_{$groupId}", 1800, function () use ($typeId, $groupId) {
|
||||
return match ($typeId) {
|
||||
3 => Hosting::where('group_id', $groupId)
|
||||
->where('status', 1)
|
||||
->with(['group', 'type'])
|
||||
->get(),
|
||||
4 => VPS::where('group_id', $groupId)
|
||||
->where('status', 1)
|
||||
->with(['group', 'type', 'operatingSystems.osGroup'])
|
||||
->get(),
|
||||
default => collect()
|
||||
};
|
||||
});
|
||||
return match ($typeId) {
|
||||
3 => Hosting::where('group_id', $groupId)
|
||||
->where('status', 1)
|
||||
->with(['group', 'type'])
|
||||
->get(),
|
||||
4 => VPS::where('group_id', $groupId)
|
||||
->where('status', 1)
|
||||
->with(['group', 'type', 'operatingSystems.osGroup'])
|
||||
->get(),
|
||||
default => collect()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get groups by type with optimization
|
||||
* Get groups by type
|
||||
*/
|
||||
private function getGroupsByType(int $typeId)
|
||||
{
|
||||
return Cache::remember("groups_type_{$typeId}", 3600, function () use ($typeId) {
|
||||
return Group::with('type')
|
||||
->where('type_id', $typeId)
|
||||
->where('status', 1)
|
||||
->get();
|
||||
});
|
||||
return Group::with('type')
|
||||
->where('type_id', $typeId)
|
||||
->where('status', 1)
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -171,34 +160,4 @@ class ShopController extends Controller
|
||||
default => null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get paginated products based on group type
|
||||
*/
|
||||
private function getPaginatedProducts(Group $group, int $page = 1, int $perPage = 12)
|
||||
{
|
||||
$query = null;
|
||||
|
||||
// Determine which model to query based on group type
|
||||
switch ($group->type_id) {
|
||||
case 3: // Hosting
|
||||
$query = Hosting::where('group_id', $group->id)
|
||||
->where('status', 1)
|
||||
->with(['group', 'type'])
|
||||
->orderBy('created_at', 'desc');
|
||||
break;
|
||||
|
||||
case 4: // VPS
|
||||
$query = VPS::where('group_id', $group->id)
|
||||
->where('status', 1)
|
||||
->with(['group', 'type'])
|
||||
->orderBy('created_at', 'desc');
|
||||
break;
|
||||
|
||||
default:
|
||||
return collect()->paginate($perPage);
|
||||
}
|
||||
|
||||
return $query->paginate($perPage, ['*'], 'page', $page);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,14 +16,52 @@ class Cart extends Model
|
||||
'price',
|
||||
'quantity',
|
||||
'months',
|
||||
'billing_cycle', // Add new field for billing cycle
|
||||
'product_pricing_id', // Add new field to link to ProductPricings
|
||||
];
|
||||
|
||||
/**
|
||||
* Get product - support both new and legacy system
|
||||
*/
|
||||
public function product()
|
||||
{
|
||||
// New flow: Check if product exists in Product table first
|
||||
if ($this->product_id) {
|
||||
$product = Product::find($this->product_id);
|
||||
if ($product) {
|
||||
return $product;
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy flow: fallback to old system based on type_product
|
||||
return match ($this->type_product) {
|
||||
3 => $this->belongsTo(Hosting::class, 'product_id'),
|
||||
4 => $this->belongsTo(VPS::class, 'product_id'),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship: Cart belongs to Product (new system)
|
||||
*/
|
||||
public function productNew()
|
||||
{
|
||||
return $this->belongsTo(Product::class, 'product_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship: Cart belongs to Customer
|
||||
*/
|
||||
public function customer()
|
||||
{
|
||||
return $this->belongsTo(Customer::class, 'customer_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship: Cart has pricing info
|
||||
*/
|
||||
public function pricing()
|
||||
{
|
||||
return $this->belongsTo(ProductPricings::class, 'product_pricing_id');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,15 @@ class OrderItem extends Model
|
||||
|
||||
public function getProductAttribute()
|
||||
{
|
||||
// New flow: Check if product exists in Product table first
|
||||
if ($this->product_id) {
|
||||
$product = Product::find($this->product_id);
|
||||
if ($product) {
|
||||
return $product;
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy flow: fallback to old system
|
||||
return match ($this->product_type) {
|
||||
3 => Hosting::find($this->product_id),
|
||||
4 => Vps::find($this->product_id),
|
||||
@@ -32,6 +41,22 @@ class OrderItem extends Model
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship: OrderItem belongs to Product (new system)
|
||||
*/
|
||||
public function product()
|
||||
{
|
||||
return $this->belongsTo(Product::class, 'product_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship: OrderItem has pricing info
|
||||
*/
|
||||
public function pricing()
|
||||
{
|
||||
return $this->belongsTo(ProductPricings::class, 'product_pricing_id');
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function os()
|
||||
|
||||
@@ -39,4 +39,24 @@ class Os extends Model
|
||||
->withTimestamps()
|
||||
->withPivot('product_type');
|
||||
}
|
||||
|
||||
/**
|
||||
* Many-to-many relationship với Products (new table: os_product)
|
||||
*/
|
||||
public function products()
|
||||
{
|
||||
return $this->belongsToMany(\App\Models\Product::class, 'os_product', 'os_id', 'product_id')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get display name with OS Group
|
||||
*/
|
||||
public function getNameAttribute()
|
||||
{
|
||||
if ($this->osGroup) {
|
||||
return $this->osGroup->name . ' ' . $this->version;
|
||||
}
|
||||
return $this->version;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,22 @@ class Product extends Model
|
||||
return $this->hasMany(ProductPricings::class, 'product_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship: Product has many order items
|
||||
*/
|
||||
public function orderItems()
|
||||
{
|
||||
return $this->hasMany(OrderItem::class, 'product_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship: Product has many orders through order items
|
||||
*/
|
||||
public function orders()
|
||||
{
|
||||
return $this->hasManyThrough(Order::class, OrderItem::class, 'product_id', 'id', 'id', 'order_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy: Many-to-many categories (if still needed)
|
||||
*/
|
||||
@@ -86,4 +102,31 @@ class Product extends Model
|
||||
{
|
||||
return $this->belongsToMany(ProductCategories::class, 'product_product_category', 'product_id', 'product_category_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get monthly pricing (or first available pricing)
|
||||
*/
|
||||
public function getMonthlyPricingAttribute()
|
||||
{
|
||||
return $this->pricings()
|
||||
->where('is_active', 1)
|
||||
->where('billing_cycle', 'monthly')
|
||||
->first()
|
||||
?? $this->pricings()->where('is_active', 1)->orderBy('price')->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get display price (sale price if available, otherwise regular price)
|
||||
*/
|
||||
public function getPriceAttribute()
|
||||
{
|
||||
$pricing = $this->monthlyPricing;
|
||||
return $pricing ? $pricing->price : 0;
|
||||
}
|
||||
|
||||
public function getPriceSaleAttribute()
|
||||
{
|
||||
$pricing = $this->monthlyPricing;
|
||||
return $pricing ? $pricing->price_sale : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ class ProductCategories extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $table = 'product_categories';
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'slug',
|
||||
@@ -30,6 +32,6 @@ class ProductCategories extends Model
|
||||
|
||||
public function products()
|
||||
{
|
||||
return $this->belongsToMany(Product::class, 'product_product_category', 'product_category_id', 'product_id');
|
||||
return $this->hasMany(Product::class, 'product_categories_id');
|
||||
}
|
||||
}
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// Add support for new Product system to carts table
|
||||
Schema::table('carts', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('carts', 'billing_cycle')) {
|
||||
$table->string('billing_cycle', 50)->nullable()->after('months')->comment('Billing cycle: monthly, semi_annually, annually, etc.');
|
||||
}
|
||||
if (!Schema::hasColumn('carts', 'product_pricing_id')) {
|
||||
$table->unsignedBigInteger('product_pricing_id')->nullable()->after('billing_cycle')->comment('Link to product_pricings table');
|
||||
$table->index('product_pricing_id');
|
||||
}
|
||||
|
||||
// Make type_product nullable for new Product system
|
||||
$table->integer('type_product')->nullable()->change();
|
||||
});
|
||||
|
||||
// Add support for new Product system to order__order_item table
|
||||
Schema::table('order__order_item', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('order__order_item', 'billing_cycle')) {
|
||||
$table->string('billing_cycle', 50)->nullable()->after('months')->comment('Billing cycle: monthly, semi_annually, annually, etc.');
|
||||
}
|
||||
if (!Schema::hasColumn('order__order_item', 'product_pricing_id')) {
|
||||
$table->unsignedBigInteger('product_pricing_id')->nullable()->after('billing_cycle')->comment('Link to product_pricings table');
|
||||
$table->index('product_pricing_id');
|
||||
}
|
||||
|
||||
// Make product_type nullable for new Product system
|
||||
if (Schema::hasColumn('order__order_item', 'product_type')) {
|
||||
$table->integer('product_type')->nullable()->change();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('carts', function (Blueprint $table) {
|
||||
$table->dropColumn(['billing_cycle', 'product_pricing_id']);
|
||||
$table->dropIndex(['product_pricing_id']);
|
||||
});
|
||||
|
||||
Schema::table('order__order_item', function (Blueprint $table) {
|
||||
$table->dropColumn(['billing_cycle', 'product_pricing_id']);
|
||||
$table->dropIndex(['product_pricing_id']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -27,15 +27,22 @@ $(document).ready(function () {
|
||||
$(document).on("click", ".dropdown-option", function (e) {
|
||||
e.stopPropagation();
|
||||
const $option = $(this);
|
||||
const $dropdown = $option.closest(".custom-dropdown");
|
||||
const $selected = $dropdown.find(".dropdown-selected");
|
||||
const $options = $dropdown.find(".dropdown-options");
|
||||
const $selectedText = $selected.find(".selected-text");
|
||||
|
||||
// Ignore disabled options
|
||||
if ($option.hasClass('disabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $dropdown = $option.closest('.custom-dropdown');
|
||||
const $selected = $dropdown.find('.dropdown-selected');
|
||||
const $options = $dropdown.find('.dropdown-options');
|
||||
const $selectedText = $selected.find('.selected-text');
|
||||
|
||||
// Get data from selected option
|
||||
const value = $option.data("value");
|
||||
const months = $option.data("months");
|
||||
const price = $option.data("price");
|
||||
const value = $option.data('value');
|
||||
const months = $option.data('months');
|
||||
const price = $option.data('price');
|
||||
const cycle = $option.data('cycle'); // Get billing cycle
|
||||
// Get text from the first span element to avoid getting badge text
|
||||
const text = $option.find('span').first().text();
|
||||
|
||||
@@ -43,19 +50,21 @@ $(document).ready(function () {
|
||||
$selectedText.text(text);
|
||||
|
||||
// Update hidden inputs
|
||||
$dropdown.closest("form").find(".hiddenMonths").val(months);
|
||||
$dropdown.closest("form").find(".hiddenTotalPrice").val(price);
|
||||
const $form = $dropdown.closest('form');
|
||||
$form.find('.hiddenMonths').val(months);
|
||||
$form.find('.hiddenProductPrice').val(price); // Update product price
|
||||
$form.find('.hiddenBillingCycle').val(cycle); // Update billing cycle
|
||||
|
||||
// Update selected state
|
||||
$options.find(".dropdown-option").removeClass("selected");
|
||||
$option.addClass("selected");
|
||||
$options.find('.dropdown-option').removeClass('selected');
|
||||
$option.addClass('selected');
|
||||
|
||||
// Close dropdown
|
||||
$options.removeClass("show");
|
||||
$selected.removeClass("active");
|
||||
$options.removeClass('show');
|
||||
$selected.removeClass('active');
|
||||
|
||||
// Trigger custom event for other components
|
||||
$dropdown.trigger("dropdown:changed", {
|
||||
$dropdown.trigger('dropdown:changed', {
|
||||
value: value,
|
||||
months: months,
|
||||
price: price,
|
||||
@@ -66,19 +75,19 @@ $(document).ready(function () {
|
||||
const $productContainer = $dropdown.closest('[data-product-id]');
|
||||
const basePrice = parseFloat($productContainer.data('base-price'));
|
||||
const selectedMonths = parseInt(months);
|
||||
|
||||
|
||||
// Calculate prices
|
||||
const originalPrice = basePrice * selectedMonths;
|
||||
const savings = selectedMonths >= 12 ? Math.round(originalPrice * 0.1) : 0;
|
||||
const finalPrice = originalPrice - savings;
|
||||
|
||||
|
||||
// Update display
|
||||
$productContainer.find('.total-price-display').text('₫' + finalPrice.toLocaleString());
|
||||
$productContainer.find('.months-display').text(selectedMonths + ' tháng');
|
||||
$productContainer.find('.price-per-month').text('₫' + basePrice.toLocaleString() + '/tháng');
|
||||
$productContainer.find('.original-price').text('₫' + originalPrice.toLocaleString());
|
||||
$productContainer.find('.savings-amount').text('₫' + savings.toLocaleString());
|
||||
|
||||
|
||||
// Show/hide savings info
|
||||
if (savings > 0) {
|
||||
$productContainer.find('.savings-info').show();
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<x-button-crm type="button" variant="primary" size="sm" route="module.vcenter.create" icon="plus-circle">Add
|
||||
<x-button-crm type="button" variant="outline-primary" size="sm" route="module.vcenter.create" icon="plus-circle">Add
|
||||
Server</x-button-crm>
|
||||
<form action="{{ route('module.vcenter.import') }}" class="d-lg-inline" enctype="multipart/form-data" method="post">
|
||||
@csrf
|
||||
<input type="file" name="file" class="d-none file-selector"
|
||||
accept=".csv,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" onchange="form.submit()" />
|
||||
<x-button-crm type="button" variant="success" size="sm" route="module.vcenter.import" icon="plus-circle">Import
|
||||
<x-button-crm type="button" variant="outline-info" size="sm" route="module.vcenter.import" icon="plus-circle">Import
|
||||
From
|
||||
Excel</x-button-crm>
|
||||
</form>
|
||||
<x-button-crm type="button" variant="secondary" size="sm" url="file/vCenter.xlsx" icon="download">File Import
|
||||
<x-button-crm type="button" variant="outline-secondary" size="sm" url="file/vCenter.xlsx" icon="download">File Import
|
||||
Demo</x-button-crm>
|
||||
@@ -44,7 +44,7 @@
|
||||
@section('content')
|
||||
@if (auth()->guard('admins')->user()->can('manager module hosting'))
|
||||
<div class="card card-primary card-outline">
|
||||
<div class="card-header">
|
||||
<div class="card-header d-flex justify-content-end gap-2">
|
||||
@include('administrator.pages.block.ModuleHosting')
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
@@ -1,2 +1,7 @@
|
||||
<x-button-crm type="button" variant="primary" size="sm" route="module.vcenter.edit" :params="['id' => $row->id]" icon="pencil">Edit</x-button-crm>
|
||||
<x-button-crm type="button" variant="danger" size="sm" route="module.vcenter.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="module.vcenter.edit" :params="['id' => $row->id]"
|
||||
icon="pencil">Edit</x-button-crm>
|
||||
<x-button-crm type="button" variant="danger" size="sm" route="module.vcenter.destroy" :params="['id' => $row->id]"
|
||||
icon="trash">Delete</x-button-crm>
|
||||
</div>
|
||||
@@ -1,2 +1,6 @@
|
||||
<x-button-crm type="button" variant="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="danger" size="sm" route="products.destroy" :params="['id' => $row->id]" icon="trash">Xóa</x-button-crm>
|
||||
<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>
|
||||
@@ -1,42 +1,65 @@
|
||||
@extends('frontend.layouts.master')
|
||||
|
||||
@section('content')
|
||||
@if (!empty($group))
|
||||
@if (!empty($category))
|
||||
<!-- Products Grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-8" x-data>
|
||||
<div class="grid grid-cols-1 gap-8 lg:grid-cols-2 xl:grid-cols-4" x-data>
|
||||
@forelse ($products as $index => $item)
|
||||
<form action="{{ route('cart.add') }}" method="post">
|
||||
@csrf
|
||||
<input name="product_id" value="{{ $item->id }}" type="hidden" />
|
||||
<input name="type_id" value="{{ $item->type_id }}" type="hidden" />
|
||||
<input name="product_price" value="{{ $item->price_sale ?: $item->price }}" type="hidden" />
|
||||
{{-- Remove type_id for new Product system --}}
|
||||
@php
|
||||
$defaultPricingForCart =
|
||||
$item->pricings->firstWhere('billing_cycle', 'semi_annually') ?? $item->pricings->first();
|
||||
$cartPrice = $defaultPricingForCart
|
||||
? floatval($defaultPricingForCart->price_sale ?: $defaultPricingForCart->price)
|
||||
: 0;
|
||||
@endphp
|
||||
<input name="product_price" value="{{ $cartPrice }}" type="hidden" class="hiddenProductPrice" />
|
||||
|
||||
<div class="group relative bg-white rounded-md hover:shadow-2xl transition-all duration-500 transform overflow-hidden border border-gray-100"
|
||||
<div class="group relative transform overflow-hidden rounded-md border border-gray-100 bg-white transition-all duration-500 hover:shadow-2xl"
|
||||
x-init="$el.style.opacity = 0;
|
||||
setTimeout(() => { $el.style.opacity = 1 }, {{ (int) $index * 150 }})" style="opacity: 0">
|
||||
|
||||
<!-- Header with gradient -->
|
||||
<div class="p-2 border-b border-dashed border-gray-200">
|
||||
<h3 class="text-xl text-primary font-bold mb-2 text-center">
|
||||
<div class="border-b border-dashed border-gray-200 p-2">
|
||||
<h3 class="text-primary mb-2 text-center text-xl font-bold">
|
||||
{{ $item->name }}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@php
|
||||
$basePrice = floatval($item->price_sale ?: $item->price);
|
||||
// Get pricing for different billing cycles
|
||||
// Note: If billing_cycle is 'onetime', treat it as monthly
|
||||
$monthlyPricing =
|
||||
$item->pricings->firstWhere('billing_cycle', 'monthly') ??
|
||||
$item->pricings->firstWhere('billing_cycle', 'onetime');
|
||||
$semiAnnuallyPricing = $item->pricings->firstWhere('billing_cycle', 'semi_annually');
|
||||
$annuallyPricing = $item->pricings->firstWhere('billing_cycle', 'annually');
|
||||
$bienniallyPricing = $item->pricings->firstWhere('billing_cycle', 'biennially');
|
||||
$trienniallyPricing = $item->pricings->firstWhere('billing_cycle', 'triennially');
|
||||
|
||||
// Use monthly or first available pricing for display
|
||||
$displayPricing = $monthlyPricing ?? $item->pricings->first();
|
||||
$basePrice = $displayPricing
|
||||
? floatval($displayPricing->price_sale ?: $displayPricing->price)
|
||||
: 0;
|
||||
@endphp
|
||||
|
||||
<div class="p-6" data-product-id="{{ $item->id }}" data-base-price="{{ $basePrice }}">
|
||||
|
||||
|
||||
<!-- Price Display -->
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="inline-flex items-center justify-center gap-2 bg-orange-50 rounded-md w-full mb-3 p-2">
|
||||
<span class="text-sm font-bold text-primary">
|
||||
≈ {{ format_price($item->price_sale ?: $item->price) }}
|
||||
class="mb-3 inline-flex w-full items-center justify-center gap-2 rounded-md bg-orange-50 p-2">
|
||||
<span class="text-primary text-sm font-bold">
|
||||
≈
|
||||
{{ $displayPricing ? format_price($displayPricing->price_sale ?: $displayPricing->price) : 'Liên hệ' }}
|
||||
</span>
|
||||
<span class="text-primary font-medium">/ tháng</span>
|
||||
@if ($displayPricing)
|
||||
<span class="text-primary font-medium">/ tháng</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<!-- Duration Selector -->
|
||||
@@ -44,143 +67,243 @@
|
||||
<div class="mb-6">
|
||||
<!-- Custom Dropdown -->
|
||||
<div class="custom-dropdown" id="durationDropdown{{ $item->id }}">
|
||||
@php
|
||||
// Default to semi_annually (6 months) if available, otherwise first available
|
||||
$defaultPricing = $semiAnnuallyPricing ?? $item->pricings->first();
|
||||
$defaultPrice = $defaultPricing
|
||||
? floatval($defaultPricing->price_sale ?: $defaultPricing->price)
|
||||
: 0;
|
||||
$defaultMonths = match ($defaultPricing?->billing_cycle) {
|
||||
'semi_annually' => 6,
|
||||
'annually' => 12,
|
||||
'biennially' => 24,
|
||||
'triennially' => 36,
|
||||
default => 1,
|
||||
};
|
||||
@endphp
|
||||
<div class="dropdown-selected">
|
||||
<span class="selected-text">{{ format_price($basePrice * 6) }} / 6 tháng</span>
|
||||
<span class="selected-text">
|
||||
@if ($defaultPricing)
|
||||
{{ format_price($defaultPrice) }} / {{ $defaultMonths }}
|
||||
{{ $defaultMonths > 1 ? 'tháng' : 'tháng' }}
|
||||
@else
|
||||
Chọn thời gian
|
||||
@endif
|
||||
</span>
|
||||
<i class="dropdown-arrow">▼</i>
|
||||
</div>
|
||||
<div class="dropdown-options">
|
||||
<div class="dropdown-option selected" data-value="6" data-months="6"
|
||||
data-price="{{ $basePrice * 6 }}">
|
||||
<div class="flex justify-between items-center">
|
||||
<span>{{ format_price($basePrice * 6) }} / 6 tháng</span>
|
||||
{{-- Monthly --}}
|
||||
<div class="dropdown-option {{ !$monthlyPricing ? 'disabled opacity-50 cursor-not-allowed' : '' }} {{ $defaultPricing?->billing_cycle === 'monthly' ? 'selected' : '' }}"
|
||||
@if ($monthlyPricing) data-value="monthly"
|
||||
data-months="1"
|
||||
data-price="{{ floatval($monthlyPricing->price_sale ?: $monthlyPricing->price) }}" @endif>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>
|
||||
@if ($monthlyPricing)
|
||||
{{ format_price($monthlyPricing->price_sale ?: $monthlyPricing->price) }}
|
||||
/ 1 tháng
|
||||
@else
|
||||
<span class="text-gray-400">1 tháng - Liên hệ</span>
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown-option" data-value="12" data-months="12"
|
||||
data-price="{{ $basePrice * 12 }}">
|
||||
<div class="flex justify-between items-center">
|
||||
<span>{{ format_price($basePrice * 12) }} / 1 năm</span>
|
||||
<span class="text-xs text-green-600 bg-green-100 px-2 py-1 rounded">Tiết
|
||||
kiệm 10%</span>
|
||||
|
||||
{{-- Semi-annually --}}
|
||||
<div class="dropdown-option {{ !$semiAnnuallyPricing ? 'disabled opacity-50 cursor-not-allowed' : '' }} {{ $defaultPricing?->billing_cycle === 'semi_annually' ? 'selected' : '' }}"
|
||||
@if ($semiAnnuallyPricing) data-value="semi_annually"
|
||||
data-months="6"
|
||||
data-price="{{ floatval($semiAnnuallyPricing->price_sale ?: $semiAnnuallyPricing->price) }}" @endif>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>
|
||||
@if ($semiAnnuallyPricing)
|
||||
{{ format_price($semiAnnuallyPricing->price_sale ?: $semiAnnuallyPricing->price) }}
|
||||
/ 6 tháng
|
||||
@else
|
||||
<span class="text-gray-400">6 tháng - Liên hệ</span>
|
||||
@endif
|
||||
</span>
|
||||
@if ($monthlyPricing && $semiAnnuallyPricing && $semiAnnuallyPricing->price < $monthlyPricing->price * 6)
|
||||
<span class="rounded bg-green-100 px-2 py-1 text-xs text-green-600">Tiết
|
||||
kiệm</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown-option" data-value="24" data-months="24"
|
||||
data-price="{{ $basePrice * 24 }}">
|
||||
<div class="flex justify-between items-center">
|
||||
<span>{{ format_price($basePrice * 24) }} / 2 năm</span>
|
||||
<span class="text-xs text-green-600 bg-green-100 px-2 py-1 rounded">Tiết
|
||||
kiệm 10%</span>
|
||||
|
||||
{{-- Annually --}}
|
||||
<div class="dropdown-option {{ !$annuallyPricing ? 'disabled opacity-50 cursor-not-allowed' : '' }} {{ $defaultPricing?->billing_cycle === 'annually' ? 'selected' : '' }}"
|
||||
@if ($annuallyPricing) data-value="annually"
|
||||
data-months="12"
|
||||
data-price="{{ floatval($annuallyPricing->price_sale ?: $annuallyPricing->price) }}" @endif>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>
|
||||
@if ($annuallyPricing)
|
||||
{{ format_price($annuallyPricing->price_sale ?: $annuallyPricing->price) }}
|
||||
/ 1 năm
|
||||
@else
|
||||
<span class="text-gray-400">1 năm - Liên hệ</span>
|
||||
@endif
|
||||
</span>
|
||||
@if ($semiAnnuallyPricing && $annuallyPricing && $annuallyPricing->price < $semiAnnuallyPricing->price * 2)
|
||||
<span class="rounded bg-green-100 px-2 py-1 text-xs text-green-600">Tiết
|
||||
kiệm</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown-option" data-value="36" data-months="36"
|
||||
data-price="{{ $basePrice * 36 }}">
|
||||
<div class="flex justify-between items-center">
|
||||
<span>{{ format_price($basePrice * 36) }} / 3 năm</span>
|
||||
<span class="text-xs text-green-600 bg-green-100 px-2 py-1 rounded">Tiết
|
||||
kiệm 10%</span>
|
||||
|
||||
{{-- Biennially --}}
|
||||
<div class="dropdown-option {{ !$bienniallyPricing ? 'disabled opacity-50 cursor-not-allowed' : '' }} {{ $defaultPricing?->billing_cycle === 'biennially' ? 'selected' : '' }}"
|
||||
@if ($bienniallyPricing) data-value="biennially"
|
||||
data-months="24"
|
||||
data-price="{{ floatval($bienniallyPricing->price_sale ?: $bienniallyPricing->price) }}" @endif>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>
|
||||
@if ($bienniallyPricing)
|
||||
{{ format_price($bienniallyPricing->price_sale ?: $bienniallyPricing->price) }}
|
||||
/ 2 năm
|
||||
@else
|
||||
<span class="text-gray-400">2 năm - Liên hệ</span>
|
||||
@endif
|
||||
</span>
|
||||
@if ($annuallyPricing && $bienniallyPricing && $bienniallyPricing->price < $annuallyPricing->price * 2)
|
||||
<span class="rounded bg-green-100 px-2 py-1 text-xs text-green-600">Tiết
|
||||
kiệm</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Triennially --}}
|
||||
<div class="dropdown-option {{ !$trienniallyPricing ? 'disabled opacity-50 cursor-not-allowed' : '' }} {{ $defaultPricing?->billing_cycle === 'triennially' ? 'selected' : '' }}"
|
||||
@if ($trienniallyPricing) data-value="triennially"
|
||||
data-months="36"
|
||||
data-price="{{ floatval($trienniallyPricing->price_sale ?: $trienniallyPricing->price) }}" @endif>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>
|
||||
@if ($trienniallyPricing)
|
||||
{{ format_price($trienniallyPricing->price_sale ?: $trienniallyPricing->price) }}
|
||||
/ 3 năm
|
||||
@else
|
||||
<span class="text-gray-400">3 năm - Liên hệ</span>
|
||||
@endif
|
||||
</span>
|
||||
@if ($bienniallyPricing && $trienniallyPricing && $trienniallyPricing->price < $bienniallyPricing->price * 1.5)
|
||||
<span class="rounded bg-green-100 px-2 py-1 text-xs text-green-600">Tiết
|
||||
kiệm</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="months" class="hiddenMonths" value="6">
|
||||
{{-- <input type="hidden" name="total_price" class="hiddenTotalPrice"
|
||||
value="{{ $basePrice }}"> --}}
|
||||
<input type="hidden" name="months" class="hiddenMonths" value="{{ $defaultMonths }}">
|
||||
<input type="hidden" name="billing_cycle" class="hiddenBillingCycle"
|
||||
value="{{ $defaultPricing?->billing_cycle ?? 'semi_annually' }}">
|
||||
</div>
|
||||
<!-- Specifications -->
|
||||
<div class="space-y-4 mb-6">
|
||||
<div class="mb-6 space-y-4">
|
||||
<ul class="space-y-1">
|
||||
<!-- CPU -->
|
||||
<li class="flex items-center justify-center gap-3">
|
||||
<div class="w-10 h-10 bg-orange-100 rounded-full flex items-center justify-center">
|
||||
<i class="fas fa-microchip text-orange-600"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<span class="text-sm text-gray-600">CPU:</span>
|
||||
<span class="font-bold text-gray-800 ml-2 vm-cpu"
|
||||
data-template-id="{{ $item->template_id }}">{{ $item->cpu }}
|
||||
vCore</span>
|
||||
</div>
|
||||
</li>
|
||||
@if ($item->cpu)
|
||||
<li class="flex items-center justify-center gap-3">
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-full bg-orange-100">
|
||||
<i class="fas fa-microchip text-orange-600"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<span class="text-sm text-gray-600">CPU:</span>
|
||||
<span class="ml-2 font-bold text-gray-800">{{ $item->cpu }} vCore</span>
|
||||
</div>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
<!-- RAM -->
|
||||
<li class="flex items-center justify-center gap-3">
|
||||
<div class="w-10 h-10 bg-orange-100 rounded-full flex items-center justify-center">
|
||||
<i class="fas fa-memory text-orange-600"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<span class="text-sm text-gray-600">RAM:</span>
|
||||
<span class="font-bold text-gray-800 ml-2 vm-ram"
|
||||
data-template-id="{{ $item->template_id }}">{{ $item->ram }} GB</span>
|
||||
</div>
|
||||
</li>
|
||||
@if ($item->ram)
|
||||
<li class="flex items-center justify-center gap-3">
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-full bg-orange-100">
|
||||
<i class="fas fa-memory text-orange-600"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<span class="text-sm text-gray-600">RAM:</span>
|
||||
<span class="ml-2 font-bold text-gray-800">{{ $item->ram }} GB</span>
|
||||
</div>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
<!-- SSD -->
|
||||
<li class="flex items-center justify-center gap-3">
|
||||
<div class="w-10 h-10 bg-orange-100 rounded-full flex items-center justify-center">
|
||||
<i class="fas fa-hdd text-orange-600"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<span class="text-sm text-gray-600">SSD:</span>
|
||||
<span class="font-bold text-gray-800 ml-2 vm-disk"
|
||||
data-template-id="{{ $item->template_id }}">{{ $item->disk }} GB</span>
|
||||
</div>
|
||||
</li>
|
||||
@if ($item->disk)
|
||||
<li class="flex items-center justify-center gap-3">
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-full bg-orange-100">
|
||||
<i class="fas fa-hdd text-orange-600"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<span class="text-sm text-gray-600">SSD:</span>
|
||||
<span class="ml-2 font-bold text-gray-800">{{ $item->disk }} GB</span>
|
||||
</div>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
<!-- GPU -->
|
||||
<li class="flex items-center justify-center gap-3">
|
||||
<div class="w-10 h-10 bg-orange-100 rounded-full flex items-center justify-center">
|
||||
<i class="fas fa-tv text-orange-600"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<span class="text-sm text-gray-600">GPU:</span>
|
||||
<span class="font-bold text-gray-800 ml-2">{{ $item->gpu }} GB</span>
|
||||
</div>
|
||||
</li>
|
||||
@if ($item->gpu)
|
||||
<li class="flex items-center justify-center gap-3">
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-full bg-orange-100">
|
||||
<i class="fas fa-tv text-orange-600"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<span class="text-sm text-gray-600">GPU:</span>
|
||||
<span class="ml-2 font-bold text-gray-800">{{ $item->gpu }} GB</span>
|
||||
</div>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
<!-- Bandwidth -->
|
||||
<li class="flex items-center justify-center gap-3">
|
||||
<div class="w-10 h-10 bg-orange-100 rounded-full flex items-center justify-center">
|
||||
<i class="fas fa-tachometer-alt text-orange-600"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<span class="text-sm text-gray-600">Băng thông:</span>
|
||||
<span class="font-bold text-gray-800 ml-2">{{ $item->bandwidth }} MB</span>
|
||||
</div>
|
||||
</li>
|
||||
@if ($item->bandwidth)
|
||||
<li class="flex items-center justify-center gap-3">
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-full bg-orange-100">
|
||||
<i class="fas fa-tachometer-alt text-orange-600"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<span class="text-sm text-gray-600">Băng thông:</span>
|
||||
<span class="ml-2 font-bold text-gray-800">{{ $item->bandwidth }} MB</span>
|
||||
</div>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
<!-- OS -->
|
||||
<li class="flex items-center justify-center gap-3">
|
||||
<div class="w-10 h-10 bg-orange-100 rounded-full flex items-center justify-center">
|
||||
<i class="fab fa-linux text-orange-600 os-icon"
|
||||
data-template-id="{{ $item->template_id }}"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<span class="text-sm text-gray-600">Hệ điều hành:</span>
|
||||
<span class="font-bold text-gray-800 ml-2 guest-os"
|
||||
data-template-id="{{ $item->template_id }}">
|
||||
@if ($item->type_id == 4 && $item->operatingSystems->isNotEmpty())
|
||||
{{ $item->operatingSystems->pluck('osGroup.name')->filter()->unique()->implode(', ') }}
|
||||
@else
|
||||
{{ $item->os ?? 'Linux' }}
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
@if ($item->osVersions && $item->osVersions->isNotEmpty())
|
||||
<li class="flex items-center justify-center gap-3">
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-full bg-orange-100">
|
||||
<i class="fab fa-linux os-icon text-orange-600"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<span class="text-sm text-gray-600">Hệ điều hành:</span>
|
||||
<span class="guest-os ml-2 font-bold text-gray-800">
|
||||
{{ $item->osVersions->pluck('name')->unique()->implode(', ') }}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Action Button -->
|
||||
<flux:button variant="primary" color="orange" type="submit" class="w-full"
|
||||
icon="shopping-bag">
|
||||
<flux:button variant="primary" color="orange" type="submit" class="w-full" icon="shopping-bag">
|
||||
Đăng ký ngay</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@empty
|
||||
<div class="col-span-full">
|
||||
<div class="text-center py-16">
|
||||
<div class="w-24 h-24 bg-orange-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<div class="py-16 text-center">
|
||||
<div class="mx-auto mb-6 flex h-24 w-24 items-center justify-center rounded-full bg-orange-100">
|
||||
<i class="fas fa-box-open text-4xl text-orange-500"></i>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-gray-800 mb-2">Không có sản phẩm</h3>
|
||||
<h3 class="mb-2 text-2xl font-bold text-gray-800">Không có sản phẩm</h3>
|
||||
<p class="text-gray-600">Danh mục này hiện chưa có sản phẩm nào.</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -188,15 +311,15 @@
|
||||
@endforelse
|
||||
</div>
|
||||
@else
|
||||
<div class="min-h-screen flex items-center justify-center">
|
||||
<div class="flex min-h-screen items-center justify-center">
|
||||
<div class="text-center">
|
||||
<div class="w-32 h-32 bg-orange-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<div class="mx-auto mb-6 flex h-32 w-32 items-center justify-center rounded-full bg-orange-100">
|
||||
<i class="fas fa-exclamation-triangle text-5xl text-orange-500"></i>
|
||||
</div>
|
||||
<h2 class="text-3xl font-bold text-gray-800 mb-4">Không tìm thấy</h2>
|
||||
<p class="text-gray-600 text-lg">Danh mục này không tồn tại hoặc đã bị xóa.</p>
|
||||
<h2 class="mb-4 text-3xl font-bold text-gray-800">Không tìm thấy</h2>
|
||||
<p class="text-lg text-gray-600">Danh mục này không tồn tại hoặc đã bị xóa.</p>
|
||||
<a href="{{ route('home') }}"
|
||||
class="inline-block mt-6 bg-orange-500 hover:bg-orange-600 text-white font-bold py-3 px-6 rounded-xl transition-colors">
|
||||
class="mt-6 inline-block rounded-xl bg-orange-500 px-6 py-3 font-bold text-white transition-colors hover:bg-orange-600">
|
||||
Về trang chủ
|
||||
</a>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ use App\Http\Controllers\frontend\ShopController;
|
||||
use App\Http\Controllers\frontend\TicketController;
|
||||
use App\Http\Controllers\frontend\Order\OrderController as ClientOrderController;
|
||||
use App\Http\Controllers\frontend\UserController as ClientController;
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::middleware("IsLoginCheck")
|
||||
->controller(ClientController::class)
|
||||
|
||||
Reference in New Issue
Block a user