diff --git a/INFRASTRUCTURE_ACTION_ITEMS.md b/INFRASTRUCTURE_ACTION_ITEMS.md new file mode 100644 index 0000000..dc5ef66 --- /dev/null +++ b/INFRASTRUCTURE_ACTION_ITEMS.md @@ -0,0 +1,118 @@ +# Infrastructure Management Module - Action Items + +## 🔴 P0 - CRITICAL (Ngay lập tức) + +### 1. Authorization - Implement Laravel Policies +- [ ] Tạo Policy classes cho: vCenter, Datacenter, Host, Datastore, Network, Folder +- [ ] Thêm middleware `can:` vào tất cả infrastructure routes trong `routes/admin.php` +- [ ] Xóa view-level RBAC checks trong Blade files +- [ ] Định nghĩa permission constants thay vì hard-coded strings + +### 2. Database - FK Constraints & Indexes +- [ ] Thêm FK constraint cho `module_folder.module_datacenter_id` +- [ ] Thêm index cho `module_network.module_host_id` +- [ ] Thêm index cho `module_datastore.module_host_id` +- [ ] Thêm composite index: `(vcenter_id, status)`, `(datacenter_id, status)` + +### 3. Database - Soft Deletes +- [ ] Thêm soft deletes cho bảng: module_vcenter, module_datacenter, module_host, module_datastore, module_network, module_folder +- [ ] Update Models thêm `SoftDeletes` trait +- [ ] Update Repositories xử lý soft delete queries + +--- + +## 🟡 P1 - HIGH PRIORITY (Trong 30 ngày) + +### 4. API Design - Versioning & Authentication +- [ ] Tạo API route group `/api/v1/` +- [ ] Implement API Resources/Transformers cho các entities +- [ ] Thêm Sanctum authentication cho API +- [ ] Thêm rate limiting cho sync endpoints + +### 5. Performance - Caching & Queries +- [ ] Implement Redis cache với tags theo vCenter/datacenter +- [ ] Fix cache invalidation khi có updates +- [ ] Thêm eager loading `with(['vCenter'])` trong controllers +- [ ] Tạo index cho `findByName` (full-text hoặc index) + +### 6. Tree Explorer - Lazy Loading +- [ ] Tạo API endpoint cho tree lazy-load: `/api/v1/infrastructure/tree` +- [ ] Implement expand-on-demand pattern +- [ ] Cache tree structure với short TTL + +--- + +## 🟢 P2 - MEDIUM PRIORITY (Trong 60 ngày) + +### 7. Domain Modeling - Refactoring +- [ ] Tạo module/Infrastructure/Resources thay thế presentation logic trong Models +- [ ] Di chuyển `getStatus()` HTML rendering từ Model sang Resource +- [ ] Thêm scope methods cho common queries + +### 8. Multi-tenancy Preparation +- [ ] Thêm cột `tenant_id` vào tất cả infrastructure tables +- [ ] Tạo global scope cho tenant filtering +- [ ] Update repositories handle tenant context + +### 9. Sync Jobs - Event Driven +- [ ] Tạo Infrastructure events: DatacenterSynced, HostSynced, etc. +- [ ] Implement event listeners cho cache invalidation +- [ ] Thêm queue prioritization cho sync jobs +- [ ] Implement job locking tránh race conditions + +--- + +## 🔵 P3 - FUTURE (60-90 ngày) + +### 10. Technical Debt +- [ ] Tạo constants class cho permissions +- [ ] Refactor password handling (统一 encryption approach) +- [ ] Thêm API documentation (Swagger/OpenAPI) +- [ ] Tách biệt Read/Write repositories + +### 11. SPA Migration Preparation +- [ ] Tạo dedicated API controllers cho SPA +- [ ] Implement proper JSON error responses +- [ ] Thêm pagination metadata vào responses +- [ ] Tạo API documentation + +--- + +## 📊 Progress Tracking + +| Category | Total | Completed | In Progress | Pending | +|----------|-------|-----------|-------------|---------| +| P0 - Critical | 10 | | | | +| P1 - High | 10 | | | | +| P2 - Medium | 10 | | | | +| P3 - Future | 4 | | | | + +--- + +## 📝 Notes + +### Database Migrations Needed: +``` +1. add_soft_deletes_to_infrastructure_tables.php +2. add_missing_indexes_and_fk.php +3. add_tenant_id_to_infrastructure_tables.php +``` + +### Files to Create: +``` +app/Policies/vCenterPolicy.php +app/Policies/DatacenterPolicy.php +app/Policies/HostPolicy.php +app/Policies/DatastorePolicy.php +app/Policies/NetworkPolicy.php +app/Policies/FolderPolicy.php +app/Http/Resources/Api/v1/vCenterResource.php +app/Http/Resources/Api/v1/DatacenterResource.php +app/Events/Infrastructure/DatacenterSynced.php +app/Listeners/Infrastructure/InvalidateDatacenterCache.php +``` + +### Key Dependencies: +- Laravel Sanctum (API auth) +- Redis (caching) +- Spatie Laravel-permission (RBAC) - optionally diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md new file mode 100644 index 0000000..05bc14d --- /dev/null +++ b/PROJECT_STRUCTURE.md @@ -0,0 +1,353 @@ +# Project Structure - Laravel Portal + +## 📊 Project Overview + +| Metric | Count | +|--------|-------| +| **Models** | 61 | +| **Controllers** | 46 | +| **Migrations** | 90 | +| **Views** | 311 | +| **Repositories** | 153 | +| **Routes** | 7 | + +--- + +## 🗂️ Directory Structure + +``` +portal/ +├── app/ +│ ├── Console/ # Artisan commands +│ │ ├── Commands/ +│ │ └── Kernel.php +│ │ +│ ├── Enums/ # Enum classes +│ │ ├── Order/ +│ │ ├── Product/ +│ │ ├── Client/ +│ │ └── Invoice/ +│ │ +│ ├── Events/ # Event classes +│ │ +│ ├── Exceptions/ # Exception handlers +│ │ +│ ├── Exports/ # Excel exports +│ │ +│ ├── Helpers/ # Helper functions +│ │ +│ ├── Http/ +│ │ ├── Controllers/ +│ │ │ ├── admin/ # Admin controllers +│ │ │ │ ├── ModulevCenterController.php +│ │ │ │ ├── ModuleHostingController.php +│ │ │ │ ├── CustomerController.php +│ │ │ │ ├── OrderController.php +│ │ │ │ ├── InvoiceController.php +│ │ │ │ ├── UserController.php +│ │ │ │ ├── AddonController.php +│ │ │ │ ├── TemplateController.php +│ │ │ │ ├── SupportController.php +│ │ │ │ ├── package/ # Package modules +│ │ │ │ │ ├── Datacenter.php +│ │ │ │ │ ├── Host.php +│ │ │ │ │ ├── Datastore.php +│ │ │ │ │ ├── Network.php +│ │ │ │ │ ├── Folder.php +│ │ │ │ │ ├── Group.php +│ │ │ │ │ ├── IPMapping.php +│ │ │ │ │ ├── OsVersion.php +│ │ │ │ │ └── vm/ +│ │ │ │ │ └── VirtualMachineController.php +│ │ │ │ ├── product/ +│ │ │ │ ├── vm/ +│ │ │ │ └── Api/ # API controllers +│ │ │ └── rest/ +│ │ ├── Middleware/ # Custom middleware +│ │ ├── Requests/ # Form requests +│ │ ├── Services/ # Services +│ │ │ ├── vCenter/ +│ │ │ │ ├── Client.php +│ │ │ │ ├── PowerShellService.php +│ │ │ │ ├── SnapshotService.php +│ │ │ │ ├── VirtualMachineService.php +│ │ │ │ └── ... +│ │ │ ├── Upload.php +│ │ │ └── ... +│ │ ├── Kernel.php +│ │ └── ... +│ │ +│ ├── Imports/ # Excel imports +│ │ +│ ├── Jobs/ # Queue jobs +│ │ ├── Hosting/ +│ │ ├── Mail/ +│ │ ├── Payment/ +│ │ ├── Ticket/ +│ │ └── VirtualMachine/ +│ │ └── Sync/ +│ │ ├── SyncDatacenter.php +│ │ ├── SyncHost.php +│ │ ├── SyncDatastore.php +│ │ ├── SyncNetwork.php +│ │ ├── SyncFolder.php +│ │ └── SyncAllPackage.php +│ │ +│ ├── Mail/ # Mails +│ │ └── Customer/ +│ │ +│ ├── Models/ # Eloquent models (61 files) +│ │ ├── User.php +│ │ ├── Customer.php +│ │ ├── Staff.php +│ │ ├── Order.php +│ │ ├── Invoice.php +│ │ ├── Product.php +│ │ ├── Hosting.php +│ │ ├── vCenter.php +│ │ ├── ModuleDatacenter.php +│ │ ├── ModuleHost.php +│ │ ├── ModuleDatastore.php +│ │ ├── ModuleNetwork.php +│ │ ├── ModuleFolder.php +│ │ ├── VirtualMachines.php +│ │ └── ... +│ │ +│ ├── Notifications/ +│ │ +│ ├── Observers/ # Model observers +│ │ +│ ├── Providers/ # Service providers +│ │ ├── AppServiceProvider.php +│ │ ├── AuthServiceProvider.php +│ │ ├── RouteServiceProvider.php +│ │ └── ... +│ │ +│ ├── Repositories/ # Repository pattern (153 files) +│ │ ├── vCenter/ +│ │ │ ├── vCenterRepository.php +│ │ │ ├── vCenterCachedRepository.php +│ │ │ └── vCenterRepositoryInterface.php +│ │ ├── Module/ +│ │ │ ├── Datacenter/ +│ │ │ ├── Host/ +│ │ │ ├── Datastore/ +│ │ │ ├── Network/ +│ │ │ ├── Folder/ +│ │ │ ├── IpAddress/ +│ │ │ ├── VirtualMachine/ +│ │ │ ├── Hosting/ +│ │ │ ├── HostingServer/ +│ │ │ ├── DnsServer/ +│ │ │ ├── Domain/ +│ │ │ ├── IPPool/ +│ │ │ └── Snapshot/ +│ │ ├── User/ +│ │ ├── Order/ +│ │ ├── Invoice/ +│ │ ├── Product/ +│ │ ├── Admin/ +│ │ └── ... +│ │ +│ ├── Services/ +│ │ +│ ├── Support/ +│ │ +│ ├── Traits/ +│ │ +│ └── View/ +│ └── Components/ # Blade components +│ └── Module/ +│ +├── bootstrap/ +│ ├── app.php +│ ├── cache/ +│ └── providers.php +│ +├── config/ # Configuration files (33 dirs) +│ ├── app.php +│ ├── auth.php +│ ├── cache.php +│ ├── database.php +│ ├── filesystems.php +│ ├── logging.php +│ ├── mail.php +│ ├── queue.php +│ ├── session.php +│ ├── queue.php +│ └── ... +│ +├── database/ +│ ├── factories/ +│ ├── migrations/ # 90 migration files +│ │ ├── 2024_01_31_165959_module_hosting_server.php +│ │ ├── 2024_02_01_080844_module_hosting.php +│ │ ├── 2024_02_01_082942_v_center.php +│ │ ├── 2024_02_01_090134_module_datacenter.php +│ │ ├── 2024_02_01_090537_module_host.php +│ │ ├── 2024_02_01_091327_module_folder.php +│ │ ├── 2024_02_01_091707_module_datastore.php +│ │ ├── 2024_02_01_092104_module_network.php +│ │ └── ... +│ └── seeders/ +│ +├── docs/ +│ +├── lang/ # Language files +│ ├── en/ +│ └── vi/ +│ +├── public/ # Public assets +│ ├── assets/ +│ ├── css/ +│ ├── js/ +│ ├── images/ +│ ├── build/ +│ ├── libraries/ +│ └── index.php +│ +├── resources/ +│ ├── views/ +│ │ ├── administrator/ # Admin views +│ │ │ ├── layout/ +│ │ │ ├── pages/ +│ │ │ │ ├── module/ # Module pages +│ │ │ │ │ ├── vcenter/ +│ │ │ │ │ ├── hosting/ +│ │ │ │ │ └── ippool/ +│ │ │ │ ├── package/ # Package pages +│ │ │ │ │ ├── vcenter/ +│ │ │ │ │ ├── datacenter/ +│ │ │ │ │ ├── host/ +│ │ │ │ │ ├── datastore/ +│ │ │ │ │ ├── network/ +│ │ │ │ │ ├── folder/ +│ │ │ │ │ ├── vm/ +│ │ │ │ │ ├── group/ +│ │ │ │ │ ├── ipaddress/ +│ │ │ │ │ └── version/ +│ │ │ │ ├── customer/ +│ │ │ │ ├── order/ +│ │ │ │ ├── invoice/ +│ │ │ │ ├── product/ +│ │ │ │ └── ... +│ │ │ └── blocks/ +│ │ ├── components/ # Blade components +│ │ ├── livewire/ # Livewire components +│ │ ├── emails/ # Email templates +│ │ ├── form/ +│ │ ├── vendor/ +│ │ └── errors/ +│ ├── js/ +│ ├── css/ +│ └── sass/ +│ +├── routes/ +│ ├── admin.php # Admin routes (30,829 bytes) +│ ├── api.php # API routes +│ ├── web.php # Web routes +│ ├── client.php # Client routes +│ ├── administrator.php +│ ├── channels.php +│ └── console.php +│ +├── scripts/ +│ +├── storage/ +│ ├── app/ +│ ├── framework/ +│ │ ├── cache/ +│ │ ├── sessions/ +│ │ └── views/ +│ └── logs/ +│ +├── tests/ +│ ├── Feature/ +│ ├── Unit/ +│ └── ... +│ +├── vendor/ # Composer dependencies +│ +├── .env # Environment config +├── .env.example +├── artisan +├── composer.json +├── package.json +├── vite.config.js +├── webpack.mix.js +├── docker-compose.yml +└── INFRASTRUCTURE_ACTION_ITEMS.md +``` + +--- + +## 🏗️ Architecture Overview + +### Pattern: Repository Pattern +``` +Controller → Repository Interface → Repository → Model +``` + +### Key Modules + +| Module | Purpose | Key Files | +|--------|---------|-----------| +| **vCenter Management** | Connect & manage vCenter servers | vCenter.php, ModulevCenterController | +| **Infrastructure** | Datacenter, Host, Datastore, Network, Folder | Module* models, package/* controllers | +| **Virtual Machines** | VM lifecycle management | VirtualMachines.php, Sync* jobs | +| **Orders & Billing** | Order processing, Invoices | Order.php, Invoice.php | +| **Customer Management** | Customer accounts, Auth | Customer.php, CustomerController | +| **Hosting** | Hosting services | ModuleHosting.php, Hosting.php | + +--- + +## 🎨 Tech Stack + +- **Backend**: Laravel 10.x +- **Frontend**: Bootstrap 5, jQuery AJAX, Livewire +- **Database**: MySQL +- **Cache**: Redis +- **Queue**: Database/Broadcast (laravel-echo-server) +- **Auth**: Session-based + Sanctum for API + +--- + +## 📁 Key Configuration Files + +| File | Purpose | +|------|---------| +| `config/app.php` | App name, locale, timezone | +| `config/database.php` | MySQL connection | +| `config/cache.php` | Redis cache config | +| `config/queue.php` | Queue connection | +| `config/sanctum.php` | API auth | +| `routes/admin.php` | All admin routes (~30KB) | + +--- + +## 🔗 Route Structure (routes/admin.php ~575 lines) + +``` +/admin +├── /vcenter → ModulevCenterController +├── /module +│ ├── /dns → ModuleDNSController +│ ├── /hosting → ModuleHostingController +│ └── /ippool → ModuleIPPoolController +├── /package +│ ├── /datacenter → DatacenterController +│ ├── /host → HostController +│ ├── /datastore → DatastoreController +│ ├── /network → NetworkController +│ ├── /folder → FolderController +│ ├── /vm → VirtualMachineController +│ └── ... +├── /customer → CustomerController +├── /order → OrderController +├── /invoice → InvoiceController +└── ... +``` + +--- + +*Generated: Mar 2026* diff --git a/app/Http/Controllers/Api/Service/OrderController.php b/app/Http/Controllers/Api/Service/OrderController.php index 07963e8..4942d15 100644 --- a/app/Http/Controllers/Api/Service/OrderController.php +++ b/app/Http/Controllers/Api/Service/OrderController.php @@ -7,6 +7,10 @@ use App\Http\Requests\Api\Order\PayOrderRequest; use App\Http\Requests\Api\Order\StoreOrderRequest; use App\Repositories\Api\Order\OrderRepository; use App\Services\Api\Order\OrderService; +use App\Enums\Order\OrderStatus; +use App\Enums\Invoice\InvoiceStatus; +use App\Models\Order as OrderModel; +use Carbon\Carbon; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -83,6 +87,305 @@ class OrderController extends Controller } } + public function getClientOrders(Request $request): JsonResponse + { + try { + $customer = $request->user(); + $orders = OrderModel::query() + ->with(['invoice']) + ->where('customer_id', $customer->id) + ->orderByDesc('created_at') + ->get(); + + $data = $orders->map(function ($order) { + return [ + 'id' => $order->id, + 'order_num' => $order->order_num, + 'status' => $order->status, + 'status_text' => OrderStatus::from((int) $order->status)->label(), + 'amount' => (float) $order->amount, + 'first_payment' => $order->first_payment, + 'next_payment' => $order->next_payment, + 'created_at' => $order->created_at?->toISOString(), + 'invoice' => [ + 'id' => $order->invoice?->id, + 'invoice_num' => $order->invoice?->invoice_num, + 'status' => $order->invoice?->status, + 'total' => $order->invoice?->total, + ], + ]; + }); + + return response()->json([ + 'success' => true, + 'data' => $data, + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => $e->getMessage(), + ], 400); + } + } + + public function getClientOrderDetail(Request $request, int $id): JsonResponse + { + try { + $customer = $request->user(); + $order = OrderModel::query() + ->with(['invoice', 'orderItems.product', 'orderItems.addons.addonItems', 'customer']) + ->where('customer_id', $customer->id) + ->where('id', $id) + ->first(); + + if (!$order) { + return response()->json([ + 'success' => false, + 'message' => 'Đơn hàng không tồn tại', + ], 404); + } + + return response()->json([ + 'success' => true, + 'data' => [ + 'id' => $order->id, + 'order_num' => $order->order_num, + 'status' => $order->status, + 'status_text' => OrderStatus::from((int) $order->status)->label(), + 'amount' => (float) $order->amount, + 'created_at' => $this->toIsoDate($order->created_at), + 'customer' => [ + 'id' => $order->customer?->id, + 'name' => $order->customer?->fullname, + 'email' => $order->customer?->email, + 'address' => $order->customer?->address, + ], + 'invoice' => [ + 'id' => $order->invoice?->id, + 'invoice_num' => $order->invoice?->invoice_num, + 'status' => $order->invoice?->status, + 'status_text' => $order->invoice?->status ? InvoiceStatus::from((int) $order->invoice->status)->label() : null, + 'sub_total' => $order->invoice?->sub_total, + 'tax' => $order->invoice?->tax, + 'total' => $order->invoice?->total, + 'due_date' => $this->toIsoDate($order->invoice?->due_date), + 'paid_at' => $this->toIsoDate($order->invoice?->date_paid), + ], + 'items' => $order->orderItems->map(function ($item) { + return [ + 'id' => $item->id, + 'product_name' => $item->product?->name, + 'billing_cycle' => $item->billing_cycle, + 'server_name' => $item->server_name, + 'price' => (float) $item->price, + 'addons' => $item->addons->map(function ($addon) { + return [ + 'id' => $addon->id, + 'name' => $addon->addonItems?->name, + 'qty' => $addon->qty, + 'price' => (float) $addon->price, + ]; + }), + ]; + }), + ], + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => $e->getMessage(), + ], 400); + } + } + + public function getClientInvoices(Request $request): JsonResponse + { + try { + $customer = $request->user(); + $orders = OrderModel::query() + ->with(['invoice', 'orderItems.product']) + ->where('customer_id', $customer->id) + ->whereNotNull('invoice_id') + ->orderByDesc('created_at') + ->get(); + + $data = $orders->map(function ($order) { + $firstItem = $order->orderItems->first(); + $invoice = $order->invoice; + + return [ + 'id' => $invoice?->id, + 'invoice_num' => $invoice?->invoice_num, + 'service_name' => $firstItem?->product?->name ?? ('Đơn hàng ' . $order->order_num), + 'amount' => (float) ($invoice?->total ?? $order->amount ?? 0), + 'status' => $invoice?->status, + 'status_text' => $invoice?->status ? InvoiceStatus::from((int) $invoice->status)->label() : null, + 'created_at' => $this->toIsoDate($invoice?->created_at ?? $order->created_at), + 'order_id' => $order->id, + 'order_num' => $order->order_num, + ]; + })->filter(fn($row) => !empty($row['id']))->values(); + + return response()->json([ + 'success' => true, + 'data' => $data, + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => $e->getMessage(), + ], 400); + } + } + + public function getClientInvoiceDetail(Request $request, int $id): JsonResponse + { + try { + $customer = $request->user(); + $order = OrderModel::query() + ->with(['invoice', 'orderItems.product', 'customer', 'vmWare.ipAddress', 'vmWare.os']) + ->where('customer_id', $customer->id) + ->where('invoice_id', $id) + ->first(); + + if (!$order || !$order->invoice) { + return response()->json([ + 'success' => false, + 'message' => 'Hóa đơn không tồn tại', + ], 404); + } + + $invoice = $order->invoice; + $firstItem = $order->orderItems->first(); + $vmware = $order->vmWare->first(); + + return response()->json([ + 'success' => true, + 'data' => [ + 'id' => $invoice->id, + 'invoice_num' => $invoice->invoice_num, + 'status' => $invoice->status, + 'status_text' => InvoiceStatus::from((int) $invoice->status)->label(), + 'sub_total' => (float) ($invoice->sub_total ?? 0), + 'tax' => (float) ($invoice->tax ?? 0), + 'total' => (float) ($invoice->total ?? 0), + 'created_at' => $this->toIsoDate($invoice->created_at), + 'due_date' => $this->toIsoDate($invoice->due_date), + 'paid_at' => $this->toIsoDate($invoice->date_paid), + 'payment_method' => $order->payment_method ?? null, + 'order' => [ + 'id' => $order->id, + 'order_num' => $order->order_num, + 'amount' => (float) $order->amount, + ], + 'service' => [ + 'name' => $firstItem?->product?->name, + 'billing_cycle' => $firstItem?->billing_cycle, + 'server_name' => $firstItem?->server_name, + ], + 'customer' => [ + 'id' => $order->customer?->id, + 'name' => $order->customer?->fullname, + 'email' => $order->customer?->email, + 'address' => $order->customer?->address, + ], + 'items' => $order->orderItems->map(function ($item) { + return [ + 'id' => $item->id, + 'product_name' => $item->product?->name, + 'billing_cycle' => $item->billing_cycle, + 'server_name' => $item->server_name, + 'price' => (float) $item->price, + ]; + }), + 'vmware' => $vmware ? [ + 'id' => $vmware->id, + 'name' => $vmware->name, + 'hostname' => $vmware->hostname, + 'ip_address' => $vmware->ipAddress?->ip, + 'os_name' => $vmware->os?->name, + 'status' => $vmware->status, + 'next_due_date' => $this->toIsoDate($vmware->next_due_date), + ] : null, + ], + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => $e->getMessage(), + ], 400); + } + } + + public function getClientServices(Request $request): JsonResponse + { + try { + $customer = $request->user(); + + $invoices = \App\Models\Invoice::query() + ->with(['order.orderItems.product', 'order.vmWare.ipAddress', 'order.vmWare.os']) + ->where('customer_id', $customer->id) + ->where('status', \App\Enums\Invoice\InvoiceStatus::PAID->value) + ->orderByDesc('created_at') + ->get(); + + $data = $invoices->map(function ($invoice) { + $order = $invoice->order; + $firstItem = $order?->orderItems->first(); + $vmware = $order?->vmWare->first(); + + return [ + 'id' => $invoice->id, + 'invoice_num' => $invoice->order_num, + 'service_name' => $firstItem?->product?->name ?? ('Đơn hàng ' . $order?->order_num), + 'server_name' => $firstItem?->server_name, + 'billing_cycle'=> $firstItem?->billing_cycle, + 'amount' => (float) ($invoice->total ?? 0), + 'vm_id' => $invoice->vm_id, + 'ip_address' => $vmware?->ipAddress?->ip, + 'os_name' => $vmware?->os?->name, + 'vm_status' => $vmware?->status, + 'paid_at' => $this->toIsoDate($invoice->date_paid), + 'next_due_date'=> $vmware ? $this->toIsoDate($vmware->next_due_date) : null, + 'created_at' => $this->toIsoDate($invoice->created_at), + 'order_id' => $order?->id, + 'order_num' => $order?->order_num, + ]; + })->values(); + + return response()->json([ + 'success' => true, + 'data' => $data, + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => $e->getMessage(), + ], 400); + } + } + + private function toIsoDate($value): ?string + { + if (empty($value)) { + return null; + } + + try { + if ($value instanceof \DateTimeInterface) { + return Carbon::instance($value)->toISOString(); + } + + if (is_numeric($value)) { + return Carbon::createFromTimestamp((int) $value)->toISOString(); + } + + return Carbon::parse($value)->toISOString(); + } catch (\Throwable) { + return null; + } + } + /** * @OA\Post( * path="/orders", @@ -135,7 +438,6 @@ class OrderController extends Controller $product = \App\Models\Product::where('uuid', $data['product_id'])->first(); if (!$product) { - // Should not happen if validation passes, but safety first (e.g. race condition or soft delete mismatch) throw new \Exception('Sản phẩm không tìm thấy hoặc đã bị xóa.'); } @@ -146,16 +448,78 @@ class OrderController extends Controller if (!$customer) { return response()->json([ 'success' => false, - 'message' => 'Ngườ;i dùng chưa đăng nhập hoặc phiên đăng nhập hết hạn.' + 'message' => 'Người dùng chưa đăng nhập hoặc phiên đăng nhập hết hạn.' ], 401); } $result = $this->orderService->create($data, $customer); + // Load relationships for response + $order = $result['order']->load(['orderItems.addons.addonItems', 'orderItems.product', 'invoice', 'customer']); + + // Format response + $responseData = [ + 'order' => [ + 'id' => $order->id, + 'order_num' => $order->order_num, + 'status' => $order->status, + 'status_text' => \App\Enums\Order\OrderStatus::from($order->status)->label(), + 'amount' => $order->amount, + 'first_payment' => $order->first_payment, + 'next_payment' => $order->next_payment, + 'created_at' => $order->created_at->toISOString(), + ], + 'invoice' => [ + 'id' => $order->invoice?->id, + 'invoice_num' => $order->invoice?->invoice_num, + 'sub_total' => $order->invoice?->sub_total, + 'tax' => $order->invoice?->tax, + 'total' => $order->invoice?->total, + 'status' => $order->invoice?->status, + 'due_date' => $order->invoice?->due_date, + ], + 'items' => $order->orderItems->map(function ($item) { + return [ + 'id' => $item->id, + 'product' => [ + 'id' => $item->product?->id, + 'uuid' => $item->product?->uuid, + 'name' => $item->product?->name, + 'product_type' => $item->product?->product_type, + 'specs' => [ + 'cpu' => $item->product?->cpu, + 'ram' => $item->product?->ram, + 'disk' => $item->product?->disk, + 'bandwidth' => $item->product?->bandwidth, + ], + ], + 'billing_cycle' => $item->billing_cycle, + 'server_name' => $item->server_name, + 'os_id' => $item->os_id, + 'price' => $item->price, + 'addons' => $item->addons->map(function ($addon) { + return [ + 'id' => $addon->id, + 'addon_id' => $addon->addon_id, + 'addon_name' => $addon->addonItems?->name, + 'qty' => $addon->qty, + 'price' => $addon->price, + ]; + }), + ]; + }), + 'customer' => [ + 'id' => $order->customer?->id, + 'email' => $order->customer?->email, + 'name' => trim($order->customer?->firstname . ' ' . $order->customer?->lastname), + ], + ]; + return response()->json([ 'success' => true, - 'data' => $result, - 'message' => 'Tạo đơn hàng thành công' + 'data' => $responseData, + 'message' => 'Tạo đơn hàng thành công', + 'notice' => 'Dịch vụ VPS sẽ được khởi tạo trong vòng 5-10 phút sau khi thanh toán. Bạn sẽ nhận được email thông báo khi hoàn tất.' ], 201); } catch (\Exception $e) { diff --git a/app/Http/Controllers/Api/Service/PaymentController.php b/app/Http/Controllers/Api/Service/PaymentController.php index f26c8cf..6251072 100644 --- a/app/Http/Controllers/Api/Service/PaymentController.php +++ b/app/Http/Controllers/Api/Service/PaymentController.php @@ -1,9 +1,14 @@ credit < $order->amount) { + // Kiểm tra số dư tài khoản (total) + if ($customer->total < $order->amount) { return response()->json([ 'error' => true, 'message' => 'Số dư không đủ để thanh toán', 'data' => [ 'required' => $order->amount, - 'current_credit' => $customer->credit, - 'shortage' => $order->amount - $customer->credit + 'current_total' => $customer->total, + 'shortage' => $order->amount - $customer->total ] ], 400); } @@ -51,6 +56,7 @@ class PaymentController return response()->json([ 'error' => false, 'message' => 'Đơn hàng đang được xử lý. Bạn sẽ nhận được thông báo khi hoàn tất.', + 'notice' => 'Dịch vụ VPS sẽ được khởi tạo trong vòng 5-10 phút. Vui lòng đợi trong giây lát.', 'data' => [ 'order_id' => $order->id, 'order_num' => $order->order_num, @@ -65,4 +71,4 @@ class PaymentController ], 500); } } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Api/Service/ProductCategoryController.php b/app/Http/Controllers/Api/Service/ProductCategoryController.php index 09ad840..cafb0a8 100644 --- a/app/Http/Controllers/Api/Service/ProductCategoryController.php +++ b/app/Http/Controllers/Api/Service/ProductCategoryController.php @@ -136,6 +136,22 @@ class ProductCategoryController extends Controller ->get(); $productResources = $products->map(function ($product) { + $billingCycleLabels = [ + 'monthly' => 'Hàng tháng', + 'quarterly' => 'Hàng quý', + 'semi_annually' => 'Nửa năm', + 'annually' => 'Hàng năm', + 'onetime' => 'Một lần', + ]; + + $periodUnits = [ + 'monthly' => 'month', + 'quarterly' => 'quarter', + 'semi_annually' => 'semi_year', + 'annually' => 'year', + 'onetime' => 'none', + ]; + return [ 'uuid' => $product->uuid, 'name' => $product->name, @@ -145,15 +161,15 @@ class ProductCategoryController extends Controller 'thumbnail' => $product->thumbnail ? asset($product->thumbnail) : null, 'product_type' => $product->product_type, 'specs' => [ - 'cpu' => $product->cpu, - 'ram' => $product->ram, - 'disk' => $product->disk, - 'bandwidth' => $product->bandwidth, + 'cpu' => (int) $product->cpu, + 'ram' => (int) $product->ram, + 'disk' => (int) $product->disk, + 'bandwidth' => (int) $product->bandwidth, 'gpu' => $product->gpu ], 'stock' => $product->stock, - 'price' => $product->price, - 'price_sale' => $product->price_sale, + 'price' => (int) $product->price, + 'price_sale' => $product->price_sale ? (int) $product->price_sale : null, 'group' => $product->category->name ?? null, 'os' => $product->osVersions->groupBy('os_group_id')->map(function ($versions) { $group = $versions->first()->osGroup; @@ -168,11 +184,17 @@ class ProductCategoryController extends Controller })->values(), 'pricing' => $product->pricings->map(fn($p) => [ 'id' => $p->id, - 'name' => $p->name, - 'price' => $p->price, - 'price_sale' => $p->price_sale, - 'period' => $p->period, - 'period_unit' => $p->period_unit, + 'name' => $billingCycleLabels[$p->billing_cycle] ?? $p->billing_cycle, + 'price' => (int) $p->price, + 'price_sale' => $p->price_sale ? (int) $p->price_sale : null, + 'period' => $p->billing_cycle ? (int) match($p->billing_cycle) { + 'monthly' => 1, + 'quarterly' => 3, + 'semi_annually' => 6, + 'annually' => 12, + default => 1, + } : 1, + 'period_unit' => $periodUnits[$p->billing_cycle] ?? 'month', ]), ]; }); @@ -279,7 +301,23 @@ class ProductCategoryController extends Controller ]) ->get(); - $productResources = $products->map(function ($product) { + $billingCycleLabels = [ + 'monthly' => 'Hàng tháng', + 'quarterly' => 'Hàng quý', + 'semi_annually' => 'Nửa năm', + 'annually' => 'Hàng năm', + 'onetime' => 'Một lần', + ]; + + $periodUnits = [ + 'monthly' => 'month', + 'quarterly' => 'quarter', + 'semi_annually' => 'semi_year', + 'annually' => 'year', + 'onetime' => 'none', + ]; + + $productResources = $products->map(function ($product) use ($billingCycleLabels, $periodUnits) { return [ 'uuid' => $product->uuid, 'name' => $product->name, @@ -289,15 +327,15 @@ class ProductCategoryController extends Controller 'thumbnail' => $product->thumbnail ? asset($product->thumbnail) : null, 'product_type' => $product->product_type, 'specs' => [ - 'cpu' => $product->cpu, - 'ram' => $product->ram, - 'disk' => $product->disk, - 'bandwidth' => $product->bandwidth, + 'cpu' => (int) $product->cpu, + 'ram' => (int) $product->ram, + 'disk' => (int) $product->disk, + 'bandwidth' => (int) $product->bandwidth, 'gpu' => $product->gpu ], 'stock' => $product->stock, - 'price' => $product->price, - 'price_sale' => $product->price_sale, + 'price' => (int) $product->price, + 'price_sale' => $product->price_sale ? (int) $product->price_sale : null, 'group' => $product->category->name ?? null, 'os' => $product->osVersions->groupBy('os_group_id')->map(function ($versions) { $group = $versions->first()->osGroup; @@ -312,11 +350,17 @@ class ProductCategoryController extends Controller })->values(), 'pricing' => $product->pricings->map(fn($p) => [ 'id' => $p->id, - 'name' => $p->name, - 'price' => $p->price, - 'price_sale' => $p->price_sale, - 'period' => $p->period, - 'period_unit' => $p->period_unit, + 'name' => $billingCycleLabels[$p->billing_cycle] ?? $p->billing_cycle, + 'price' => (int) $p->price, + 'price_sale' => $p->price_sale ? (int) $p->price_sale : null, + 'period' => $p->billing_cycle ? (int) match($p->billing_cycle) { + 'monthly' => 1, + 'quarterly' => 3, + 'semi_annually' => 6, + 'annually' => 12, + default => 1, + } : 1, + 'period_unit' => $periodUnits[$p->billing_cycle] ?? 'month', ]), ]; }); diff --git a/app/Http/Controllers/Api/Service/ServiceController.php b/app/Http/Controllers/Api/Service/ServiceController.php index 08d73c1..bc506ed 100644 --- a/app/Http/Controllers/Api/Service/ServiceController.php +++ b/app/Http/Controllers/Api/Service/ServiceController.php @@ -178,4 +178,57 @@ class ServiceController extends Controller 'data' => new ServiceResource($product), ]); } + + /** + * Get addons for a specific product + * GET /v1/products/{productId}/addons + */ + public function getProductAddons(Request $request, string $productId) + { + $product = Product::where('uuid', $productId)->first(); + + if (!$product) { + return response()->json([ + 'error' => true, + 'message' => 'Sản phẩm không tồn tại' + ], 404); + } + + $addons = $product->addons() + ->where('status', true) + ->get(); + + $groupedAddons = [ + 'cpu' => [], + 'ram' => [], + 'disk' => [], + 'other' => [] + ]; + + foreach ($addons as $addon) { + // Group by code field (cpu, ram, disk, gpu, bandwidth, etc.) + $key = strtolower(trim($addon->code)); + if (!in_array($key, ['cpu', 'ram', 'disk'])) { + $key = 'other'; + } + $groupedAddons[$key][] = [ + 'id' => $addon->id, + 'name' => $addon->name, + 'type' => $addon->type, + 'code' => $addon->code, + 'price' => $addon->price, + 'description' => $addon->description, + 'min' => $addon->min, + 'max' => $addon->max, + 'step' => $addon->step, + 'prefix' => $addon->prefix, + ]; + } + + return response()->json([ + 'error' => false, + 'message' => 'Lấy dữ liệu thành công', + 'data' => $groupedAddons + ]); + } } \ No newline at end of file diff --git a/app/Http/Requests/Api/Order/StoreOrderRequest.php b/app/Http/Requests/Api/Order/StoreOrderRequest.php index 9a17227..c9d79dd 100644 --- a/app/Http/Requests/Api/Order/StoreOrderRequest.php +++ b/app/Http/Requests/Api/Order/StoreOrderRequest.php @@ -18,12 +18,12 @@ class StoreOrderRequest extends FormRequest { return [ 'product_id' => ['required', Rule::exists('products', 'uuid')->whereNull('deleted_at')], - 'billing_cycle'=> ['required', Rule::enum(ProductBillingCycle::class), 'exists:product_pricings,billing_cycle'], + 'billing_cycle'=> ['required', Rule::enum(ProductBillingCycle::class)], 'server_name' => 'nullable|string|max:255', - 'os_id' => 'nullable|exists:os,id', + 'os_id' => 'nullable|integer', 'addons' => 'array', - 'addons.*.addon_id' => 'required|exists:product_addon,id', - 'addons.*.qty' => 'required|integer|min:1', + 'addons.*.addon_id' => 'nullable|exists:product_addon,id', + 'addons.*.qty' => 'nullable|integer|min:0', ]; } diff --git a/app/Http/Resources/Api/Service/ServiceResource.php b/app/Http/Resources/Api/Service/ServiceResource.php index 12c8130..a1abc46 100644 --- a/app/Http/Resources/Api/Service/ServiceResource.php +++ b/app/Http/Resources/Api/Service/ServiceResource.php @@ -14,6 +14,22 @@ class ServiceResource extends JsonResource */ public function toArray(Request $request): array { + $billingCycleLabels = [ + 'monthly' => 'Hàng tháng', + 'quarterly' => 'Hàng quý', + 'semi_annually' => 'Nửa năm', + 'annually' => 'Hàng năm', + 'onetime' => 'Một lần', + ]; + + $periodUnits = [ + 'monthly' => 'month', + 'quarterly' => 'quarter', + 'semi_annually' => 'semi_year', + 'annually' => 'year', + 'onetime' => 'none', + ]; + return [ 'uuid' => $this->uuid, 'name' => $this->name, @@ -23,27 +39,42 @@ class ServiceResource extends JsonResource 'thumbnail' => $this->thumbnail ? asset($this->thumbnail) : null, 'product_type' => $this->product_type, 'specs' => [ - 'cpu' => $this->cpu, - 'ram' => $this->ram, - 'disk' => $this->disk, - 'bandwidth' => $this->bandwidth, + 'cpu' => (int) $this->cpu, + 'ram' => (int) $this->ram, + 'disk' => (int) $this->disk, + 'bandwidth' => (int) $this->bandwidth, 'gpu' => $this->gpu ], 'stock' => $this->stock, - 'price' => $this->price, - 'price_sale' => $this->price_sale, - 'group' => $this->category->name, + 'price' => (int) $this->price, + 'price_sale' => $this->price_sale ? (int) $this->price_sale : null, + 'group' => $this->category->name ?? null, 'os' => $this->osVersions->groupBy('os_group_id')->map(function ($versions) { $group = $versions->first()->osGroup; return [ 'name' => $group?->name, 'thumbnail' => $group?->thumbnail, 'versions' => $versions->map(fn($os) => [ + 'id' => $os->id, 'version' => $os->version, 'name' => $os->name, ])->values(), ]; })->values(), + 'pricing' => $this->pricings->map(fn($p) => [ + 'id' => $p->id, + 'name' => $billingCycleLabels[$p->billing_cycle] ?? $p->billing_cycle, + 'price' => (int) $p->price, + 'price_sale' => $p->price_sale ? (int) $p->price_sale : null, + 'period' => $p->billing_cycle ? (int) match($p->billing_cycle) { + 'monthly' => 1, + 'quarterly' => 3, + 'semi_annually' => 6, + 'annually' => 12, + default => 1, + } : 1, + 'period_unit' => $periodUnits[$p->billing_cycle] ?? 'month', + ]), ]; } } diff --git a/app/Jobs/VirtualMachine/Create/CreateVirtualMachineJob.php b/app/Jobs/VirtualMachine/Create/CreateVirtualMachineJob.php index 688d544..fb7f270 100644 --- a/app/Jobs/VirtualMachine/Create/CreateVirtualMachineJob.php +++ b/app/Jobs/VirtualMachine/Create/CreateVirtualMachineJob.php @@ -13,6 +13,7 @@ use App\Models\Os; use App\Models\ModuleVmware; use App\Models\ModuleIpAddress; use App\Models\IpPool; +use App\Models\Log as CustomerLog; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; @@ -52,15 +53,33 @@ class CreateVirtualMachineJob implements ShouldQueue if (!$this->isRetry) { $this->sendFailureNotification($errorMessage); + $this->writeCustomerFailureLog($errorMessage); } return false; } - $host = $product->host; // từ module_host_id + $osModel = Os::with('osGroup.host.datacenter.vcenter')->find($this->item->os_id); + + if (!$osModel) { + $errorMessage = "Không tìm thấy OS id={$this->item->os_id} trong OrderItem #{$this->item->id}. os_id không hợp lệ."; + Log::error($errorMessage); + $telegram->SendErrorMessage([ + 'error' => $errorMessage, + 'os_id' => $this->item->os_id, + 'order_item_id' => $this->item->id, + 'product_id' => $product->id, + ], 'CreateVirtualMachineJob - Invalid OS ID'); + if (!$this->isRetry) { + $this->sendFailureNotification($errorMessage); + $this->writeCustomerFailureLog($errorMessage); + } + return; + } + + $template = $osModel->template_id; + $host = $osModel->osGroup?->host; // từ os_group.module_host_id $datacenter = $host?->datacenter; $vcenter = $datacenter?->vcenter; - $osModel = Os::find($this->item->os_id); - $template = $osModel?->template_id; if (!$vcenter || !$host || !$datacenter) { $errorMessage = "Thiếu thông tin host/datacenter/vcenter từ sản phẩm ID #{$product->id}"; @@ -76,6 +95,7 @@ class CreateVirtualMachineJob implements ShouldQueue if (!$this->isRetry) { $this->sendFailureNotification($errorMessage); + $this->writeCustomerFailureLog($errorMessage); } return; } @@ -94,6 +114,7 @@ class CreateVirtualMachineJob implements ShouldQueue if (!$this->isRetry) { $this->sendFailureNotification($errorMessage); + $this->writeCustomerFailureLog($errorMessage); } return false; } @@ -154,6 +175,7 @@ class CreateVirtualMachineJob implements ShouldQueue if (!$this->isRetry) { $this->sendFailureNotification($errorMessage); + $this->writeCustomerFailureLog($errorMessage); } return false; } elseif ($result === 'Login_Again') { @@ -168,6 +190,7 @@ class CreateVirtualMachineJob implements ShouldQueue if (!$this->isRetry) { $this->sendFailureNotification($errorMessage); + $this->writeCustomerFailureLog($errorMessage); } return false; } else { @@ -261,6 +284,7 @@ class CreateVirtualMachineJob implements ShouldQueue 'vm_started' => isset($startResult) && is_array($startResult) && $startResult['error'] === false ] ); + $this->writeCustomerSuccessLog(); return true; } @@ -282,11 +306,58 @@ class CreateVirtualMachineJob implements ShouldQueue if (!$this->isRetry) { $this->sendFailureNotification($errorMessage); + $this->writeCustomerFailureLog($errorMessage); } return false; } } + private function writeCustomerSuccessLog(): void + { + try { + $order = $this->item->order; + $customer = $order?->customer; + + if (!$order || !$customer) { + return; + } + + CustomerLog::create([ + 'agent' => 'system', + 'client_ip' => null, + 'customer_id' => $customer->id, + 'description' => 'Khởi tạo dịch vụ thành công cho đơn hàng #' . $order->order_num + . ' (product_id: ' . $this->item->product_id . ', os_id: ' . $this->item->os_id . ').', + ]); + } catch (\Throwable $e) { + Log::warning('Không thể ghi log thành công khởi tạo VM: ' . $e->getMessage()); + } + } + + private function writeCustomerFailureLog(string $errorMessage): void + { + try { + $order = $this->item->order; + $customer = $order?->customer; + + if (!$order || !$customer) { + return; + } + + CustomerLog::create([ + 'agent' => 'system', + 'client_ip' => null, + 'customer_id' => $customer->id, + 'description' => 'Khởi tạo dịch vụ thất bại cho đơn hàng #' . $order->order_num + . ' (product_id: ' . $this->item->product_id . ', os_id: ' . $this->item->os_id . ').' + . ' Lỗi: ' . $errorMessage . '. ' + . 'Nếu có lỗi xảy ra khi khởi tạo dịch vụ quý khách nhấn khởi tạo tại đây, hoặc quý khách có thể liên hệ với kỹ thuật support@tgs.com.vn', + ]); + } catch (\Throwable $e) { + Log::warning('Không thể ghi log thất bại khởi tạo VM: ' . $e->getMessage()); + } + } + private function getAvailableIPFromPool(int $networkId): string { // Lấy tất cả IP từ IPPool theo network_id, order by ip_address diff --git a/app/Mail/Customer/VMCreationFailed.php b/app/Mail/Customer/VMCreationFailed.php index 813efb1..533a85c 100644 --- a/app/Mail/Customer/VMCreationFailed.php +++ b/app/Mail/Customer/VMCreationFailed.php @@ -48,6 +48,7 @@ class VMCreationFailed extends Mailable 'errorMessage' => $this->data['error_message'] ?? 'Lỗi không xác định', 'supportEmail' => config('mail.support_email', 'support@example.com'), 'supportPhone' => config('mail.support_phone', '1900-xxxx'), + 'invoiceUrl' => rtrim(env('FRONTEND_URL', config('app.url')), '/') . '/hoa-don/' . ($this->data['order_id'] ?? ''), ], ); } diff --git a/app/Models/ModuleVmware.php b/app/Models/ModuleVmware.php index ef53276..57bccb2 100644 --- a/app/Models/ModuleVmware.php +++ b/app/Models/ModuleVmware.php @@ -24,6 +24,7 @@ class ModuleVmware extends Model 'password', 'hostname', 'vmware_id', + 'vcenter_id', 'os_id', 'customer_id', 'status' @@ -38,6 +39,10 @@ class ModuleVmware extends Model { return $this->belongsTo(\App\Models\ModuleIpAddress::class, 'module_ip_address_id', 'id'); } + public function vCenter() + { + return $this->belongsTo(\App\Models\vCenter::class, 'vcenter_id', 'id'); + } public function os() { return $this->belongsTo(\App\Models\Os::class); diff --git a/app/Models/Product.php b/app/Models/Product.php index c9dc541..cc46a80 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -119,12 +119,11 @@ class Product extends Model /** * Relationship: Product has many addons (through product type) - * Addons are linked via type_id in product_addon table + * Addons are linked via type in product_addon table */ public function addons() { - // Assuming product_addon has type_id field that links to product type - return $this->hasMany(Addon::class, 'type_id', 'product_type') + return $this->hasMany(Addon::class, 'type', 'product_type') ->orderBy('sort_order'); } diff --git a/app/Repositories/Api/Invoice/InvoiceRepository.php b/app/Repositories/Api/Invoice/InvoiceRepository.php index afd3d74..7e0d2f1 100644 --- a/app/Repositories/Api/Invoice/InvoiceRepository.php +++ b/app/Repositories/Api/Invoice/InvoiceRepository.php @@ -24,6 +24,7 @@ class InvoiceRepository extends BaseApiRepository implements InvoiceRepositoryIn { $invoice = $this->model->create([ 'customer_id' => $order->customer_id, + 'order_id' => $order->id, 'order_num' => \App\Helpers\OrderHelper::generateOrderNumber('INV'), 'date' => now(), 'due_date' => now()->addDays(3), diff --git a/app/Repositories/Api/Order/OrderRepository.php b/app/Repositories/Api/Order/OrderRepository.php index cf4c877..013bb60 100644 --- a/app/Repositories/Api/Order/OrderRepository.php +++ b/app/Repositories/Api/Order/OrderRepository.php @@ -44,18 +44,7 @@ class OrderRepository extends BaseApiRepository implements OrderRepositoryInterf { // Update Order Status $order->update([ - 'status' => \App\Enums\Order\OrderStatus::ACTIVE->value, // Or PROCESSING depending on flow, but user said 'thanh toán thực hiện order tiếp' usually implies Active/Paid. - // Let's assume ACTIVE means Paid/Completed effectively for order flow permission - 'payment_method' => $method, // Column might not exist in standard migration, checking migration... - // Migration 2024_01_31_155621_order.php does NOT have payment_method or paid_at. - // It has 'status' (tinyInteger), 'date' (int), 'date_paid' (int) in invoice but not order. - // Order table has: customer_id, date, user_id, discount_id, invoice_id, discount_value, amount, note, client_ip, status. - // So we can only update status. - // BUT order table has 'payment_id' in Model $fillable but migration didn't show it in up() method I saw earlier (2024_01_31_155621_order.php). - // Let's check migration again if needed. But safe bet is update status. - - // Wait, user request: "phải thanh toán order mới thực hiện được order tiếp theo" - // So logic checks for PENDING. If we change to ACTIVE (1) or PROCESSING (3), it allows new order. + 'status' => \App\Enums\Order\OrderStatus::ACTIVE->value, ]); // Update Invoice Status diff --git a/app/Services/Api/Order/OrderService.php b/app/Services/Api/Order/OrderService.php index 222a0db..63ff610 100644 --- a/app/Services/Api/Order/OrderService.php +++ b/app/Services/Api/Order/OrderService.php @@ -8,6 +8,12 @@ use App\Repositories\Api\Order\OrderRepository; use Illuminate\Support\Facades\DB; use App\Enums\Order\OrderStatus; use Exception; +use App\Jobs\VirtualMachine\Create\CreateVMJobs; +use App\Models\Log as CustomerLog; +use App\Models\ModuleIpAddress; +use App\Models\ModuleVmware; +use App\Models\Os; +use Illuminate\Support\Facades\Log; class OrderService { @@ -24,20 +30,37 @@ class OrderService throw new Exception('Bạn có đơn hàng chưa thanh toán. Vui lòng thanh toán trước khi tạo đơn mới.'); } - return DB::transaction(function () use ($data, $customer) { + $product = $this->productRepo->find($data['product_id']); - $product = $this->productRepo->find($data['product_id']); + if (!$product) { + throw new Exception('Sản phẩm không tồn tại'); + } - if (!$product) { - throw new Exception('Sản phẩm không tồn tại'); + $price = $this->productRepo->getPricing($product->id, $data['billing_cycle']); + + if (is_null($price)) { + throw new Exception('Không tìm thấy giá cho sản phẩm này'); + } + + // Tính tổng tiền bao gồm addons + $addonsPrice = 0; + if (!empty($data['addons'])) { + foreach ($data['addons'] as $addonData) { + $addon = \App\Models\Addon::find($addonData['addon_id']); + if ($addon) { + $addonsPrice += $addon->price * ($addonData['qty'] ?? 1); + } } + } - $price = $this->productRepo - ->getPricing($product->id, $data['billing_cycle']); + $totalPrice = $price + $addonsPrice; - if (is_null($price)) { - throw new Exception('Không tìm thấy giá cho sản phẩm này'); - } + // Kiểm tra số dư tài khoản (cột total) trước khi tạo đơn + if ($customer->total < $totalPrice) { + throw new Exception('Số dư không đủ để đặt hàng. Vui lòng nạp thêm tiền. Số dư hiện tại: ' . number_format($customer->total) . ' VNĐ, cần: ' . number_format($totalPrice) . ' VNĐ'); + } + + return DB::transaction(function () use ($data, $customer, $product, $price, $totalPrice) { $order = $this->orderRepo->create([ 'order_num' => \App\Helpers\OrderHelper::generateOrderNumber(), @@ -66,10 +89,112 @@ class OrderService public function pay($order, string $method) { - if ($order->status !== OrderStatus::PENDING->value) { - throw new Exception('Đơn hàng đã được xử lý hoặc hủy bỏ.'); + if (in_array($order->status, [OrderStatus::ACTIVE->value, OrderStatus::PROCESSING->value], true)) { + throw new Exception('Đơn hàng đã được thanh toán hoặc đang xử lý.'); } - return $this->orderRepo->processPayment($order, $method); + return DB::transaction(function () use ($order, $method) { + $customer = $order->customer; + + // Kiểm tra số dư và trừ tiền + if ($customer->total < $order->amount) { + throw new Exception('Số dư không đủ để thanh toán. Vui lòng nạp thêm tiền. Số dư hiện tại: ' . number_format($customer->total) . ' VNĐ, cần: ' . number_format($order->amount) . ' VNĐ'); + } + + $customer->total -= $order->amount; + $customer->save(); + + CustomerLog::create([ + 'agent' => 'api', + 'client_ip' => request()?->ip(), + 'customer_id' => $customer->id, + 'description' => 'Thanh toán đơn hàng #' . $order->order_num + . ' thành công. Số tiền: ' . number_format((int) $order->amount) . ' VNĐ' + . '. Số dư sau thanh toán: ' . number_format((int) $customer->total) . ' VNĐ', + ]); + + // Cho phép thanh toán lại đơn đã hủy bằng cách đưa về pending trước khi process. + if ($order->status === OrderStatus::CANCELLED->value) { + $order->status = OrderStatus::PENDING->value; + $order->save(); + } + + $processedOrder = $this->orderRepo->processPayment($order, $method); + + // Sau khi thanh toán, tạo module_vmware rồi dispatch CreateVMJobs. + foreach ($order->orderItems as $item) { + if (empty($item->product_id) || empty($item->os_id)) { + Log::warning('Skip VM job: product_id/os_id is missing', [ + 'order_id' => $order->id, + 'order_item_id' => $item->id, + 'product_id' => $item->product_id, + 'os_id' => $item->os_id, + ]); + continue; + } + + try { + $os = Os::with('osGroup.host')->find($item->os_id); + + if (!$os || !$os->osGroup || !$os->osGroup->module_host_id) { + Log::warning('Skip VM job: không tìm thấy OS/OsGroup/Host', [ + 'order_id' => $order->id, + 'order_item_id' => $item->id, + 'os_id' => $item->os_id, + ]); + continue; + } + + $ip = ModuleIpAddress::where('module_host_id', $os->osGroup->module_host_id) + ->where('status', 0) + ->lockForUpdate() + ->first(); + + if (!$ip) { + Log::warning('Skip VM job: không còn IP khả dụng', [ + 'order_id' => $order->id, + 'module_host_id' => $os->osGroup->module_host_id, + ]); + continue; + } + + $ip->update(['status' => 1, 'vm_name' => $item->server_name ?? 'VM-' . $order->id]); + + $moduleVmware = ModuleVmware::create([ + 'name' => $item->server_name ?? 'VM-' . $order->id, + 'module_ip_address_id' => $ip->id, + 'port' => 22, + 'order_id' => $order->id, + 'first_payment' => $order->amount, + 'next_payment' => $order->amount, + 'next_due_date' => null, + 'next_invoice' => 0, + 'user' => 'Administrator', + 'password' => '', + 'hostname' => $item->server_name ?? 'vm-' . $order->id, + 'vmware_id' => null, + 'os_id' => $item->os_id, + 'customer_id' => $order->customer_id, + 'status' => 0, + ]); + + dispatch(new CreateVMJobs($moduleVmware->id))->delay(now()->addSeconds(10)); + + Log::info('Dispatched CreateVMJobs', [ + 'order_id' => $order->id, + 'module_vmware_id' => $moduleVmware->id, + 'ip' => $ip->ip, + ]); + } catch (\Exception $e) { + Log::error('Lỗi khi tạo module_vmware hoặc dispatch job', [ + 'order_id' => $order->id, + 'order_item_id' => $item->id, + 'error' => $e->getMessage(), + ]); + } + } + + return $processedOrder; + }); } } diff --git a/config/assets.php b/config/assets.php index 44dc14e..d4fe0c0 100644 --- a/config/assets.php +++ b/config/assets.php @@ -4,13 +4,11 @@ return [ 'vite' => [ 'css' => [ 'resources/sass/app.scss', - 'resources/css/components/micromodal.css', ], 'js' => [ - 'resources/js/pages/color-modes.js', 'resources/js/app-vite.js', + // 'build/plugins/jquery/jquery.min.js', 'resources/js/pages/template.js', - 'resources/js/components/micromodal.js', ], ], 'static' => [ @@ -27,7 +25,7 @@ return [ ], ], 'datatable' => [ - 'css'=> [ + 'css' => [ 'build/plugins/select2/select2.min.css' ], 'js' => [ diff --git a/database/migrations/2024_05_13_165053_transaction.php b/database/migrations/2024_05_13_165053_transaction.php index f45a88d..7f37cc2 100644 --- a/database/migrations/2024_05_13_165053_transaction.php +++ b/database/migrations/2024_05_13_165053_transaction.php @@ -12,7 +12,7 @@ return new class extends Migration { $table->uuid(); $table->string('trans')->nullable(); $table->integer('amount')->nullable(); - + $table->string('public_code')->nullable(); $table->foreignId('customer_id') ->constrained('customers') ->cascadeOnDelete(); diff --git a/database/migrations/2026_03_01_184412_drop_product_type_from_product_os_table.php b/database/migrations/2026_03_01_184412_drop_product_type_from_product_os_table.php new file mode 100644 index 0000000..2f77e6d --- /dev/null +++ b/database/migrations/2026_03_01_184412_drop_product_type_from_product_os_table.php @@ -0,0 +1,28 @@ +dropColumn('product_type'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('product_os', function (Blueprint $table) { + // + }); + } +}; diff --git a/database/migrations/2026_03_01_223038_add_vcenter_id_to_module_vmware_table.php b/database/migrations/2026_03_01_223038_add_vcenter_id_to_module_vmware_table.php new file mode 100644 index 0000000..00c3889 --- /dev/null +++ b/database/migrations/2026_03_01_223038_add_vcenter_id_to_module_vmware_table.php @@ -0,0 +1,27 @@ +unsignedBigInteger('vcenter_id')->nullable()->after('module_ip_address_id'); + $table->foreign('vcenter_id')->references('id')->on('module_vcenter')->nullOnDelete(); + }); + } + + public function down(): void + { + Schema::table('module_vmware', function (Blueprint $table) { + $table->dropForeign(['vcenter_id']); + $table->dropColumn('vcenter_id'); + }); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index a485b40..3c812f0 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -29,5 +29,6 @@ class DatabaseSeeder extends Seeder $this->call(MenuSeeder::class); $this->call(ControlPanelSeeder::class); $this->call(VcenterSeeder::class); + $this->call(VPSProductSeeder::class); } } diff --git a/database/seeders/VPSProductSeeder.php b/database/seeders/VPSProductSeeder.php new file mode 100644 index 0000000..65c2f48 --- /dev/null +++ b/database/seeders/VPSProductSeeder.php @@ -0,0 +1,382 @@ + 'vps'], + [ + 'name' => 'VPS', + 'is_active' => true, + 'sort_order' => 1, + 'type' => 'vps', + ] + ); + + // Create child categories for VPS + $ubuntuCategory = ProductCategories::updateOrCreate( + ['slug' => 'vps-ubuntu'], + [ + 'name' => 'VPS Ubuntu', + 'parent_id' => $vpsCategory->id, + 'is_active' => true, + 'sort_order' => 1, + 'type' => 'vps', + ] + ); + + $windowsCategory = ProductCategories::updateOrCreate( + ['slug' => 'vps-windows'], + [ + 'name' => 'VPS Windows', + 'parent_id' => $vpsCategory->id, + 'is_active' => true, + 'sort_order' => 2, + 'type' => 'vps', + ] + ); + + // Get or create Ubuntu OS Group - each version has different template_id from vCenter + $ubuntuGroup = OsGroup::updateOrCreate( + ['name' => 'Ubuntu'], + [ + 'thumbnail' => 'ubuntu.png', + 'status' => 1, + ] + ); + + // Create Ubuntu OS versions - each with DIFFERENT template_id from vCenter + $ubuntuVersions = [ + ['version' => '20.04 LTS', 'template_id' => 'vm-3005'], // Ubuntu 20.04 + ['version' => '22.04 LTS', 'template_id' => 'vm-3010'], // Ubuntu 22.04 + ['version' => '24.04 LTS', 'template_id' => 'vm-3011'], // Ubuntu 24.04 + ]; + + $ubuntuOS = []; + foreach ($ubuntuVersions as $uv) { + $os = Os::updateOrCreate( + [ + 'os_group_id' => $ubuntuGroup->id, + 'version' => $uv['version'], + ], + [ + 'template_id' => $uv['template_id'], + 'status' => 1, + ] + ); + $ubuntuOS[] = $os; + $this->command->info("Created Ubuntu {$uv['version']} with template_id: {$uv['template_id']}"); + } + + // Create Windows OS Group - each version has different template_id from vCenter + $windowsGroup = OsGroup::updateOrCreate( + ['name' => 'Windows Server'], + [ + 'thumbnail' => 'windows.png', + 'status' => 1, + ] + ); + + $windowsVersions = [ + ['version' => '2019', 'template_id' => 'vm-3006'], // Windows 2019 + ['version' => '2022', 'template_id' => 'vm-3007'], // Windows 2022 + ]; + + $windowsOS = []; + foreach ($windowsVersions as $wv) { + $os = Os::updateOrCreate( + [ + 'os_group_id' => $windowsGroup->id, + 'version' => $wv['version'], + ], + [ + 'template_id' => $wv['template_id'], + 'status' => 1, + ] + ); + $windowsOS[] = $os; + $this->command->info("Created Windows {$wv['version']} with template_id: {$wv['template_id']}"); + } + + // VPS Products Data - using vm-3008 as template_id for items + $vpsProducts = [ + // Ubuntu VPS Plans + [ + 'name' => 'VPS Basic Ubuntu', + 'slug' => 'vps-basic-ubuntu', + 'short_description' => 'VPS Basic với Ubuntu - Phù hợp cho website nhỏ', + 'description' => '
VPS Basic với Ubuntu 22.04 LTS - Phù hợp cho website cá nhân, blog
', + 'thumbnail' => '/images/vps/basic.png', + 'cpu' => 1, + 'ram' => 1, + 'disk' => 25, + 'bandwidth' => 100, + 'stock' => 100, + 'is_active' => true, + 'product_categories_id' => $ubuntuCategory->id, + 'template_id' => 'vm-3008', + 'pricing' => [ + ['billing_cycle' => 'monthly', 'price' => 150000, 'price_sale' => 99000], + ['billing_cycle' => 'quarterly', 'price' => 450000, 'price_sale' => 297000], + ['billing_cycle' => 'semi_annually', 'price' => 900000, 'price_sale' => 594000], + ['billing_cycle' => 'annually', 'price' => 1800000, 'price_sale' => 1188000], + ], + 'os' => [0, 1, 2], // Ubuntu versions + ], + [ + 'name' => 'VPS Standard Ubuntu', + 'slug' => 'vps-standard-ubuntu', + 'short_description' => 'VPS Standard với Ubuntu - Cân bằng giá và hiệu suất', + 'description' => 'VPS Standard với Ubuntu 22.04 LTS - Phù hợp cho website vừa và nhỏ
', + 'thumbnail' => '/images/vps/standard.png', + 'cpu' => 2, + 'ram' => 2, + 'disk' => 50, + 'bandwidth' => 200, + 'stock' => 100, + 'is_active' => true, + 'product_categories_id' => $ubuntuCategory->id, + 'template_id' => 'vm-3008', + 'pricing' => [ + ['billing_cycle' => 'monthly', 'price' => 250000, 'price_sale' => 199000], + ['billing_cycle' => 'quarterly', 'price' => 750000, 'price_sale' => 597000], + ['billing_cycle' => 'semi_annually', 'price' => 1500000, 'price_sale' => 1194000], + ['billing_cycle' => 'annually', 'price' => 3000000, 'price_sale' => 2388000], + ], + 'os' => [0, 1, 2], + ], + [ + 'name' => 'VPS Professional Ubuntu', + 'slug' => 'vps-professional-ubuntu', + 'short_description' => 'VPS Professional với Ubuntu - Cho ứng dụng web', + 'description' => 'VPS Professional với Ubuntu 22.04 LTS - Phù hợp cho website thương mại điện tử
', + 'thumbnail' => '/images/vps/professional.png', + 'cpu' => 4, + 'ram' => 4, + 'disk' => 100, + 'bandwidth' => 500, + 'stock' => 50, + 'is_active' => true, + 'product_categories_id' => $ubuntuCategory->id, + 'template_id' => 'vm-3008', + 'pricing' => [ + ['billing_cycle' => 'monthly', 'price' => 450000, 'price_sale' => 399000], + ['billing_cycle' => 'quarterly', 'price' => 1350000, 'price_sale' => 1197000], + ['billing_cycle' => 'semi_annually', 'price' => 2700000, 'price_sale' => 2394000], + ['billing_cycle' => 'annually', 'price' => 5400000, 'price_sale' => 4788000], + ], + 'os' => [0, 1, 2], + ], + [ + 'name' => 'VPS Enterprise Ubuntu', + 'slug' => 'vps-enterprise-ubuntu', + 'short_description' => 'VPS Enterprise với Ubuntu - Hiệu suất cao', + 'description' => 'VPS Enterprise với Ubuntu 22.04 LTS - Cho ứng dụng enterprise
', + 'thumbnail' => '/images/vps/enterprise.png', + 'cpu' => 8, + 'ram' => 8, + 'disk' => 200, + 'bandwidth' => 1000, + 'stock' => 20, + 'is_active' => true, + 'product_categories_id' => $ubuntuCategory->id, + 'template_id' => 'vm-3008', + 'pricing' => [ + ['billing_cycle' => 'monthly', 'price' => 850000, 'price_sale' => 799000], + ['billing_cycle' => 'quarterly', 'price' => 2550000, 'price_sale' => 2397000], + ['billing_cycle' => 'semi_annually', 'price' => 5100000, 'price_sale' => 4794000], + ['billing_cycle' => 'annually', 'price' => 10200000, 'price_sale' => 9588000], + ], + 'os' => [0, 1, 2], + ], + // Windows VPS Plans + [ + 'name' => 'VPS Basic Windows', + 'slug' => 'vps-basic-windows', + 'short_description' => 'VPS Basic với Windows Server', + 'description' => 'VPS Basic với Windows Server 2019 - Phù hợp cho ứng dụng .NET
', + 'thumbnail' => '/images/vps/windows-basic.png', + 'cpu' => 1, + 'ram' => 2, + 'disk' => 40, + 'bandwidth' => 100, + 'stock' => 50, + 'is_active' => true, + 'product_categories_id' => $windowsCategory->id, + 'template_id' => 'vm-3008', + 'pricing' => [ + ['billing_cycle' => 'monthly', 'price' => 250000, 'price_sale' => 199000], + ['billing_cycle' => 'quarterly', 'price' => 750000, 'price_sale' => 597000], + ['billing_cycle' => 'semi_annually', 'price' => 1500000, 'price_sale' => 1194000], + ['billing_cycle' => 'annually', 'price' => 3000000, 'price_sale' => 2388000], + ], + 'os' => [0, 1], // Windows versions + ], + [ + 'name' => 'VPS Standard Windows', + 'slug' => 'vps-standard-windows', + 'short_description' => 'VPS Standard với Windows Server', + 'description' => 'VPS Standard với Windows Server 2022 - Phù hợp cho enterprise
', + 'thumbnail' => '/images/vps/windows-standard.png', + 'cpu' => 2, + 'ram' => 4, + 'disk' => 80, + 'bandwidth' => 200, + 'stock' => 30, + 'is_active' => true, + 'product_categories_id' => $windowsCategory->id, + 'template_id' => 'vm-3008', + 'pricing' => [ + ['billing_cycle' => 'monthly', 'price' => 450000, 'price_sale' => 399000], + ['billing_cycle' => 'quarterly', 'price' => 1350000, 'price_sale' => 1197000], + ['billing_cycle' => 'semi_annually', 'price' => 2700000, 'price_sale' => 2394000], + ['billing_cycle' => 'annually', 'price' => 5400000, 'price_sale' => 4788000], + ], + 'os' => [0, 1], + ], + [ + 'name' => 'VPS Professional Windows', + 'slug' => 'vps-professional-windows', + 'short_description' => 'VPS Professional với Windows Server', + 'description' => 'VPS Professional với Windows Server 2022 - Cho enterprise
', + 'thumbnail' => '/images/vps/windows-professional.png', + 'cpu' => 4, + 'ram' => 8, + 'disk' => 160, + 'bandwidth' => 500, + 'stock' => 15, + 'is_active' => true, + 'product_categories_id' => $windowsCategory->id, + 'template_id' => 'vm-3008', + 'pricing' => [ + ['billing_cycle' => 'monthly', 'price' => 850000, 'price_sale' => 749000], + ['billing_cycle' => 'quarterly', 'price' => 2550000, 'price_sale' => 2247000], + ['billing_cycle' => 'semi_annually', 'price' => 5100000, 'price_sale' => 4494000], + ['billing_cycle' => 'annually', 'price' => 10200000, 'price_sale' => 8988000], + ], + 'os' => [0, 1], + ], + ]; + + // Create Products + foreach ($vpsProducts as $productData) { + $productCategoriesId = $productData['product_categories_id']; + $templateId = $productData['template_id']; + $pricingData = $productData['pricing']; + $osIndices = $productData['os']; + unset($productData['product_categories_id'], $productData['template_id'], $productData['pricing'], $productData['os']); + + // Determine OS list based on category + $osList = ($productCategoriesId === $ubuntuCategory->id) ? $ubuntuOS : $windowsOS; + + $product = Product::updateOrCreate( + ['slug' => $productData['slug']], + array_merge($productData, [ + 'product_categories_id' => $productCategoriesId, + ]) + ); + + // Add template_id as attribute (stored in a separate table or as meta) + // For now, we store it as part of product's custom attributes if needed + // The template_id will be used when creating VM + + // Attach OS versions + $osToAttach = []; + foreach ($osIndices as $index) { + if (isset($osList[$index])) { + $osToAttach[] = $osList[$index]->id; + } + } + if (!empty($osToAttach)) { + $product->osVersions()->sync($osToAttach); + } + + // Create pricing tiers + foreach ($pricingData as $pricing) { + ProductPricings::updateOrCreate( + [ + 'product_id' => $product->id, + 'billing_cycle' => $pricing['billing_cycle'], + ], + [ + 'price' => $pricing['price'], + 'price_sale' => $pricing['price_sale'], + 'currency' => 'VND', + 'is_active' => true, + ] + ); + } + + $this->command->info("Created product: {$product->name}"); + } + + // Make sure we have VPS addons + $this->ensureVPSAddons(); + + $this->command->info('VPS Products seeding completed!'); + } + + private function ensureVPSAddons(): void + { + $vpsAddons = [ + [ + 'type' => 'vps', + 'code' => 'cpu', + 'name' => 'CPU', + 'price' => 25000, + 'min' => 1, + 'max' => 32, + 'step' => 1, + 'prefix' => 'vCore', + 'input_type' => 'number', + 'status' => true, + 'sort_order' => 0, + ], + [ + 'type' => 'vps', + 'code' => 'ram', + 'name' => 'RAM', + 'price' => 15000, + 'min' => 1, + 'max' => 64, + 'step' => 1, + 'prefix' => 'GB', + 'input_type' => 'number', + 'status' => true, + 'sort_order' => 1, + ], + [ + 'type' => 'vps', + 'code' => 'disk', + 'name' => 'SSD', + 'price' => 5000, + 'min' => 10, + 'max' => 1000, + 'step' => 10, + 'prefix' => 'GB', + 'input_type' => 'number', + 'status' => true, + 'sort_order' => 2, + ], + ]; + + foreach ($vpsAddons as $addon) { + Addon::updateOrCreate( + ['code' => $addon['code'], 'type' => $addon['type']], + $addon + ); + } + } +} diff --git a/public/hot b/public/hot deleted file mode 100644 index 75e8839..0000000 --- a/public/hot +++ /dev/null @@ -1 +0,0 @@ -http://[::1]:5174 \ No newline at end of file diff --git a/resources/views/administrator/layout/master.blade.php b/resources/views/administrator/layout/master.blade.php index 2bd0c12..eb2ca75 100644 --- a/resources/views/administrator/layout/master.blade.php +++ b/resources/views/administrator/layout/master.blade.php @@ -72,16 +72,6 @@ @stack('plugin-scripts') - - - document.addEventListener('DOMContentLoaded', function() { - if (typeof ModalManager !== 'undefined') { - ModalManager.init(); - console.log('ModalManager initialized globally'); - } else { - console.warn('ModalManager not loaded'); - } - }); @include('administrator.layout.partials.toast') diff --git a/resources/views/administrator/pages/auth/login.blade.php b/resources/views/administrator/pages/auth/login.blade.php index a9f870e..751573b 100644 --- a/resources/views/administrator/pages/auth/login.blade.php +++ b/resources/views/administrator/pages/auth/login.blade.php @@ -1,9 +1,4 @@ -@extends('administrator.layout.master2') - - -@push('plugin-styles') - -@endpush +@extends('administrator.layout.master') @section('content')