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 4121233..6338d46 100644 Binary files a/app/Http/.DS_Store and b/app/Http/.DS_Store differ diff --git a/app/Http/Controllers/admin/ProductController.php b/app/Http/Controllers/admin/ProductController.php deleted file mode 100644 index edff89a..0000000 --- a/app/Http/Controllers/admin/ProductController.php +++ /dev/null @@ -1,418 +0,0 @@ -groupRepository->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 ' -
'; + 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 @@| Product Name | -Type | -- | - |
|---|---|---|---|
| - Group Name: {{ $item->name }} - | -- - - - | -- - | -|
| - {{ $_item->name }} - | -- {{ $_item->Type->name }} - | -- - - - | -- @if (auth()->guard('admins')->user()->can('delete product')) - - @endif - | -