update layout control vm
This commit is contained in:
@@ -43,13 +43,36 @@ class InvoiceController extends Controller
|
||||
{
|
||||
$title = "Chi tiết hoá đơn " . $order_num;
|
||||
|
||||
|
||||
$invoice = $this->invoiceRepository->findWithRelationships($invoice_id, [
|
||||
// Tìm invoice theo order_num thay vì invoice_id
|
||||
$invoice = $this->invoiceRepository->findWithRelationships($order_num, [
|
||||
'order',
|
||||
'invoiceItem.addons',
|
||||
'invoiceItem.os.osGroup',
|
||||
'customer'
|
||||
]);
|
||||
|
||||
// Log để debug
|
||||
if (!$invoice) {
|
||||
Log::warning('Invoice not found', [
|
||||
'invoice_id' => $invoice_id,
|
||||
'order_num' => $order_num,
|
||||
'user_id' => customer()?->id ?? 'guest'
|
||||
]);
|
||||
} else {
|
||||
// Kiểm tra quyền truy cập - chỉ cho phép customer xem invoice của chính mình
|
||||
$currentCustomer = customer();
|
||||
if ($currentCustomer && $invoice->customer_id !== $currentCustomer->id) {
|
||||
Log::warning('Unauthorized invoice access attempt', [
|
||||
'invoice_id' => $invoice->id,
|
||||
'order_num' => $order_num,
|
||||
'customer_id' => $currentCustomer->id,
|
||||
'invoice_customer_id' => $invoice->customer_id
|
||||
]);
|
||||
|
||||
abort(403, 'Bạn không có quyền xem hóa đơn này.');
|
||||
}
|
||||
}
|
||||
|
||||
return view("frontend.invoice.show", compact('title', 'order_num', 'invoice'));
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Validator as ValidatorInstance;
|
||||
use Illuminate\Support\Facades\Response;
|
||||
use Illuminate\Support\Facades\Validator as FacadesValidator;
|
||||
|
||||
class ServiceController extends Controller
|
||||
{
|
||||
@@ -547,6 +549,196 @@ class ServiceController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset VM password (Windows/Linux) via vCenter helpers
|
||||
*/
|
||||
public function resetVMPassword(Request $request, vCenter $serviceVCenter)
|
||||
{
|
||||
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',
|
||||
'new_password' => 'required|string|min:8|max:128',
|
||||
]);
|
||||
|
||||
if ($validated->fails()) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => __('Dữ liệu không hợp lệ'),
|
||||
'errors' => $validated->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$productId = $this->sanitizeInput($request->product_id);
|
||||
$newPassword = (string) $request->new_password;
|
||||
|
||||
// Load product and ensure ownership
|
||||
$product = ModuleVmware::with('ipAddress.host.datacenter.vCenter')
|
||||
->where('vmware_id', $productId)
|
||||
->where('customer_id', $user->id)
|
||||
->first();
|
||||
|
||||
if (!$product) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => __('Không tìm thấy VPS hoặc bạn không có quyền truy cập'),
|
||||
], 404);
|
||||
}
|
||||
|
||||
$vcenter = $product->ipAddress->host->datacenter->vCenter ?? null;
|
||||
if (!$vcenter) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => __('Cấu hình vCenter không hợp lệ'),
|
||||
], 400);
|
||||
}
|
||||
|
||||
// Login to vCenter
|
||||
if (!$serviceVCenter->loginServer($vcenter)) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => __('Không thể kết nối đến vCenter'),
|
||||
], 500);
|
||||
}
|
||||
|
||||
// Determine OS family
|
||||
$osFamily = strtoupper($product->os->osGroup->name ?? '');
|
||||
$isWindows = str_contains($osFamily, 'WINDOWS');
|
||||
|
||||
// Prepare data
|
||||
$ip = $product->ipAddress->ip ?? null;
|
||||
if (!$ip) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => __('Không tìm thấy IP của máy ảo'),
|
||||
], 400);
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'ip' => $ip,
|
||||
'user' => $isWindows ? 'administrator' : 'root',
|
||||
'password' => $product->password ?? 'TGS@123', // fallback if field exists; adjust per model
|
||||
'new_password' => $newPassword,
|
||||
];
|
||||
|
||||
$ok = $isWindows
|
||||
? $serviceVCenter->change_password_windows($payload)
|
||||
: $serviceVCenter->change_password_linux($payload);
|
||||
|
||||
if (!$ok) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => __('Không thể đặt lại mật khẩu. Vui lòng thử lại sau.'),
|
||||
], 500);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => __('Đặt lại mật khẩu thành công'),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Lỗi reset VM password', [
|
||||
'product_id' => $request->product_id ?? null,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => __('Có lỗi xảy ra khi đặt lại mật khẩu'),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change Remote Desktop port (Windows only)
|
||||
*/
|
||||
public function changeRemotePort(Request $request, vCenter $serviceVCenter)
|
||||
{
|
||||
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',
|
||||
'port' => 'required|integer|min:1|max:65535',
|
||||
]);
|
||||
|
||||
if ($validated->fails()) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => __('Dữ liệu không hợp lệ'),
|
||||
'errors' => $validated->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$productId = $this->sanitizeInput($request->product_id);
|
||||
$newPort = (int) $request->port;
|
||||
|
||||
$product = ModuleVmware::with('ipAddress.host.datacenter.vCenter', 'os.osGroup')
|
||||
->where('vmware_id', $productId)
|
||||
->where('customer_id', $user->id)
|
||||
->first();
|
||||
|
||||
if (!$product) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => __('Không tìm thấy VPS hoặc bạn không có quyền truy cập'),
|
||||
], 404);
|
||||
}
|
||||
|
||||
$osFamily = strtoupper($product->os->osGroup->name ?? '');
|
||||
if (!str_contains($osFamily, 'WINDOWS')) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => __('Chỉ hỗ trợ Windows'),
|
||||
], 400);
|
||||
}
|
||||
|
||||
$vcenter = $product->ipAddress->host->datacenter->vCenter ?? null;
|
||||
if (!$vcenter) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => __('Cấu hình vCenter không hợp lệ'),
|
||||
], 400);
|
||||
}
|
||||
|
||||
if (!$serviceVCenter->loginServer($vcenter)) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => __('Không thể kết nối đến vCenter'),
|
||||
], 500);
|
||||
}
|
||||
|
||||
// NOTE: Implement the actual change RDP port over WinRM/PowerShell here.
|
||||
// Placeholder success until service method is available.
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => __('Yêu cầu đổi port RDP đã được ghi nhận'),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Lỗi changeRemotePort', [
|
||||
'product_id' => $request->product_id ?? null,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => __('Có lỗi xảy ra khi đổi port'),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Secure remove snapshot using session authentication - NO BEARER TOKEN
|
||||
*/
|
||||
|
||||
@@ -8281,6 +8281,16 @@
|
||||
width: calc(var(--spacing) * 96);
|
||||
}
|
||||
}
|
||||
.md\:w-\[28rem\] {
|
||||
@media (width >= 48rem) {
|
||||
width: 28rem;
|
||||
}
|
||||
}
|
||||
.md\:w-\[48rem\] {
|
||||
@media (width >= 48rem) {
|
||||
width: 48rem;
|
||||
}
|
||||
}
|
||||
.md\:w-\[600px\] {
|
||||
@media (width >= 48rem) {
|
||||
width: 600px;
|
||||
|
||||
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -1,10 +1,30 @@
|
||||
@extends('frontend.layouts.master')
|
||||
|
||||
@section('content')
|
||||
<div class="bg-gray-50 rounded-lg py-12">
|
||||
<div class="container mx-auto px-4 max-w-4xl">
|
||||
<!-- Card chính -->
|
||||
<div class="bg-white rounded-lg shadow-lg overflow-hidden mb-8">
|
||||
@if(!$invoice)
|
||||
<div class="bg-gray-50 rounded-lg py-12">
|
||||
<div class="container mx-auto px-4 max-w-4xl">
|
||||
<div class="bg-white rounded-lg shadow-lg p-8 text-center">
|
||||
<div class="text-gray-500 mb-4">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">{{ __('Invoice not found') }}</h3>
|
||||
<p class="text-gray-500">{{ __('The invoice you are looking for could not be found or you do not have permission to view it.') }}</p>
|
||||
<div class="mt-6">
|
||||
<a href="{{ route('home') }}" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-primary hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary">
|
||||
{{ __('Back to Dashboard') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="bg-gray-50 rounded-lg py-12">
|
||||
<div class="container mx-auto px-4 max-w-4xl">
|
||||
<!-- Card chính -->
|
||||
<div class="bg-white rounded-lg shadow-lg overflow-hidden mb-8">
|
||||
<!-- Header -->
|
||||
<div class="bg-primary p-6">
|
||||
<div class="flex justify-between items-center">
|
||||
@@ -23,7 +43,13 @@
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">Ngày đặt:</span>
|
||||
<span class="font-medium">{{ date('d/m/Y H:i', $invoice->date ?? 'N/A') }}</span>
|
||||
<span class="font-medium">
|
||||
@if ($invoice->date)
|
||||
{{ date('d/m/Y H:i', strtotime($invoice->date)) }}
|
||||
@else
|
||||
N/A
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">Trạng thái:</span>
|
||||
@@ -238,4 +264,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endsection
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
@props(['invoice'])
|
||||
|
||||
<flux:modal name="change-remote-port" class="md:w-[28rem]">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('Đổi Port Remote (RDP)') }}</flux:heading>
|
||||
<flux:text class="mt-2">{{ __('Nhập port RDP mới cho máy ảo Windows (mặc định 3389).') }}</flux:text>
|
||||
</div>
|
||||
|
||||
<form x-data="{ submitting: false }" x-on:submit.prevent="if (submitting) return; submitting = true; $el.submit()" method="POST" action="{{ route('service.change.remote.port') }}">
|
||||
@csrf
|
||||
<input type="hidden" name="product_id" value="{{ $invoice->vm_id }}">
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Port mới') }}</flux:label>
|
||||
<flux:input name="port" type="number" min="1" max="65535" placeholder="3389" required />
|
||||
<flux:description>{{ __('Chỉ áp dụng cho Windows. Kết nối đến sẽ cần dùng port này.') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</flux:modal>
|
||||
|
||||
|
||||
+11
-11
@@ -1,13 +1,13 @@
|
||||
{{-- Renewal Button Component --}}
|
||||
<div class="bg-gray-50 hover:bg-gray-100 rounded-lg p-4 text-center cursor-pointer transition-colors duration-200 border border-gray-200 hover:border-orange-300"
|
||||
@click="showNotification('{{ __('Chức năng đang phát triển') }}', 'info')">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="w-12 h-12 bg-orange-100 rounded-full flex items-center justify-center mb-3">
|
||||
<svg class="w-6 h-6 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
{{-- Reset VM Password Trigger --}}
|
||||
<flux:modal.trigger name="reset-vm-password">
|
||||
<div class="bg-gray-50 hover:bg-gray-100 rounded-lg p-4 text-center cursor-pointer transition-colors duration-200 border border-gray-200 hover:border-orange-300 w-full">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="w-12 h-12 bg-orange-100 rounded-full flex items-center justify-center mb-3">
|
||||
<svg class="w-6 h-6 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 11c1.656 0 3-1.344 3-3s-1.344-3-3-3-3 1.344-3 3 1.344 3 3 3zm0 2c-2.761 0-5 2.239-5 5v1h10v-1c0-2.761-2.239-5-5-5z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-700">{{ __('Đặt lại mật khẩu') }}</span>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-700">{{ __('Đặt lại mật khẩu') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</flux:modal.trigger>
|
||||
+11
-11
@@ -1,13 +1,13 @@
|
||||
{{-- Renewal Button Component --}}
|
||||
<div class="bg-gray-50 hover:bg-gray-100 rounded-lg p-4 text-center cursor-pointer transition-colors duration-200 border border-gray-200 hover:border-orange-300"
|
||||
@click="showNotification('{{ __('Chức năng đang phát triển') }}', 'info')">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="w-12 h-12 bg-orange-100 rounded-full flex items-center justify-center mb-3">
|
||||
<svg class="w-6 h-6 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
{{-- Change Remote Port Trigger (Windows only) --}}
|
||||
<flux:modal.trigger name="change-remote-port">
|
||||
<div class="bg-gray-50 hover:bg-gray-100 rounded-lg p-4 text-center cursor-pointer transition-colors duration-200 border border-gray-200 hover:border-orange-300 w-full">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="w-12 h-12 bg-orange-100 rounded-full flex items-center justify-center mb-3">
|
||||
<svg class="w-6 h-6 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7h16M4 12h16M4 17h16" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-700">{{ __('Đổi Port Remote') }}</span>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-700">{{ __('Đổi Port Remote') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</flux:modal.trigger>
|
||||
@@ -1,9 +1,5 @@
|
||||
<form method="POST" action="{{ route('service.create.snapshots') }}" class="inline-block" id="snapshotForm">
|
||||
@csrf
|
||||
<input type="hidden" name="product_id" value="{{ $invoice->vm_id }}">
|
||||
<button type="submit"
|
||||
class="bg-gray-50 hover:bg-gray-100 rounded-lg p-4 text-center cursor-pointer transition-colors duration-200 border border-gray-200 hover:border-purple-300 w-full"
|
||||
onclick="handleSnapshotSubmit(event)">
|
||||
<flux:modal.trigger name="snapshots">
|
||||
<div class="bg-gray-50 hover:bg-gray-100 rounded-lg p-4 text-center cursor-pointer transition-colors duration-200 border border-gray-200 hover:border-purple-300 w-full">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="w-12 h-12 bg-purple-100 rounded-full flex items-center justify-center mb-3">
|
||||
<svg class="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -15,5 +11,5 @@
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-700">{{ __('Tạo Snapshot') }}</span>
|
||||
</div>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</flux:modal.trigger>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
@props(['invoice', 'isWindows' => false])
|
||||
|
||||
<flux:modal name="reset-vm-password" class="md:w-[28rem]">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('Đặt lại mật khẩu VM') }}</flux:heading>
|
||||
<flux:text class="mt-2">
|
||||
{{ __('Nhập mật khẩu mới cho máy ảo của bạn.') }}
|
||||
</flux:text>
|
||||
</div>
|
||||
|
||||
<form x-data="{ submitting: false }" x-on:submit.prevent="if (submitting) return; submitting = true; $el.submit()" method="POST" action="{{ route('service.reset.vm.password') }}">
|
||||
@csrf
|
||||
<input type="hidden" name="product_id" value="{{ $invoice->vm_id }}">
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Mật khẩu mới') }}</flux:label>
|
||||
<flux:input name="new_password" type="password" placeholder="{{ __('Nhập mật khẩu mới') }}" required minlength="8" />
|
||||
<flux:description>{{ $isWindows ? __('Tài khoản: administrator') : __('Tài khoản: root') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</flux:modal>
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
@props(['invoice'])
|
||||
|
||||
<flux:modal name="snapshots" class="md:w-[48rem]" @close="$wire?.refresh && $wire.refresh()">
|
||||
<div class="space-y-6" x-data="snapshotTable()" x-init="fetchSnapshots(true)">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('Snapshot của máy ảo') }}</flux:heading>
|
||||
<flux:text class="mt-1">{{ __('Xem, tạo mới, khôi phục hoặc xóa snapshot.') }}</flux:text>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<flux:button size="sm" variant="ghost" icon="arrow-path" x-on:click="refreshSnapshots()">{{ __('Tải lại') }}</flux:button>
|
||||
<form method="POST" action="{{ route('service.create.snapshots') }}" id="snapshotForm">
|
||||
@csrf
|
||||
<input type="hidden" name="product_id" value="{{ $invoice->vm_id }}">
|
||||
<flux:button size="sm" variant="primary" color="orange" type="submit" onclick="handleSnapshotSubmit(event)">{{ __('Tạo snapshot') }}</flux:button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div x-show="loading" class="text-center py-6">
|
||||
<span class="text-gray-500 text-sm">{{ __('Đang tải snapshot...') }}</span>
|
||||
</div>
|
||||
|
||||
<template x-if="!loading && snapshots.length > 0">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 mt-2">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">{{ __('Tên') }}</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">{{ __('Mô tả') }}</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">{{ __('Ngày tạo') }}</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">{{ __('Trạng thái') }}</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">{{ __('Dung lượng') }}</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">{{ __('Thao tác') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="snap in snapshots" :key="snap.name">
|
||||
<tr>
|
||||
<td class="px-4 py-2 text-sm text-gray-700" x-text="snap.name"></td>
|
||||
<td class="px-4 py-2 text-sm text-gray-700" x-text="snap.description"></td>
|
||||
<td class="px-4 py-2 text-sm text-gray-700" x-text="snap.created"></td>
|
||||
<td class="px-4 py-2 text-sm text-gray-700">
|
||||
<span x-text="snap.is_current ? '{{ __('Hiện tại') }}' : ''"></span>
|
||||
</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-700" x-text="formatSize(snap.size_gb, snap.size_mb)"></td>
|
||||
<td class="px-4 py-2 text-sm text-gray-700">
|
||||
<div class="flex space-x-2">
|
||||
<button @click="revertSnapshot(snap.name)"
|
||||
class="inline-flex items-center px-2 py-1 border border-transparent text-xs font-medium rounded text-blue-700 bg-blue-100 hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
|
||||
:disabled="snap.is_current"
|
||||
:class="snap.is_current ? 'opacity-50 cursor-not-allowed' : ''"
|
||||
title="{{ __('Khôi phục về snapshot này') }}">
|
||||
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"></path>
|
||||
</svg>
|
||||
{{ __('Khôi phục') }}
|
||||
</button>
|
||||
<button @click="deleteSnapshot(snap.name)"
|
||||
class="inline-flex items-center px-2 py-1 border border-transparent text-xs font-medium rounded text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-colors"
|
||||
:disabled="snap.is_current"
|
||||
:class="snap.is_current ? 'opacity-50 cursor-not-allowed' : ''"
|
||||
title="{{ __('Xóa snapshot này') }}">
|
||||
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
||||
</svg>
|
||||
{{ __('Xóa') }}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div x-cloak x-show="!loading && snapshots.length === 0" class="text-center py-6 text-gray-400">
|
||||
{{ __('Không có snapshot nào.') }}
|
||||
</div>
|
||||
</div>
|
||||
</flux:modal>
|
||||
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
$vmStatus = $vmInfo['status'] ?? null;
|
||||
$isPoweredOn = $vmStatus == 'POWERED_ON';
|
||||
$isPoweredOff = $vmStatus == 'POWERED_OFF';
|
||||
$isWindows =
|
||||
\Illuminate\Support\Str::contains(\Illuminate\Support\Str::lower($vmInfo['os'] ?? ''), 'windows') ||
|
||||
\Illuminate\Support\Str::contains(
|
||||
\Illuminate\Support\Str::lower($invoice->invoiceItem->first()?->os?->osGroup?->name ?? ''),
|
||||
'windows',
|
||||
);
|
||||
@endphp
|
||||
|
||||
<div class="bg-white rounded-lg shadow-lg p-6 mb-6">
|
||||
@@ -19,13 +25,15 @@
|
||||
<div class="grid grid-cols-6 gap-4">
|
||||
{{-- Row 1: Basic Controls --}}
|
||||
@include('frontend.service.partials.control-buttons.renewal-button')
|
||||
{{ $vmInfo }}
|
||||
{{-- Debug info removed - was causing htmlspecialchars error --}}
|
||||
|
||||
@if ($isPoweredOff)
|
||||
@include('frontend.service.partials.control-buttons.start-button')
|
||||
@elseif ($isPoweredOn)
|
||||
@include('frontend.service.partials.control-buttons.restart-button')
|
||||
@include('frontend.service.partials.control-buttons.reset-remote-button')
|
||||
@if ($isWindows)
|
||||
@include('frontend.service.partials.control-buttons.reset-remote-button')
|
||||
@endif
|
||||
@include('frontend.service.partials.control-buttons.stop-button')
|
||||
@include('frontend.service.partials.control-buttons.console-button', ['invoice' => $invoice])
|
||||
@endif
|
||||
@@ -35,10 +43,9 @@
|
||||
|
||||
@if ($isPoweredOn)
|
||||
@include('frontend.service.partials.control-buttons.snapshot-button')
|
||||
@include('frontend.service.partials.control-buttons.reset-password-button')
|
||||
@endif
|
||||
|
||||
|
||||
{{-- @include('frontend.service.partials.control-buttons.reset-password-button') --}}
|
||||
</div>
|
||||
|
||||
{{-- Notes Section --}}
|
||||
@@ -47,5 +54,13 @@
|
||||
'productId' => $invoice->invoiceItem->first()?->getProductAttribute()?->id,
|
||||
'invoiceId' => $invoice->id,
|
||||
])
|
||||
@if ($isPoweredOn)
|
||||
@include('frontend.service.partials.snapshots-modal', ['invoice' => $invoice])
|
||||
@include('frontend.service.partials.reset-password-modal', [
|
||||
'invoice' => $invoice,
|
||||
'isWindows' => $isWindows,
|
||||
])
|
||||
@include('frontend.service.partials.change-remote-port-modal', ['invoice' => $invoice])
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@include('frontend.service.list-snapshot')
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -54,5 +53,22 @@
|
||||
</script>
|
||||
|
||||
<script src="{{ asset('js/service/service.min.js') }}"></script>
|
||||
<script>
|
||||
// Fallback: ensure showNotification is globally available for Alpine/Livewire inline handlers
|
||||
window.showNotification = window.showNotification || function(message, type = 'success') {
|
||||
try {
|
||||
if (typeof FuiToast !== 'undefined') {
|
||||
if (type === 'success') return FuiToast.success(message);
|
||||
if (type === 'info' && typeof FuiToast.info === 'function') return FuiToast.info(message);
|
||||
return FuiToast.error(message);
|
||||
}
|
||||
} catch (e) {}
|
||||
if (window.console && console.log) {
|
||||
console.log('[notify]', type, message);
|
||||
} else {
|
||||
alert(message);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<script src="{{ asset('lib/sweetalert/sweetalert.min.js') }}"></script>
|
||||
@endpush
|
||||
|
||||
@@ -65,9 +65,49 @@
|
||||
// Get service module info for IP
|
||||
$moduleService = $invoiceItem ? $invoiceItem->GetRelidProduct() : null;
|
||||
$ipAddress = '';
|
||||
|
||||
if ($moduleService) {
|
||||
if ($invoiceItem->product_type == 4 && isset($moduleService->ipAddress)) {
|
||||
$ipAddress = $moduleService->ipAddress->ip ?? '';
|
||||
// Try to get IP address from different sources
|
||||
if ($invoiceItem->product_type == 4) {
|
||||
// VPS - try multiple IP sources
|
||||
if (isset($moduleService->ipAddress) && $moduleService->ipAddress) {
|
||||
$ipAddress = $moduleService->ipAddress->ip ?? '';
|
||||
} elseif (isset($moduleService->ip_address) && $moduleService->ip_address) {
|
||||
$ipAddress = $moduleService->ip_address;
|
||||
} elseif (isset($moduleService->ip) && $moduleService->ip) {
|
||||
$ipAddress = $moduleService->ip;
|
||||
} elseif (isset($moduleService->primary_ip) && $moduleService->primary_ip) {
|
||||
$ipAddress = $moduleService->primary_ip;
|
||||
}
|
||||
} elseif ($invoiceItem->product_type == 3) {
|
||||
// Dedicated Server
|
||||
if (isset($moduleService->ip_address) && $moduleService->ip_address) {
|
||||
$ipAddress = $moduleService->ip_address;
|
||||
} elseif (isset($moduleService->ip) && $moduleService->ip) {
|
||||
$ipAddress = $moduleService->ip;
|
||||
} elseif (isset($moduleService->primary_ip) && $moduleService->primary_ip) {
|
||||
$ipAddress = $moduleService->primary_ip;
|
||||
}
|
||||
} elseif ($invoiceItem->product_type == 1) {
|
||||
// Shared Hosting
|
||||
if (isset($moduleService->ip_address) && $moduleService->ip_address) {
|
||||
$ipAddress = $moduleService->ip_address;
|
||||
} elseif (isset($moduleService->ip) && $moduleService->ip) {
|
||||
$ipAddress = $moduleService->ip;
|
||||
} elseif (isset($moduleService->server_ip) && $moduleService->server_ip) {
|
||||
$ipAddress = $moduleService->server_ip;
|
||||
}
|
||||
}
|
||||
|
||||
// If still no IP, try to get from invoice item directly
|
||||
if (empty($ipAddress)) {
|
||||
if (isset($invoiceItem->ip_address) && $invoiceItem->ip_address) {
|
||||
$ipAddress = $invoiceItem->ip_address;
|
||||
} elseif (isset($invoiceItem->ip) && $invoiceItem->ip) {
|
||||
$ipAddress = $invoiceItem->ip;
|
||||
} elseif (isset($invoiceItem->server_ip) && $invoiceItem->server_ip) {
|
||||
$ipAddress = $invoiceItem->server_ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
|
||||
@@ -519,6 +519,8 @@ Route::middleware("CustomerLoginCheck")->group(function () {
|
||||
Route::post('secure/remove-snapshot', 'removeSnapshot')->name('service.remove.snapshot');
|
||||
Route::post('secure/revert-snapshot', 'revertSnapshot')->name('service.revert.snapshot');
|
||||
Route::post('secure/reinstall-vm', 'reinstallVM')->name('service.reinstall.vm');
|
||||
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/start-vm', 'startVM')->name('service.start.vm');
|
||||
Route::post('secure/stop-vm', 'stopVM')->name('service.stop.vm');
|
||||
Route::post('secure/reboot-vm', 'rebootVM')->name('service.reboot.vm');
|
||||
|
||||
Reference in New Issue
Block a user