Add admin options functionality

This commit is contained in:
Matteo Gheza 2024-01-18 15:51:02 +01:00
parent 2fda7a0651
commit 3de1246c50
15 changed files with 317 additions and 2 deletions

View File

@ -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([

View File

@ -0,0 +1,16 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Option extends Model
{
protected $table = 'options';
protected $fillable = ['name', 'value', 'default', 'type', 'min', 'max'];
protected $casts = [
'options' => 'array'
];
}

View File

@ -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' => [

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('options', function (Blueprint $table) {
$table->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');
}
};

View File

@ -0,0 +1,28 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class OptionsSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$options = [
[
'name' => 'service_place_selection_manual',
'value' => true,
'type' => 'boolean'
]
];
foreach ($options as $option) {
$option['default'] = $option['value'];
\App\Models\Option::create($option);
}
}
}

View File

@ -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']);
});

View File

@ -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),

View File

@ -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'] }
];

View File

@ -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 { }

View File

@ -0,0 +1,48 @@
<table class="table table-responsive table-striped">
<thead>
<tr>
<th>{{ 'name'|translate|ftitlecase }}</th>
<th>{{ 'value'|translate|ftitlecase }}</th>
<th>{{ 'action'|translate|ftitlecase }}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let option of options">
<td>{{ 'options.'+option.name|translate }}</td>
<!-- Data type specific input fields -->
<td *ngIf="option.type === 'number'">
<input
type="number" class="form-control"
[(ngModel)]="option.value" name="{{ option.name }}" [min]="option.min" [max]="option.max"
/>
</td>
<td *ngIf="option.type === 'string'">
<input
type="text" class="form-control"
[(ngModel)]="option.value" name="{{ option.name }}"
/>
</td>
<td *ngIf="option.type === 'boolean'">
<div class="form-check form-switch">
<input
class="form-check-input custom-check-input" type="checkbox" role="switch"
[(ngModel)]="option.value" name="{{ option.name }}"
/>
</div>
</td>
<td *ngIf="option.type === 'select'">
<select *ngIf="option.options" class="form-select" [(ngModel)]="option.value" name="{{ option.name }}">
<option *ngFor="let optionValue of option.options" [value]="optionValue">{{ 'options.'+optionValue|translate }}</option>
</select>
<div class="alert alert-warning" role="alert" *ngIf="!option.options">
{{ 'options.no_selection_available'|translate }}
</div>
</td>
<td>
<button (click)="updateOption(option.id)" class="btn btn-primary" [disabled]="option.value === option._origValue || option._updating">{{ 'update'|translate|ftitlecase }}</button>
</td>
</tr>
</tbody>
</table>

View File

@ -0,0 +1,4 @@
.custom-check-input {
width: 3.6rem;
height: 1.8rem;
}

View File

@ -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;
});
}
}

View File

@ -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 { }

View File

@ -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",

View File

@ -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",