feat: Implement VM suspension feature with email notification

- Added suspend method in VCenterManagerController to handle VM suspension requests.
- Introduced VmSuspendedMail for notifying customers about VM suspension.
- Created email template for VM suspension notification.
- Updated API routes to include VM suspend functionality.
- Modified cors.php to allow additional local development URL.
- Updated DatabaseSeeder to include AdminSeeder and commented out unused seeders.
- Removed deprecated Livewire components related to cart, customer email history, invoices, orders, tickets, transactions, user settings, and VPS reinstall modal.
This commit is contained in:
baodev0
2026-05-21 10:17:16 +07:00
parent 4a308cd19a
commit a31d238135
18 changed files with 226 additions and 796 deletions
+29
View File
@@ -0,0 +1,29 @@
{
"permissions": {
"allow": [
"Bash(ls:*)",
"Bash(php artisan:*)",
"Bash(composer show:*)",
"Bash(php:*)",
"Bash(do php:*)",
"Bash(/usr/bin/php:*)",
"Bash(xargs cat:*)",
"Bash(composer require:*)",
"Bash(grep -rn \"reinstall\\\\|vm\" /Users/baro/Desktop/Workspace/thegioiso/web/portal-infra/portal/routes/*.php)",
"Bash(grep -l \"module_vmware\\\\|ip_pool\\\\|module_ip_address\" /Users/baro/Desktop/Workspace/thegioiso/web/portal-infra/portal/database/migrations/*.php)",
"Bash(composer why-not *)",
"Bash(npx skills *)",
"Bash(xargs -I {} basename {})",
"Read(//Users/baro/Desktop/Workspace/thegioiso/web/portal-infra/portal-admin/src/**)",
"Read(//Users/baro/Desktop/Workspace/thegioiso/web/portal-infra/portal-admin/**)",
"Bash(mkdir -p /Users/baro/Desktop/Workspace/thegioiso/web/portal-infra/portal-admin/src/features/activity-logs/components)",
"Bash(mkdir -p /Users/baro/Desktop/Workspace/thegioiso/web/portal-infra/portal-admin/src/routes/_authenticated/activity-logs)",
"Bash(npx tsr *)",
"Bash(npx tsc *)",
"Bash(chmod +x *)",
"Bash(composer check-platform-reqs *)",
"Bash(composer config *)",
"Bash(composer update *)"
]
}
}
@@ -3,6 +3,7 @@
namespace App\Http\Controllers\admin;
use App\Http\Controllers\Controller;
use App\Mail\VmSuspendedMail;
use App\Models\ModuleIpAddress;
use App\Models\ModuleVmware;
use App\Models\VirtualMachines;
@@ -15,6 +16,7 @@ use App\Repositories\Module\Network\NetworkRepositoryInterface;
use App\Repositories\vCenter\vCenterRepositoryInterface;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail;
use Yajra\DataTables\Facades\DataTables;
class VCenterManagerController extends Controller
@@ -678,4 +680,71 @@ class VCenterManagerController extends Controller
'data' => $vms
]);
}
public function suspend(Request $request)
{
$vmId = $request->input('vm_id');
$vm = VirtualMachines::where('vm_id', $vmId)->first();
if (!$vm) {
return response()->json(['error' => true, 'message' => 'VM not found'], 404);
}
if ($vm->power_state === 'SUSPENDED') {
return response()->json(['error' => true, 'message' => 'VM is already suspended'], 400);
}
$vcenter = $vm->datacenter?->vCenter ?? $vm->host?->datacenter?->vCenter;
if (!$vcenter) {
return response()->json(['error' => true, 'message' => 'vCenter not found'], 400);
}
$vCenterService = new \App\Http\Services\vCenter();
if (!$vCenterService->loginServer($vcenter)) {
return response()->json(['error' => true, 'message' => 'Cannot login to vCenter'], 400);
}
$response = Http::withoutVerifying()
->withHeaders(['vmware-api-session-id' => $vCenterService->token])
->post("https://{$vCenterService->apiUri}/rest/vcenter/vm/$vmId/power/suspend");
if ($response->status() === 200) {
$vm->update(['power_state' => 'SUSPENDED']);
$this->sendSuspendNotification($vm);
return response()->json(['error' => false, 'message' => 'Suspend command sent successfully']);
}
$errorData = $response->json();
if (isset($errorData['error_type']) && $errorData['error_type'] === 'ALREADY_IN_DESIRED_STATE') {
$vm->update(['power_state' => 'SUSPENDED']);
$this->sendSuspendNotification($vm);
return response()->json(['error' => false, 'message' => 'VM is already suspended']);
}
return response()->json([
'error' => true,
'message' => $errorData['value'] ?? $errorData['error_type'] ?? 'Failed to suspend VM',
], 400);
}
private function sendSuspendNotification(VirtualMachines $vm): void
{
$vmware = ModuleVmware::with('customer')
->where('vmware_id', $vm->vm_id)
->first();
if (!$vmware || !$vmware->customer || !$vmware->customer->email) {
return;
}
try {
Mail::to($vmware->customer->email)
->queue(new VmSuspendedMail($vmware->customer, $vm->name, $vm->vm_id));
} catch (\Throwable) {
// Email failure must not block the suspend response
}
}
}
-133
View File
@@ -1,133 +0,0 @@
<?php
namespace App\Http\Livewire\Cart;
use App\Models\Addon;
use Livewire\Component;
class SummaryAddons extends Component
{
public $product;
public $addons = [];
public $index;
public $months = 1;
public $cachedTotalPrice = null;
public function mount($product, $index = null, $months = 1)
{
$this->product = $product;
$this->months = $months;
// Lấy tất cả addon từ database
$addonsFromDb = Addon::all()->toArray();
// Chuyển đổi dữ liệu addon từ database sang định dạng phù hợp với component
$this->addons = collect($addonsFromDb)->map(function ($addon) {
return [
'id' => $addon['id'],
'name' => $addon['name'],
'price' => $addon['price'],
'min' => $addon['min'],
'max' => $addon['max'],
'step' => $addon['step'],
'prefix' => $addon['prefix'],
'type_id' => $addon['type_id'],
'qty' => 0, // Khởi tạo số lượng ban đầu là 0
];
})->toArray();
$this->index = $index;
}
public function increment($key)
{
if ($this->addons[$key]['qty'] < ($this->addons[$key]['max'] ?? PHP_INT_MAX)) {
$this->addons[$key]['qty'] += ($this->addons[$key]['step'] ?? 1);
}
$this->calculateTotal();
$this->dispatch('addonUpdated', addons: $this->addons);
}
public function decrement($key)
{
if ($this->addons[$key]['qty'] > ($this->addons[$key]['min'] ?? 0)) {
$this->addons[$key]['qty'] -= ($this->addons[$key]['step'] ?? 1);
}
$this->calculateTotal();
$this->dispatch('addonUpdated', addons: $this->addons);
}
// Triggered when input fields are updated directly
public function updated($name, $value)
{
// Check if the updated property is related to addon quantity
if (strpos($name, 'addons.') !== false && strpos($name, '.qty') !== false) {
$parts = explode('.', $name);
$key = $parts[1] ?? null;
if ($key !== null && isset($this->addons[$key])) {
// Ensure quantity is within bounds
$min = $this->addons[$key]['min'] ?? 0;
$max = $this->addons[$key]['max'] ?? PHP_INT_MAX;
$step = $this->addons[$key]['step'] ?? 1;
// Round to the nearest step
$this->addons[$key]['qty'] = round($value / $step) * $step;
// Apply min/max constraints
$this->addons[$key]['qty'] = max($min, $this->addons[$key]['qty']);
$this->addons[$key]['qty'] = min($max, $this->addons[$key]['qty']);
// Ensure total is recalculated immediately
$this->calculateTotal();
// Notify listeners that addon has been updated
$this->dispatch('addonUpdated', addons: $this->addons);
}
}
}
public function hydrate()
{
// This method is called on every Livewire request
// Perfect place to make sure our computed properties are up-to-date
$this->calculateTotal();
}
protected function calculateTotal()
{
// Lấy giá sản phẩm ưu tiên sale nếu có
$basePrice = $this->product->price_sale ?: $this->product->price;
// Nhân với số tháng
$total = $basePrice * $this->months;
foreach ($this->addons as $addon) {
$total += $addon['qty'] * $addon['price'] * $this->months;
}
$this->cachedTotalPrice = $total;
return $total;
}
public function getTotalAddonPriceProperty()
{
return collect($this->addons)->sum(function ($addon) {
return $addon['qty'] * ($addon['price'] ?? 0) * $this->months;
});
}
public function getTotalPriceProperty()
{
// Use cached value if available, otherwise calculate
return $this->cachedTotalPrice ?? $this->calculateTotal();
}
public function render()
{
return view('livewire.cart.summary-addons');
}
}
@@ -1,21 +0,0 @@
<?php
namespace App\Http\Livewire;
use App\Models\Mail;
use Illuminate\Http\Request;
use Livewire\Component;
class CustomerEmailHistory extends Component
{
public $search = '';
public function render(Request $request)
{
return view('livewire.customer-email-history',[
'emails' => Mail::where('customer_id',auth('users')->id())
->where('subject', 'like',"%$this->search%")
->latest('id')
->paginate(10)
]);
}
}
-32
View File
@@ -1,32 +0,0 @@
<?php
namespace App\Http\Livewire;
use App\Models\Invoice as CustomerInvoice;
use Livewire\Component;
use Livewire\WithPagination;
class Invoice extends Component
{
use WithPagination;
public $status = "full";
public function render()
{
return view('livewire.invoice', [
'invoice' => CustomerInvoice::where(function ($query) {
switch ($this->status) {
case "0":
case "1":
case "2":
$query->where('status', $this->status);
break;
}
})
->where('customer_id', customer()->id)
->latest()
->paginate(10)
]);
}
}
-36
View File
@@ -1,36 +0,0 @@
<?php
namespace App\Http\Livewire\Module;
use App\Repositories\vCenter\vCenterRepositoryInterface;
use Livewire\Component;
class Vcenter extends Component
{
public $model;
public $vcenters = [];
protected $vCenterRepository;
public function boot(vCenterRepositoryInterface $vCenterRepository)
{
$this->vCenterRepository = $vCenterRepository;
}
public function mount($model = null)
{
$this->model = $model;
$this->vcenters = $this->vCenterRepository->filter(
['status' => 1],
[],
[],
['id', 'host']
);
}
public function render()
{
return view('livewire.module.vcenter');
}
}
-68
View File
@@ -1,68 +0,0 @@
<?php
namespace App\Http\Livewire;
use App\Models\Invoice;
use App\Repositories\Invoice\InvoiceRepositoryInterface;
use Livewire\Component;
use Livewire\WithoutUrlPagination;
use Livewire\WithPagination;
class MyService extends Component
{
use WithPagination, WithoutUrlPagination;
public int|string $status = 'full';
public $type = '';
public $search = '';
protected $invoiceRepository;
public function mount(InvoiceRepositoryInterface $invoiceRepository)
{
$this->invoiceRepository = $invoiceRepository;
}
public function updatedSearch()
{
$this->resetPage();
}
public function updatedStatus()
{
$this->resetPage();
}
public function updatedType()
{
$this->resetPage();
}
public function render()
{
$query = Invoice::where('customer_id', customer()->id)
->with([
'invoiceItem.hosting',
'invoiceItem.vps',
'invoiceItem.os.osGroup',
'invoiceItem.addons.addonItems'
]);
// Filter by type if specified
if (!empty($this->type)) {
$query->whereHas('invoiceItem', function($q) {
$q->where('product_type', $this->type);
});
}
// Filter by server_name search if specified
if (!empty($this->search)) {
$query->whereHas('invoiceItem', function($q) {
$q->where('server_name', 'like', '%' . $this->search . '%');
});
}
return view('livewire.my-service', [
'services' => $query->orderBy('created_at', 'desc')->paginate(10)
]);
}
}
-22
View File
@@ -1,22 +0,0 @@
<?php
namespace App\Http\Livewire\Order;
use App\Models\Order as CustomerOrder;
use Livewire\Component;
use Livewire\WithoutUrlPagination;
use Livewire\WithPagination;
class OrderList extends Component
{
use WithPagination, WithoutUrlPagination;
public function render()
{
return view('livewire.order.order-list', [
'orders' => CustomerOrder::where('customer_id', customer()->id)
->latest()
->paginate(10)
]);
}
}
-24
View File
@@ -1,24 +0,0 @@
<?php
namespace App\Http\Livewire;
use Livewire\Component;
use App\Models\Ticket as CustomerSupport;
use Illuminate\Http\Request;
use Livewire\WithPagination;
class Ticket extends Component
{
use WithPagination;
public $search = '';
public function render(Request $request)
{
return view('livewire.ticket',[
'tickets' => CustomerSupport::where('customer_id',$request->user('users')->id)
->where('subject', 'like',"%$this->search%")
->where('customer_id',auth('users')->id())
->paginate(10)
]);
}
}
-29
View File
@@ -1,29 +0,0 @@
<?php
namespace App\Http\Livewire;
use App\Models\Transaction as Trans;
use Illuminate\Http\Request;
use Livewire\Component;
use Livewire\WithPagination;
class Transaction extends Component
{
use WithPagination;
public $search = '';
public function render(Request $request)
{
return view('livewire.transaction', [
'transactions' => Trans::where(function ($query) use ($request) {
$query->where('customer_id', $request->user('users')->id);
if (! empty($this->search))
{
$query->where('trans','LIKE',"%$this->search%");
}
})
->paginate(10)
]);
}
}
-39
View File
@@ -1,39 +0,0 @@
<?php
namespace App\Http\Livewire;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Livewire\Component;
class UserSettings extends Component
{
public bool $email_marketing;
public bool $receive_deposit;
public function mount()
{
$user = auth('users')->user();
// Gán giá trị bool từ database tinyInteger
$this->email_marketing = (bool) $user->email_marketing;
$this->receive_deposit = (bool) $user->receive_deposit;
}
public function updated($propertyName)
{
$user = auth('users')->user();
// Cập nhật database với giá trị int 0 hoặc 1
$user->update([
$propertyName => $this->$propertyName ? 1 : 0,
]);
}
public function render()
{
return view('livewire.user-settings', [
'user' => auth('users')->user(), // Truyền user để hiển thị thông tin
]);
}
}
-326
View File
@@ -1,326 +0,0 @@
<?php
namespace App\Http\Livewire;
use Livewire\Component;
use App\Models\Os;
use App\Models\OsGroup;
use App\Models\VPS;
use App\Models\InvoiceItem;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use App\Models\Invoice;
use App\Models\ModuleVmware;
class VPSReinstallModal extends Component
{
public $productId;
public $selectedOsGroupId;
public $selectedOsId;
public $osGroups = [];
public $operatingSystems = [];
public $currentOs;
public $invoiceId;
public $isLoading = false;
public $selectedOsDetails = null;
public $vpsInfo = null;
public $moduleVmware = null;
public $vpsIpAddress = null;
public $invoiceItem = null;
public $invoice = null;
protected $listeners = ['showOsModal' => 'loadOsData'];
public function mount($productId = null, $invoiceId = null)
{
$this->productId = $productId;
$this->invoiceId = $invoiceId;
$this->operatingSystems = collect(); // Initialize as empty collection
if ($productId) {
$this->loadOsData($productId);
}
}
public function loadOsData($productId)
{
$this->productId = $productId;
$this->isLoading = true;
try {
// Get the VPS product
$vps = VPS::with('operatingSystems.osGroup')->find($productId);
if (!$vps) {
session()->flash('error', __('Không tìm thấy thông tin VPS.'));
return;
}
// Store VPS info for confirmation message
$this->vpsInfo = $vps;
// Get OS groups that are assigned to this VPS product
$osGroupIds = $vps->operatingSystems->pluck('osGroup.id')->filter()->unique();
$this->osGroups = OsGroup::with(['os' => function ($query) use ($vps) {
// Only get OS versions that are assigned to this specific VPS product
$query->whereHas('vpsProducts', function ($q) use ($vps) {
$q->where('product_id', $vps->id)
->where('product_type', 4);
})->where('status', 1);
}])
->whereIn('id', $osGroupIds)
->where('status', 1)
->get();
// Get current OS and ModuleVmware info from invoice_item
if ($this->invoiceId) {
$this->invoiceItem = InvoiceItem::where('invoice_id', $this->invoiceId)
->where('product_id', $productId)
->where('product_type', 4)
->with('os.osGroup')
->first();
$this->currentOs = $this->invoiceItem?->os;
// Debug InvoiceItem info first
Log::info('InvoiceItem debug info', [
'invoice_id' => $this->invoiceId,
'product_id' => $productId,
'product_type' => 4,
'invoice_item_found' => $this->invoiceItem ? 'yes' : 'no',
'invoice_item_id' => $this->invoiceItem?->id ?? 'null',
'invoice_item_relid' => $this->invoiceItem?->relid ?? 'null',
'invoice_item_type_id' => $this->invoiceItem?->type_id ?? 'null',
'invoice_item_product_id' => $this->invoiceItem?->product_id ?? 'null',
'invoice_item_product_type' => $this->invoiceItem?->product_type ?? 'null'
]);
// Get ModuleVmware info to access IP address
if ($this->invoiceItem) {
// Try to get ModuleVmware directly using invoice_id and customer_id
$this->invoice = Invoice::find($this->invoiceId);
if ($this->invoice && $this->invoice->customer_id) {
$this->moduleVmware = ModuleVmware::where('customer_id', $this->invoice->customer_id)
->where('order_id', $this->invoiceId)
->with('ipAddress')
->first();
// If not found by order_id, try to find by customer_id and product info
if (!$this->moduleVmware) {
$this->moduleVmware = ModuleVmware::where('customer_id', $this->invoice->customer_id)
->with('ipAddress')
->first();
}
}
// Try to get IP from vCenter API first, fallback to database
$this->vpsIpAddress = $this->getIPFromVCenter() ?? $this->moduleVmware?->ipAddress?->ip ?? null;
} else {
Log::warning('InvoiceItem not found', [
'invoice_id' => $this->invoiceId,
'product_id' => $productId,
'product_type' => 4
]);
}
}
} catch (\Exception $e) {
Log::error('Error loading OS data', [
'product_id' => $productId,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
session()->flash('error', __('Có lỗi xảy ra khi tải dữ liệu hệ điều hành.'));
} finally {
$this->isLoading = false;
}
}
public function updatedSelectedOsGroupId($value)
{
// Reset selected OS when changing group
$this->selectedOsId = null;
$this->selectedOsDetails = null;
$this->isLoading = true;
try {
if ($value) {
$this->operatingSystems = Os::where('os_group_id', $value)
->where('status', 1)
->whereHas('vpsProducts', function ($query) {
$query->where('product_id', $this->productId)
->where('product_type', 4);
})
->with('osGroup')
->get();
Log::info('Operating systems loaded for group', [
'os_group_id' => $value,
'os_count' => is_object($this->operatingSystems) ? $this->operatingSystems->count() : count($this->operatingSystems),
'selected_os_id' => $this->selectedOsId
]);
} else {
$this->operatingSystems = collect();
}
} catch (\Exception $e) {
Log::error('Error loading operating systems', [
'os_group_id' => $value,
'error' => $e->getMessage()
]);
session()->flash('error', __('Có lỗi xảy ra khi tải danh sách phiên bản hệ điều hành.'));
$this->operatingSystems = collect();
} finally {
$this->isLoading = false;
}
}
public function updatedSelectedOsId($value)
{
Log::info('Selected OS ID updated', [
'selected_os_id' => $value,
'is_loading' => $this->isLoading
]);
// Store selected OS details for confirmation message
if ($value) {
$this->selectedOsDetails = Os::with('osGroup')->find($value);
Log::info('OS details loaded', [
'os_id' => $value,
'os_name' => $this->selectedOsDetails?->osGroup?->name ?? 'null',
'os_version' => $this->selectedOsDetails?->version ?? 'null'
]);
} else {
$this->selectedOsDetails = null;
}
}
public function confirmReinstall()
{
Log::info('confirmReinstall called', [
'selected_os_id' => $this->selectedOsId,
'invoice_id' => $this->invoiceId,
'is_loading' => $this->isLoading
]);
if (!$this->selectedOsId) {
session()->flash('error', __('Vui lòng chọn hệ điều hành.'));
return;
}
if (!$this->invoiceId) {
session()->flash('error', __('Thiếu thông tin hóa đơn.'));
return;
}
$this->isLoading = true;
try {
$response = Http::withoutVerifying()
->withHeaders([
'X-CSRF-TOKEN' => csrf_token(),
'Accept' => 'application/json',
'Content-Type' => 'application/json',
])
->post(route('service.reinstall.vm'), [
'invoice_id' => $this->invoiceId,
'os_id' => $this->selectedOsId,
]);
Log::info('Reinstall response', [
'status' => $response->status(),
'body' => $response->body()
]);
if ($response->successful()) {
session()->flash('success', __('Yêu cầu cài lại hệ điều hành đã được gửi thành công.'));
$this->dispatch('close-reinstall-modal');
} else {
$errorMessage = $response->json('message') ?? __('Có lỗi xảy ra khi gửi yêu cầu.');
session()->flash('error', $errorMessage);
}
} catch (\Exception $e) {
Log::error('Error during reinstall request', [
'invoice_id' => $this->invoiceId,
'os_id' => $this->selectedOsId,
'error' => $e->getMessage()
]);
session()->flash('error', __('Có lỗi xảy ra khi gửi yêu cầu cài lại.'));
} finally {
$this->isLoading = false;
}
}
/**
* Get IP address from vCenter API
*/
private function getIPFromVCenter()
{
try {
if (!$this->moduleVmware || !$this->moduleVmware->vmware_id) {
Log::warning('No ModuleVmware or vmware_id found for vCenter IP lookup');
return null;
}
// Get vCenter configuration from ModuleVmware
$vcenter = $this->moduleVmware->ipAddress?->host?->datacenter?->vCenter;
if (!$vcenter) {
Log::warning('No vCenter configuration found for ModuleVmware', [
'module_vmware_id' => $this->moduleVmware->id,
'vmware_id' => $this->moduleVmware->vmware_id
]);
return null;
}
// Initialize vCenter service
$vCenterService = new \App\Http\Services\vCenter();
// Login to vCenter
$loginSuccess = $vCenterService->loginServer($vcenter);
if (!$loginSuccess) {
Log::warning('Failed to login to vCenter for IP lookup', [
'vmware_id' => $this->moduleVmware->vmware_id,
'vcenter_id' => $vcenter->id
]);
return null;
}
// Get IP address from vCenter
$ipInfo = $vCenterService->GetIPAddressVirtualMachine($this->moduleVmware->vmware_id);
Log::info('vCenter IP lookup result', [
'vmware_id' => $this->moduleVmware->vmware_id,
'ip_info' => $ipInfo
]);
if (is_array($ipInfo) && isset($ipInfo['error']) && $ipInfo['error'] === false) {
// Try different possible structures
if (isset($ipInfo['data']['ip_address'])) {
return $ipInfo['data']['ip_address'];
} elseif (isset($ipInfo['data']['value']['ip_address'])) {
return $ipInfo['data']['value']['ip_address'];
}
}
Log::warning('No valid IP address found in vCenter response', [
'vmware_id' => $this->moduleVmware->vmware_id,
'ip_info' => $ipInfo
]);
return null;
} catch (\Exception $e) {
Log::error('Error getting IP from vCenter', [
'vmware_id' => $this->moduleVmware?->vmware_id ?? 'unknown',
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
]);
return null;
}
}
public function render()
{
return view('livewire.v-p-s-reinstall-modal');
}
}
@@ -1,55 +0,0 @@
<?php
namespace App\Http\Livewire;
use App\Repositories\Module\Datacenter\DatacenterRepositoryInterface;
use App\Repositories\vCenter\vCenterRepositoryInterface;
use Livewire\Component;
class VcenterDatacenterSelect extends Component
{
public $vcenter_id;
public $datacenter_id;
public $vcenters = [];
public $datacenters = [];
protected $vCenterRepository;
protected $datacenterRepository;
public function boot(
vCenterRepositoryInterface $vCenterRepository,
DatacenterRepositoryInterface $datacenterRepository
)
{
$this->vCenterRepository = $vCenterRepository;
$this->datacenterRepository = $datacenterRepository;
}
public function mount()
{
$this->vcenters = $this->vCenterRepository->filter(['status' => 1], [], [], ['id', 'host']);
}
public function updatedVcenterId($value)
{
if (!empty($value)) {
$this->datacenters = $this->datacenterRepository->filter(
['vcenter_id' => $value],
[],
[],
['id', 'name']
);
} else {
$this->datacenters = [];
}
$this->datacenter_id = null; // reset datacenter nếu vcenter đổi
}
public function render()
{
return view('livewire.vcenter-datacenter-select',
[
'vcenters' => $this->vcenters
]);
}
}
+46
View File
@@ -0,0 +1,46 @@
<?php
namespace App\Mail;
use App\Models\Customer;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class VmSuspendedMail extends Mailable implements ShouldQueue
{
use Queueable, SerializesModels;
public function __construct(
public readonly Customer $customer,
public readonly string $vmName,
public readonly string $vmwareId,
) {}
public function envelope(): Envelope
{
return new Envelope(
subject: '[Thế Giới Số] Dịch vụ Cloud VPS của bạn đã bị tạm ngưng',
);
}
public function content(): Content
{
return new Content(
view: 'emails.customer.vm-suspended',
with: [
'customerName' => $this->customer->fullname,
'vmName' => $this->vmName,
'vmwareId' => $this->vmwareId,
],
);
}
public function attachments(): array
{
return [];
}
}
+1
View File
@@ -22,6 +22,7 @@ return [
'http://localhost:3000',
'http://localhost:5173',
'http://localhost:5174',
'http://localhost:4173',
'https://portal.tgs.com.vn',
'https://demo.client.tgs.com.vn',
'https://demo.admin.tgs.com.vn',
+3 -2
View File
@@ -16,13 +16,14 @@ class DatabaseSeeder extends Seeder
public function run(): void
{
$this->call(BankTransferPaymentSeeder::class);
$this->call(AdminSeeder::class);
$this->call(RoleSeeder::class);
$this->call(RolesAndPermissions::class);
$user = User::find(1);
$user->assignRole('Full Administrator');
$this->call(TemplateSeeder::class);
$this->call(Setting::class);
$this->call(ControlPanelSeeder::class);
$this->call(VPSProductSeeder::class);
// $this->call(ControlPanelSeeder::class);
// $this->call(VPSProductSeeder::class);
}
}
@@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dịch vụ Cloud VPS bị tạm ngưng</title>
<style>
* { box-sizing: border-box; }
body { font-family: 'Segoe UI', Arial, sans-serif; background: #f0f4f8; margin: 0; padding: 0; }
.wrapper { max-width: 620px; margin: 30px auto; }
.card { background: #fff; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 20px rgba(0,0,0,0.08); }
.header { background: linear-gradient(135deg, #d97706 0%, #b45309 100%); padding: 36px 40px; text-align: center; }
.header .icon { font-size: 48px; margin-bottom: 12px; }
.header h1 { color: #fff; margin: 0 0 6px; font-size: 24px; font-weight: 700; }
.header p { color: #fde68a; margin: 0; font-size: 14px; }
.body { padding: 36px 40px; }
.greeting { font-size: 16px; color: #374151; margin-bottom: 20px; line-height: 1.6; }
.section-label { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: #9ca3af; margin: 28px 0 10px; }
.info-table { width: 100%; border-collapse: collapse; background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; }
.info-table td { padding: 10px 16px; font-size: 14px; border-bottom: 1px solid #f3f4f6; }
.info-table tr:last-child td { border-bottom: none; }
.info-table td:first-child { color: #6b7280; width: 45%; }
.info-table td:last-child { font-weight: 600; color: #111827; }
.warning-box { background: #fffbeb; border: 1.5px solid #f59e0b; border-radius: 10px; padding: 20px 24px; margin-top: 20px; }
.warning-box p { margin: 0 0 10px; font-size: 14px; color: #92400e; line-height: 1.7; }
.warning-box p:last-child { margin: 0; }
.danger-notice { background: #fef2f2; border-left: 3px solid #ef4444; padding: 12px 16px; border-radius: 0 6px 6px 0; font-size: 13px; color: #991b1b; margin-top: 24px; line-height: 1.6; font-weight: 600; }
.cta { text-align: center; margin-top: 28px; }
.cta a { display: inline-block; background: #2563eb; color: #fff; text-decoration: none; padding: 12px 32px; border-radius: 8px; font-size: 15px; font-weight: 600; }
.footer { text-align: center; padding: 20px 40px; border-top: 1px solid #f3f4f6; font-size: 12px; color: #9ca3af; }
</style>
</head>
<body>
<div class="wrapper">
<div class="card">
<div class="header">
<div class="icon">⚠️</div>
<h1>Dịch vụ Cloud VPS đã bị tạm ngưng</h1>
<p>Vui lòng thanh toán để khôi phục dịch vụ</p>
</div>
<div class="body">
<p class="greeting">
Xin chào <strong>{{ $customerName ?? 'Quý khách' }}</strong>,<br>
Dịch vụ Cloud VPS của bạn hiện đã bị <strong>tạm ngưng (Suspend)</strong> do chưa thanh toán gia hạn.
Vui lòng nhanh chóng liên hệ hoặc thanh toán để tránh mất dữ liệu.
</p>
<div class="section-label">Thông tin dịch vụ bị tạm ngưng</div>
<table class="info-table">
<tr><td>Tên dịch vụ</td><td>{{ $vmName }}</td></tr>
<tr><td>VM ID</td><td>{{ $vmwareId }}</td></tr>
<tr><td>Thời gian tạm ngưng</td><td>{{ now()->setTimezone('Asia/Ho_Chi_Minh')->format('d/m/Y H:i') }}</td></tr>
</table>
<div class="warning-box">
<p>⏰ <strong>Sau 30 ngày kể từ ngày tạm ngưng</strong>, nếu không có thanh toán, dịch vụ của bạn sẽ bị <strong>xóa vĩnh viễn</strong> và toàn bộ dữ liệu sẽ không thể khôi phục.</p>
<p>💡 Vui lòng đăng nhập vào cổng khách hàng để gia hạn dịch vụ hoặc liên hệ bộ phận hỗ trợ để được giải quyết kịp thời.</p>
</div>
<div class="danger-notice">
🚨 CẢNH BÁO: Sau 30 ngày không thanh toán, toàn bộ dữ liệu trên máy chủ ảo sẽ bị XÓA VĨNH VIỄN và không thể phục hồi.
</div>
<div class="cta">
<a href="{{ config('app.client_url', '#') }}/dashboard">Đăng nhập thanh toán ngay</a>
</div>
</div>
<div class="footer">
Email này được gửi tự động từ hệ thống Thế Giới Số. Vui lòng không trả lời email này.<br>
Nếu cần hỗ trợ, liên hệ: <strong>support@thegioiso.vn</strong>
</div>
</div>
</div>
</body>
</html>
+1 -9
View File
@@ -45,15 +45,6 @@ use Illuminate\Support\Facades\Route;
*/
Route::prefix('v1')
->group(function () {
Route::get('/debug-customers', function () {
$customers = \App\Models\Customer::orderByDesc('created_at')
->limit(5)
->get(['id', 'uuid', 'fullname', 'email', 'total', 'created_at']);
return response()->json($customers);
});
})
->group(function () {
// Invoice Test — Send invoice email with PDF attachment
Route::prefix('invoice-test')
@@ -398,6 +389,7 @@ Route::prefix('v1')
Route::post('vm/power-off', 'powerOff')->middleware('perm:module-vcenter.edit');
Route::post('vm/restart', 'restart')->middleware('perm:module-vcenter.edit');
Route::post('vm/snapshot', 'snapshot')->middleware('perm:module-vcenter.edit');
Route::post('vm/suspend', 'suspend')->middleware('perm:module-vcenter.edit');
Route::get('vms/powered-off', 'getPoweredOffVms')->middleware('perm:module-vcenter.view');
});