From 4f2fdbaaa5807c1214fb7780aed71317ed20ec5a Mon Sep 17 00:00:00 2001 From: pbao9 Date: Sat, 17 Jan 2026 11:27:54 +0700 Subject: [PATCH] update change port --- app/Http/.DS_Store | Bin 0 -> 6148 bytes .../frontend/ServiceController.php | 15 +---- app/Http/Services/vCenter.php | 46 ++++++++----- app/Jobs/VirtualMachine/.DS_Store | Bin 0 -> 6148 bytes .../Action/ChangeRemotePortJob.php | 26 +------- .../Create/ConfigCLICommand.php | 5 -- .../Create/CreateVirtualMachineJob.php | 62 +----------------- .../VirtualMachine/Sync/SyncDatastore.php | 7 -- app/Models/Invoice.php | 14 ---- app/Models/Order.php | 4 ++ .../Components/Module/OsServerVersion.php | 6 +- .../views/frontend/service/console.blade.php | 10 +++ .../partials/configuration-info.blade.php | 2 +- .../partials/transaction-info.blade.php | 20 +++++- 14 files changed, 70 insertions(+), 147 deletions(-) create mode 100644 app/Http/.DS_Store create mode 100644 app/Jobs/VirtualMachine/.DS_Store diff --git a/app/Http/.DS_Store b/app/Http/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..4121233479517c1a8c630e6e2a2b2f85505194c3 GIT binary patch literal 6148 zcmeHK%}T>S5Z-O8O({YS3OxqA7Ob@%ikA@U3mDOZN=;1AV9b^zZ4RZ7v%Zi|;`2DO zy8%mc@FZeqVDrt+&+g6#*&oIjcjmz{VywPm!?6yI=?R)oG6mIV2^UU$mTkKs4 z5&Mp;KgsDu3f)GOPZlWX*g(K!kkjY$EIXWOyv$tHHj8FU4Xf#^& zt>t()0DU}MtvbfR;nCUU^d){x#G4|M1MgDyG*<8q%0^uaH%(%ZJb|yutKtcX0b+m{ zAO^OF0euD--R&uvDkTPpfgdt}`-24y(biZhlv@XU@cNAY79tAh_?AGlG};WjNd3e@Z&>oroute('service.index')->with('error', 'Không tìm thấy dịch vụ.'); } - // Kiểm tra quyền truy cập - chỉ cho phép customer sở hữu if ($invoice->customer_id !== customer()->id) { return redirect()->route('service.index')->with('error', 'Bạn không có quyền truy cập dịch vụ này.'); } - // Lấy thông tin VM nếu có - WITH CACHING AND BACKGROUND UPDATE $vmInfo = null; if ($invoice->vm_id) { $vmInfo = $this->getVMInfoFromVCenter($invoice, $vCenterService); - - // Dispatch background job to update VM info if cache is stale $this->dispatchVMInfoUpdateJob($invoice); } - $title = "Chi tiết dịch vụ #" . $invoice->id; - + dd($vmInfo); return view('frontend.service.show', [ 'title' => $title, 'invoice' => $invoice, @@ -82,11 +77,9 @@ class ServiceController extends Controller */ private function getVMInfoFromVCenter($invoice, $vCenterService) { - // Cache key for this VM info $cacheKey = "vm_info_{$invoice->vm_id}"; $cacheDuration = 300; // 5 minutes - // Try to get from cache first if (Cache::has($cacheKey)) { return Cache::get($cacheKey); } @@ -118,7 +111,6 @@ class ServiceController extends Controller return $defaultInfo; } - // Login vào vCenter - with timeout $loginSuccess = false; try { $loginSuccess = $vCenterService->loginServer($vcenter); @@ -132,7 +124,6 @@ class ServiceController extends Controller return $defaultInfo; } - // Lấy thông tin VM từ vCenter API - with timeout handling $vmDetail = null; $ipInfo = null; @@ -180,7 +171,7 @@ class ServiceController extends Controller 'cpu' => $cpuCount . ' cores', 'ram' => $memoryGB . ' GB', 'disk' => $diskSizeGB . ' GB', - 'uptime' => 'N/A', // Có thể lấy từ guest info nếu cần + 'uptime' => 'N/A', 'last_updated' => now()->format('Y-m-d H:i:s'), 'vm_name' => $vmData['name'] ?? 'Unknown', 'vm_folder' => $vmData['folder'] ?? 'Unknown', @@ -611,7 +602,7 @@ class ServiceController extends Controller $validated = Validator::make($request->all(), [ 'product_id' => 'required|string|max:255', 'new_port' => 'required|string|min:4|max:128', - 'password' => 'required|string|min:8|max:128', + 'password' => 'required|string|min:4|max:128', ]); if ($validated->fails()) { return response()->json(['error' => true, 'message' => __('Dữ liệu không hợp lệ'), 'errors' => $validated->errors()], 422); diff --git a/app/Http/Services/vCenter.php b/app/Http/Services/vCenter.php index ba40b8b..7c44186 100644 --- a/app/Http/Services/vCenter.php +++ b/app/Http/Services/vCenter.php @@ -898,11 +898,6 @@ PS; ]) ->patch("https://{$this->apiUri}/rest/vcenter/vm/{$vmId}/hardware/cpu", $payload); - Log::info("📥 Response CPU", [ - 'status' => $response->status(), - 'body' => $response->body() - ]); - if ($response->successful()) { Log::info("✅ API call cập nhật CPU thành công cho VM {$vmId}"); return true; @@ -938,8 +933,6 @@ PS; ] ]; - Log::info("📤 Payload RAM", ['payload' => $payload]); - $response = Http::withoutVerifying() ->timeout(30) ->withHeaders([ @@ -948,11 +941,6 @@ PS; ]) ->patch("https://{$this->apiUri}/rest/vcenter/vm/{$vmId}/hardware/memory", $payload); - Log::info("📥 Response RAM", [ - 'status' => $response->status(), - 'body' => $response->body() - ]); - if ($response->successful()) { Log::info("✅ API call cập nhật RAM thành công cho VM {$vmId}"); return true; @@ -2029,8 +2017,7 @@ PS; // Kiểm tra hệ điều hành if (str_contains($this->os, "Windows")) { $user = $data['user'] ?? 'administartor'; - $this->process = new WinRMClient($ip, $user, $password); - ; + $this->process = new WinRMClient($ip, $user, $password);; return $this->process; } @@ -2345,7 +2332,6 @@ PS; 'error' => false, 'message' => 'Test addon update completed' ]; - } catch (\Exception $e) { Log::error("🧪 TEST: Lỗi trong test addon update: " . $e->getMessage()); return [ @@ -2379,7 +2365,6 @@ PS; 'message' => 'Test getSnapShot completed', 'result' => $result ]; - } catch (\Exception $e) { Log::error("🧪 TEST: Lỗi trong test getSnapShot: " . $e->getMessage()); return [ @@ -2417,7 +2402,6 @@ PS; $dns = $product->ipAddress->dns ?? '8.8.8.8'; $vmware_name = $product->name ?? $vmware_id; $vCenter = $product->ipAddress->host->datacenter->vCenter ?? null; - } elseif ($product && isset($product->IpAddress) && $product->IpAddress) { Log::info('✅ Using old Product structure for assignIPWindows', [ 'product_id' => $product->id, @@ -2482,7 +2466,6 @@ PS; 'output' => $output ]; } - } catch (\Exception $e) { Log::channel('Custom')->error("Exception in assignIPWindows: " . $e->getMessage()); return [ @@ -2659,4 +2642,31 @@ PS; return $snapshots; } + + + /* + * Get Window Remote Port + */ + public function getWindowsRdpPort(string $vmwareId, string $guestUser, string $guestPassword): ?int + { + $ps = "(Get-ItemProperty 'HKLM:\\System\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp' -Name PortNumber).PortNumber"; + + $cmd = "Invoke-VMScript -VM '$vmwareId' " + . "-ScriptText \"$ps\" " + . "-ScriptType Powershell " + . "-GuestUser '$guestUser' " + . "-GuestPassword '$guestPassword'"; + + $output = $this->Command($cmd); + + if (!$output || str_contains($output, 'Login_Fail')) { + return null; + } + + if (preg_match('/\b(\d{2,5})\b/', $output, $m)) { + return (int) $m[1]; + } + + return null; + } } diff --git a/app/Jobs/VirtualMachine/.DS_Store b/app/Jobs/VirtualMachine/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..c12b39a028358a9375204494d5f4ace8efea9dec GIT binary patch literal 6148 zcmeHKyGjH>5Ukck4lcRSaDKr*SkChc_6HO_Ok{;a1+$I(EgoY;Oaw?fcUWumP~5JL1cSrTM=5!Y(S}XmQ3H_8-Gxe0d$87umlDoO^{^9I(d& z=6s%s4g)?h;_=719+yc0DIf);fE17drzuc%bhSQ>E|wIK0%xp%e;*p%u`iqw7B};py4k8jvACV_7U{4) zQKJ-)0&@i}bGh;Qe@FkL|DThzk^)lTtQ4?rH|(~2Qq|VQQsBQ8I0E{R8gT#s literal 0 HcmV?d00001 diff --git a/app/Jobs/VirtualMachine/Action/ChangeRemotePortJob.php b/app/Jobs/VirtualMachine/Action/ChangeRemotePortJob.php index 1398d90..a6903b3 100644 --- a/app/Jobs/VirtualMachine/Action/ChangeRemotePortJob.php +++ b/app/Jobs/VirtualMachine/Action/ChangeRemotePortJob.php @@ -73,31 +73,21 @@ class ChangeRemotePortJob implements ShouldQueue throw new \RuntimeException('SSH runner login failed.'); } if (is_string($result)) { - // Script trả non-JSON → bắn lỗi kèm raw để debug throw new \RuntimeException('Script returned non-JSON output: ' . $result); } - /** - * Chuẩn hoá $row: - * - Script 1-VM: trả 1 object: ['vm'=>.., 'status'=>.., 'message'=>.., 'newPort'=>..] - * - (Phòng hờ) Nếu lỡ trả array nhiều item, chọn item có 'vm' trùng $vmName, không thì lấy phần tử đầu. - */ $row = null; if (is_array($result)) { - // case: object đơn if (isset($result['vm']) && isset($result['status'])) { $row = $result; } else { - // case: mảng các object - // Tìm theo vmName foreach ($result as $it) { if (is_array($it) && (($it['vm'] ?? null) === $vmName)) { $row = $it; break; } } - // fallback: phần tử đầu tiên nếu chưa có if ($row === null && isset($result[0]) && is_array($result[0])) { $row = $result[0]; } @@ -109,33 +99,23 @@ class ChangeRemotePortJob implements ShouldQueue throw new \RuntimeException('Invalid script response.'); } - // Đọc trường returned $status = strtolower((string) ($row['status'] ?? '')); $message = (string) ($row['message'] ?? ''); $newPort = (string) ($row['newPort'] ?? $this->newPort); - // Xét thành công: status === 'ok' $ok = ($status === 'ok'); - Log::info('ChangeRemotePortJob: script result', [ - 'vm' => $vmName, - 'status' => $status, - 'message' => $message, - 'newPort' => $newPort, - 'raw' => $row, + ModuleVmware::where('id', $product->id)->update([ + 'port' => (int) $newPort, + 'updated_at' => now(), ]); if (!$ok) { - // Nếu muốn cho phép 'warning' pass, có thể đổi điều kiện ở trên hoặc thêm nhánh dưới: - // if ($status === 'warning' && str_contains(strtolower($message), 'rdp port is now')) { $ok = true; } throw new \RuntimeException($message !== '' ? $message : ('Change RDP port failed with status: ' . $status)); } - // Gửi mail thông báo Mail::to($this->email)->send( new ChangePortRemote(vmName: $vmName, newPort: (string) $newPort) ); - - Log::info('ChangeRemotePortJob: success', ['vm' => $vmName, 'newPort' => $newPort]); } } diff --git a/app/Jobs/VirtualMachine/Create/ConfigCLICommand.php b/app/Jobs/VirtualMachine/Create/ConfigCLICommand.php index 17e2865..abe8402 100644 --- a/app/Jobs/VirtualMachine/Create/ConfigCLICommand.php +++ b/app/Jobs/VirtualMachine/Create/ConfigCLICommand.php @@ -40,15 +40,10 @@ class ConfigCLICommand implements ShouldQueue $usersName = ''; $port = ''; - // Lưu password vào database ngay khi tạo try { $vm = \App\Models\ModuleVmware::where('vmware_id', $this->vmware_id)->first(); if ($vm) { $vm->update(['password' => \App\Http\Facade\SignRSA::EncryptFunc($PasswordVirtualMachine)]); - Log::info('✅ Đã lưu password vào database', [ - 'vm_id' => $this->vmware_id, - 'password' => $PasswordVirtualMachine - ]); } } catch (\Exception $e) { Log::error('❌ Lỗi khi lưu password: ' . $e->getMessage()); diff --git a/app/Jobs/VirtualMachine/Create/CreateVirtualMachineJob.php b/app/Jobs/VirtualMachine/Create/CreateVirtualMachineJob.php index 995dbb8..688d544 100644 --- a/app/Jobs/VirtualMachine/Create/CreateVirtualMachineJob.php +++ b/app/Jobs/VirtualMachine/Create/CreateVirtualMachineJob.php @@ -43,7 +43,7 @@ class CreateVirtualMachineJob implements ShouldQueue public function handle(vCenter $vCenter, Telegram $telegram) { try { - $product = $this->item->getProductAttribute(); // dùng helper trong OrderItem + $product = $this->item->getProductAttribute(); if (!$product) { $errorMessage = "Không tìm thấy sản phẩm từ OrderItem #{$this->item->id}"; @@ -139,22 +139,6 @@ class CreateVirtualMachineJob implements ShouldQueue 'addon' => $addonData, ]); - Log::info('Thông tin vCenter khi tạo máy ảo', [ - 'product' => $product, - 'item' => $this->item, - 'host_id' => $host?->id, - 'host_name' => $host?->name, - 'host_vcenter_id' => $host?->vcenter_id, - 'datacenter_id' => $datacenter?->id, - 'datacenter_name' => $datacenter?->name, - 'datacenter_vcenter_id' => $datacenter?->vcenter_id, - 'vcenter' => $vcenter, - 'vcenter_id' => $vcenter?->id, - 'vcenter_host' => $vcenter?->host, - 'vcenter_user' => $vcenter?->user, - 'vcenter_password' => SignRSA::DecryptFunc($vcenter?->password) - ]); - if (is_array($result) && ($result['error'] ?? false)) { $errorMessage = 'Tạo máy ảo thất bại: ' . json_encode($result); Log::warning($errorMessage); @@ -214,11 +198,9 @@ class CreateVirtualMachineJob implements ShouldQueue $customer = $this->item->order->customer; $serverName = $this->item->server_name ?? 'VM-' . $vmId; - // 1. Lấy IP available từ IPPool $networkId = $product->ipAddress?->module_network_id ?? 1; $availableIP = $this->getAvailableIPFromPool($networkId); - // 2. Tạo ModuleIpAddress với IP từ pool $moduleIpAddress = ModuleIpAddress::create([ 'vm_name' => $serverName, 'module_host_id' => $host->id, @@ -230,7 +212,6 @@ class CreateVirtualMachineJob implements ShouldQueue 'status' => 1 // 1 = Assigned ]); - // 3. Tạo ModuleVmware với IP address vừa tạo $moduleVmware = ModuleVmware::create([ 'name' => $serverName, 'module_ip_address_id' => $moduleIpAddress->id, @@ -249,26 +230,8 @@ class CreateVirtualMachineJob implements ShouldQueue 'status' => 1 ]); - Log::info('✅ Lưu dữ liệu VM và IP address thành công', [ - 'vmware_id' => $vmId, - 'module_vmware_id' => $moduleVmware->id, - 'module_ip_address_id' => $moduleIpAddress->id, - 'assigned_ip' => $availableIP, - 'name' => $serverName, - 'order_item_id' => $this->item->id - ]); - - // Dispatch AssignIPJob để chạy sau 5 phút AssignIPJob::dispatch($vmId, $moduleVmware->id, $moduleIpAddress->ip) ->delay(now()->addMinutes(5)); - - Log::info('✅ Đã dispatch AssignIPJob để chạy sau 5 phút', [ - 'vm_id' => $vmId, - 'module_vmware_id' => $moduleVmware->id, - 'assigned_ip' => $moduleIpAddress->ip, - 'delay_minutes' => 5 - ]); - } catch (\Exception $e) { Log::error('❌ Lỗi khi lưu dữ liệu VM và IP address: ' . $e->getMessage(), [ 'vmware_id' => $vmId, @@ -286,7 +249,6 @@ class CreateVirtualMachineJob implements ShouldQueue } } - // Gửi thông báo thành công $telegram->SendJobRunInfo( 'CreateVirtualMachineJob', 'success', @@ -325,9 +287,6 @@ class CreateVirtualMachineJob implements ShouldQueue } } - /** - * Get available IP from IPPool that doesn't exist in ModuleIpAddress - */ private function getAvailableIPFromPool(int $networkId): string { // Lấy tất cả IP từ IPPool theo network_id, order by ip_address @@ -335,39 +294,20 @@ class CreateVirtualMachineJob implements ShouldQueue ->orderBy('ip_address') ->get(); - Log::info('✅ Lấy IP available từ pool', [ - 'ip_pool_list' => $ipPoolList, - 'network_id' => $networkId - ]); - if ($ipPoolList->isEmpty()) { // Nếu không có IP trong pool, trả về IP default return '0.0.0.0'; } - // Lấy tất cả IP đã được assign trong ModuleIpAddress $assignedIPs = ModuleIpAddress::where('module_network_id', $networkId) ->pluck('ip') ->toArray(); - // Tìm IP đầu tiên chưa được assign foreach ($ipPoolList as $ipPool) { if (!in_array($ipPool->ip_address, $assignedIPs)) { - Log::info('✅ Lấy IP available từ pool', [ - 'ip' => $ipPool->ip_address, - 'network_id' => $networkId - ]); return $ipPool->ip_address; } } - - // Nếu tất cả IP đã được assign, log warning và trả về IP default - Log::warning('⚠️ Không có IP available trong pool', [ - 'network_id' => $networkId, - 'total_ips_in_pool' => $ipPoolList->count(), - 'assigned_ips' => count($assignedIPs) - ]); - return '0.0.0.0'; } diff --git a/app/Jobs/VirtualMachine/Sync/SyncDatastore.php b/app/Jobs/VirtualMachine/Sync/SyncDatastore.php index f6eeee5..35f24e1 100644 --- a/app/Jobs/VirtualMachine/Sync/SyncDatastore.php +++ b/app/Jobs/VirtualMachine/Sync/SyncDatastore.php @@ -63,16 +63,11 @@ class SyncDatastore implements ShouldQueue } $dataCenter = ModuleDatacenter::where('vcenter_id', $item->id)->select('id', 'datacenter_id')->get(); - Log::info('Datacenter SyncDatastore', ['data' => $dataCenter]); foreach ($dataCenter as $_item) { - $allHost = ModuleHost::where('module_datacenter_id', $_item->id)->select('id', 'name')->get(); - - Log::info('Allhost Data', ['data' => $allHost]); if ($allHost) { foreach ($allHost as $hostItem) { $datastore = $this->vCenter->loadDatastore($item, $hostItem->name); - Log::info('Datastore', ['data' => $datastore]); if ($datastore === "Login_Again") { $token = $this->vCenter->loginServer($item); @@ -81,8 +76,6 @@ class SyncDatastore implements ShouldQueue $this->vCenter->token = $token; } } - $datastore = $this->vCenter->loadDatastore($item, $hostItem->name); - if ($datastore) { foreach ($datastore as $_subItem) { diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 54febdd..ab79a67 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -12,20 +12,6 @@ class Invoice extends Model protected $table = 'invoice'; public $timestamps = true; - // protected $fillable = [ - // 'date', - // 'due_date', - // 'date_paid', - // 'sub_total', - // 'tax', - // 'total', - // 'payment_id', - // 'customer_id', - // 'invoice_type', - // 'note', - // 'status' - // ]; - protected $guarded = []; public function customer() { diff --git a/app/Models/Order.php b/app/Models/Order.php index a73ceaf..f5b91a3 100644 --- a/app/Models/Order.php +++ b/app/Models/Order.php @@ -63,4 +63,8 @@ class Order extends Model return $this->hasMany(OrderItem::class, 'order_id'); } + public function vmWare() + { + return $this->hasMany(\App\Models\ModuleVmware::class, 'order_id', 'id'); + } } diff --git a/app/View/Components/Module/OsServerVersion.php b/app/View/Components/Module/OsServerVersion.php index 35fe06e..8150ca1 100644 --- a/app/View/Components/Module/OsServerVersion.php +++ b/app/View/Components/Module/OsServerVersion.php @@ -11,9 +11,9 @@ use Illuminate\View\Component; class OsServerVersion extends Component { public function __construct( - public $model = null, public vCenter $vCenter, - public vCenterRepositoryInterface $vCenterRepository + public vCenterRepositoryInterface $vCenterRepository, + public $model = null ) { } @@ -25,7 +25,7 @@ class OsServerVersion extends Component protected function fetchVersions() { - $vCenter = optional($this->model?->osGroup?->host?->datacenter?->vCenter); + $vCenter = $this->model?->osGroup?->host?->datacenter?->vCenter; if (!$vCenter || !$vCenter->host) { return null; diff --git a/resources/views/frontend/service/console.blade.php b/resources/views/frontend/service/console.blade.php index 1f98eab..0367a42 100644 --- a/resources/views/frontend/service/console.blade.php +++ b/resources/views/frontend/service/console.blade.php @@ -92,10 +92,18 @@ }); let WMKSContainer = $('#wmksContainer'); + // Retry and connection state variables + var isConnected = false; + var retryCount = 0; + const maxRetries = 30; + const retryDelay = 500; // ms + var wmks = WMKS.createWMKS("wmksContainer", {}) .register(WMKS.CONST.Events.CONNECTION_STATE_CHANGE, function(event, data) { if (data.state === WMKS.CONST.ConnectionState.CONNECTED) { console.log("Connected successfully"); + isConnected = true; + retryCount = 0; // reset retries on success // Update status $('#statusDot').removeClass('status-connecting').addClass('status-connected'); @@ -105,6 +113,8 @@ $('#loadingState').addClass('hidden'); $('#wmksContainer').removeClass('hidden'); } else if (data.state === WMKS.CONST.ConnectionState.DISCONNECTED) { + console.log('WMKS disconnected'); + isConnected = false; $('#statusDot').removeClass('status-connecting status-connected').addClass('status-error'); $('#statusText').text('Disconnected'); } diff --git a/resources/views/frontend/service/partials/configuration-info.blade.php b/resources/views/frontend/service/partials/configuration-info.blade.php index 2445539..f9e6acf 100644 --- a/resources/views/frontend/service/partials/configuration-info.blade.php +++ b/resources/views/frontend/service/partials/configuration-info.blade.php @@ -3,7 +3,7 @@ $isWindows = Str::contains(Str::lower($vmInfo['os'] ?? ''), 'windows') || Str::contains(Str::lower($invoice->invoiceItem->first()->os->osGroup->name ?? ''), 'windows'); $vmIp = $vmInfo['ip_address'] ?? '103.42.56.177'; - $vmPort = $isWindows ? '3389' : '22'; + $vmPort = $invoice->order->vmWare->first()->port ?? '3389'; $vmUser = $isWindows ? 'administrator' : 'root'; $osName = $invoice->invoiceItem->first()->os->osGroup->name ?? 'Unknown'; $osVersion = $invoice->invoiceItem->first()->os->version ?? ''; diff --git a/resources/views/frontend/service/partials/transaction-info.blade.php b/resources/views/frontend/service/partials/transaction-info.blade.php index 7f96049..eabbc82 100644 --- a/resources/views/frontend/service/partials/transaction-info.blade.php +++ b/resources/views/frontend/service/partials/transaction-info.blade.php @@ -1,9 +1,17 @@ {{-- Transaction Info Partial - Optimized with date helpers --}} @php - $registrationDate = $invoice->date ? \Carbon\Carbon::parse($invoice->date)->format('d/m/Y') : 'N/A'; - $dueDate = $invoice->due_date ? \Carbon\Carbon::parse($invoice->due_date)->format('d/m/Y') : 'N/A'; + // Keep Carbon instances for calculations and formatted strings for display + $registrationCarbon = $invoice->date ? \Carbon\Carbon::parse($invoice->date) : null; + $dueCarbon = $invoice->due_date ? \Carbon\Carbon::parse($invoice->due_date) : null; + + $registrationDate = $registrationCarbon ? $registrationCarbon->format('d/m/Y') : 'N/A'; + $dueDate = $dueCarbon ? $dueCarbon->format('d/m/Y') : 'N/A'; + $totalAmount = number_format($invoice->total ?? 0, 0, ',', '.'); $isExpired = $invoice->status == 1; + + // Precompute days remaining (use 'N/A' when dates are missing) + $daysRemaining = $dueCarbon && $registrationCarbon ? $dueCarbon->diffInDays($registrationCarbon) : 'N/A'; @endphp
@@ -47,10 +55,16 @@ {{ $dueDate }}
+
+ {{ __('Số ngày còn lại') }} + {{ $daysRemaining }} Ngày +
+ + {{-- Auto Renewal Status --}}
{{ __('Trạng thái gia hạn tự động') }} OFF
- \ No newline at end of file +