ipdate
This commit is contained in:
@@ -23,6 +23,7 @@ use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Validator as ValidatorInstance;
|
||||
use Illuminate\Support\Facades\Response;
|
||||
use Illuminate\Support\Facades\Validator as FacadesValidator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ServiceController extends Controller
|
||||
{
|
||||
@@ -628,78 +629,7 @@ class ServiceController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renew service for selected months
|
||||
*/
|
||||
public function renew(Request $request)
|
||||
{
|
||||
try {
|
||||
$user = customer();
|
||||
if (!$user) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => __('Chưa đăng nhập. Vui lòng đăng nhập để sử dụng tính năng này.'),
|
||||
], 401);
|
||||
}
|
||||
|
||||
$validated = FacadesValidator::make($request->all(), [
|
||||
'product_id' => 'required|string|max:255',
|
||||
'plan' => 'required|string|in:3,6,12,24,other',
|
||||
'months' => 'nullable|integer|min:1|max:60',
|
||||
]);
|
||||
|
||||
if ($validated->fails()) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => __('Dữ liệu không hợp lệ'),
|
||||
'errors' => $validated->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$months = $request->plan === 'other' ? (int) ($request->months ?? 1) : (int) $request->plan;
|
||||
$productId = $this->sanitizeInput($request->product_id);
|
||||
|
||||
// Fetch product price based on invoice item link
|
||||
$invoiceItem = InvoiceItem::whereHas('invoice', function ($q) use ($user) {
|
||||
$q->where('customer_id', $user->id);
|
||||
})
|
||||
->where('product_type', 4) // VPS
|
||||
->whereHas('invoice', fn($q) => $q->where('status', '!=', 0))
|
||||
->first();
|
||||
|
||||
$product = $invoiceItem?->getProductAttribute();
|
||||
$unitPrice = (int) ($product?->price_sale ?? $product?->price ?? 0);
|
||||
$total = $unitPrice * max($months, 1);
|
||||
|
||||
Log::info('Khách yêu cầu gia hạn dịch vụ', [
|
||||
'user_id' => $user->id,
|
||||
'product_vmware_id' => $productId,
|
||||
'months' => $months,
|
||||
'unit_price' => $unitPrice,
|
||||
'total' => $total,
|
||||
]);
|
||||
|
||||
// TODO: implement actual renewal billing/workflow
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => __('Yêu cầu gia hạn đã được ghi nhận'),
|
||||
'data' => [
|
||||
'unit_price' => $unitPrice,
|
||||
'months' => $months,
|
||||
'total' => $total,
|
||||
],
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Lỗi renew', [
|
||||
'user_id' => customer()?->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => __('Có lỗi xảy ra khi gia hạn'),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Secure remove snapshot using session authentication - NO BEARER TOKEN
|
||||
@@ -1316,4 +1246,238 @@ class ServiceController extends Controller
|
||||
|
||||
return $processed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate renewal price (AJAX)
|
||||
*/
|
||||
public function renewPrice(Request $request)
|
||||
{
|
||||
try {
|
||||
$user = customer();
|
||||
if (!$user) {
|
||||
return response()->json(['error' => true, 'message' => __('Vui lòng đăng nhập.')], 401);
|
||||
}
|
||||
|
||||
$vmware_id = $this->sanitizeInput($request->product_id);
|
||||
$plan = $request->input('plan');
|
||||
$months = $plan === 'other' ? (int) $request->input('months') : (int) $plan;
|
||||
|
||||
if ($months < 1)
|
||||
$months = 1;
|
||||
|
||||
$vm = ModuleVmware::where('vmware_id', $vmware_id)
|
||||
->where('customer_id', $user->id)
|
||||
->first();
|
||||
|
||||
if (!$vm) {
|
||||
return response()->json(['error' => true, 'message' => __('Không tìm thấy dịch vụ.')], 404);
|
||||
}
|
||||
|
||||
$order = Order::find($vm->order_id);
|
||||
if (!$order) {
|
||||
return response()->json(['error' => true, 'message' => __('Không tìm thấy đơn hàng gốc.')], 404);
|
||||
}
|
||||
|
||||
// Find OrderItem in order__order_item
|
||||
// Rule: product_type = 4 (VPS) or 3 (Hosting)
|
||||
// Ideally we match the product_id too, but ModuleVmware may not store original product_id directly except via relation?
|
||||
// ModuleVmware has no 'product_id' column in the schema we saw, but it has 'order_id'.
|
||||
// Let's assume we look for the item that created this VM.
|
||||
// Since we are in ServiceController for VM, we look for Type 4 (VPS).
|
||||
|
||||
$orderItem = \App\Models\OrderItem::where('order_id', $order->id)
|
||||
->where('product_type', 4) // VPS
|
||||
->first();
|
||||
|
||||
if (!$orderItem) {
|
||||
// Fallback or error
|
||||
return response()->json(['error' => true, 'message' => __('Không tìm thấy thông tin sản phẩm trong đơn hàng.')], 404);
|
||||
}
|
||||
|
||||
// Get Product Price
|
||||
$product = null;
|
||||
if ($orderItem->product_type == 4) {
|
||||
$product = \App\Models\VPS::find($orderItem->product_id);
|
||||
} elseif ($orderItem->product_type == 3) {
|
||||
$product = \App\Models\Hosting::find($orderItem->product_id);
|
||||
}
|
||||
|
||||
if (!$product) {
|
||||
return response()->json(['error' => true, 'message' => __('Sản phẩm không còn tồn tại.')], 404);
|
||||
}
|
||||
|
||||
// Priority: Price Sale -> Price
|
||||
$base_unit_price = (float) ($product->price_sale > 0 ? $product->price_sale : $product->price);
|
||||
|
||||
// Calculate Addons Price
|
||||
// Look into order__order_addons via OrderItem relationship
|
||||
$addons_unit_price = 0;
|
||||
if ($orderItem->addons && $orderItem->addons->count() > 0) {
|
||||
foreach ($orderItem->addons as $addonPivot) {
|
||||
// $addonPivot is OrderAddon model
|
||||
// We need current price of the addon? User said "lấy giá đã lưu" (take stored price)
|
||||
// BUT for renewal, usually we take CURRENT price of the addon or the STORED price?
|
||||
// User said: "lấy sản order hiện tại kèm các addon nếu có lấy giá đã lưu * với số tháng"
|
||||
// "giá đã lưu" implies the price stored in the order/product table?
|
||||
// But then user said: "nếu product có price_sale thì ưu tiên lấy price_sale còn lại thì lấy price".
|
||||
// This implies looking at the ACTUAL Product table for the base Item.
|
||||
|
||||
// For Addons: "kèm các addon nếu có".
|
||||
// Let's look at the Addon model (Current Price).
|
||||
if ($addonPivot->addonItems) {
|
||||
$addons_unit_price += (float) ($addonPivot->addonItems->price);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$total_unit_price = $base_unit_price + $addons_unit_price;
|
||||
$total = $total_unit_price * $months;
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => [
|
||||
'unit_price' => $total_unit_price,
|
||||
'months' => $months,
|
||||
'total' => $total
|
||||
]
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Error calculating renew price: " . $e->getMessage());
|
||||
return response()->json(['error' => true, 'message' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renew VPS Service
|
||||
*/
|
||||
public function renew(Request $request)
|
||||
{
|
||||
try {
|
||||
$user = customer();
|
||||
if (!$user) {
|
||||
return redirect()->route('user.login')->with('error', __('Vui lòng đăng nhập.'));
|
||||
}
|
||||
|
||||
// 1. Validate Input
|
||||
$validator = FacadesValidator::make($request->all(), [
|
||||
'product_id' => 'required|string',
|
||||
'plan' => 'required',
|
||||
'months' => 'nullable|integer|min:2|max:60'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return redirect()->back()->withErrors($validator)->withInput()->with('error', __('Dữ liệu không hợp lệ.'));
|
||||
}
|
||||
|
||||
$vmware_id = $this->sanitizeInput($request->product_id);
|
||||
$plan = $request->input('plan');
|
||||
$months = $plan === 'other' ? (int) $request->input('months') : (int) $plan;
|
||||
|
||||
if ($months < 2) {
|
||||
return redirect()->back()->with('error', __('Thời hạn gia hạn tối thiểu là 2 tháng.'));
|
||||
}
|
||||
|
||||
// 2. Find Service & Verify Ownership
|
||||
$vm = ModuleVmware::where('vmware_id', $vmware_id)
|
||||
->where('customer_id', $user->id)
|
||||
->first();
|
||||
|
||||
if (!$vm) {
|
||||
return redirect()->back()->with('error', __('Không tìm thấy dịch vụ hoặc bạn không có quyền truy cập.'));
|
||||
}
|
||||
|
||||
// 3. Calculate Price based on Original Order -> OrderItem
|
||||
$order = Order::find($vm->order_id);
|
||||
if (!$order) {
|
||||
return redirect()->back()->with('error', __('Không tìm thấy đơn hàng gốc của dịch vụ này.'));
|
||||
}
|
||||
|
||||
$orderItem = \App\Models\OrderItem::where('order_id', $order->id)
|
||||
->where('product_type', 4) // VPS
|
||||
->first();
|
||||
|
||||
if (!$orderItem) {
|
||||
return redirect()->back()->with('error', __('Không tìm thấy thông tin sản phẩm trong đơn hàng.'));
|
||||
}
|
||||
|
||||
// Get Product Price
|
||||
$product = null;
|
||||
if ($orderItem->product_type == 4) {
|
||||
$product = \App\Models\VPS::find($orderItem->product_id);
|
||||
} elseif ($orderItem->product_type == 3) {
|
||||
$product = \App\Models\Hosting::find($orderItem->product_id);
|
||||
}
|
||||
|
||||
if (!$product) {
|
||||
return redirect()->back()->with('error', __('Sản phẩm không còn tồn tại.'));
|
||||
}
|
||||
|
||||
// Priority: Price Sale -> Price
|
||||
$base_unit_price = (float) ($product->price_sale > 0 ? $product->price_sale : $product->price);
|
||||
|
||||
// Calculate Addons Price
|
||||
$addons_unit_price = 0;
|
||||
if ($orderItem->addons && $orderItem->addons->count() > 0) {
|
||||
foreach ($orderItem->addons as $addonPivot) {
|
||||
if ($addonPivot->addonItems) {
|
||||
$addons_unit_price += (float) ($addonPivot->addonItems->price);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$total_unit_price = $base_unit_price + $addons_unit_price;
|
||||
$total_cost = $total_unit_price * $months;
|
||||
|
||||
// 4. Check Balance
|
||||
if ($user->credit < $total_cost) {
|
||||
return redirect()->back()->with('error', __('Số dư tài khoản không đủ để gia hạn (Cần: :amount). Vui lòng nạp thêm tiền.', ['amount' => number_format($total_cost) . ' VNĐ']));
|
||||
}
|
||||
|
||||
// 5. Process Renewal
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
// Deduct Credit
|
||||
$user->decrement('credit', $total_cost);
|
||||
|
||||
// Create Renewal Invoice (Paid)
|
||||
$newInvoice = Invoice::create([
|
||||
'customer_id' => $user->id,
|
||||
'status' => 1, // Paid
|
||||
'date' => now()->timestamp, // Using timestamp as per schema
|
||||
'due_date' => now()->timestamp,
|
||||
'total' => $total_cost,
|
||||
// 'payment_method' => 'credit_balance', // Schema doesn't have this, maybe note?
|
||||
'note' => "Gia hạn dịch vụ VPS #{$vm->name} ({$months} tháng)"
|
||||
]);
|
||||
|
||||
// Update VM Expiry
|
||||
$current_due = $vm->next_due_date ? \Carbon\Carbon::parse($vm->next_due_date) : now();
|
||||
$new_due_date = $current_due->copy()->addMonths($months);
|
||||
|
||||
$vm->update([
|
||||
'next_due_date' => $new_due_date,
|
||||
'status' => 1,
|
||||
]);
|
||||
|
||||
// TODO: Update OrderItem duration/dates if needed? Or just create new invoice record?
|
||||
// The prompt was about "gia hạn thêm sản phẩm" (add renewal product?), but generally it just extends the service.
|
||||
|
||||
DB::commit();
|
||||
|
||||
// Clear cache
|
||||
Cache::forget("vm_info_{$vm->vmware_id}");
|
||||
|
||||
return redirect()->back()->with('success', __('Gia hạn dịch vụ thành công! Hạn sử dụng mới: :date', ['date' => $new_due_date->format('d/m/Y')]));
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Error renewing VM: " . $e->getMessage());
|
||||
return redirect()->back()->with('error', __('Có lỗi xảy ra khi gia hạn dịch vụ. Vui lòng thử lại sau.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
|
||||
/**
|
||||
* Renew VPS Service
|
||||
*/
|
||||
public function renew(Request $request)
|
||||
{
|
||||
try {
|
||||
$user = customer();
|
||||
if (!$user) {
|
||||
return redirect()->route('user.login')->with('error', __('Vui lòng đăng nhập.'));
|
||||
}
|
||||
|
||||
// 1. Validate Input
|
||||
$validator = FacadesValidator::make($request->all(), [
|
||||
'product_id' => 'required|string',
|
||||
'plan' => 'required',
|
||||
'months' => 'nullable|integer|min:2|max:60'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return redirect()->back()->withErrors($validator)->withInput()->with('error', __('Dữ liệu không hợp lệ.'));
|
||||
}
|
||||
|
||||
$vmware_id = $this->sanitizeInput($request->product_id);
|
||||
$plan = $request->input('plan');
|
||||
$months = $plan === 'other' ? (int)$request->input('months') : (int)$plan;
|
||||
|
||||
if ($months < 2) {
|
||||
return redirect()->back()->with('error', __('Thời hạn gia hạn tối thiểu là 2 tháng.'));
|
||||
}
|
||||
|
||||
// 2. Find Service & Verify Ownership
|
||||
$vm = ModuleVmware::where('vmware_id', $vmware_id)
|
||||
->where('customer_id', $user->id)
|
||||
->first();
|
||||
|
||||
if (!$vm) {
|
||||
return redirect()->back()->with('error', __('Không tìm thấy dịch vụ hoặc bạn không có quyền truy cập.'));
|
||||
}
|
||||
|
||||
// 3. Calculate Price based on Original Order/InvoiceItem
|
||||
// Strategy: Find the InvoiceItem from the Order associated with this VM.
|
||||
// If multiple items, try to match type VPS (4).
|
||||
|
||||
$order = Order::find($vm->order_id);
|
||||
if (!$order) {
|
||||
// Fallback if order missing (rare)
|
||||
// Try finding by invoice_id if stored on VM? ModuleVmware has next_invoice but not current_invoice.
|
||||
return redirect()->back()->with('error', __('Không tìm thấy đơn hàng gốc của dịch vụ này.'));
|
||||
}
|
||||
|
||||
// Find InvoiceItem
|
||||
// Order has relationship to Invoice (order->invoice)
|
||||
// Or Order has orderItems (which are InvoiceItems)
|
||||
$invoiceItem = null;
|
||||
if ($order->invoice) {
|
||||
// Try finding item in the invoice related to this order
|
||||
// Note: The modal uses $invoice->invoiceItem->first(). We should try to be specific.
|
||||
// We don't have a direct link from ModuleVmware to InvoiceItem.
|
||||
// But ModuleVmware links to Order, which links to items.
|
||||
// Let's take the first item of type 4 (VPS) from the order's invoice.
|
||||
$invoiceItem = $order->invoice->invoiceItem->where('type_id', 4)->first();
|
||||
if (!$invoiceItem) {
|
||||
$invoiceItem = $order->invoice->invoiceItem->first();
|
||||
}
|
||||
}
|
||||
|
||||
if (!$invoiceItem) {
|
||||
// Extreme fallback or failure
|
||||
return redirect()->back()->with('error', __('Không tìm thấy thông tin giá của dịch vụ.'));
|
||||
}
|
||||
|
||||
// Calculate Unit Price (Monthly)
|
||||
// 'amount' in InvoiceItem is total price for 'months' duration.
|
||||
$billing_months = $invoiceItem->months > 0 ? $invoiceItem->months : 1;
|
||||
if ($invoiceItem->amount <= 0) {
|
||||
// If amount is 0 (free?), unit price is 0.
|
||||
$base_unit_price = 0;
|
||||
} else {
|
||||
$base_unit_price = $invoiceItem->amount / $billing_months;
|
||||
}
|
||||
|
||||
// Calculate Addons Unit Price
|
||||
$addons_unit_price = 0;
|
||||
if ($invoiceItem->addons && $invoiceItem->addons->count() > 0) {
|
||||
foreach ($invoiceItem->addons as $addonPivot) {
|
||||
// Try to find price. Assuming Addon model has current price.
|
||||
// Ideally we should use the price stored in pivot if available, but pivot structure is unknown.
|
||||
// We'll use the Addon catalog price as fallback or primary if pivot doesn't store it.
|
||||
// $addonPivot->addonItems returns the Addon model.
|
||||
if ($addonPivot->addonItems) {
|
||||
$addons_unit_price += $addonPivot->addonItems->price;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$total_unit_price = $base_unit_price + $addons_unit_price;
|
||||
$total_cost = $total_unit_price * $months;
|
||||
|
||||
// 4. Check Balance
|
||||
if ($user->credit < $total_cost) {
|
||||
return redirect()->back()->with('error', __('Số dư tài khoản không đủ để gia hạn (Cần: :amount). Vui lòng nạp thêm tiền.', ['amount' => number_format($total_cost) . ' VNĐ']));
|
||||
}
|
||||
|
||||
// 5. Process Renewal
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
// Deduct Credit
|
||||
$user->decrement('credit', $total_cost);
|
||||
|
||||
// Create Renewal Invoice (Paid) - Optional but good for history
|
||||
$newInvoice = Invoice::create([
|
||||
'customer_id' => $user->id,
|
||||
'status' => 1, // Paid
|
||||
'date' => now(),
|
||||
'due_date' => now(),
|
||||
'total' => $total_cost,
|
||||
'payment_method' => 'credit_balance', // or similar
|
||||
'description' => "Gia hạn dịch vụ VPS #{$vm->name} ({$months} tháng)"
|
||||
]);
|
||||
|
||||
// Update VM Expiry
|
||||
// If current expiry is in the past, renew from NOW? Or from expiry?
|
||||
// Usually from expiry if it's recent grace period. If long expired, maybe from now.
|
||||
// Let's simply add to next_due_date.
|
||||
|
||||
$current_due = $vm->next_due_date ? \Carbon\Carbon::parse($vm->next_due_date) : now();
|
||||
if ($current_due->isPast()) {
|
||||
// If expired, maybe we should start from today?
|
||||
// Or just extend the gap. User usually pays for the gap.
|
||||
// Let's extend from current_due to maintain billing cycle.
|
||||
}
|
||||
$new_due_date = $current_due->copy()->addMonths($months);
|
||||
|
||||
$vm->update([
|
||||
'next_due_date' => $new_due_date,
|
||||
'status' => 1, // Ensure active if it was suspended
|
||||
]);
|
||||
|
||||
// Log or Notify (Optional)
|
||||
|
||||
DB::commit();
|
||||
|
||||
// Clear cache
|
||||
Cache::forget("vm_info_{$vm->vmware_id}");
|
||||
|
||||
return redirect()->back()->with('success', __('Gia hạn dịch vụ thành công! Hạn sử dụng mới: :date', ['date' => $new_due_date->format('d/m/Y')]));
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Error renewing VM: " . $e->getMessage());
|
||||
return redirect()->back()->with('error', __('Có lỗi xảy ra khi gia hạn dịch vụ. Vui lòng thử lại sau.'));
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class WaitStatusVirtualMachine implements ShouldQueue
|
||||
{
|
||||
@@ -39,18 +40,75 @@ class WaitStatusVirtualMachine implements ShouldQueue
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
Log::info("WaitStatusVirtualMachine: Started for VM {$this->vmware_id}, Target Status: " . ($this->status ? 'True' : 'False'));
|
||||
|
||||
$status = $this->serviceVCetner->CheckVirtualMachine($this->vmware_id);
|
||||
|
||||
if ($status['error'] === true) {
|
||||
Log::error("WaitStatusVirtualMachine: Error checking status for VM {$this->vmware_id}: " . json_encode($status['message']));
|
||||
throw new \Exception(json_encode($status['message']));
|
||||
}
|
||||
|
||||
Log::info("WaitStatusVirtualMachine: Initial operations_ready: " . ($status['data']['operations_ready'] ? 'True' : 'False'));
|
||||
|
||||
// Wait loop: "Wait while operations_ready matches $this->status"
|
||||
// Example: Stop Job passes true. We wait while it is true. When it becomes false (Off), we exit.
|
||||
if ($status['data']['operations_ready'] === $this->status) {
|
||||
Log::info("WaitStatusVirtualMachine: Entering wait loop...");
|
||||
do {
|
||||
sleep(10);
|
||||
$status = $this->serviceVCetner->CheckVirtualMachine($this->vmware_id);
|
||||
|
||||
if ($status['error'] === true) {
|
||||
Log::warning("WaitStatusVirtualMachine: Failed to check status in loop. Breaking.");
|
||||
// If check fails, maybe network glitch? We can break or continue.
|
||||
// Let's break to avoid infinite loop on error
|
||||
break;
|
||||
}
|
||||
|
||||
Log::info("WaitStatusVirtualMachine: Loop check - operations_ready: " . ($status['data']['operations_ready'] ? 'True' : 'False'));
|
||||
|
||||
} while ($status['data']['operations_ready'] === $this->status);
|
||||
} else {
|
||||
Log::info("WaitStatusVirtualMachine: Skipping wait loop, condition already met.");
|
||||
}
|
||||
|
||||
Event(new VirtualMachine_Panel_Status('Yêu cầu được xử lý thành công',$this->user));
|
||||
// Action Completed
|
||||
// 1. Clear Cache again (to ensure next fetch gets new state)
|
||||
\Illuminate\Support\Facades\Cache::forget('vm_info_' . $this->vmware_id);
|
||||
|
||||
// 2. Update DB Status and Dispatch Event
|
||||
$product = \App\Models\ModuleVmware::where('vmware_id', $this->vmware_id)->first();
|
||||
|
||||
if ($product) {
|
||||
Log::info("WaitStatusVirtualMachine: Product found. Updating status...");
|
||||
// If waiting for True (On) to become False (Off) -> New Status 0 (Off)
|
||||
// If waiting for False (Off) to become True (On) -> New Status 1 (Active)
|
||||
$newStatus = ($this->status === true) ? 0 : 1;
|
||||
|
||||
Log::info("WaitStatusVirtualMachine: Setting status to $newStatus");
|
||||
|
||||
$product->update(['status' => $newStatus]);
|
||||
|
||||
$msg = ($newStatus == 1)
|
||||
? __('Máy ảo đã hoạt động trở lại.')
|
||||
: __('Máy ảo đã tắt hoàn toàn.');
|
||||
|
||||
$statusStr = ($newStatus == 1) ? 'running' : 'stopped';
|
||||
|
||||
Log::info("WaitStatusVirtualMachine: Dispatching VirtualMachineStatusChanged event.");
|
||||
|
||||
\App\Events\VirtualMachineStatusChanged::dispatch(
|
||||
$this->user,
|
||||
$this->vmware_id,
|
||||
$statusStr,
|
||||
$msg
|
||||
);
|
||||
} else {
|
||||
Log::error("WaitStatusVirtualMachine: Product not found for vmware_id: {$this->vmware_id}");
|
||||
}
|
||||
|
||||
// Legacy event (optional, keep to avoid breaking other things if they listen to it)
|
||||
Event(new VirtualMachine_Panel_Status('Yêu cầu được xử lý thành công', $this->user));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,15 @@ class RebootVmJob implements ShouldQueue
|
||||
// Clear VM info cache
|
||||
\Illuminate\Support\Facades\Cache::forget('vm_info_' . $product->vmware_id);
|
||||
|
||||
$product->update(['status' => 3]); // Processing
|
||||
// Dispatch Event to Frontend
|
||||
\App\Events\VirtualMachineStatusChanged::dispatch(
|
||||
$this->userId,
|
||||
$product->vmware_id,
|
||||
'rebooted',
|
||||
__('Máy ảo đã được khởi động lại thành công.')
|
||||
);
|
||||
|
||||
$product->update(['status' => 3]); // Processing
|
||||
|
||||
// In VMJobs: $jobs = new WaitStatusVirtualMachine($product->vmware_id,$serviceVCenter,$product->customer_id,false);
|
||||
|
||||
|
||||
@@ -60,8 +60,16 @@ class StartVmJob implements ShouldQueue
|
||||
|
||||
// Clear VM info cache
|
||||
\Illuminate\Support\Facades\Cache::forget('vm_info_' . $product->vmware_id);
|
||||
|
||||
// Update status to 'processing' (e.g., 3) if needed, or leave it to WaitStatus
|
||||
|
||||
// Dispatch Event to Frontend
|
||||
\App\Events\VirtualMachineStatusChanged::dispatch(
|
||||
$this->userId,
|
||||
$product->vmware_id,
|
||||
'running',
|
||||
__('Máy ảo đã được khởi động thành công.')
|
||||
);
|
||||
|
||||
// Update status to 'processing' (e.g., 3) if needed, or leave it to WaitStatus
|
||||
$product->update(['status' => 3]); // 3 = Processing/Pending
|
||||
|
||||
// Dispatch wait status job
|
||||
|
||||
@@ -59,7 +59,15 @@ class StopVmJob implements ShouldQueue
|
||||
// Clear VM info cache
|
||||
\Illuminate\Support\Facades\Cache::forget('vm_info_' . $product->vmware_id);
|
||||
|
||||
$product->update(['status' => 3]); // Processing
|
||||
// Dispatch Event to Frontend
|
||||
\App\Events\VirtualMachineStatusChanged::dispatch(
|
||||
$this->userId,
|
||||
$product->vmware_id,
|
||||
'stopped',
|
||||
__('Máy ảo đã được tắt thành công.')
|
||||
);
|
||||
|
||||
$product->update(['status' => 3]); // Processing
|
||||
|
||||
dispatch(new WaitStatusVirtualMachine(
|
||||
$product->vmware_id,
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// 1. Modify columns to match referenced types (unsignedBigInteger) and make them nullable
|
||||
Schema::table('order', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('customer_id')->nullable()->default(null)->change();
|
||||
$table->unsignedBigInteger('user_id')->nullable()->default(null)->change();
|
||||
$table->unsignedBigInteger('discount_id')->nullable()->default(null)->change();
|
||||
$table->unsignedBigInteger('invoice_id')->nullable()->default(null)->change();
|
||||
});
|
||||
|
||||
// 2. Convert default 0 values to null to ensure foreign key integrity
|
||||
DB::table('order')->where('customer_id', 0)->update(['customer_id' => null]);
|
||||
DB::table('order')->where('user_id', 0)->update(['user_id' => null]);
|
||||
DB::table('order')->where('discount_id', 0)->update(['discount_id' => null]);
|
||||
DB::table('order')->where('invoice_id', 0)->update(['invoice_id' => null]);
|
||||
|
||||
// 3. Add foreign key constraints
|
||||
Schema::table('order', function (Blueprint $table) {
|
||||
$table->foreign('customer_id')->references('id')->on('customer')->onDelete('set null');
|
||||
$table->foreign('user_id')->references('id')->on('user')->onDelete('set null');
|
||||
$table->foreign('discount_id')->references('id')->on('discount')->onDelete('set null');
|
||||
$table->foreign('invoice_id')->references('id')->on('invoice')->onDelete('set null');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('order', function (Blueprint $table) {
|
||||
// Drop foreign keys
|
||||
$table->dropForeign(['customer_id']);
|
||||
$table->dropForeign(['user_id']);
|
||||
$table->dropForeign(['discount_id']);
|
||||
$table->dropForeign(['invoice_id']);
|
||||
|
||||
// Revert columns to previous state (integers/bigInteger with default 0)
|
||||
// Note: This matches the original migration definitions
|
||||
$table->bigInteger('customer_id')->default(0)->nullable(false)->change();
|
||||
$table->integer('user_id')->default(0)->nullable(false)->change();
|
||||
$table->integer('discount_id')->default(0)->nullable(false)->change();
|
||||
$table->integer('invoice_id')->default(0)->nullable(false)->change();
|
||||
});
|
||||
|
||||
// Restore 0 values where null (optional, but keeps consistency with down())
|
||||
DB::table('order')->whereNull('customer_id')->update(['customer_id' => 0]);
|
||||
DB::table('order')->whereNull('user_id')->update(['user_id' => 0]);
|
||||
DB::table('order')->whereNull('discount_id')->update(['discount_id' => 0]);
|
||||
DB::table('order')->whereNull('invoice_id')->update(['invoice_id' => 0]);
|
||||
}
|
||||
};
|
||||
+26
-12
@@ -1,5 +1,5 @@
|
||||
@import url(https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Lexend:wght@100..900&display=swap);
|
||||
/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */
|
||||
/*! tailwindcss v4.1.4 | MIT License | https://tailwindcss.com */
|
||||
@layer properties;
|
||||
@layer theme, base, components, utilities;
|
||||
@layer theme {
|
||||
@@ -435,9 +435,6 @@
|
||||
::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
|
||||
padding-block: 0;
|
||||
}
|
||||
::-webkit-calendar-picker-indicator {
|
||||
line-height: 1;
|
||||
}
|
||||
:-moz-ui-invalid {
|
||||
box-shadow: none;
|
||||
}
|
||||
@@ -479,7 +476,7 @@
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
@@ -1723,19 +1720,19 @@
|
||||
border-collapse: separate;
|
||||
}
|
||||
.origin-bottom-left {
|
||||
transform-origin: 0 100%;
|
||||
transform-origin: bottom left;
|
||||
}
|
||||
.origin-bottom-right {
|
||||
transform-origin: 100% 100%;
|
||||
transform-origin: bottom right;
|
||||
}
|
||||
.origin-left {
|
||||
transform-origin: 0;
|
||||
transform-origin: left;
|
||||
}
|
||||
.origin-top-left {
|
||||
transform-origin: 0 0;
|
||||
transform-origin: top left;
|
||||
}
|
||||
.origin-top-right {
|
||||
transform-origin: 100% 0;
|
||||
transform-origin: top right;
|
||||
}
|
||||
.-translate-x-1\/2 {
|
||||
--tw-translate-x: calc(calc(1/2 * 100%) * -1);
|
||||
@@ -3489,6 +3486,12 @@
|
||||
background-color: color-mix(in oklab, var(--color-white) 20%, transparent);
|
||||
}
|
||||
}
|
||||
.bg-white\/50 {
|
||||
background-color: color-mix(in srgb, #fff 50%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
background-color: color-mix(in oklab, var(--color-white) 50%, transparent);
|
||||
}
|
||||
}
|
||||
.bg-white\/80 {
|
||||
background-color: color-mix(in srgb, #fff 80%, transparent);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
@@ -4810,43 +4813,51 @@
|
||||
}
|
||||
.backdrop-blur-2xl {
|
||||
--tw-backdrop-blur: blur(var(--blur-2xl));
|
||||
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||
}
|
||||
.backdrop-blur-3xl {
|
||||
--tw-backdrop-blur: blur(var(--blur-3xl));
|
||||
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||
}
|
||||
.backdrop-blur-lg {
|
||||
--tw-backdrop-blur: blur(var(--blur-lg));
|
||||
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||
}
|
||||
.backdrop-blur-md {
|
||||
--tw-backdrop-blur: blur(var(--blur-md));
|
||||
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||
}
|
||||
.backdrop-blur-none {
|
||||
--tw-backdrop-blur: ;
|
||||
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||
}
|
||||
.backdrop-blur-sm {
|
||||
--tw-backdrop-blur: blur(var(--blur-sm));
|
||||
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||
}
|
||||
.backdrop-blur-xl {
|
||||
--tw-backdrop-blur: blur(var(--blur-xl));
|
||||
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||
}
|
||||
.backdrop-blur-xs {
|
||||
--tw-backdrop-blur: blur(var(--blur-xs));
|
||||
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||
}
|
||||
.transition {
|
||||
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, backdrop-filter, display, content-visibility, overlay, pointer-events;
|
||||
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter;
|
||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||
}
|
||||
.transition\! {
|
||||
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, backdrop-filter, display, content-visibility, overlay, pointer-events !important;
|
||||
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter !important;
|
||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)) !important;
|
||||
transition-duration: var(--tw-duration, var(--default-transition-duration)) !important;
|
||||
}
|
||||
@@ -5919,12 +5930,14 @@
|
||||
}
|
||||
.before\:content-\[\'\'\] {
|
||||
&::before {
|
||||
content: var(--tw-content);
|
||||
--tw-content: '';
|
||||
content: var(--tw-content);
|
||||
}
|
||||
}
|
||||
.before\:content-\[\\\'\\\'\] {
|
||||
&::before {
|
||||
content: var(--tw-content);
|
||||
--tw-content: \'\';
|
||||
content: var(--tw-content);
|
||||
}
|
||||
@@ -6082,6 +6095,7 @@
|
||||
}
|
||||
.after\:content-\[\'\'\] {
|
||||
&::after {
|
||||
content: var(--tw-content);
|
||||
--tw-content: '';
|
||||
content: var(--tw-content);
|
||||
}
|
||||
|
||||
+74
-46
File diff suppressed because one or more lines are too long
Vendored
+4
-4
File diff suppressed because one or more lines are too long
Vendored
+4
-4
File diff suppressed because one or more lines are too long
Vendored
+4
-4
File diff suppressed because one or more lines are too long
Vendored
+4
-4
File diff suppressed because one or more lines are too long
Vendored
+4
-4
File diff suppressed because one or more lines are too long
@@ -5,8 +5,5 @@
|
||||
"/js/core.min.js": "/js/core.min.js",
|
||||
"/js/main.min.js": "/js/main.min.js",
|
||||
"/js/service/vminfo.min.js": "/js/service/vminfo.min.js",
|
||||
"/css/app.css": "/css/app.css",
|
||||
"/fonts/AvertaPE-Regular.woff2?d2975139654f9cae8bac2e858686ffa8": "/fonts/AvertaPE-Regular.woff2?d2975139654f9cae8bac2e858686ffa8",
|
||||
"/fonts/AvertaPE-Bold.woff2?0c3fa07354777de57ba64f1838b591dc": "/fonts/AvertaPE-Bold.woff2?0c3fa07354777de57ba64f1838b591dc",
|
||||
"/fonts/AvertaPE-Extrabold.woff2?45d04f8ca8d830e0dd8e1c11b97e3bd4": "/fonts/AvertaPE-Extrabold.woff2?45d04f8ca8d830e0dd8e1c11b97e3bd4"
|
||||
"/css/app.css": "/css/app.css"
|
||||
}
|
||||
|
||||
Vendored
+10
-8
@@ -16,16 +16,18 @@ window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
* allows your team to easily build robust real-time web applications.
|
||||
*/
|
||||
|
||||
// import Echo from 'laravel-echo';
|
||||
|
||||
// window.Pusher = require('pusher-js');
|
||||
import Echo from "laravel-echo";
|
||||
|
||||
// window.Echo = new Echo({
|
||||
// broadcaster: 'pusher',
|
||||
// key: process.env.MIX_PUSHER_APP_KEY,
|
||||
// cluster: process.env.MIX_PUSHER_APP_CLUSTER,
|
||||
// forceTLS: true
|
||||
// });
|
||||
window.Pusher = require("pusher-js");
|
||||
|
||||
window.Echo = new Echo({
|
||||
broadcaster: "pusher",
|
||||
key: process.env.MIX_PUSHER_APP_KEY,
|
||||
cluster: process.env.MIX_PUSHER_APP_CLUSTER,
|
||||
forceTLS: true,
|
||||
encrypted: true,
|
||||
});
|
||||
|
||||
// import Echo from 'laravel-echo'
|
||||
// import socketio from 'socket.io-client'
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
@if(customer())
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 1. Check & Request Notification Permission
|
||||
function checkNotificationPermission() {
|
||||
if (!("Notification" in window)) {
|
||||
console.warn("This browser does not support desktop notification");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Notification.permission === "granted") {
|
||||
// Already granted
|
||||
} else if (Notification.permission !== "denied") {
|
||||
Notification.requestPermission().then(function (permission) {
|
||||
if (permission === "granted") {
|
||||
console.log("Notification permission granted!");
|
||||
new Notification("Thông báo hệ thống", {
|
||||
body: "Bạn sẽ nhận được thông báo khi các tác vụ hoàn tất.",
|
||||
icon: "{{ asset('images/favicon.png') }}"
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Call check on load
|
||||
checkNotificationPermission();
|
||||
|
||||
// 2. Listen for Private Channel Events
|
||||
if (window.Echo) {
|
||||
const userId = {{ customer()->id }};
|
||||
window.Echo.private('user.' + userId)
|
||||
.listen('.vm.status.changed', (e) => {
|
||||
console.log("VM Status Changed:", e);
|
||||
|
||||
// Show Browser Notification
|
||||
if (Notification.permission === "granted") {
|
||||
const notification = new Notification("Thông báo từ Portal", {
|
||||
body: e.message,
|
||||
icon: "{{ asset('images/favicon.png') }}", // Replace with actual logo path
|
||||
tag: 'vm-status-' + e.vmId
|
||||
});
|
||||
|
||||
notification.onclick = function() {
|
||||
window.focus();
|
||||
// Optional: navigating to VM detail page
|
||||
// window.location.href = '/service/' + e.vmId;
|
||||
this.close();
|
||||
};
|
||||
}
|
||||
|
||||
// Show In-App Toast (using FuiToast based on your script.blade.php)
|
||||
if (typeof FuiToast !== 'undefined') {
|
||||
FuiToast.success(e.message);
|
||||
} else if (typeof toastr !== 'undefined') {
|
||||
toastr.success(e.message);
|
||||
}
|
||||
|
||||
// Optional: Reload Livewire components if needed
|
||||
if (window.Livewire) {
|
||||
// Livewire.dispatch('refreshComponent');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endif
|
||||
@@ -14,3 +14,5 @@
|
||||
@stack('viewScripts')
|
||||
<script src="{{ asset('js/main.min.js') }}"></script>
|
||||
@stack('consoleScripts')
|
||||
@include('frontend.layouts.partials.notification-script')
|
||||
|
||||
|
||||
@@ -1,26 +1,107 @@
|
||||
@props(['invoice'])
|
||||
|
||||
@php
|
||||
$initialPrice = (int) ($invoice->invoiceItem->first()?->getProductAttribute()?->price_sale ?? $invoice->invoiceItem->first()?->getProductAttribute()?->price ?? 0);
|
||||
@endphp
|
||||
|
||||
<flux:modal name="renewal-modal" class="md:w-[28rem]">
|
||||
<div class="space-y-6" x-data="{ submitting: false, plan: '12', months: 12, unitPrice: 0, total: 0 }" x-init="
|
||||
(() => {
|
||||
const p = {{ (int) ($invoice->invoiceItem->first()?->getProductAttribute()?->price_sale ?? $invoice->invoiceItem->first()?->getProductAttribute()?->price ?? 0) }};
|
||||
unitPrice = p; total = p * months;
|
||||
$watch('plan', value => { months = (value === 'other') ? months : parseInt(value); total = unitPrice * months; });
|
||||
$watch('months', value => { if (plan === 'other') { total = unitPrice * (parseInt(value) || 0); } });
|
||||
})()
|
||||
">
|
||||
<div class="space-y-6" x-data="{
|
||||
submitting: false,
|
||||
plan: '12',
|
||||
months: 12,
|
||||
unitPrice: {{ $initialPrice }},
|
||||
total: {{ $initialPrice * 12 }},
|
||||
loading: false,
|
||||
|
||||
updateTotal() {
|
||||
// Immediate client-side update for responsiveness
|
||||
let m = (this.plan === 'other') ? (parseInt(this.months) || 0) : parseInt(this.plan);
|
||||
if (m > 0) {
|
||||
this.total = this.unitPrice * m;
|
||||
}
|
||||
},
|
||||
|
||||
async fetchPrice() {
|
||||
this.loading = true;
|
||||
try {
|
||||
// Determine months based on plan
|
||||
let m = (this.plan === 'other') ? (parseInt(this.months) || 1) : parseInt(this.plan);
|
||||
if (m < 2 && this.plan === 'other') m = 2;
|
||||
if (m < 1 && this.plan !== 'other') m = parseInt(this.plan);
|
||||
|
||||
// Optimistic UI update
|
||||
if (this.plan !== 'other') this.months = m;
|
||||
this.updateTotal();
|
||||
|
||||
const response = await fetch('{{ route('service.renew.price') }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
product_id: '{{ $invoice->vm_id }}',
|
||||
plan: this.plan,
|
||||
months: m
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.error) {
|
||||
this.unitPrice = data.data.unit_price;
|
||||
this.total = data.data.total;
|
||||
// Ensure months sync
|
||||
if (this.plan !== 'other') {
|
||||
this.months = m;
|
||||
}
|
||||
} else {
|
||||
console.error('Renew Price Error:', data.message);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Fetch Error:', e);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
init() {
|
||||
// Fetch fresh price on load to be sure
|
||||
this.fetchPrice();
|
||||
|
||||
this.$watch('plan', (value) => {
|
||||
console.log('Plan changed:', value);
|
||||
this.fetchPrice();
|
||||
});
|
||||
|
||||
this.$watch('months', (value) => {
|
||||
if (this.plan === 'other') {
|
||||
if (value >= 2) {
|
||||
// Debounce slightly or just fetch
|
||||
this.fetchPrice();
|
||||
} else {
|
||||
this.updateTotal(); // Just update math locally
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('Gia hạn dịch vụ') }}</flux:heading>
|
||||
<flux:text class="mt-2">{{ __('Chọn thời hạn gia hạn hoặc nhập số tháng mong muốn.') }}</flux:text>
|
||||
<flux:text class="mt-2">{{ __('Chọn thời hạn gia hạn hoặc nhập số tháng mong muốn (Tối thiểu 2 tháng).') }}
|
||||
</flux:text>
|
||||
</div>
|
||||
|
||||
<form x-on:submit.prevent="if (submitting) return; submitting = true; $el.submit()" method="POST" action="{{ route('service.renew') }}">
|
||||
|
||||
<form x-on:submit.prevent="if (submitting) return; submitting = true; $el.submit()" method="POST"
|
||||
action="{{ route('service.renew') }}">
|
||||
@csrf
|
||||
<input type="hidden" name="product_id" value="{{ $invoice->vm_id }}">
|
||||
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Chọn thời hạn') }}</flux:label>
|
||||
<flux:select x-model="plan" name="plan">
|
||||
<flux:select x-model="plan" name="plan" x-on:change="fetchPrice()">
|
||||
{{-- <flux:select.option value="1">{{ __('1 tháng') }}</flux:select.option> --}}
|
||||
<!-- Min 2 months required -->
|
||||
<flux:select.option value="3">{{ __('3 tháng') }}</flux:select.option>
|
||||
<flux:select.option value="6">{{ __('6 tháng') }}</flux:select.option>
|
||||
<flux:select.option value="12">{{ __('12 tháng') }}</flux:select.option>
|
||||
@@ -28,25 +109,46 @@
|
||||
<flux:select.option value="other">{{ __('Khác...') }}</flux:select.option>
|
||||
</flux:select>
|
||||
</flux:field>
|
||||
|
||||
<div x-show="plan === 'other'" class="mt-2">
|
||||
|
||||
<div x-show="plan === 'other'" class="mt-2" x-transition>
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Số tháng') }}</flux:label>
|
||||
<flux:input x-model.number="months" name="months" type="number" min="1" max="60" placeholder="{{ __('Nhập số tháng') }}" />
|
||||
<flux:description>{{ __('Nhập số tháng muốn gia hạn (tối đa 60).') }}</flux:description>
|
||||
<flux:input x-model.number="months" name="months" type="number" min="2" max="60"
|
||||
placeholder="{{ __('Nhập số tháng') }}" />
|
||||
<flux:description>{{ __('Nhập số tháng muốn gia hạn (tối thiểu 2, tối đa 60).') }}</flux:description>
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-sm text-gray-700">
|
||||
<div class="flex justify-between"><span>{{ __('Đơn giá / tháng') }}</span><span x-text="new Intl.NumberFormat('vi-VN').format(unitPrice) + ' đ'"></span></div>
|
||||
<div class="flex justify-between font-semibold"><span>{{ __('Tổng tiền') }}</span><span x-text="new Intl.NumberFormat('vi-VN').format(total) + ' đ'"></span></div>
|
||||
|
||||
<div class="mt-4 p-4 bg-gray-50 rounded-lg border border-gray-200 text-sm text-gray-700 relative">
|
||||
<div x-show="loading" class="absolute inset-0 bg-white/50 flex items-center justify-center z-10 backdrop-blur-sm">
|
||||
<div class="animate-spin rounded-full h-5 w-5 border-b-2 border-orange-500"></div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="text-gray-500">{{ __('Đơn giá / tháng') }}</span>
|
||||
<span class="font-medium"
|
||||
x-text="new Intl.NumberFormat('vi-VN', { style: 'currency', currency: 'VND' }).format(unitPrice)"></span>
|
||||
</div>
|
||||
<div class="border-t border-dashed border-gray-300 my-2"></div>
|
||||
<div class="flex justify-between items-center text-base">
|
||||
<span class="font-semibold text-gray-800">{{ __('Tổng thanh toán') }}</span>
|
||||
<span class="font-bold text-orange-600 text-lg"
|
||||
x-text="new Intl.NumberFormat('vi-VN', { style: 'currency', currency: 'VND' }).format(total)"></span>
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-gray-400 text-right italic">
|
||||
{{ __('(Đã bao gồm VAT nếu có)') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex gap-2 justify-end mt-6">
|
||||
<flux:modal.close>
|
||||
<flux:button variant="ghost">{{ __('Hủy') }}</flux:button>
|
||||
</flux:modal.close>
|
||||
<flux:button type="submit" variant="primary" color="orange" x-bind:disabled="submitting">{{ __('Đồng ý') }}</flux:button>
|
||||
<flux:button type="submit" variant="primary" color="orange"
|
||||
x-bind:disabled="submitting || loading || (plan === 'other' && months < 2)">
|
||||
<span x-show="!submitting">{{ __('Thanh toán gia hạn') }}</span>
|
||||
<span x-show="submitting">{{ __('Đang xử lý...') }}</span>
|
||||
</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -520,6 +520,7 @@ Route::middleware("CustomerLoginCheck")->group(function () {
|
||||
Route::post('secure/reset-vm-password', 'resetVMPassword')->name('service.reset.vm.password');
|
||||
Route::post('secure/change-remote-port', 'changeRemotePort')->name('service.change.remote.port');
|
||||
Route::post('secure/change-ip', 'changeIP')->name('service.change.ip');
|
||||
Route::post('secure/renew-price', 'renewPrice')->name('service.renew.price');
|
||||
Route::post('secure/renew', 'renew')->name('service.renew');
|
||||
Route::post('secure/start-vm', 'startVM')->name('service.start.vm');
|
||||
Route::post('secure/stop-vm', 'stopVM')->name('service.stop.vm');
|
||||
|
||||
Reference in New Issue
Block a user