From 14183e390c4c2d903e897185631f8a9e58ac61e4 Mon Sep 17 00:00:00 2001 From: Matteo Gheza Date: Sun, 22 Oct 2023 14:22:12 +0200 Subject: [PATCH] WIP support for Alerts --- .../app/Http/Controllers/AlertController.php | 152 ++++++++++++++++++ backend/app/Models/Alert.php | 48 ++++++ backend/app/Models/AlertCrew.php | 39 +++++ .../2023_10_22_000915_create_alerts_table.php | 53 ++++++ backend/routes/api.php | 6 + .../modal-alert/modal-alert.component.html | 53 +++--- .../modal-alert/modal-alert.component.ts | 93 +++++++---- .../src/app/_routes/list/list.component.html | 6 +- .../src/app/_routes/list/list.component.ts | 41 ++--- .../src/app/_services/api-client.service.ts | 9 ++ frontend/src/app/app.component.ts | 2 - frontend/src/assets/i18n/en.json | 7 + frontend/src/assets/i18n/it.json | 7 + 13 files changed, 419 insertions(+), 97 deletions(-) create mode 100644 backend/app/Http/Controllers/AlertController.php create mode 100644 backend/app/Models/Alert.php create mode 100644 backend/app/Models/AlertCrew.php create mode 100644 backend/database/migrations/2023_10_22_000915_create_alerts_table.php diff --git a/backend/app/Http/Controllers/AlertController.php b/backend/app/Http/Controllers/AlertController.php new file mode 100644 index 0000000..c98ae27 --- /dev/null +++ b/backend/app/Http/Controllers/AlertController.php @@ -0,0 +1,152 @@ +json( + Alert::with('crew.user') + ->where('closed', false) + ->orderBy('created_at', 'desc') + ->get() + ); + } + + /** + * Show the form for creating a new resource. + */ + public function create() + { + // + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + $type = $request->input('type', 'support'); + $ignoreWarning = $request->input('ignoreWarning', false); + + //Count users when not hidden and available + $count = User::where('hidden', false)->where('available', true)->count(); + + if($count == 0) { + return response()->json([ + 'status' => 'error', + 'message' => 'Nessun utente disponibile.', + 'ignorable' => false, + ], 400); + } + + //Check if there is at least one chief available (not hidden) + $chiefCount = User::where([ + ['hidden', '=', false], + ['available', '=', true], + ['chief', '=', true] + ])->count(); + if($chiefCount == 0 && !$ignoreWarning) { + return response()->json([ + 'status' => 'error', + 'message' => 'Nessun caposquadra disponibile. Sei sicuro di voler proseguire?', + 'ignorable' => true, + ], 400); + } + + //Check if there is at least one driver available (not hidden) + $driverCount = User::where([ + ['hidden', '=', false], + ['available', '=', true], + ['driver', '=', true] + ])->count(); + if($driverCount == 0 && !$ignoreWarning) { + return response()->json([ + 'status' => 'error', + 'message' => 'Nessun autista disponibile. Sei sicuro di voler proseguire?', + 'ignorable' => true, + ], 400); + } + + //Select call list (everyone not hidden and available) + $users = User::where('hidden', false)->where('available', true)->get(); + if(count($users) < 5 && $type = "full") $type = "support"; + + //Create alert + $alert = new Alert; + $alert->type = $type; + $alert->addedBy()->associate(auth()->user()); + $alert->updatedBy()->associate(auth()->user()); + $alert->save(); + + //Create alert crew + $alertCrewIds = []; + foreach($users as $user) { + $alertCrew = new AlertCrew(); + $alertCrew->user_id = $user->id; + $alertCrew->save(); + + $alertCrewIds[] = $alertCrew->id; + } + $alert->crew()->attach($alertCrewIds); + $alert->save(); + + //Return response + return response()->json([ + 'status' => 'success', + 'alert' => $alert, + ], 200); + } + + /** + * Get single Alert + */ + public function show(Request $request, $id) + { + User::where('id', $request->user()->id)->update(['last_access' => now()]); + + return response()->json( + Alert::with('crew.user')->find($id) + ); + } + + /** + * Show the form for editing the specified resource. + */ + public function edit(Alert $Alert) + { + // + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, $id) + { + //TODO: improve permissions and roles + if(!$request->user()->hasPermission("users-read")) abort(401); + $alert = Alert::find($id); + $alert->notes = $request->input('notes', $alert->notes); + $alert->closed = $request->input('closed', $alert->closed); + $alert->updatedBy()->associate(auth()->user()); + $alert->save(); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(Alert $Alert) + { + // + } +} diff --git a/backend/app/Models/Alert.php b/backend/app/Models/Alert.php new file mode 100644 index 0000000..b031919 --- /dev/null +++ b/backend/app/Models/Alert.php @@ -0,0 +1,48 @@ + + */ + protected $fillable = [ + 'type', + 'closed', + 'notes' + ]; + + public function crew(): BelongsToMany + { + return $this->belongsToMany( + AlertCrew::class, + 'alerts_crew_associations' + ); + } + + /** + * Get the user. + */ + public function addedBy(): BelongsTo + { + return $this->belongsTo(User::class); + } + + /** + * Get the user. + */ + public function updatedBy(): BelongsTo + { + return $this->belongsTo(User::class); + } +} diff --git a/backend/app/Models/AlertCrew.php b/backend/app/Models/AlertCrew.php new file mode 100644 index 0000000..4d589f9 --- /dev/null +++ b/backend/app/Models/AlertCrew.php @@ -0,0 +1,39 @@ + + */ + protected $fillable = [ + 'accepted' + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'respondedAt' => 'datetime' + ]; + + /** + * Get the user. + */ + public function user(): BelongsTo { + return $this->belongsTo(User::class); + } +} diff --git a/backend/database/migrations/2023_10_22_000915_create_alerts_table.php b/backend/database/migrations/2023_10_22_000915_create_alerts_table.php new file mode 100644 index 0000000..853a1f4 --- /dev/null +++ b/backend/database/migrations/2023_10_22_000915_create_alerts_table.php @@ -0,0 +1,53 @@ +id(); + $table->string('type'); + $table->boolean('closed')->default(false); + $table->text('notes')->nullable(); + $table->foreignId('added_by_id')->constrained('users'); + $table->foreignId('updated_by_id')->constrained('users'); + $table->timestamps(); + }); + + Schema::create('alert_crews', function (Blueprint $table) { + $table->id(); + $table->boolean('accepted')->nullable(); + $table->foreignId('user_id')->constrained('users'); + $table->dateTime('responded_at')->nullable(); + }); + + Schema::create('alerts_crew_associations', function (Blueprint $table) { + $table->bigIncrements('id'); + $table->unsignedBigInteger('alert_crew_id')->unsigned(); + $table->foreign('alert_crew_id') + ->references('id') + ->on('alert_crews')->onDelete('cascade'); + $table->unsignedBigInteger('alert_id')->unsigned(); + $table->foreign('alert_id') + ->references('id') + ->on('alerts')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('alerts_crew_associations'); + Schema::dropIfExists('alerts'); + Schema::dropIfExists('alert_crews'); + } +}; diff --git a/backend/routes/api.php b/backend/routes/api.php index 5fa22c3..89593a6 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -5,6 +5,7 @@ use App\Http\Controllers\AuthController; use App\Http\Controllers\UserController; use App\Http\Controllers\ScheduleSlotsController; use App\Http\Controllers\AvailabilityController; +use App\Http\Controllers\AlertController; use App\Http\Controllers\LogsController; use App\Http\Controllers\TelegramController; use App\Http\Controllers\ServiceController; @@ -47,6 +48,11 @@ Route::middleware('auth:sanctum')->group( function () { Route::post('/availability', [AvailabilityController::class, 'updateAvailability']); Route::post('/manual_mode', [AvailabilityController::class, 'updateAvailabilityManualMode']); + Route::get('/alerts', [AlertController::class, 'index']); + Route::post('/alerts', [AlertController::class, 'store']); + Route::get('/alerts/{id}', [AlertController::class, 'show']); + Route::patch('/alerts/{id}', [AlertController::class, 'update']); + Route::get('/services', [ServiceController::class, 'index']); Route::post('/services', [ServiceController::class, 'createOrUpdate']); Route::get('/services/{id}', [ServiceController::class, 'show']); diff --git a/frontend/src/app/_components/modal-alert/modal-alert.component.html b/frontend/src/app/_components/modal-alert/modal-alert.component.html index a755b2f..6af7273 100644 --- a/frontend/src/app/_components/modal-alert/modal-alert.component.html +++ b/frontend/src/app/_components/modal-alert/modal-alert.component.html @@ -10,6 +10,9 @@ \ No newline at end of file diff --git a/frontend/src/app/_components/modal-alert/modal-alert.component.ts b/frontend/src/app/_components/modal-alert/modal-alert.component.ts index 9013aab..11f6d9f 100644 --- a/frontend/src/app/_components/modal-alert/modal-alert.component.ts +++ b/frontend/src/app/_components/modal-alert/modal-alert.component.ts @@ -3,6 +3,7 @@ import { BsModalRef } from 'ngx-bootstrap/modal'; import { ApiClientService } from 'src/app/_services/api-client.service'; import { AuthService } from 'src/app/_services/auth.service'; import { ToastrService } from 'ngx-toastr'; +import { TranslateService } from '@ngx-translate/core'; import Swal from 'sweetalert2'; const isEqual = (...objects: any[]) => objects.every(obj => JSON.stringify(obj) === JSON.stringify(objects[0])); @@ -15,28 +16,35 @@ const isEqual = (...objects: any[]) => objects.every(obj => JSON.stringify(obj) export class ModalAlertComponent implements OnInit, OnDestroy { id = 0; - users: any[] = []; + crewUsers: any[] = []; - isAdvancedCollapsed = true; loadDataInterval: NodeJS.Timer | undefined = undefined; notes = ""; + originalNotes = ""; + notesHasUnsavedChanges = false; - alertEnabled = true; + alertClosed = 0; constructor( public bsModalRef: BsModalRef, private api: ApiClientService, public auth: AuthService, - private toastr: ToastrService + private toastr: ToastrService, + private translate: TranslateService ) { } loadResponsesData() { + //TODO: do not update data if not changed. Support for content hash in response header? this.api.get(`alerts/${this.id}`).then((response) => { - if(this.alertEnabled !== response.enabled) this.alertEnabled = response.enabled; - if(!isEqual(this.users, response.crew)) this.users = response.crew; - if (this.notes === "" || this.notes === null) { - if(!isEqual(this.notes, response.notes)) this.notes = response.notes; + console.log(response, this.alertClosed, response.closed); + if(this.alertClosed !== response.closed) this.alertClosed = response.closed; + if(!isEqual(this.crewUsers, response.crew)) this.crewUsers = response.crew; + if (!this.notesHasUnsavedChanges) { + if(this.notes !== response.notes) { + this.notes = response.notes; + this.originalNotes = response.notes; + } } }); } @@ -59,43 +67,58 @@ export class ModalAlertComponent implements OnInit, OnDestroy { } } + notesUpdated() { + this.notesHasUnsavedChanges = this.notes !== this.originalNotes; + } + saveAlertSettings() { if(!this.auth.profile.can('users-read')) return; - this.api.post(`alerts/${this.id}/settings`, { + this.api.patch(`alerts/${this.id}`, { notes: this.notes }).then((response) => { - this.toastr.success("Impostazioni salvate con successo"); + this.translate.get('alert.settings_updated_successfully').subscribe((res: string) => { + this.toastr.success(res); + }); + this.notesHasUnsavedChanges = false; + this.originalNotes = this.notes; }); } deleteAlert() { if(!this.auth.profile.can('users-read')) return; - Swal.fire({ - title: "Sei sicuro di voler ritirare l'allarme?", - text: "I vigili verranno avvisati dell'azione", - icon: 'warning', - showCancelButton: true, - confirmButtonColor: '#3085d6', - cancelButtonColor: '#d33', - confirmButtonText: "Si, rimuovi", - cancelButtonText: "Annulla" - }).then((result: any) => { - if (result.isConfirmed) { - this.api.delete(`alerts/${this.id}`).then((response) => { - console.log(response); - this.bsModalRef.hide(); - this.api.alertsChanged.next(); - /* - this.translate.get('table.service_deleted_successfully').subscribe((res: string) => { - this.toastr.success(res); + this.translate.get([ + 'alert.delete_confirm_title', + 'alert.delete_confirm_text', + 'table.yes_remove', + 'table.cancel', + 'alert.deleted_successfully', + 'alert.delete_failed' + ]).subscribe((res: any) => { + console.log(res); + Swal.fire({ + title: res['alert.delete_confirm_title'], + text: res['alert.delete_confirm_text'], + icon: 'warning', + showCancelButton: true, + confirmButtonColor: '#3085d6', + cancelButtonColor: '#d33', + confirmButtonText: res['table.yes_remove'], + cancelButtonText: res['table.cancel'] + }).then((result: any) => { + if (result.isConfirmed) { + this.api.patch(`alerts/${this.id}`, { + closed: true + }).then((response) => { + console.log(response); + this.bsModalRef.hide(); + this.api.alertsChanged.next(); + this.toastr.success(res['alert.deleted_successfully']); + this.api.alertsChanged.next(); + }).catch((e) => { + this.toastr.error(res['alert.delete_failed']); }); - this.loadTableData(); - }).catch((e) => { - this.translate.get('table.service_deleted_error').subscribe((res: string) => { - this.toastr.error(res); - */ - }); - } + } + }); }); } } diff --git a/frontend/src/app/_routes/list/list.component.html b/frontend/src/app/_routes/list/list.component.html index a36f936..f8983fe 100644 --- a/frontend/src/app/_routes/list/list.component.html +++ b/frontend/src/app/_routes/list/list.component.html @@ -18,12 +18,12 @@ -
+
- -
diff --git a/frontend/src/app/_routes/list/list.component.ts b/frontend/src/app/_routes/list/list.component.ts index f082d1a..0916cd0 100644 --- a/frontend/src/app/_routes/list/list.component.ts +++ b/frontend/src/app/_routes/list/list.component.ts @@ -84,47 +84,34 @@ export class ListComponent implements OnInit, OnDestroy { this.scheduleModalRef = this.modalService.show(ModalAvailabilityScheduleComponent, Object.assign({}, { class: 'modal-custom' })); } - addAlertFull() { + addAlert(type: string, ignoreWarning = false) { this.alertLoading = true; if(!this.auth.profile.can('users-read')) return; this.api.post("alerts", { - type: "full" + type, + ignoreWarning }).then((response) => { + console.log(response); this.alertLoading = false; - if(response?.status === "error") { - this.toastr.error(response.message, undefined, { - timeOut: 5000 - }); - return; - } this.alertModalRef = this.modalService.show(ModalAlertComponent, { initialState: { - id: response.id + id: response.alert.id } }); this.api.alertsChanged.next(); - }); - } - - addAlertSupport() { - this.alertLoading = true; - if(!this.auth.profile.can('users-read')) return; - this.api.post("alerts", { - type: "support" - }).then((response) => { + }).catch((err) => { + console.log(err); this.alertLoading = false; - if(response?.status === "error") { - this.toastr.error(response.message, undefined, { - timeOut: 5000 - }); + if(err.error?.ignorable === true) { + if(confirm(err.error.message)) { + this.addAlert(type, true); + } return; } - this.alertModalRef = this.modalService.show(ModalAlertComponent, { - initialState: { - id: response.id - } + if(err.error?.message === undefined) err.error.message = "Errore sconosciuto"; + this.toastr.error(err.error.message, undefined, { + timeOut: 5000 }); - this.api.alertsChanged.next(); }); } diff --git a/frontend/src/app/_services/api-client.service.ts b/frontend/src/app/_services/api-client.service.ts index 8f3391e..4d579d0 100644 --- a/frontend/src/app/_services/api-client.service.ts +++ b/frontend/src/app/_services/api-client.service.ts @@ -49,6 +49,15 @@ export class ApiClientService { }); } + public patch(endpoint: string, data: any = {}) { + return new Promise((resolve, reject) => { + this.http.patch(this.apiEndpoint(endpoint), data).subscribe({ + next: (v) => resolve(v), + error: (e) => reject(e) + }); + }); + } + public delete(endpoint: string) { return new Promise((resolve, reject) => { this.http.delete(this.apiEndpoint(endpoint)).subscribe({ diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index ecccb35..9281804 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -52,7 +52,6 @@ export class AppComponent { } }); - /* this.loadAlertsInterval = setInterval(() => { console.log("Refreshing alerts..."); this.loadAlerts(); @@ -62,7 +61,6 @@ export class AppComponent { this.api.alertsChanged.subscribe(() => { this.loadAlerts(); }); - */ } openAlert(id: number) { diff --git a/frontend/src/assets/i18n/en.json b/frontend/src/assets/i18n/en.json index 3dfc251..f643c11 100644 --- a/frontend/src/assets/i18n/en.json +++ b/frontend/src/assets/i18n/en.json @@ -31,6 +31,13 @@ "manual_mode_update_failed": "Manual mode could not be updated. Please try again", "telegram_bot_token_request_failed": "Telegram bot token could not be generated. Please try again" }, + "alert": { + "delete_confirm_title": "Are you sure you want to remove this alert?", + "delete_confirm_text": "This action cannot be undone.", + "deleted_successfully": "Alert removed successfully", + "delete_failed": "Alert could not be removed. Please try again", + "settings_updated_successfully": "Settings updated successfully" + }, "login": { "username": "username", "password": "password", diff --git a/frontend/src/assets/i18n/it.json b/frontend/src/assets/i18n/it.json index 9c18c64..1b683c6 100644 --- a/frontend/src/assets/i18n/it.json +++ b/frontend/src/assets/i18n/it.json @@ -31,6 +31,13 @@ "manual_mode_update_failed": "Errore durante l'aggiornamento della modalità manuale. Riprova più tardi", "telegram_bot_token_request_failed": "Errore durante la richiesta del token del bot Telegram. Riprova più tardi" }, + "alert": { + "delete_confirm_title": "Sei sicuro di voler rimuovere questa allerta?", + "delete_confirm_text": "I vigili saranno avvisati della rimozione.", + "deleted_successfully": "Allerta rimossa con successo", + "delete_failed": "L'eliminazione dell'allerta è fallita. Riprova più tardi", + "settings_updated_successfully": "Impostazioni aggiornate con successo" + }, "login": { "username": "username", "password": "password",