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:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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
@@ -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');
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user