diff --git a/.DS_Store b/.DS_Store index c579f32..359e843 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/app/.DS_Store b/app/.DS_Store index d5c3a09..32630bc 100644 Binary files a/app/.DS_Store and b/app/.DS_Store differ diff --git a/app/Services/.DS_Store b/app/Services/.DS_Store index 8940d66..831d459 100644 Binary files a/app/Services/.DS_Store and b/app/Services/.DS_Store differ diff --git a/database/.DS_Store b/database/.DS_Store index 66a4dbb..fe22461 100644 Binary files a/database/.DS_Store and b/database/.DS_Store differ diff --git a/database/seeders/RolesAndPermissions.php b/database/seeders/RolesAndPermissions.php index be4427d..56c2c85 100644 --- a/database/seeders/RolesAndPermissions.php +++ b/database/seeders/RolesAndPermissions.php @@ -112,6 +112,13 @@ class RolesAndPermissions extends Seeder 'package-module.destroy', 'package-module.manager', + // Menu + 'menu.view', + 'menu.create', + 'menu.edit', + 'menu.destroy', + 'menu.manager', + // Email Template 'email-template.view', 'email-template.create', diff --git a/modules/Menu/App/Http/Controllers/Api/Admin/MenuController.php b/modules/Menu/App/Http/Controllers/Api/Admin/MenuController.php new file mode 100644 index 0000000..09409db --- /dev/null +++ b/modules/Menu/App/Http/Controllers/Api/Admin/MenuController.php @@ -0,0 +1,89 @@ +withCount('items')->orderBy('key')->get(); + + return response()->json([ + 'success' => true, + 'data' => MenuResource::collection($menus), + ]); + } + + public function show(int $id): JsonResponse + { + $menu = Menu::withCount('items')->find($id); + + if (! $menu) { + return response()->json([ + 'success' => false, + 'message' => 'Không tìm thấy menu', + ], 404); + } + + return response()->json([ + 'success' => true, + 'data' => MenuResource::make($menu), + ]); + } + + public function store(StoreMenuRequest $request): JsonResponse + { + $menu = Menu::create($request->validated()); + + return response()->json([ + 'success' => true, + 'message' => 'Tạo menu thành công', + 'data' => MenuResource::make($menu), + ], 201); + } + + public function update(int $id, StoreMenuRequest $request): JsonResponse + { + $menu = Menu::find($id); + + if (! $menu) { + return response()->json([ + 'success' => false, + 'message' => 'Không tìm thấy menu', + ], 404); + } + + $menu->update($request->validated()); + + return response()->json([ + 'success' => true, + 'message' => 'Cập nhật menu thành công', + 'data' => MenuResource::make($menu), + ]); + } + + public function destroy(int $id): JsonResponse + { + $menu = Menu::find($id); + + if (! $menu) { + return response()->json([ + 'success' => false, + 'message' => 'Không tìm thấy menu', + ], 404); + } + + $menu->delete(); + + return response()->json([ + 'success' => true, + 'message' => 'Xoá menu thành công', + ]); + } +} diff --git a/modules/Menu/App/Http/Controllers/Api/Admin/MenuItemController.php b/modules/Menu/App/Http/Controllers/Api/Admin/MenuItemController.php new file mode 100644 index 0000000..6f5fa5a --- /dev/null +++ b/modules/Menu/App/Http/Controllers/Api/Admin/MenuItemController.php @@ -0,0 +1,93 @@ +when($request->integer('menu_id'), fn ($q, $menuId) => $q->where('menu_id', $menuId)) + ->orderBy('id') + ->get(); + + return response()->json([ + 'success' => true, + 'data' => MenuItemResource::collection($items), + ]); + } + + public function show(int $id): JsonResponse + { + $item = MenuItem::find($id); + + if (! $item) { + return response()->json([ + 'success' => false, + 'message' => 'Không tìm thấy menu item', + ], 404); + } + + return response()->json([ + 'success' => true, + 'data' => MenuItemResource::make($item), + ]); + } + + public function store(StoreMenuItemRequest $request): JsonResponse + { + $item = MenuItem::create($request->validated()); + + return response()->json([ + 'success' => true, + 'message' => 'Tạo menu item thành công', + 'data' => MenuItemResource::make($item), + ], 201); + } + + public function update(int $id, StoreMenuItemRequest $request): JsonResponse + { + $item = MenuItem::find($id); + + if (! $item) { + return response()->json([ + 'success' => false, + 'message' => 'Không tìm thấy menu item', + ], 404); + } + + $item->update($request->validated()); + + return response()->json([ + 'success' => true, + 'message' => 'Cập nhật menu item thành công', + 'data' => MenuItemResource::make($item), + ]); + } + + public function destroy(int $id): JsonResponse + { + $item = MenuItem::find($id); + + if (! $item) { + return response()->json([ + 'success' => false, + 'message' => 'Không tìm thấy menu item', + ], 404); + } + + $item->delete(); + + return response()->json([ + 'success' => true, + 'message' => 'Xoá menu item thành công', + ]); + } +} diff --git a/modules/Menu/App/Http/Requests/StoreMenuItemRequest.php b/modules/Menu/App/Http/Requests/StoreMenuItemRequest.php new file mode 100644 index 0000000..49ce0d2 --- /dev/null +++ b/modules/Menu/App/Http/Requests/StoreMenuItemRequest.php @@ -0,0 +1,23 @@ + ['required', 'integer', 'exists:nav_menus,id'], + 'label' => ['required', 'string', 'max:255'], + 'url' => ['required', 'string', 'max:255'], + 'icon' => ['nullable', 'string', 'max:255'], + ]; + } +} diff --git a/modules/Menu/App/Http/Requests/StoreMenuRequest.php b/modules/Menu/App/Http/Requests/StoreMenuRequest.php new file mode 100644 index 0000000..b9e1c96 --- /dev/null +++ b/modules/Menu/App/Http/Requests/StoreMenuRequest.php @@ -0,0 +1,21 @@ + ['required', 'string', 'max:255', Rule::unique('nav_menus', 'key')->ignore($this->route('id'))], + ]; + } +} diff --git a/modules/Menu/App/Http/Resources/MenuItemResource.php b/modules/Menu/App/Http/Resources/MenuItemResource.php new file mode 100644 index 0000000..51b18f8 --- /dev/null +++ b/modules/Menu/App/Http/Resources/MenuItemResource.php @@ -0,0 +1,22 @@ + $this->id, + 'menu_id' => $this->menu_id, + 'label' => $this->label, + 'url' => $this->url, + 'icon' => $this->icon, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/modules/Menu/App/Http/Resources/MenuResource.php b/modules/Menu/App/Http/Resources/MenuResource.php new file mode 100644 index 0000000..12d9991 --- /dev/null +++ b/modules/Menu/App/Http/Resources/MenuResource.php @@ -0,0 +1,20 @@ + $this->id, + 'key' => $this->key, + 'items_count' => $this->when(isset($this->items_count), fn () => (int) $this->items_count), + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/modules/Menu/App/Models/Menu.php b/modules/Menu/App/Models/Menu.php new file mode 100644 index 0000000..0b3350e --- /dev/null +++ b/modules/Menu/App/Models/Menu.php @@ -0,0 +1,21 @@ +hasMany(MenuItem::class); + } +} diff --git a/modules/Menu/App/Models/MenuItem.php b/modules/Menu/App/Models/MenuItem.php new file mode 100644 index 0000000..5a11cae --- /dev/null +++ b/modules/Menu/App/Models/MenuItem.php @@ -0,0 +1,21 @@ +belongsTo(Menu::class); + } +} diff --git a/modules/Menu/App/Providers/MenuServiceProvider.php b/modules/Menu/App/Providers/MenuServiceProvider.php new file mode 100644 index 0000000..c003ab2 --- /dev/null +++ b/modules/Menu/App/Providers/MenuServiceProvider.php @@ -0,0 +1,37 @@ +registerConfig(); + $this->registerMigrations(); + $this->app->register(RouteServiceProvider::class); + } + + public function register(): void + { + // + } + + protected function registerConfig(): void + { + $this->mergeConfigFrom( + module_path($this->moduleName, 'config/config.php'), + $this->moduleNameLower + ); + } + + protected function registerMigrations(): void + { + $this->loadMigrationsFrom(module_path($this->moduleName, 'Database/migrations')); + } +} diff --git a/modules/Menu/App/Providers/RouteServiceProvider.php b/modules/Menu/App/Providers/RouteServiceProvider.php new file mode 100644 index 0000000..90500be --- /dev/null +++ b/modules/Menu/App/Providers/RouteServiceProvider.php @@ -0,0 +1,29 @@ +mapApiRoutes(); + } + + protected function mapApiRoutes(): void + { + Route::prefix('api') + ->middleware('api') + ->namespace($this->moduleNamespace) + ->group(module_path('Menu', '/routes/api.php')); + } +} diff --git a/modules/Menu/Database/migrations/2026_07_27_120000_create_nav_menus_table.php b/modules/Menu/Database/migrations/2026_07_27_120000_create_nav_menus_table.php new file mode 100644 index 0000000..d99dd5b --- /dev/null +++ b/modules/Menu/Database/migrations/2026_07_27_120000_create_nav_menus_table.php @@ -0,0 +1,22 @@ +id(); + $table->string('key')->unique(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('nav_menus'); + } +}; diff --git a/modules/Menu/Database/migrations/2026_07_27_120001_create_nav_menu_items_table.php b/modules/Menu/Database/migrations/2026_07_27_120001_create_nav_menu_items_table.php new file mode 100644 index 0000000..acf9e9c --- /dev/null +++ b/modules/Menu/Database/migrations/2026_07_27_120001_create_nav_menu_items_table.php @@ -0,0 +1,25 @@ +id(); + $table->foreignId('menu_id')->constrained('nav_menus')->cascadeOnDelete(); + $table->string('label'); + $table->string('url'); + $table->string('icon')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('nav_menu_items'); + } +}; diff --git a/modules/Menu/composer.json b/modules/Menu/composer.json new file mode 100644 index 0000000..bf421af --- /dev/null +++ b/modules/Menu/composer.json @@ -0,0 +1,31 @@ +{ + "name": "nwidart/menu", + "description": "", + "authors": [ + { + "name": "Nicolas Widart", + "email": "n.widart@gmail.com" + } + ], + "extra": { + "laravel": { + "providers": [], + "aliases": { + + } + } + }, + "autoload": { + "psr-4": { + "Modules\\Menu\\": "", + "Modules\\Menu\\App\\": "App/", + "Modules\\Menu\\Database\\Factories\\": "Database/Factories/", + "Modules\\Menu\\Database\\Seeders\\": "Database/Seeders/" + } + }, + "autoload-dev": { + "psr-4": { + "Modules\\Menu\\Tests\\": "tests/" + } + } +} diff --git a/modules/Menu/config/config.php b/modules/Menu/config/config.php new file mode 100644 index 0000000..1cccead --- /dev/null +++ b/modules/Menu/config/config.php @@ -0,0 +1,5 @@ + 'Menu', +]; diff --git a/modules/Menu/module.json b/modules/Menu/module.json new file mode 100644 index 0000000..a07641f --- /dev/null +++ b/modules/Menu/module.json @@ -0,0 +1,11 @@ +{ + "name": "Menu", + "alias": "menu", + "description": "Quan ly Menu va Menu Item", + "keywords": [], + "priority": 0, + "providers": [ + "Modules\\Menu\\App\\Providers\\MenuServiceProvider" + ], + "files": [] +} diff --git a/modules/Menu/routes/api.php b/modules/Menu/routes/api.php new file mode 100644 index 0000000..cfcbac8 --- /dev/null +++ b/modules/Menu/routes/api.php @@ -0,0 +1,41 @@ +group(function () { + Route::prefix('administrator') + ->middleware('api.auth:admin') + ->group(function () { + Route::prefix('menus') + ->controller(MenuController::class) + ->group(function () { + Route::get('/', 'index')->middleware('perm:menu.view'); + Route::post('/', 'store')->middleware('perm:menu.create'); + Route::get('{id}', 'show')->middleware('perm:menu.view'); + Route::put('{id}', 'update')->middleware('perm:menu.edit'); + Route::delete('{id}', 'destroy')->middleware('perm:menu.destroy'); + }); + + Route::prefix('menu-items') + ->controller(MenuItemController::class) + ->group(function () { + Route::get('/', 'index')->middleware('perm:menu.view'); + Route::post('/', 'store')->middleware('perm:menu.create'); + Route::get('{id}', 'show')->middleware('perm:menu.view'); + Route::put('{id}', 'update')->middleware('perm:menu.edit'); + Route::delete('{id}', 'destroy')->middleware('perm:menu.destroy'); + }); + }); + }); diff --git a/modules_statuses.json b/modules_statuses.json index 6663f72..b6232d8 100644 --- a/modules_statuses.json +++ b/modules_statuses.json @@ -1,3 +1,4 @@ { - "DomainFluctuation": true + "DomainFluctuation": true, + "Menu": true } diff --git a/storage/.DS_Store b/storage/.DS_Store index 5e70cc7..bb9a9d2 100644 Binary files a/storage/.DS_Store and b/storage/.DS_Store differ diff --git a/tests/.DS_Store b/tests/.DS_Store index dde8d2d..572cc6b 100644 Binary files a/tests/.DS_Store and b/tests/.DS_Store differ