From 92151261b3e2f436b114d3d9c11f704b40589017 Mon Sep 17 00:00:00 2001 From: pbao9 Date: Fri, 6 Feb 2026 11:05:18 +0700 Subject: [PATCH] feat: Implement product and product category management with dedicated CRUD operations, services, and repositories. --- app/Enums/Product/ProductTypeEnums.php | 3 + app/Http/.DS_Store | Bin 6148 -> 6148 bytes .../Controllers/admin/ProductController.php | 418 ------------------ .../Controllers/admin/SettingController.php | 4 +- .../product/ProductCategoryController.php | 88 ++++ .../admin/product/ProductController.php | 149 +++++++ .../admin/vm/VirtualMachineController.php | 12 +- .../Requests/Product/StoreProductRequest.php | 43 ++ .../Requests/Product/UpdateProductRequest.php | 41 ++ .../ProductCategoryRequest.php | 87 ++++ .../ProductCategoryService.php | 32 ++ .../ProductCategoryServiceInterface.php | 9 + app/Http/Services/ProductService.php | 118 +++++ .../VirtualMachine/Sync/SyncAllPackage.php | 1 + app/Models/Product.php | 7 +- app/Models/ProductCategories.php | 35 ++ app/Providers/RepositoryProvider.php | 16 + app/Providers/ServiceServiceProvider.php | 33 ++ .../Admin/Product/ProductRepository.php | 63 +++ .../Product/ProductRepositoryInterface.php | 16 + .../ProductCategoryRepository.php | 63 +++ .../ProductCategoryRepositoryInterface.php | 16 + app/View/Components/AdminBadge.php | 73 +++ config/admin-sidebar.php | 2 +- config/app.php | 1 + .../2024_02_01_090537_module_host.php | 6 +- .../2024_02_01_091707_module_datastore.php | 4 +- .../2024_02_01_092104_module_network.php | 22 +- .../2024_02_01_094827_module_ipadress.php | 6 +- .../2024_02_02_194120_os__group.php | 6 +- database/migrations/2024_02_02_395629_os.php | 3 +- ...223604_create_product_categories_table.php | 36 ++ ...026_01_27_172907_create_products_table.php | 5 +- ...327_create_product_v_m_templates_table.php | 27 -- ...40_update_table_module_datastore_table.php | 32 -- resources/css/app.css | 5 + resources/sass/_variables.scss | 4 +- .../pages/block/ModuleHosting.blade.php | 2 +- .../pages/block/ModuleIPPool.blade.php | 2 +- .../administrator/pages/block/btn.blade.php | 2 - .../pages/block/product/btn.blade.php | 6 + .../pages/module/ippool/index.blade.php | 54 +-- .../module/vcenter/datatable/status.blade.php | 10 +- .../pages/module/vcenter/index.blade.php | 2 +- .../package/vm/datatable/action.blade.php | 2 + .../pages/package/vm/index.blade.php | 1 - .../pages/product/categories/create.blade.php | 118 +++++ .../categories/datatable/action.blade.php | 2 + .../categories/datatable/status.blade.php | 13 + .../pages/product/categories/edit.blade.php | 111 +++++ .../pages/product/categories/index.blade.php | 62 +++ .../pages/product/create.blade.php | 295 ++++++++++++ .../pages/product/datatable/action.blade.php | 2 + .../pages/product/edit.blade.php | 269 +++++++++++ .../pages/product/first.blade.php | 4 +- .../pages/product/index.blade.php | 140 +++--- .../pages/settings/general.blade.php | 311 +++++++++++++ .../views/components/admin-badge.blade.php | 8 + .../views/components/button-c-r-m.blade.php | 53 ++- .../livewire/v-p-s-reinstall-modal.blade.php | 4 +- routes/admin.php | 28 +- 61 files changed, 2331 insertions(+), 656 deletions(-) delete mode 100644 app/Http/Controllers/admin/ProductController.php create mode 100644 app/Http/Controllers/admin/product/ProductCategoryController.php create mode 100644 app/Http/Controllers/admin/product/ProductController.php create mode 100644 app/Http/Requests/Product/StoreProductRequest.php create mode 100644 app/Http/Requests/Product/UpdateProductRequest.php create mode 100644 app/Http/Requests/ProductCategories/ProductCategoryRequest.php create mode 100644 app/Http/Services/Admin/ProductCategory/ProductCategoryService.php create mode 100644 app/Http/Services/Admin/ProductCategory/ProductCategoryServiceInterface.php create mode 100644 app/Http/Services/ProductService.php create mode 100644 app/Models/ProductCategories.php create mode 100644 app/Providers/ServiceServiceProvider.php create mode 100644 app/Repositories/Admin/Product/ProductRepository.php create mode 100644 app/Repositories/Admin/Product/ProductRepositoryInterface.php create mode 100644 app/Repositories/Admin/ProductCategory/ProductCategoryRepository.php create mode 100644 app/Repositories/Admin/ProductCategory/ProductCategoryRepositoryInterface.php create mode 100644 app/View/Components/AdminBadge.php create mode 100644 database/migrations/2026_01_26_223604_create_product_categories_table.php delete mode 100644 database/migrations/2026_01_29_120327_create_product_v_m_templates_table.php delete mode 100644 database/migrations/2026_01_31_111040_update_table_module_datastore_table.php delete mode 100644 resources/views/administrator/pages/block/btn.blade.php create mode 100644 resources/views/administrator/pages/block/product/btn.blade.php create mode 100644 resources/views/administrator/pages/package/vm/datatable/action.blade.php create mode 100644 resources/views/administrator/pages/product/categories/create.blade.php create mode 100644 resources/views/administrator/pages/product/categories/datatable/action.blade.php create mode 100644 resources/views/administrator/pages/product/categories/datatable/status.blade.php create mode 100644 resources/views/administrator/pages/product/categories/edit.blade.php create mode 100644 resources/views/administrator/pages/product/categories/index.blade.php create mode 100644 resources/views/administrator/pages/product/create.blade.php create mode 100644 resources/views/administrator/pages/product/datatable/action.blade.php create mode 100644 resources/views/administrator/pages/product/edit.blade.php create mode 100644 resources/views/administrator/pages/settings/general.blade.php create mode 100644 resources/views/components/admin-badge.blade.php diff --git a/app/Enums/Product/ProductTypeEnums.php b/app/Enums/Product/ProductTypeEnums.php index 0f99c6b..6d8292b 100644 --- a/app/Enums/Product/ProductTypeEnums.php +++ b/app/Enums/Product/ProductTypeEnums.php @@ -11,6 +11,7 @@ enum ProductTypeEnums: string case SSL = 'ssl'; case DOMAIN = 'domain'; case WEB_DESIGN = 'web design'; + case ADDON = 'addon'; public function label(): string { @@ -22,6 +23,7 @@ enum ProductTypeEnums: string self::SSL => 'SSL', self::DOMAIN => 'Domain', self::WEB_DESIGN => 'Web Design', + self::ADDON => 'Addon', }; } @@ -35,6 +37,7 @@ enum ProductTypeEnums: string self::SSL => 'teal', self::DOMAIN => 'purple', self::WEB_DESIGN => 'pink', + self::ADDON => 'orange', }; } } \ No newline at end of file diff --git a/app/Http/.DS_Store b/app/Http/.DS_Store index 4121233479517c1a8c630e6e2a2b2f85505194c3..6338d46d2325cc882b30c4592faf7f1b1639ca1d 100644 GIT binary patch delta 158 zcmZoMXfc=|#>B!ku~2NHo}wrV0|Nsi1A_nqLlHwhLk9?bv% delta 93 zcmZoMXfc=|#>B`mu~2NHo}wrd0|Nsi1A_nqLncEqLn=dYQh9N~#KPs14MbQr3$uP< p+N{9A!m^p2gP#MaX|o{5cjn3bB9=h)V0|F%ApIgroupRepository->filter( - [ - 'page' => $request->get('page') ?? 1 - ], - [] - ); - $groups->getCollection()->transform(function ($item) { - $item->products = $this->groupRepository->getProduct($item->id, $item->type_id); - return $item; - }); - - $title = "List All Product"; - - return view('administrator.pages.product.index', compact('title', 'groups')); - } - - public function create(Request $request) - { - $step = $request->get('step') ?? 1; - $title = 'Add new product'; - $group = $this->groupRepository->getAll(); - $type = $this->typeRepository->getAll(); - if ($step === 1) { - return view("administrator.pages.product.first", compact('group', 'type', 'title')); - } - - $validator = Validator::make($request->except('_token'), [ - 'step' => 'required', - 'type' => 'required' - ]); - if ($validator->fails()) { - return back()->withErrors($validator->getMessageBag()); - } - - $type_id = (int) $request->type; - $group_id = (int) $request->group; - $product_name = $request->name; - - $view = null; - - $view = match ((int) $type_id) { - 1 => "administrator.pages.product.addon", - 2 => "administrator.pages.product.domain.add", - 3 => "administrator.pages.product.hosting", - 4 => "administrator.pages.product.vps" - }; - - // Thêm dữ liệu OS cho VPS - $data = compact('type_id', 'group_id', 'title', 'group', 'type', 'product_name'); - if ((int) $type_id === 4) { - $data['os'] = $this->osRepository->filter(['status' => 1], ['osGroup']); - } - - return view($view, $data); - } - - public function store(Request $request) - { - if (!$request->type_id) - return back()->withErrors('Please choose Type Product'); - $validator = null; - $repository = match ((int) $request->type_id) { - 1 => $this->addonRepository, - 2 => $this->domainRepository, - 3 => $this->hostingRepository, - 4 => $this->vpsRepository, - default => throw new \UnexpectedValueException("Invalid type_id: " . $request->type_id) - }; - $route = match ((int) $request->type_id) { - 1 => route('addon.index'), - 2 => route('domain.index'), - default => route('product.index') - }; - $validator = match ((int) $request->type_id) { - 1 => Validator::make($request->all(), [ - 'name' => 'required', - 'price' => 'required|numeric', - 'min' => 'required|numeric', - 'max' => 'required|numeric', - 'step' => 'required|numeric', - 'prefix' => 'required', - 'type_id' => 'required|numeric' - ]), - 2 => Validator::make($request->all(), [ - 'name' => 'required', - 'price' => 'required', - 'renewal' => 'required', - 'transfer' => 'required', - 'type_id' => 'required', - 'auto_reg' => 'required', - 'status' => 'required' - ]), - 3 => Validator::make($request->all(), [ - 'name' => 'required', - 'price' => 'required', - 'description' => 'required', - 'template' => 'required', - 'type_id' => 'required', - 'group_id' => 'required', - 'status' => 'required' - ]), - 4 => Validator::make($request->all(), [ - 'name' => 'required', - 'cpu' => 'required', - 'ram' => 'required', - 'disk' => 'required', - 'bandwidth' => 'required', - 'module_host_id' => 'required|not_in:0', - 'price' => 'required', - 'type_id' => 'required', - 'group_id' => 'required', - 'status' => 'required' - ]) - }; - - if ($validator->fails()) { - return back()->withErrors($validator->getMessageBag()); - } - if ($repository) { - DB::beginTransaction(); - try { - if ((int) $request->type_id === 4 && method_exists($repository, 'createWithOs')) { - // VPS with OS relationship - $osIds = $request->input('operating_systems', []); - $repository->createWithOs($request->all(), $osIds); - } else { - $repository->create($request->all()); - } - - // Clear all related caches after successful creation - $this->clearProductCaches($request->type_id, $request->group_id); - - DB::commit(); - } catch (\Exception $error) { - DB::rollBack(); - return back()->withErrors($error->getMessage()); - } - - return redirect($route)->with('success', 'Add new product success'); - } - return back()->withErrors("Can't find a model by Type"); - } - - public function edit(Request $request) - { - $validator = Validator::make($request->all(), [ - 'product_id' => 'required', - 'type_id' => 'required' - ]); - if ($validator->fails()) { - return back()->withErrors($validator->getMessageBag()); - } - $type_id = (int) $request->type_id; - - $title = 'View product item'; - $group = $this->groupRepository->getAll(); - $type = $this->typeRepository->getAll(); - $view = match ((int) $type_id) { - 1 => "administrator.pages.product.addon", - 2 => "administrator.pages.product.domain.add", - 3 => "administrator.pages.product.hosting", - 4 => "administrator.pages.product.vps" - }; - $repository = match ((int) $type_id) { - 1 => $this->addonRepository, - 2 => $this->domainRepository, - 3 => $this->hostingRepository, - 4 => $this->vpsRepository, - default => throw new \UnexpectedValueException("Invalid type_id: " . $request->type_id) - }; - if ($repository && method_exists($repository, 'findWithRelationships')) { - // Load relationships based on product type - $relationships = ['host.datacenter']; - if ((int) $type_id === 4) { - $relationships[] = 'operatingSystems'; - } - - $product = $repository->findWithRelationships($request->product_id, $relationships); - - // Thêm dữ liệu OS cho VPS - $data = compact('type_id', 'product', 'title', 'group', 'type'); - if ((int) $type_id === 4) { - $data['os'] = $this->osRepository->filter(['status' => 1], ['osGroup']); - } - - return view($view, $data); - } - return back()->withErrors("Can't find a model by Type"); - } - - public function update(Request $request) - { - $validator = Validator::make($request->all(), [ - 'product_id' => 'required', - 'type_id' => 'required' - ]); - if ($validator->fails()) { - return back()->withErrors($validator->getMessageBag()); - } - $route = match ((int) $request->type_id) { - 1 => route('addon.index'), - 2 => route('domain.index'), - default => route('product.index') - }; - $repository = match ((int) $request->type_id) { - 1 => $this->addonRepository, - 2 => $this->domainRepository, - 3 => $this->hostingRepository, - 4 => $this->vpsRepository, - default => throw new \UnexpectedValueException("Invalid type_id: " . $request->type_id) - }; - - $product = $repository->find($request->product_id); - if (!$product) - return back()->withErrors("Can't find a product item"); - - $validator = match ((int) $request->type_id) { - 1 => Validator::make($request->all(), [ - 'name' => 'required', - 'price' => 'required|numeric', - 'min' => 'required|numeric', - 'max' => 'required|numeric', - 'step' => 'required|numeric', - 'prefix' => 'required', - 'type_id' => 'required|numeric' - ]), - 2 => Validator::make($request->all(), [ - 'name' => 'required', - 'price' => 'required', - 'renewal' => 'required', - 'transfer' => 'required', - 'type_id' => 'required', - 'auto_reg' => 'required', - 'status' => 'required' - ]), - 3 => Validator::make($request->all(), [ - 'name' => 'required', - 'price' => 'required', - 'description' => 'required', - 'cpu' => 'required', - 'ram' => 'required', - 'disk' => 'required', - 'bandwidth' => 'required', - 'ip' => 'required', - 'template' => 'required', - 'type_id' => 'required', - 'group_id' => 'required', - 'status' => 'required' - ]), - 4 => Validator::make($request->all(), [ - 'name' => 'required', - 'cpu' => 'required', - 'ram' => 'required', - 'disk' => 'required', - 'bandwidth' => 'required', - 'module_host_id' => 'required|not_in:0', - 'price' => 'required', - 'type_id' => 'required', - 'group_id' => 'required', - 'status' => 'required' - ]) - }; - - if ($validator->fails()) { - return back()->withErrors($validator->getMessageBag()); - } - if ($repository) { - try { - DB::beginTransaction(); - if ((int) $request->type_id === 4 && method_exists($repository, 'updateWithOs')) { - // VPS with OS relationship - $osIds = $request->input('operating_systems', []); - $repository->updateWithOs($request->get('product_id'), $request->all(), $osIds); - } else { - $repository->update($request->get('product_id'), $request->all()); - } - - // Clear all related caches after successful update - $this->clearProductCaches($request->type_id, $request->group_id); - - DB::commit(); - } catch (\Exception $error) { - DB::rollBack(); - return back()->withErrors($error->getMessage()); - } - return redirect($route)->with('success', 'Update new information product success'); - } - return back()->withErrors("Can't find a model by Type"); - } - - public function destroy(Request $request) - { - $validator = Validator::make($request->all(), [ - 'product_id' => 'required', - 'type' => 'required' - ]); - if ($validator->fails()) { - return back()->withErrors($validator->getMessageBag()); - } - - - $repository = match ((int) $request->type) { - 1 => $this->addonRepository, - 2 => $this->domainRepository, - 3 => $this->hostingRepository, - 4 => $this->vpsRepository, - default => throw new \UnexpectedValueException("Invalid type_id: " . $request->type_id) - }; - $route = match ((int) $request->type) { - 1 => route('addon.index'), - 2 => route('domain.index'), - default => route('product.index') - }; - - if ($repository) { - - $product = $repository->find($request->product_id); - - if (!$product) - return back()->withErrors("Can't find a product item"); - - try { - DB::beginTransaction(); - $repository->delete($request->get('product_id')); - - // Clear all related caches after successful deletion - $this->clearProductCaches($request->type, $product->group_id ?? null); - - DB::commit(); - } catch (\Exception $error) { - DB::rollBack(); - return back()->withErrors($error->getMessage()); - } - return redirect($route)->with('success', 'Deleted successfully'); - } - return back()->withErrors("Can't find a Product"); - } - - /** - * Clear all product-related caches - */ - private function clearProductCaches($typeId, $groupId = null) - { - // Clear general shop caches - Cache::forget('shop_types_with_groups'); - - if ($typeId) { - // Clear type-specific group cache - Cache::forget("groups_type_{$typeId}"); - - if ($groupId) { - // Clear product cache for specific type and group - Cache::forget("products_type_{$typeId}_group_{$groupId}"); - - // Clear paginated product caches - we need to clear multiple pages - $group = $this->groupRepository->find($groupId); - if ($group && $group->slug) { - // Clear group product caches for multiple pages (assuming max 10 pages) - for ($page = 1; $page <= 10; $page++) { - Cache::forget("group_products_{$group->slug}_page_{$page}"); - } - } - } - } - - // Clear all VPS and Hosting caches if type is not specified - if (!$typeId) { - $types = [3, 4]; // Hosting and VPS - foreach ($types as $type) { - Cache::forget("groups_type_{$type}"); - - // Clear all group caches for this type - $groups = $this->groupRepository->filter(['type_id' => $type]); - foreach ($groups as $group) { - Cache::forget("products_type_{$type}_group_{$group->id}"); - if ($group->slug) { - for ($page = 1; $page <= 10; $page++) { - Cache::forget("group_products_{$group->slug}_page_{$page}"); - } - } - } - } - } - } -} diff --git a/app/Http/Controllers/admin/SettingController.php b/app/Http/Controllers/admin/SettingController.php index 43a5aa5..66b5c29 100644 --- a/app/Http/Controllers/admin/SettingController.php +++ b/app/Http/Controllers/admin/SettingController.php @@ -23,14 +23,14 @@ class SettingController extends Controller { $title = 'General Setting'; - return view('administrator.pagesetting', compact('title')); + return view('administrator.pages.settings.general', compact('title')); } public function smtp() { $title = 'SMTP Setting'; - return view('administrator.pagesmtp', compact('title')); + return view('administrator.pages.smtp', compact('title')); } public function store(Request $request) diff --git a/app/Http/Controllers/admin/product/ProductCategoryController.php b/app/Http/Controllers/admin/product/ProductCategoryController.php new file mode 100644 index 0000000..df3d707 --- /dev/null +++ b/app/Http/Controllers/admin/product/ProductCategoryController.php @@ -0,0 +1,88 @@ +productCategoryService->store($request->validated()); + return redirect()->route('products.group.edit', ['id' => $category->id])->with('success', 'Create Group Success'); + } + + public function edit(int $id) + { + $category = $this->productCategoryRepository->find($id); + if (!$category) { + return redirect()->route('products.group.index')->with('error', 'Category not found'); + } + $categories = \App\Models\ProductCategories::where('id', '!=', $id)->get(); + return view('administrator.pages.product.categories.edit', compact('category', 'categories')); + } + + public function update(ProductCategoryRequest $request, int $id) + { + $this->productCategoryService->update($id, $request->validated()); + return redirect()->route('products.group.edit', ['id' => $id])->with('success', 'Update Group Success'); + } + + public function destroy(int $id) + { + $this->productCategoryService->delete($id); + return response()->json(['success' => true]); + } + + public function datatable(Request $request) + { + $query = $this->productCategoryRepository->filter( + $request->all(), + [], + [], + ['*'], + true + ); + + return DataTables::eloquent($query) + ->addColumn( + 'action', + function ($d) { + return view('administrator.pages.product.categories.datatable.action', ['row' => $d])->render(); + } + ) + ->addColumn( + 'is_active', + function ($d) { + return view('administrator.pages.product.categories.datatable.status', ['row' => $d])->render(); + } + ) + ->rawColumns(['action', 'is_active']) + ->make(true); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/admin/product/ProductController.php b/app/Http/Controllers/admin/product/ProductController.php new file mode 100644 index 0000000..b8d570a --- /dev/null +++ b/app/Http/Controllers/admin/product/ProductController.php @@ -0,0 +1,149 @@ +productRepository->filter( + [ + 'page' => $request->get('page') ?? 1 + ] + ); + + $title = "List All Product"; + + return view('administrator.pages.product.index', compact('title', 'products')); + } + + public function create(Request $request) + { + $step = $request->get('step') ?? 1; + $title = 'Add new product'; + $group = $this->groupRepository->getAll(); + $type = $this->typeRepository->getAll(); + + if ($step === 1) { + return view("administrator.pages.product.create", compact('group', 'type', 'title')); + } + + if (!$request->has(['step', 'type'])) { + return back()->withErrors('Missing step or type'); + } + + $type_id = (int) $request->type; + $product_name = $request->name; + + $view = "administrator.pages.product.create"; + + $data = compact('type_id', 'title', 'group', 'type', 'product_name'); + + return view($view, $data); + } + + public function store(StoreProductRequest $request) + { + try { + $this->productService->store($request->validated()); + + return redirect()->route('product.index')->with('success', 'Add new product success'); + } catch (Exception $e) { + return back()->withErrors($e->getMessage())->withInput(); + } + } + + public function edit(Request $request) + { + $product = $this->productRepository->find($request->product_id); + + if (!$product) { + return back()->withErrors("Can't find product."); + } + + $title = 'View product item'; + $group = $this->groupRepository->getAll(); + $type = $this->typeRepository->getAll(); + + $productType = ProductTypeEnums::tryFrom($product->product_type); + $type_id = $this->mapEnumToTypeId($productType); + + $view = "administrator.pages.product.edit"; + + $data = compact('product', 'title', 'group', 'type', 'type_id'); + + return view($view, $data); + } + + public function update(UpdateProductRequest $request) + { + try { + $this->productService->update($request->product_id, $request->validated()); + return redirect()->route('product.index')->with('success', 'Update product success'); + } catch (Exception $e) { + return back()->withErrors($e->getMessage())->withInput(); + } + } + + public function destroy(Request $request) + { + try { + $this->productService->delete($request->product_id); + return redirect()->route('product.index')->with('success', 'Deleted successfully'); + } catch (Exception $e) { + return back()->withErrors($e->getMessage()); + } + } + + public function datatable(Request $request) + { + $query = $this->productRepository->filter( + $request->all(), + [], + [], + ['*'], + true + ); + + return DataTables::eloquent($query) + ->addColumn( + 'action', + function ($d) { + return view('administrator.pages.product.datatable.action', ['row' => $d])->render(); + } + ) + ->make(true); + } + + private function mapEnumToTypeId(?ProductTypeEnums $enum): int + { + return match ($enum) { + ProductTypeEnums::ADDON => 1, + ProductTypeEnums::DOMAIN => 2, + ProductTypeEnums::HOSTING => 3, + ProductTypeEnums::VPS => 4, + default => 0 + }; + } +} diff --git a/app/Http/Controllers/admin/vm/VirtualMachineController.php b/app/Http/Controllers/admin/vm/VirtualMachineController.php index a2ad24b..449212d 100644 --- a/app/Http/Controllers/admin/vm/VirtualMachineController.php +++ b/app/Http/Controllers/admin/vm/VirtualMachineController.php @@ -121,7 +121,7 @@ class VirtualMachineController extends Controller { $query = $this->virtualMachineRepository->filter( $request->all(), - ['datacenter', 'host'], + ['datacenter'], [], ['*'], true @@ -131,17 +131,15 @@ class VirtualMachineController extends Controller ->addColumn( 'action', function ($d) { - return ' -
- ' . csrf_field() . ' - ' . method_field('delete') . ' - -
'; + return view('administrator.pages.package.vm.datatable.action', ['row' => $d])->render(); } ) ->editColumn('updated_at', function ($d) { return $d->updated_at ? $d->updated_at->format('H:i:s d/m/Y') : ''; }) + ->editColumn('datacenter.name', function ($d) { + return $d->datacenter ? $d->datacenter->name . ' - ' . $d->datacenter->datacenter_id : 'N/A'; + }) ->rawColumns(['action']) ->make(true); } diff --git a/app/Http/Requests/Product/StoreProductRequest.php b/app/Http/Requests/Product/StoreProductRequest.php new file mode 100644 index 0000000..22f063d --- /dev/null +++ b/app/Http/Requests/Product/StoreProductRequest.php @@ -0,0 +1,43 @@ + ['required', 'string', 'max:255'], + 'price' => ['required', 'numeric', 'min:0'], + 'price_sale' => ['nullable', 'numeric', 'min:0'], + 'description' => ['nullable', 'string'], + 'status' => ['required', 'integer'], // Value from Status Enum + 'type_id' => ['required', 'integer'], + ]; + + // Infer product type from type_id (Legacy mapping needs to be consistent with Controller) + // Controller map: 1=>Addon, 2=>Domain, 3=>Hosting, 4=>VPS + // We can access input 'type_id' + $typeId = (int) $this->input('type_id'); + + if ($typeId === 4) { // VPS + $rules = array_merge($rules, [ + 'cpu' => ['required'], + 'ram' => ['required'], + 'disk' => ['required'], + 'bandwidth' => ['required'], + ]); + } + + return $rules; + } +} diff --git a/app/Http/Requests/Product/UpdateProductRequest.php b/app/Http/Requests/Product/UpdateProductRequest.php new file mode 100644 index 0000000..80bc266 --- /dev/null +++ b/app/Http/Requests/Product/UpdateProductRequest.php @@ -0,0 +1,41 @@ + ['required', 'string', 'max:255'], + 'price' => ['required', 'numeric', 'min:0'], + 'product_id' => ['required', 'exists:products,id'], + ]; + + $typeId = (int) $this->input('type_id'); + + if ($typeId === 4) { // VPS + $rules = array_merge($rules, [ + 'cpu' => ['required'], + 'ram' => ['required'], + 'disk' => ['required'], + ]); + } + + return $rules; + } +} diff --git a/app/Http/Requests/ProductCategories/ProductCategoryRequest.php b/app/Http/Requests/ProductCategories/ProductCategoryRequest.php new file mode 100644 index 0000000..b991b57 --- /dev/null +++ b/app/Http/Requests/ProductCategories/ProductCategoryRequest.php @@ -0,0 +1,87 @@ +route('id'); + // hoặc: $this->route('product_category') + + return [ + 'name' => [ + 'required', + 'string', + 'max:255', + ], + + 'slug' => [ + 'nullable', + 'string', + 'max:255', + Rule::unique('product_categories', 'slug') + ->ignore($categoryId), + ], + + 'parent_id' => [ + 'nullable', + 'integer', + 'exists:product_categories,id', + ], + + 'is_active' => [ + 'nullable', + 'boolean', + ], + + 'sort_order' => [ + 'nullable', + 'integer', + 'min:0', + ], + ]; + } + + /** + * Chuẩn hóa dữ liệu trước khi validate + */ + protected function prepareForValidation(): void + { + $this->merge([ + 'slug' => $this->slug + ? \Str::slug($this->slug) + : \Str::slug($this->name), + + 'is_active' => $this->has('is_active') + ? (bool) $this->is_active + : true, + ]); + } + + /** + * Custom message (tuỳ chọn) + */ + public function messages(): array + { + return [ + 'name.required' => 'Tên danh mục không được để trống.', + 'slug.unique' => 'Slug đã tồn tại.', + 'parent_id.exists' => 'Danh mục cha không tồn tại.', + ]; + } +} diff --git a/app/Http/Services/Admin/ProductCategory/ProductCategoryService.php b/app/Http/Services/Admin/ProductCategory/ProductCategoryService.php new file mode 100644 index 0000000..fd3f615 --- /dev/null +++ b/app/Http/Services/Admin/ProductCategory/ProductCategoryService.php @@ -0,0 +1,32 @@ +repository = $repository; + } + + public function store(array $data) + { + return $this->repository->create($data); + } + + public function update(int $id, array $data) + { + return $this->repository->update($id, $data); + } + + public function delete(int $id) + { + return $this->repository->delete($id); + } +} \ No newline at end of file diff --git a/app/Http/Services/Admin/ProductCategory/ProductCategoryServiceInterface.php b/app/Http/Services/Admin/ProductCategory/ProductCategoryServiceInterface.php new file mode 100644 index 0000000..d96b0f2 --- /dev/null +++ b/app/Http/Services/Admin/ProductCategory/ProductCategoryServiceInterface.php @@ -0,0 +1,9 @@ +mapTypeIdToEnum($typeId); + + if (!$productType) { + throw new Exception('Invalid Product Type'); + } + + $prepareData = [ + 'uuid' => (string) Str::uuid(), + 'name' => $data['name'], + 'slug' => Str::slug($data['name']), + 'product_type' => $productType->value, + 'is_active' => isset($data['status']) && (int)$data['status'] === Status::ACTIVE->value, + + 'price' => $data['price'] ?? 0, + 'price_sale' => $data['price_sale'] ?? null, + 'description' => $data['description'] ?? null, + + // VPS Columns + 'cpu' => $data['cpu'] ?? null, + 'ram' => $data['ram'] ?? null, + 'disk' => $data['disk'] ?? null, + 'bandwidth' => $data['bandwidth'] ?? null, + 'gpu' => $data['gpu'] ?? null, // if added later + + // Extra fields + 'sort_order' => $data['sort_order'] ?? 0, + 'billing_cycle' => $data['billing_cycle'] ?? 'monthly', + ]; + + $product = $this->productRepository->create($prepareData); + + DB::commit(); + return $product; + } catch (Exception $e) { + DB::rollBack(); + throw $e; + } + } + + public function update(int $id, array $data) + { + DB::beginTransaction(); + try { + $prepareData = []; + + if (isset($data['name'])) { + $prepareData['name'] = $data['name']; + $prepareData['slug'] = Str::slug($data['name']); + } + + if (isset($data['status'])) { + $prepareData['is_active'] = (int)$data['status'] === Status::ACTIVE->value; + } + + // Map other fields that overlap with fillable + $fillable = ['price', 'price_sale', 'billing_cycle', 'cpu', 'ram', 'gpu', 'disk', 'bandwidth', 'sort_order', 'description']; + foreach ($fillable as $field) { + if (array_key_exists($field, $data)) { + $prepareData[$field] = $data[$field]; + } + } + + $result = $this->productRepository->update($id, $prepareData); + + DB::commit(); + return $result; + } catch (Exception $e) { + DB::rollBack(); + throw $e; + } + } + + public function delete(int $id) + { + DB::beginTransaction(); + try { + $this->productRepository->delete($id); + DB::commit(); + return true; + } catch (Exception $e) { + DB::rollBack(); + throw $e; + } + } + + private function mapTypeIdToEnum(int $typeId): ?ProductTypeEnums + { + return match ($typeId) { + 1 => ProductTypeEnums::ADDON, + 2 => ProductTypeEnums::DOMAIN, + 3 => ProductTypeEnums::HOSTING, + 4 => ProductTypeEnums::VPS, + default => null + }; + } +} diff --git a/app/Jobs/VirtualMachine/Sync/SyncAllPackage.php b/app/Jobs/VirtualMachine/Sync/SyncAllPackage.php index e660c9e..8cc3327 100644 --- a/app/Jobs/VirtualMachine/Sync/SyncAllPackage.php +++ b/app/Jobs/VirtualMachine/Sync/SyncAllPackage.php @@ -34,5 +34,6 @@ class SyncAllPackage implements ShouldQueue SyncDatastore::dispatch(); SyncNetwork::dispatch(); SyncFolder::dispatch(); + SyncVirtualMachine::dispatch(); } } diff --git a/app/Models/Product.php b/app/Models/Product.php index af9d9fc..17c01ad 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -5,10 +5,11 @@ namespace App\Models; use App\Traits\HasUuid; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\SoftDeletes; class Product extends Model { - use HasFactory, HasUuid; + use HasFactory, HasUuid, SoftDeletes; protected array $uuidColumns = ['uuid']; protected string $slugColumn = 'name'; @@ -29,4 +30,8 @@ class Product extends Model 'sort_order', ]; + public function categories() + { + return $this->belongsToMany(ProductCategories::class, 'product_product_category', 'product_id', 'product_category_id'); + } } diff --git a/app/Models/ProductCategories.php b/app/Models/ProductCategories.php new file mode 100644 index 0000000..512f258 --- /dev/null +++ b/app/Models/ProductCategories.php @@ -0,0 +1,35 @@ +belongsTo(ProductCategories::class, 'parent_id'); + } + + public function children() + { + return $this->hasMany(ProductCategories::class, 'parent_id'); + } + + public function products() + { + return $this->belongsToMany(Product::class, 'product_product_category', 'product_category_id', 'product_id'); + } +} diff --git a/app/Providers/RepositoryProvider.php b/app/Providers/RepositoryProvider.php index fd1bceb..641a0c0 100644 --- a/app/Providers/RepositoryProvider.php +++ b/app/Providers/RepositoryProvider.php @@ -4,6 +4,8 @@ namespace App\Providers; use App\Repositories\Addon\AddonCachedRepository; use App\Repositories\Addon\AddonRepositoryInterface; +use App\Repositories\Admin\ProductCategory\ProductCategoryRepository; +use App\Repositories\Admin\ProductCategory\ProductCategoryRepositoryInterface; use App\Repositories\Banned\BannedCachedRepository; use App\Repositories\Banned\BannedRepositoryInterface; use App\Repositories\Customer\CustomerCachedRepository; @@ -88,6 +90,8 @@ use App\Repositories\Module\Vmware\VmwareCachedRepository; use App\Repositories\Module\Vmware\VmwareRepositoryInterface; use App\Repositories\Type\TypeCachedRepository; use App\Repositories\Type\TypeRepositoryInterface; +use App\Repositories\Admin\Product\ProductRepository; +use App\Repositories\Admin\Product\ProductRepositoryInterface; use App\Repositories\User\UserCachedRepository; use App\Repositories\User\UserRepositoryInterface; use App\Repositories\vCenter\vCenterCachedRepository; @@ -343,6 +347,18 @@ class RepositoryProvider extends ServiceProvider VirtualMachineRepositoryInterface::class, VirtualMachineRepository::class ); + + // Product + $this->app->bind( + ProductRepositoryInterface::class, + ProductRepository::class + ); + + // Product Category + $this->app->bind( + ProductCategoryRepositoryInterface::class, + ProductCategoryRepository::class + ); } /** diff --git a/app/Providers/ServiceServiceProvider.php b/app/Providers/ServiceServiceProvider.php new file mode 100644 index 0000000..a9a9513 --- /dev/null +++ b/app/Providers/ServiceServiceProvider.php @@ -0,0 +1,33 @@ +app->bind( + ProductCategoryServiceInterface::class, + ProductCategoryService::class + ); + } + + /** + * Bootstrap services. + * + * @return void + */ + public function boot() + { + // + } +} diff --git a/app/Repositories/Admin/Product/ProductRepository.php b/app/Repositories/Admin/Product/ProductRepository.php new file mode 100644 index 0000000..feab3aa --- /dev/null +++ b/app/Repositories/Admin/Product/ProductRepository.php @@ -0,0 +1,63 @@ +model + ->select($select) + ->with($relationships); + + // if (!empty($data['vcenter_id'])) { + // $query->where('vcenter_id', $data['vcenter_id']); + // } + + if (!empty($withCount)) { + $query->withCount($withCount); + } + + if ($returnQuery) { + return $query; + } + + if (!empty($data['paginate'])) { + return $query->paginate( + $data['per_page'] ?? 15 + ); + } + + return $query->get(); + } +} diff --git a/app/Repositories/Admin/Product/ProductRepositoryInterface.php b/app/Repositories/Admin/Product/ProductRepositoryInterface.php new file mode 100644 index 0000000..d7bf0ba --- /dev/null +++ b/app/Repositories/Admin/Product/ProductRepositoryInterface.php @@ -0,0 +1,16 @@ +model + ->select($select) + ->with($relationships); + + // if (!empty($data['vcenter_id'])) { + // $query->where('vcenter_id', $data['vcenter_id']); + // } + + if (!empty($withCount)) { + $query->withCount($withCount); + } + + if ($returnQuery) { + return $query; + } + + if (!empty($data['paginate'])) { + return $query->paginate( + $data['per_page'] ?? 15 + ); + } + + return $query->get(); + } +} diff --git a/app/Repositories/Admin/ProductCategory/ProductCategoryRepositoryInterface.php b/app/Repositories/Admin/ProductCategory/ProductCategoryRepositoryInterface.php new file mode 100644 index 0000000..e5d00a1 --- /dev/null +++ b/app/Repositories/Admin/ProductCategory/ProductCategoryRepositoryInterface.php @@ -0,0 +1,16 @@ +variant = $variant; + + $styles = [ + 'success' => 'bg-success-subtle text-success border border-success-subtle', + 'danger' => 'bg-danger-subtle text-danger border border-danger-subtle', + 'warning' => 'bg-warning-subtle text-warning border border-warning-subtle', + 'info' => 'bg-info-subtle text-info border border-info-subtle', + 'secondary' => 'bg-secondary-subtle text-secondary border border-secondary-subtle', + ]; + + // Status aliases + $aliases = [ + 1 => 'success', + 0 => 'danger', + 2 => 'warning', + 3 => 'info', + 'active' => 'success', + 'inactive' => 'danger', + 'pending' => 'warning' + ]; + + // Resolve alias if exists + $resolvedVariant = $aliases[$variant] ?? $variant; + + // Auto-icon logic if default 'circle' is used and we have a specific mapping + if ($icon === 'circle') { + $iconMap = [ + 'success' => 'check-circle', + 'danger' => 'x-circle', + 'warning' => 'alert-circle', + 'info' => 'info', + ]; + $this->icon = $iconMap[$resolvedVariant] ?? 'circle'; + } else { + $this->icon = $icon; + } + + $this->class = $styles[$resolvedVariant] ?? $styles['info']; + } + + /** + * Get the view / contents that represent the component. + */ + public function render(): View|Closure|string + { + return view('components.admin-badge'); + } +} diff --git a/config/admin-sidebar.php b/config/admin-sidebar.php index 17a471d..28b7396 100644 --- a/config/admin-sidebar.php +++ b/config/admin-sidebar.php @@ -64,7 +64,7 @@ return [ 'icon' => 'cloud', 'active' => ['product.*', 'addon.*', 'domain.*'], 'children' => [ - ['title' => 'All Product/Services', 'route' => 'product.index', 'active' => ['product.*']], + ['title' => 'All Product/Services', 'route' => 'products.index', 'active' => ['product.*']], ['title' => 'Product Addon', 'route' => 'addon.index', 'active' => ['addon.*']], ['title' => 'Domain Pricing', 'route' => 'domain.index', 'active' => ['domain.*']], ], diff --git a/config/app.php b/config/app.php index fbb3ee9..6bad407 100644 --- a/config/app.php +++ b/config/app.php @@ -183,6 +183,7 @@ return [ App\Providers\EventServiceProvider::class, App\Providers\HorizonServiceProvider::class, App\Providers\RouteServiceProvider::class, + App\Providers\ServiceServiceProvider::class, App\Providers\RepositoryProvider::class ], diff --git a/database/migrations/2024_02_01_090537_module_host.php b/database/migrations/2024_02_01_090537_module_host.php index 2c60e6a..b1945f1 100644 --- a/database/migrations/2024_02_01_090537_module_host.php +++ b/database/migrations/2024_02_01_090537_module_host.php @@ -14,11 +14,11 @@ return new class extends Migration public function up() { Schema::create('module_host',function (Blueprint $table) { - $table->id()->autoIncrement(); + $table->id(); $table->string('name',100)->nullable(); $table->string('host_id',255)->nullable(); $table->string('template',255)->nullable(); - $table->foreignId('module_datacenter_id')->constrained('module_datacenter')->cascadeOnDelete()->nullable()->index(); + $table->foreignId('module_datacenter_id')->nullable()->constrained('module_datacenter', 'id', 'fk_mod_host_mod_dc'); $table->string('console',255)->nullable(); $table->string('iso_path',255)->nullable(); $table->timestamps(); @@ -32,6 +32,6 @@ return new class extends Migration */ public function down() { - Schema::dropIfExists('module_hosts'); + Schema::dropIfExists('module_host'); } }; diff --git a/database/migrations/2024_02_01_091707_module_datastore.php b/database/migrations/2024_02_01_091707_module_datastore.php index 3b72ffa..0d7cdf5 100644 --- a/database/migrations/2024_02_01_091707_module_datastore.php +++ b/database/migrations/2024_02_01_091707_module_datastore.php @@ -13,12 +13,12 @@ return new class extends Migration { public function up() { Schema::create('module_datastore', function (Blueprint $table) { - $table->id()->autoIncrement(); + $table->id(); $table->string('name', 255)->nullable(); $table->string('datastore', 255)->nullable(); $table->string('type', 100)->nullable(); $table->integer('space')->nullable(); - $table->foreignId('module_host_id')->index()->constrained('module_hosts')->cascadeOnDelete(); + $table->foreignId('module_host_id')->index()->constrained('module_host')->nullable(); $table->tinyInteger('status')->default(1); $table->timestamps(); }); diff --git a/database/migrations/2024_02_01_092104_module_network.php b/database/migrations/2024_02_01_092104_module_network.php index 6995c4a..5634203 100644 --- a/database/migrations/2024_02_01_092104_module_network.php +++ b/database/migrations/2024_02_01_092104_module_network.php @@ -4,8 +4,7 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class extends Migration { /** * Run the migrations. * @@ -13,12 +12,19 @@ return new class extends Migration */ public function up() { - Schema::create('module_network',function (Blueprint $table) { - $table->id()->autoIncrement(); - $table->string('name',255)->nullable(); - $table->string('type',100)->nullable(); - $table->string('network',100)->nullable(); - $table->foreignId('module_host_id')->constrained('module_host')->index()->cascadeOnDelete(); + Schema::create('module_network', function (Blueprint $table) { + $table->id(); + $table->string('name', 255)->nullable(); + $table->string('type', 100)->nullable(); + $table->string('network', 100)->nullable(); + + $table->unsignedBigInteger('module_host_id')->nullable(); + + $table->foreign('module_host_id', 'fk_module_network_host') + ->references('id') + ->on('module_host') + ->nullOnDelete(); + $table->timestamps(); }); } diff --git a/database/migrations/2024_02_01_094827_module_ipadress.php b/database/migrations/2024_02_01_094827_module_ipadress.php index a968b90..a22d130 100644 --- a/database/migrations/2024_02_01_094827_module_ipadress.php +++ b/database/migrations/2024_02_01_094827_module_ipadress.php @@ -16,13 +16,17 @@ return new class extends Migration Schema::create('module_ip_address',function (Blueprint $table) { $table->id()->autoIncrement(); $table->string('vm_name',255)->nullable(); - $table->foreignId('module_network_id')->nullable()->constrained('module_network')->onDelete('set null')->index(); + $table->unsignedBigInteger('module_network_id')->nullable(); $table->string('ip',100)->nullable(); $table->string('gateway',100)->nullable(); $table->string('netmask',100)->nullable(); $table->string('dns',100)->nullable(); $table->tinyInteger('status')->default(0)->comment('1: assign;0: free'); $table->timestamps(); + $table->foreign('module_network_id', 'fk_module_ip_network') + ->references('id') + ->on('module_network') + ->nullOnDelete(); }); } diff --git a/database/migrations/2024_02_02_194120_os__group.php b/database/migrations/2024_02_02_194120_os__group.php index 5694184..621a62f 100644 --- a/database/migrations/2024_02_02_194120_os__group.php +++ b/database/migrations/2024_02_02_194120_os__group.php @@ -17,9 +17,13 @@ return new class extends Migration $table->id()->autoIncrement(); $table->string('name')->nullable(); $table->string('thumbnail')->nullable(); - $table->foreignId('module_host_id')->constrained('module_host')->index()->nullable(); + $table->unsignedBigInteger('module_host_id')->nullable(); $table->tinyInteger('status')->default(1); $table->timestamps(); + $table->foreign('module_host_id', 'fk_os_group_host') + ->references('id') + ->on('module_host') + ->nullOnDelete(); }); } diff --git a/database/migrations/2024_02_02_395629_os.php b/database/migrations/2024_02_02_395629_os.php index 5cc5643..1fee964 100644 --- a/database/migrations/2024_02_02_395629_os.php +++ b/database/migrations/2024_02_02_395629_os.php @@ -15,10 +15,11 @@ return new class extends Migration { Schema::create('os', function (Blueprint $table) { $table->id(); $table->string('version', 100)->nullable(); - $table->foreignId('os_group_id')->constrained('os_group')->index(); + $table->unsignedBigInteger('os_group_id')->nullable(); $table->string('template_id')->nullable(); $table->tinyInteger('status')->default(1); $table->timestamps(); + $table->foreign('os_group_id', 'fk_os_group_id')->references('id')->on('os_group')->nullOnDelete(); }); } diff --git a/database/migrations/2026_01_26_223604_create_product_categories_table.php b/database/migrations/2026_01_26_223604_create_product_categories_table.php new file mode 100644 index 0000000..91cff23 --- /dev/null +++ b/database/migrations/2026_01_26_223604_create_product_categories_table.php @@ -0,0 +1,36 @@ +id(); + $table->string('name'); + $table->string('slug')->unique(); + $table->foreignId('parent_id')->nullable() + ->constrained('product_categories') + ->nullOnDelete(); + + $table->boolean('is_active')->default(true); + $table->integer('sort_order')->default(0); + + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('product_categories'); + } +}; diff --git a/database/migrations/2026_01_27_172907_create_products_table.php b/database/migrations/2026_01_27_172907_create_products_table.php index d45d146..33089fb 100644 --- a/database/migrations/2026_01_27_172907_create_products_table.php +++ b/database/migrations/2026_01_27_172907_create_products_table.php @@ -13,7 +13,7 @@ return new class extends Migration { // Product type $table->string('product_type', 50)->index(); - + $table->unsignedBigInteger('product_categories_id')->index(); // Basic info $table->string('name'); $table->string('slug')->unique(); @@ -47,6 +47,9 @@ return new class extends Migration { $table->integer('sort_order')->default(0); $table->timestamp('published_at')->nullable(); + + $table->foreign('product_categories_id')->references('id')->on('product_categories')->onDelete('cascade'); + $table->softDeletes(); $table->timestamps(); }); diff --git a/database/migrations/2026_01_29_120327_create_product_v_m_templates_table.php b/database/migrations/2026_01_29_120327_create_product_v_m_templates_table.php deleted file mode 100644 index 3bfa2ab..0000000 --- a/database/migrations/2026_01_29_120327_create_product_v_m_templates_table.php +++ /dev/null @@ -1,27 +0,0 @@ -foreignId('product_id')->constrained('products')->cascadeOnDelete(); - // $table->foreignId('vm_template_id')->constrained('vm_templates')->cascadeOnDelete(); - // }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('product_vm_templates'); - } -}; diff --git a/database/migrations/2026_01_31_111040_update_table_module_datastore_table.php b/database/migrations/2026_01_31_111040_update_table_module_datastore_table.php deleted file mode 100644 index c4d9120..0000000 --- a/database/migrations/2026_01_31_111040_update_table_module_datastore_table.php +++ /dev/null @@ -1,32 +0,0 @@ -unsignedBigInteger('module_host_id')->default(0)->change(); - }); - - Schema::table('module_datastore', function (Blueprint $table) { - $table->foreign('module_host_id') - ->references('id') - ->on('module_host') - ->cascadeOnDelete(); - }); - } - - public function down(): void - { - Schema::table('module_datastore', function (Blueprint $table) { - $table->dropForeign(['module_host_id']); - }); - - Schema::table('module_datastore', function (Blueprint $table) { - $table->bigInteger('module_host_id')->default(0)->change(); - }); - } -}; diff --git a/resources/css/app.css b/resources/css/app.css index e56a7ab..e085d90 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -17,12 +17,17 @@ --font-lexend: var(--lexend); --font-inter: var(--inter); --font-averta: "Averta", sans-serif; + --color-info: var(--color-info); + --color-success: var(--color-success); } :root { --color-primary: #f66d2f; --color-primary-light: #ff914d; --color-primary-dark: #9a4b1f; + --color-info: #005C8A; + --color-success: #22C55E; + --color-primary-100: #f9b8a0; --color-primary-200: #f88a73; diff --git a/resources/sass/_variables.scss b/resources/sass/_variables.scss index 19c82cf..abfc971 100644 --- a/resources/sass/_variables.scss +++ b/resources/sass/_variables.scss @@ -35,8 +35,8 @@ $cyan: #0dcaf0 !default; // $primary: #F16723 !default; $secondary: $gray-600 !default; -$success: #10B981 !default; -$info: #0F172A !default; +$success: #22C55E !default; +$info: #005C8A !default; $warning: #F59E0B !default; $danger: #dc3545 !default; $light: $gray-200 !default; diff --git a/resources/views/administrator/pages/block/ModuleHosting.blade.php b/resources/views/administrator/pages/block/ModuleHosting.blade.php index 9bd66be..8faf2dd 100644 --- a/resources/views/administrator/pages/block/ModuleHosting.blade.php +++ b/resources/views/administrator/pages/block/ModuleHosting.blade.php @@ -7,4 +7,4 @@ Import From Excel -File Import Demo \ No newline at end of file +File Import Demo \ No newline at end of file diff --git a/resources/views/administrator/pages/block/ModuleIPPool.blade.php b/resources/views/administrator/pages/block/ModuleIPPool.blade.php index 6453a92..437cccc 100644 --- a/resources/views/administrator/pages/block/ModuleIPPool.blade.php +++ b/resources/views/administrator/pages/block/ModuleIPPool.blade.php @@ -4,7 +4,7 @@ @csrf - Import From Excel + Import From Excel Download Template \ No newline at end of file diff --git a/resources/views/administrator/pages/block/btn.blade.php b/resources/views/administrator/pages/block/btn.blade.php deleted file mode 100644 index 58960b1..0000000 --- a/resources/views/administrator/pages/block/btn.blade.php +++ /dev/null @@ -1,2 +0,0 @@ - Add New Product Group - Add New Product diff --git a/resources/views/administrator/pages/block/product/btn.blade.php b/resources/views/administrator/pages/block/product/btn.blade.php new file mode 100644 index 0000000..a65a43c --- /dev/null +++ b/resources/views/administrator/pages/block/product/btn.blade.php @@ -0,0 +1,6 @@ +@if(auth()->guard('admins')->user()->can("manager product")) + Add + Product + Add + Group +@endif \ No newline at end of file diff --git a/resources/views/administrator/pages/module/ippool/index.blade.php b/resources/views/administrator/pages/module/ippool/index.blade.php index f01fdb6..6eba37c 100644 --- a/resources/views/administrator/pages/module/ippool/index.blade.php +++ b/resources/views/administrator/pages/module/ippool/index.blade.php @@ -7,44 +7,44 @@ @section('breadcrumb') @php - $breadcrumbItems = [ - ['label' => 'Modules'], - ['label' => 'IP Pools', 'active' => true], - ]; +$breadcrumbItems = [ + ['label' => 'Modules'], + ['label' => 'IP Pools', 'active' => true], +]; @endphp @endsection @section('tab') @php - $tabs = [ - [ - 'label' => 'DNS', - 'route' => 'module.dns.index', - 'icon' => 'server', - ], - [ - 'label' => 'Hosting', - 'route' => 'module.hosting.index', - 'icon' => 'server', - ], - [ - 'label' => 'vCenter', - 'route' => 'module.vcenter.index', - 'icon' => 'server', - ], - [ - 'label' => 'IpPool', - 'route' => 'module.ippool.index', - 'icon' => 'server', - ], - ]; +$tabs = [ + [ + 'label' => 'DNS', + 'route' => 'module.dns.index', + 'icon' => 'server', + ], + [ + 'label' => 'Hosting', + 'route' => 'module.hosting.index', + 'icon' => 'server', + ], + [ + 'label' => 'vCenter', + 'route' => 'module.vcenter.index', + 'icon' => 'server', + ], + [ + 'label' => 'IpPool', + 'route' => 'module.ippool.index', + 'icon' => 'server', + ], +]; @endphp @endsection @section('content') @if (auth()->guard('admins')->user()->can('manager module ippool'))
-
+
@include('administrator.pages.block.ModuleIPPool')
diff --git a/resources/views/administrator/pages/module/vcenter/datatable/status.blade.php b/resources/views/administrator/pages/module/vcenter/datatable/status.blade.php index 8d59163..4d490a2 100644 --- a/resources/views/administrator/pages/module/vcenter/datatable/status.blade.php +++ b/resources/views/administrator/pages/module/vcenter/datatable/status.blade.php @@ -3,7 +3,11 @@ @endphp @if($status) - {!! $status->badge() !!} + + {{ $status->label() }} + @else - Unknown -@endif + + Unknown + +@endif \ No newline at end of file diff --git a/resources/views/administrator/pages/module/vcenter/index.blade.php b/resources/views/administrator/pages/module/vcenter/index.blade.php index 39428ff..530d19b 100644 --- a/resources/views/administrator/pages/module/vcenter/index.blade.php +++ b/resources/views/administrator/pages/module/vcenter/index.blade.php @@ -52,7 +52,7 @@ ['key' => 'host', 'label' => 'Host'], ['key' => 'user', 'label' => 'User'], ['key' => 'status', 'label' => 'Status'], - ['key' => 'action', 'label' => 'Action'], + ['key' => 'action', 'label' => 'Action', 'searchable' => false], ]" />
diff --git a/resources/views/administrator/pages/package/vm/datatable/action.blade.php b/resources/views/administrator/pages/package/vm/datatable/action.blade.php new file mode 100644 index 0000000..3c5a354 --- /dev/null +++ b/resources/views/administrator/pages/package/vm/datatable/action.blade.php @@ -0,0 +1,2 @@ +Edit +Delete \ No newline at end of file diff --git a/resources/views/administrator/pages/package/vm/index.blade.php b/resources/views/administrator/pages/package/vm/index.blade.php index f25ff05..2cea173 100644 --- a/resources/views/administrator/pages/package/vm/index.blade.php +++ b/resources/views/administrator/pages/package/vm/index.blade.php @@ -29,7 +29,6 @@ ['key' => 'cpu', 'label' => 'CPU'], ['key' => 'ram_mb', 'label' => 'RAM (MB)'], ['key' => 'datacenter.name', 'label' => 'Datacenter'], - ['key' => 'host.name', 'label' => 'Host'], ['key' => 'action', 'label' => 'Action', 'orderable' => false, 'searchable' => false], ]" />
diff --git a/resources/views/administrator/pages/product/categories/create.blade.php b/resources/views/administrator/pages/product/categories/create.blade.php new file mode 100644 index 0000000..00bde70 --- /dev/null +++ b/resources/views/administrator/pages/product/categories/create.blade.php @@ -0,0 +1,118 @@ +@extends('administrator.layout.master') +@push('plugin-styles') + @foreach (config('assets.datatable.css') as $datatable) + + @endforeach +@endpush + +@section('breadcrumb') + @php + $breadcrumbItems = [ + ['label' => 'Product'], + ['label' => 'Product / Services', 'url' => route('products.index')], + ['label' => 'Product Categories', 'url' => route('products.group.index')], + ['label' => 'Add Product Group', 'active' => true], + ]; + @endphp + +@endsection +@section('tab') + @php + $tabs = [ + [ + 'label' => 'Product', + 'route' => 'products.create', + 'icon' => 'server', + ], + [ + 'label' => 'Product Group', + 'route' => 'products.group.create', + 'icon' => 'server', + ], + ]; + @endphp + +@endsection + +@section('content') +
+ @csrf +
+
+
+
+
Basic Information
+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+ +
+
+
+
Classification
+
+
+
+ + +
+ +
+ + +
+
+
+
+
+ + + Cancel + Create Category + +
+@endsection + +@push('plugin-scripts') + @foreach (config('assets.datatable.js') as $datatable) + + @endforeach + +@endpush \ No newline at end of file diff --git a/resources/views/administrator/pages/product/categories/datatable/action.blade.php b/resources/views/administrator/pages/product/categories/datatable/action.blade.php new file mode 100644 index 0000000..f7d4e22 --- /dev/null +++ b/resources/views/administrator/pages/product/categories/datatable/action.blade.php @@ -0,0 +1,2 @@ +Edit +Delete \ No newline at end of file diff --git a/resources/views/administrator/pages/product/categories/datatable/status.blade.php b/resources/views/administrator/pages/product/categories/datatable/status.blade.php new file mode 100644 index 0000000..ff4ffc1 --- /dev/null +++ b/resources/views/administrator/pages/product/categories/datatable/status.blade.php @@ -0,0 +1,13 @@ +@php + $status = App\Enums\Status::tryFrom($row->is_active); +@endphp + +@if($status) + + {{ $status->label() }} + +@else + + Unknown + +@endif \ No newline at end of file diff --git a/resources/views/administrator/pages/product/categories/edit.blade.php b/resources/views/administrator/pages/product/categories/edit.blade.php new file mode 100644 index 0000000..81a4d97 --- /dev/null +++ b/resources/views/administrator/pages/product/categories/edit.blade.php @@ -0,0 +1,111 @@ +@extends('administrator.layout.master') +@push('plugin-styles') + @foreach (config('assets.datatable.css') as $datatable) + + @endforeach +@endpush + +@section('breadcrumb') +@php + $breadcrumbItems = [ + ['label' => 'Product'], + ['label' => 'Product / Services', 'url' => route('products.index')], + ['label' => 'Product Group', 'url' => route('products.group.index')], + ['label' => 'Edit Product Group', 'active' => true], + ]; + @endphp + +@endsection +@section('tab') + @php + $tabs = [ + [ + 'label' => 'Product', + 'route' => 'products.index', + 'icon' => 'server', + ], + [ + 'label' => 'Product Group', + 'route' => 'products.group.index', + 'icon' => 'server', + ], + ]; + @endphp + +@endsection + +@section('content') +
+ @csrf + @method('PUT') +
+
+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+ +
+
+
+
Classification
+
+
+
+ + +
+ +
+ + +
+
+
+
+
+ + + Cancel + Update Category + +
+@endsection + +@push('plugin-scripts') + @foreach (config('assets.datatable.js') as $datatable) + + @endforeach + +@endpush diff --git a/resources/views/administrator/pages/product/categories/index.blade.php b/resources/views/administrator/pages/product/categories/index.blade.php new file mode 100644 index 0000000..d66070d --- /dev/null +++ b/resources/views/administrator/pages/product/categories/index.blade.php @@ -0,0 +1,62 @@ +@extends('administrator.layout.master') +@push('plugin-styles') + @foreach (config('assets.datatable.css') as $datatable) + + @endforeach +@endpush + +@section('breadcrumb') + @php + $breadcrumbItems = [ + ['label' => 'Product'], + ['label' => 'Product Group'], + ['label' => 'Product Group List', 'active' => true] + ]; + @endphp + +@endsection +@section('tab') + @php + $tabs = [ + [ + 'label' => 'Product', + 'route' => 'products.index', + 'icon' => 'server', + ], + [ + 'label' => 'Product Group', + 'route' => 'products.group.index', + 'icon' => 'server', + ], + ]; + @endphp + +@endsection +@section('content') + @if (auth()->guard('admins')->user()->can('manager product')) +
+
+ @include('administrator.pages.block.product.btn') +
+
+ +
+ +
+ @else +
+
Access Denied - You are not assigned to any Product + management department
+
+ @endif +@endsection +@push('plugin-scripts') + @foreach (config('assets.datatable.js') as $datatable) + + @endforeach +@endpush \ No newline at end of file diff --git a/resources/views/administrator/pages/product/create.blade.php b/resources/views/administrator/pages/product/create.blade.php new file mode 100644 index 0000000..9d84262 --- /dev/null +++ b/resources/views/administrator/pages/product/create.blade.php @@ -0,0 +1,295 @@ +@extends('administrator.layout.master') +@push('plugin-styles') + @foreach (config('assets.datatable.css') as $datatable) + + @endforeach +@endpush + +@section('breadcrumb') + @php + $breadcrumbItems = [ + ['label' => 'Product'], + ['label' => 'Product / Services', 'url' => route('products.index')], + ['label' => 'Add Product / Services', 'active' => true], + ]; + @endphp + +@endsection +@section('tab') + @php + $tabs = [ + [ + 'label' => 'Product', + 'route' => 'products.create', + 'icon' => 'server', + ], + [ + 'label' => 'Product Group', + 'route' => 'products.group.create', + 'icon' => 'server', + ], + ]; + @endphp + +@endsection +@section('content') +
+ @csrf +
+ {{-- Left Column: Basic Info & Pricing --}} +
+
+
+
Basic Information
+
+
+ + {{-- Basic Information --}} +
+
Basic Information
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+ + +
+ +
+ + +
+
+ + {{-- Pricing & Billing --}} +
+
Pricing & Billing
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + {{-- Hardware Specs (VPS/Dedicated) --}} +
+
Hardware Specifications
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ +
+
+
+ + {{-- Right Column: Classification & Settings --}} +
+
+
+
Classification
+
+
+ {{-- Product Category --}} +
+
+ + + + Thêm Group + +
+ +
+ + {{-- Product Type --}} +
+ + +
+ + {{-- Status --}} +
+ + +
+ +
+ + +
+
+
+ +
+
+
Inventory & Limits
+
+
+
+ + +
+ +
+
+ + +
+
+ +
+ + +
+
+
+
+
+ + + Cancel + Create Product + +
+@endsection +@push('plugin-scripts') + @foreach (config('assets.datatable.js') as $datatable) + + @endforeach + + + +@endpush diff --git a/resources/views/administrator/pages/product/datatable/action.blade.php b/resources/views/administrator/pages/product/datatable/action.blade.php new file mode 100644 index 0000000..4b8555f --- /dev/null +++ b/resources/views/administrator/pages/product/datatable/action.blade.php @@ -0,0 +1,2 @@ +Edit +Delete \ No newline at end of file diff --git a/resources/views/administrator/pages/product/edit.blade.php b/resources/views/administrator/pages/product/edit.blade.php new file mode 100644 index 0000000..93af70d --- /dev/null +++ b/resources/views/administrator/pages/product/edit.blade.php @@ -0,0 +1,269 @@ +@extends('administrator.layout.master') +@push('plugin-styles') + @foreach (config('assets.datatable.css') as $datatable) + + @endforeach +@endpush + +@section('breadcrumb') + @php + $breadcrumbItems = [ + ['label' => 'Product'], + ['label' => 'Product / Services', 'url' => route('products.index')], + ['label' => 'Add Product / Services', 'active' => true], + ]; + @endphp + +@endsection +@section('content') +
+ @csrf +
+ {{-- Left Column: Basic Info & Pricing --}} +
+
+
+ + {{-- Basic Information --}} +
+
Basic Information
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+ + +
+ +
+ + +
+
+ + {{-- Pricing & Billing --}} +
+
Pricing & Billing
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + {{-- Hardware Specs (VPS/Dedicated) --}} +
+
Hardware Specifications
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ +
+
+
+ + {{-- Right Column: Classification & Settings --}} +
+
+
+
Classification
+
+
+ {{-- Product Category --}} +
+ + +
+ + {{-- Product Type --}} +
+ + +
+ + {{-- Status --}} +
+ + +
+ +
+ + +
+
+
+ +
+
+
Inventory & Limits
+
+
+
+ + +
+ +
+
+ + +
+
+ +
+ + +
+
+
+
+
+ + + Cancel + Create Product + +
+@endsection +@push('plugin-scripts') + @foreach (config('assets.datatable.js') as $datatable) + + @endforeach + + + +@endpush diff --git a/resources/views/administrator/pages/product/first.blade.php b/resources/views/administrator/pages/product/first.blade.php index 604025c..010a19e 100644 --- a/resources/views/administrator/pages/product/first.blade.php +++ b/resources/views/administrator/pages/product/first.blade.php @@ -2,7 +2,7 @@ @section("content") @if(auth()->guard('admins')->user()->can("add product"))
-
+ @csrf
@@ -49,7 +49,7 @@
diff --git a/resources/views/administrator/pages/product/index.blade.php b/resources/views/administrator/pages/product/index.blade.php index c38add5..b22ccaf 100644 --- a/resources/views/administrator/pages/product/index.blade.php +++ b/resources/views/administrator/pages/product/index.blade.php @@ -1,90 +1,62 @@ @extends('administrator.layout.master') +@push('plugin-styles') + @foreach (config('assets.datatable.css') as $datatable) + + @endforeach +@endpush + +@section('breadcrumb') + @php + $breadcrumbItems = [ + ['label' => 'Product'], + ['label' => 'Product / Services'], + ['label' => 'Product List', 'active' => true] + ]; + @endphp + +@endsection +@section('tab') + @php + $tabs = [ + [ + 'label' => 'Product', + 'route' => 'products.index', + 'icon' => 'server', + ], + [ + 'label' => 'Product Group', + 'route' => 'products.group.index', + 'icon' => 'server', + ], + ]; + @endphp + +@endsection @section('content') @if (auth()->guard('admins')->user()->can('manager product'))
-
- @include('administrator.pages.block.btn') -
-
- - - - - - - - - - - @if (count($groups) > 0) - @foreach ($groups->all() as $item) - - - - - - @if (!empty($item->products) && $item->products->count()) - @foreach ($item->products->all() as $_item) - - - - - - - @endforeach - @endif - @endforeach - @endif - -
Product NameType
- Group Name: {{ $item->name }} - - - - - -
- @method('delete') - @csrf - - -
-
- {{ $_item->name }} - - {{ $_item->Type->name }} - - - - - - @if (auth()->guard('admins')->user()->can('delete product')) -
- @csrf - @method('delete') - -
- @endif -
-
- -
+
+ @include('administrator.pages.block.product.btn') +
+
+ +
+ + @else -
-
Access Denied - You are not assigned to any Product - management department
-
- @endif +
+
Access Denied - You are not assigned to any Product + management department
+
+ @endif @endsection +@push('plugin-scripts') + @foreach (config('assets.datatable.js') as $datatable) + + @endforeach +@endpush \ No newline at end of file diff --git a/resources/views/administrator/pages/settings/general.blade.php b/resources/views/administrator/pages/settings/general.blade.php new file mode 100644 index 0000000..bc8a465 --- /dev/null +++ b/resources/views/administrator/pages/settings/general.blade.php @@ -0,0 +1,311 @@ +@extends('administrator.layout.master') +@push('plugin-styles') + @foreach (config('assets.datatable.css') as $datatable) + + @endforeach +@endpush + +@section('breadcrumb') + @php + $breadcrumbItems = [ + ['label' => 'Settings'], + ['label' => 'General', 'active' => true], + ]; + @endphp + +@endsection +@section('content') + @if (auth()->guard('admins')->user()->can('manager setting')) +
+
+
Chỉnh sửa cài đặt
+
+
+ @csrf + @method('put') +
+
+
+ {{-- Title --}} +
+ +
+ +
+
+ {{-- Company name --}} +
+ +
+ +
+
+ + {{-- Tax --}} +
+ +
+ +
+
+ + {{-- Email --}} +
+ +
+ +
+
+ {{-- Address --}} +
+ +
+ +
+
+ {{-- Status --}} +
+ +
+ +
+
+ {{-- Script --}} +
+ +
+ +
+
+ + {{-- Discord Webhook ID --}} +
+ +
+ +
+
+ {{-- Discord Webhook Token --}} +
+ +
+ +
+
+
+
+ {{-- Logo Url --}} +
+ +
+
+
+ +
+
+ +
+
+
+
+ {{-- Icon falling login/register --}} +
+ +
+
+
+ +
+
+ +
+
+
+
+ {{-- Ảnh ở login/register customer --}} +
+ +
+
+
+ +
+
+ +
+
+
+
+ {{-- Ma Mau --}} +
+ +
+ +
+
+ {{-- Tel --}} +
+ +
+ +
+
+ {{-- Support Email --}} +
+ +
+ +
+
+ {{-- Telegram Bot ID --}} +
+ +
+ +
+
+ {{-- Telegram Bot Token --}} +
+ +
+ +
+
+ + {{-- VNPAY_TMN_CODE --}} +
+ +
+ +
+
+ + {{-- VNPAY_HASH_SECRET --}} +
+ +
+ +
+
+ + +
+
+
+ +
+ +
+ @else +
+
Access Denied - You are not assigned to setting management + department
+
+ @endif +@endsection +@push('scripts') + + + +@endpush +@push('plugin-scripts') + @foreach (config('assets.datatable.js') as $datatable) + + @endforeach + +@endpush \ No newline at end of file diff --git a/resources/views/components/admin-badge.blade.php b/resources/views/components/admin-badge.blade.php new file mode 100644 index 0000000..12ac10d --- /dev/null +++ b/resources/views/components/admin-badge.blade.php @@ -0,0 +1,8 @@ +merge(['class' => 'rounded-pill px-3 py-2 d-inline-flex align-items-center justify-content-center']) }}> + @if($icon) + + @endif + + {{ $slot }} + + \ No newline at end of file diff --git a/resources/views/components/button-c-r-m.blade.php b/resources/views/components/button-c-r-m.blade.php index d26197f..a91d927 100644 --- a/resources/views/components/button-c-r-m.blade.php +++ b/resources/views/components/button-c-r-m.blade.php @@ -20,7 +20,7 @@ $tag = 'a'; } - $baseClasses = 'd-inline-flex gap-2 align-items-center justify-content-center text-white border-0 px-3 py-2 rounded-pill'; + $baseClasses = 'd-inline-flex gap-2 align-items-center justify-content-center text-white px-3 py-2 rounded-3 transition-all'; $variants = [ 'primary' => 'bg-primary', @@ -35,6 +35,17 @@ 'info' => 'bg-info' ]; + // Darker border colors for the "3D" effect + $borderColors = [ + 'primary' => '#D9561C', + 'secondary' => '#565e64', + 'success' => '#146c43', + 'danger' => '#b02a37', + 'warning' => '#997404', + 'info' => '#004C73', + 'destructive' => '#b02a37', + ]; + $sizes = [ 'default' => 'h-10', 'sm' => 'h-6', @@ -44,22 +55,30 @@ $variantClass = $variants[$variant ?? 'primary'] ?? $variants['primary']; $sizeClass = $sizes[$size ?? 'md'] ?? $sizes['default']; + $borderColor = $borderColors[$variant ?? 'primary'] ?? 'transparent'; + + // Check if variant is outline/ghost/link to avoid weird borders + if (in_array($variant, ['outline', 'ghost', 'link'])) { + $borderStyle = ''; + } else { + $borderStyle = "border-bottom: 3px solid {$borderColor};"; + } $classes = "{$baseClasses} {$variantClass} {$sizeClass}"; @endphp - -@if($tag === 'a') - merge(['class' => $classes]) }}> - @if($icon) - - @endif - {{ $slot }} - -@else - -@endif \ No newline at end of file + + @if($tag === 'a') + merge(['class' => $classes, 'style' => $borderStyle]) }}> + @if($icon) + + @endif + {{ $slot }} + + @else + + @endif \ No newline at end of file diff --git a/resources/views/livewire/v-p-s-reinstall-modal.blade.php b/resources/views/livewire/v-p-s-reinstall-modal.blade.php index 5efbf07..10aefe6 100644 --- a/resources/views/livewire/v-p-s-reinstall-modal.blade.php +++ b/resources/views/livewire/v-p-s-reinstall-modal.blade.php @@ -26,7 +26,7 @@ @endif - @if ($osGroups->count() > 0) + @if (count($osGroups) > 0)
- @elseif ($operatingSystems->count() > 0) + @elseif (count($operatingSystems) > 0)
@foreach ($operatingSystems as $os)