From 3de1246c50d8fd6f30de55dcd12888dcc8da83ac Mon Sep 17 00:00:00 2001 From: Matteo Gheza Date: Thu, 18 Jan 2024 15:51:02 +0100 Subject: [PATCH] Add admin options functionality --- .../app/Http/Controllers/AdminController.php | 42 +++++++++ backend/app/Models/Option.php | 16 ++++ backend/config/laratrust_seeder.php | 2 + ...2024_01_18_005858_create_options_table.php | 34 ++++++++ backend/database/seeders/OptionsSeeder.php | 28 ++++++ backend/routes/api.php | 3 + .../app/_routes/admin/admin-routing.module.ts | 6 ++ .../src/app/_routes/admin/admin.component.ts | 1 + .../options/admin-options-routing.module.ts | 11 +++ .../options/admin-options.component.html | 48 +++++++++++ .../options/admin-options.component.scss | 4 + .../admin/options/admin-options.component.ts | 85 +++++++++++++++++++ .../admin/options/admin-options.module.ts | 23 +++++ frontend/src/assets/i18n/en.json | 8 +- frontend/src/assets/i18n/it.json | 8 +- 15 files changed, 317 insertions(+), 2 deletions(-) create mode 100644 backend/app/Models/Option.php create mode 100644 backend/database/migrations/2024_01_18_005858_create_options_table.php create mode 100644 backend/database/seeders/OptionsSeeder.php create mode 100644 frontend/src/app/_routes/admin/options/admin-options-routing.module.ts create mode 100644 frontend/src/app/_routes/admin/options/admin-options.component.html create mode 100644 frontend/src/app/_routes/admin/options/admin-options.component.scss create mode 100644 frontend/src/app/_routes/admin/options/admin-options.component.ts create mode 100644 frontend/src/app/_routes/admin/options/admin-options.module.ts diff --git a/backend/app/Http/Controllers/AdminController.php b/backend/app/Http/Controllers/AdminController.php index 879201c..9a4b7e6 100644 --- a/backend/app/Http/Controllers/AdminController.php +++ b/backend/app/Http/Controllers/AdminController.php @@ -10,6 +10,7 @@ use Illuminate\Support\Str; use App\Models\User; use App\Models\Role; use App\Models\Permission; +use App\Models\Option; class AdminController extends Controller { @@ -316,6 +317,47 @@ class AdminController extends Controller ]); } + public function getOptions() { + if(!request()->user()->hasPermission("admin-options-read")) abort(401); + + return response()->json(Option::all()); + } + + public function updateOption(Request $request, Option $option) { + if(!request()->user()->hasPermission("admin-options-update")) abort(401); + + switch($option->type) { + case 'number': + $type_validation = 'numeric'; + if($option->min) $type_validation .= '|min:'.$option->min; + if($option->max) $type_validation .= '|max:'.$option->max; + break; + case 'boolean': + $type_validation = 'boolean'; + break; + case 'select': + $type_validation = 'in:'.implode(',', $option->options); + break; + default: + $type_validation = 'string'; + break; + } + + $request->validate([ + 'value' => [ + 'required', + $type_validation + ] + ]); + + $option->value = request()->input('value'); + $option->save(); + + return response()->json([ + 'message' => 'Option updated successfully' + ]); + } + public function getPermissionsAndRoles() { if(!request()->user()->hasPermission("admin-roles-read")) abort(401); return response()->json([ diff --git a/backend/app/Models/Option.php b/backend/app/Models/Option.php new file mode 100644 index 0000000..8cbc11a --- /dev/null +++ b/backend/app/Models/Option.php @@ -0,0 +1,16 @@ + 'array' + ]; +} diff --git a/backend/config/laratrust_seeder.php b/backend/config/laratrust_seeder.php index 1da57d1..0bde93b 100644 --- a/backend/config/laratrust_seeder.php +++ b/backend/config/laratrust_seeder.php @@ -22,6 +22,7 @@ return [ 'admin' => 'r', 'admin-info' => 'r,u', 'admin-maintenance' => 'r,u', + 'admin-options' => 'r,u', 'admin-roles' => 'r,u' ], 'admin' => [ @@ -33,6 +34,7 @@ return [ 'logs' => 'lr', 'admin' => 'r', 'admin-info' => 'r,u', + 'admin-options' => 'r,u', 'admin-roles' => 'r,u' ], 'chief' => [ diff --git a/backend/database/migrations/2024_01_18_005858_create_options_table.php b/backend/database/migrations/2024_01_18_005858_create_options_table.php new file mode 100644 index 0000000..1db04c7 --- /dev/null +++ b/backend/database/migrations/2024_01_18_005858_create_options_table.php @@ -0,0 +1,34 @@ +id(); + $table->string('name')->unique(); + $table->text('value')->nullable(); + $table->text('default')->nullable(); + $table->enum('type', ['number', 'string', 'boolean', 'select'])->default('string'); + $table->json('options')->nullable(); + $table->float('min')->nullable(); + $table->float('max')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('options'); + } +}; diff --git a/backend/database/seeders/OptionsSeeder.php b/backend/database/seeders/OptionsSeeder.php new file mode 100644 index 0000000..6bd177e --- /dev/null +++ b/backend/database/seeders/OptionsSeeder.php @@ -0,0 +1,28 @@ + 'service_place_selection_manual', + 'value' => true, + 'type' => 'boolean' + ] + ]; + + foreach ($options as $option) { + $option['default'] = $option['value']; + \App\Models\Option::create($option); + } + } +} diff --git a/backend/routes/api.php b/backend/routes/api.php index 102b5c4..d4e1ca4 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -117,6 +117,9 @@ Route::middleware('auth:sanctum')->group( function () { Route::post('/admin/telegramBot/setWebhook', [AdminController::class, 'setTelegramWebhook']); Route::post('/admin/telegramBot/unsetWebhook', [AdminController::class, 'unsetTelegramWebhook']); + Route::get('/admin/options', [AdminController::class, 'getOptions']); + Route::put('/admin/options/{option}', [AdminController::class, 'updateOption']); + Route::get('/admin/permissionsAndRoles', [AdminController::class, 'getPermissionsAndRoles']); Route::post('/admin/roles', [AdminController::class, 'updateRoles']); }); diff --git a/frontend/src/app/_routes/admin/admin-routing.module.ts b/frontend/src/app/_routes/admin/admin-routing.module.ts index 71121dd..e09d6d7 100644 --- a/frontend/src/app/_routes/admin/admin-routing.module.ts +++ b/frontend/src/app/_routes/admin/admin-routing.module.ts @@ -19,6 +19,12 @@ const routes: Routes = [{ canActivate: [AuthorizeGuard], data: {permissionsRequired: ['admin-read', 'admin-maintenance-read']} }, + { + path: 'options', + loadChildren: () => import('./options/admin-options.module').then(m => m.AdminOptionsModule), + canActivate: [AuthorizeGuard], + data: {permissionsRequired: ['admin-read', 'admin-options-read']} + }, { path: 'roles', loadChildren: () => import('./roles/admin-roles.module').then(m => m.AdminRolesModule), diff --git a/frontend/src/app/_routes/admin/admin.component.ts b/frontend/src/app/_routes/admin/admin.component.ts index c487eea..0e44b8d 100644 --- a/frontend/src/app/_routes/admin/admin.component.ts +++ b/frontend/src/app/_routes/admin/admin.component.ts @@ -21,6 +21,7 @@ export class AdminComponent implements OnInit { tabs: ITab[] = [ { title: 'info', id: 'info', active: false, permissionsRequired: ['admin-read', 'admin-info-read'] }, { title: 'maintenance', id: 'maintenance', active: false, permissionsRequired: ['admin-read', 'admin-maintenance-read'] }, + { title: 'options', id: 'options', active: false, permissionsRequired: ['admin-read', 'admin-options-read'] }, { title: 'roles', id: 'roles', active: false, permissionsRequired: ['admin-read', 'admin-roles-read'] } ]; diff --git a/frontend/src/app/_routes/admin/options/admin-options-routing.module.ts b/frontend/src/app/_routes/admin/options/admin-options-routing.module.ts new file mode 100644 index 0000000..fa67651 --- /dev/null +++ b/frontend/src/app/_routes/admin/options/admin-options-routing.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { AdminOptionsComponent } from './admin-options.component'; + +const routes: Routes = [{ path: '', component: AdminOptionsComponent }]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class AdminOptionsRoutingModule { } diff --git a/frontend/src/app/_routes/admin/options/admin-options.component.html b/frontend/src/app/_routes/admin/options/admin-options.component.html new file mode 100644 index 0000000..b5e1b95 --- /dev/null +++ b/frontend/src/app/_routes/admin/options/admin-options.component.html @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + +
{{ 'name'|translate|ftitlecase }}{{ 'value'|translate|ftitlecase }}{{ 'action'|translate|ftitlecase }}
{{ 'options.'+option.name|translate }} + + + + +
+ +
+
+ + + + +
diff --git a/frontend/src/app/_routes/admin/options/admin-options.component.scss b/frontend/src/app/_routes/admin/options/admin-options.component.scss new file mode 100644 index 0000000..e708199 --- /dev/null +++ b/frontend/src/app/_routes/admin/options/admin-options.component.scss @@ -0,0 +1,4 @@ +.custom-check-input { + width: 3.6rem; + height: 1.8rem; +} \ No newline at end of file diff --git a/frontend/src/app/_routes/admin/options/admin-options.component.ts b/frontend/src/app/_routes/admin/options/admin-options.component.ts new file mode 100644 index 0000000..16d0b82 --- /dev/null +++ b/frontend/src/app/_routes/admin/options/admin-options.component.ts @@ -0,0 +1,85 @@ +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { TranslateService } from '@ngx-translate/core'; +import { ApiClientService } from 'src/app/_services/api-client.service'; +import { AuthService } from 'src/app/_services/auth.service'; +import Swal from 'sweetalert2'; + +@Component({ + selector: 'app-admin-options', + templateUrl: './admin-options.component.html', + styleUrls: ['./admin-options.component.scss'] +}) +export class AdminOptionsComponent implements OnInit { + options: any[] = []; + + constructor( + private translateService: TranslateService, + private api: ApiClientService, + public auth: AuthService, + private router: Router + ) { } + + getOptions() { + this.api.get('admin/options').then((res: any) => { + res.forEach((option: any) => { + switch (option.type) { + case 'boolean': + option.value = option.value === '1' || option.value === 'true'; + break; + case 'number': + option.value = parseFloat(option.value); + break; + case 'string': + option.value = option.value.toString(); + break; + } + //Add properties used in the UI + option._origValue = option.value; + option._updating = false; + }); + this.options = res; + console.log(this.options); + }).catch((err: any) => { + console.error(err); + Swal.fire({ + title: this.translateService.instant("error_title"), + text: err.error.message, + icon: 'error', + confirmButtonText: 'Ok' + }); + }); + } + + ngOnInit() { + this.getOptions(); + } + + updateOption(optionId: number) { + let option = this.options.find(o => o.id === optionId); + option._updating = true; + console.log(option); + this.api.put('admin/options/'+option.id, { + value: option.value + }).then((res: any) => { + console.log(res); + option._origValue = option.value; + Swal.fire({ + title: this.translateService.instant("success_title"), + text: this.translateService.instant("admin.option_update_success"), + icon: 'success', + confirmButtonText: 'Ok' + }); + }).catch((err: any) => { + console.error(err); + Swal.fire({ + title: this.translateService.instant("error_title"), + text: err.error.message, + icon: 'error', + confirmButtonText: 'Ok' + }); + }).finally(() => { + option._updating = false; + }); + } +} diff --git a/frontend/src/app/_routes/admin/options/admin-options.module.ts b/frontend/src/app/_routes/admin/options/admin-options.module.ts new file mode 100644 index 0000000..33801bc --- /dev/null +++ b/frontend/src/app/_routes/admin/options/admin-options.module.ts @@ -0,0 +1,23 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import { TranslationModule } from '../../../translation.module'; +import { FirstLetterUppercasePipe } from '../../../_pipes/first-letter-uppercase.pipe'; + +import { AdminOptionsComponent } from './admin-options.component'; +import { AdminOptionsRoutingModule } from './admin-options-routing.module'; + +@NgModule({ + declarations: [ + AdminOptionsComponent + ], + imports: [ + CommonModule, + FormsModule, + AdminOptionsRoutingModule, + TranslationModule, + FirstLetterUppercasePipe + ] +}) +export class AdminOptionsModule { } diff --git a/frontend/src/assets/i18n/en.json b/frontend/src/assets/i18n/en.json index fa25e89..e592e09 100644 --- a/frontend/src/assets/i18n/en.json +++ b/frontend/src/assets/i18n/en.json @@ -65,7 +65,9 @@ "env_delete": "delete .env", "env_delete_title": "Delete .env file", "env_delete_confirm": "Are you sure you want to delete the .env file?", - "env_delete_success": ".env deleted successfully" + "env_delete_success": ".env deleted successfully", + "options": "Options", + "option_update_success": "Option updated successfully" }, "table": { "remove_service_confirm": "Are you sure you want to remove this service?", @@ -185,6 +187,10 @@ "file_too_big": "File too big", "password_min_length": "Password must be at least 6 characters long" }, + "options": { + "service_place_selection_manual": "Manual place selection for services", + "no_selection_available": "No selection available" + }, "update_available": "Update available", "update_available_text": "A new version of the application is available. Do you want to update now?", "update_now": "Update now", diff --git a/frontend/src/assets/i18n/it.json b/frontend/src/assets/i18n/it.json index e3037b1..2f961d0 100644 --- a/frontend/src/assets/i18n/it.json +++ b/frontend/src/assets/i18n/it.json @@ -65,7 +65,9 @@ "env_delete": "rimuovi .env", "env_delete_title": "Rimuovi il file .env", "env_delete_text": "Sei sicuro di voler rimuovere il file .env?", - "env_delete_success": ".env rimosso con successo" + "env_delete_success": ".env rimosso con successo", + "options": "Opzioni", + "option_update_success": "Opzione aggiornata con successo" }, "table": { "remove_service_confirm": "Sei sicuro di voler rimuovere questo intervento?", @@ -185,6 +187,10 @@ "file_too_big": "File troppo grande", "password_min_length": "La password deve essere di almeno 6 caratteri" }, + "options": { + "service_place_selection_manual": "Seleziona manualmente il luogo dell'intervento", + "no_selection_available": "Nessuna selezione disponibile" + }, "update_available": "Aggiornamento disponibile", "update_available_text": "È disponibile un aggiornamento per Allerta. Vuoi aggiornare ora?", "update_now": "Aggiorna ora",