diff --git a/app/Http/Controllers/FeedbackController.php b/app/Http/Controllers/FeedbackController.php new file mode 100644 index 000000000..10057c444 --- /dev/null +++ b/app/Http/Controllers/FeedbackController.php @@ -0,0 +1,71 @@ +route('dashboard')->withErrors('Feedback is currently disabled.'); + } + + $positions = Position::all(); + $controllers = User::where('atc_active', true)->get(); + + return view('feedback.create', compact('positions', 'controllers')); + } + + /** + * Store a newly created resource in storage. + * + * @return \Illuminate\Http\Response + */ + public function store(Request $request) + { + + if (! Setting::get('feedbackEnabled')) { + return redirect()->route('dashboard')->withErrors('Feedback is currently disabled.'); + } + + $data = $request->validate([ + 'position' => 'nullable|exists:positions,callsign', + 'controller' => 'nullable|exists:users,id', + 'feedback' => 'required', + ]); + + $position = isset($data['position']) ? Position::where('callsign', $data['position'])->get()->first() : null; + $controller = isset($data['controller']) ? User::find($data['controller']) : null; + $feedback = $data['feedback']; + + $submitter = auth()->user(); + + $feedback = Feedback::create([ + 'feedback' => $feedback, + 'submitter_user_id' => $submitter->id, + 'reference_user_id' => isset($controller) ? $controller->id : null, + 'reference_position_id' => isset($position) ? $position->id : null, + ]); + + // Forward email if configured + if (Setting::get('feedbackForwardEmail')) { + $feedback->notify(new FeedbackNotification($feedback)); + } + + return redirect()->route('dashboard')->with('success', 'Feedback submitted!'); + + } +} diff --git a/app/Http/Controllers/GlobalSettingController.php b/app/Http/Controllers/GlobalSettingController.php index 8eb219b76..f9c6c6428 100644 --- a/app/Http/Controllers/GlobalSettingController.php +++ b/app/Http/Controllers/GlobalSettingController.php @@ -59,6 +59,8 @@ public function edit(Request $request, Setting $setting) 'linkVisiting' => 'required|url', 'linkDiscord' => 'required|url', 'linkMoodle' => '', + 'feedbackEnabled' => '', + 'feedbackForwardEmail' => 'nullable|email', 'telemetryEnabled' => '', ]); @@ -67,9 +69,11 @@ public function edit(Request $request, Setting $setting) isset($data['atcActivityNotifyInactive']) ? $atcActivityNotifyInactive = true : $atcActivityNotifyInactive = false; isset($data['atcActivityAllowReactivation']) ? $atcActivityAllowReactivation = true : $atcActivityAllowReactivation = false; isset($data['atcActivityAllowInactiveControlling']) ? $atcActivityAllowInactiveControlling = true : $atcActivityAllowInactiveControlling = false; + isset($data['feedbackEnabled']) ? $feedbackEnabled = true : $feedbackEnabled = false; // The setting dependency doesn't support null values, so we need to set it to false if it's not set isset($data['linkMoodle']) ? $linkMoodle = $data['linkMoodle'] : $linkMoodle = false; + isset($data['feedbackForwardEmail']) ? $feedbackForwardEmail = $data['feedbackForwardEmail'] : $feedbackForwardEmail = false; isset($data['trainingExamTemplate']) ? $trainingExamTemplate = $data['trainingExamTemplate'] : $trainingExamTemplate = false; Setting::set('trainingEnabled', $trainingEnabled); @@ -93,6 +97,8 @@ public function edit(Request $request, Setting $setting) Setting::set('linkVisiting', $data['linkVisiting']); Setting::set('linkDiscord', $data['linkDiscord']); Setting::set('linkMoodle', $linkMoodle); + Setting::set('feedbackEnabled', $feedbackEnabled); + Setting::set('feedbackForwardEmail', $feedbackForwardEmail); Setting::set('telemetryEnabled', $telemetryEnabled); Setting::save(); diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php index 1b9787c0f..e56345feb 100644 --- a/app/Http/Controllers/ReportController.php +++ b/app/Http/Controllers/ReportController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers; use App\Models\Area; +use App\Models\Feedback; use App\Models\Group; use App\Models\ManagementReport; use App\Models\Rating; @@ -122,6 +123,18 @@ public function mentors() return view('reports.mentors', compact('mentors', 'statuses')); } + /** + * Index received feedback + * + * @return \Illuminate\View\View + */ + public function feedback() + { + $feedback = Feedback::all()->sortByDesc('created_at'); + + return view('reports.feedback', compact('feedback')); + } + /** * Return the statistics for the cards (in queue, in training, awaiting exam, completed this year) on top of the page * diff --git a/app/Mail/StaffNoticeMail.php b/app/Mail/StaffNoticeMail.php index 398603131..7047f3712 100644 --- a/app/Mail/StaffNoticeMail.php +++ b/app/Mail/StaffNoticeMail.php @@ -2,7 +2,6 @@ namespace App\Mail; -use App\Models\User; use Illuminate\Bus\Queueable; use Illuminate\Mail\Mailable; use Illuminate\Queue\SerializesModels; @@ -13,25 +12,16 @@ class StaffNoticeMail extends Mailable private $mailSubject; - private $user; - private $textLines; /** * Create a new message instance. * - * @param Endorsement $endorsement - * @param array $textLines an array of markdown lines to add - * @param string $contactMail optional contact e-mail to put in footer - * @param string $actionUrl optinal action button url - * @param string $actionText optional action button text - * @param string $actionColor optional bootstrap button color override * @return void */ - public function __construct(string $mailSubject, User $user, array $textLines) + public function __construct(string $mailSubject, array $textLines) { $this->mailSubject = $mailSubject; - $this->user = $user; $this->textLines = $textLines; } diff --git a/app/Models/Feedback.php b/app/Models/Feedback.php new file mode 100644 index 000000000..9f1c6dc1c --- /dev/null +++ b/app/Models/Feedback.php @@ -0,0 +1,30 @@ +belongsTo(User::class, 'submitter_user_id'); + } + + public function referenceUser() + { + return $this->belongsTo(User::class, 'reference_user_id'); + } + + public function referencePosition() + { + return $this->belongsTo(Position::class, 'reference_position_id'); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 5bbbe97d9..d15b3b0b7 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -133,6 +133,16 @@ public function getNameAttribute() return $this->first_name . ' ' . $this->last_name; } + public function submittedFeedback() + { + return $this->hasMany(Feedback::class, 'submitter_user_id'); + } + + public function receivedFeedback() + { + return $this->hasMany(Feedback::class, 'reference_user_id'); + } + /** * @todo: Convert to to new v9.x+ mutators https://laravel.com/docs/9.x/eloquent-mutators */ diff --git a/app/Notifications/FeedbackNotification.php b/app/Notifications/FeedbackNotification.php new file mode 100644 index 000000000..d6a8a9efd --- /dev/null +++ b/app/Notifications/FeedbackNotification.php @@ -0,0 +1,83 @@ +feedback = $feedback; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['mail', 'database']; + } + + /** + * Get the mail representation of the notification. + * + * @param mixed $notifiable + * @return EndorsementMail + */ + public function toMail($notifiable) + { + + if (! Setting::get('feedbackEnabled')) { + return false; + } + + $position = isset($this->feedback->referencePosition) ? $this->feedback->referencePosition->callsign : 'N/A'; + $controller = isset($this->feedback->referenceUser) ? $this->feedback->referenceUser->name : 'N/A'; + + $textLines = [ + 'New feedback has been submitted by ' . $this->feedback->submitter->name . ' (' . $this->feedback->submitter->id . '). You may respond by replying to this email.', + '___', + '**Controller**: ' . $controller, + '**Position**: ' . $position, + '___', + '**Feedback**', + $this->feedback->feedback, + + ]; + + $feedbackForward = Setting::get('feedbackForwardEmail'); + + return (new StaffNoticeMail('Feedback submited', $textLines)) + ->to($feedbackForward) + ->replyTo($this->feedback->submitter->email, $this->feedback->submitter->name); + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return []; + } +} diff --git a/app/Notifications/InactiveOnlineStaffNotification.php b/app/Notifications/InactiveOnlineStaffNotification.php index 184a63f4f..8aeff90b1 100644 --- a/app/Notifications/InactiveOnlineStaffNotification.php +++ b/app/Notifications/InactiveOnlineStaffNotification.php @@ -60,7 +60,7 @@ public function toMail($notifiable) 'All admins and moderators in area in question has been notified.', ]; - return (new StaffNoticeMail('Unauthorized network logon recorded', $this->user, $textLines)) + return (new StaffNoticeMail('Unauthorized network logon recorded', $textLines)) ->to($this->sendTo->pluck('email')); } diff --git a/database/migrations/2023_10_08_144001_add_feedback.php b/database/migrations/2023_10_08_144001_add_feedback.php new file mode 100644 index 000000000..e03643582 --- /dev/null +++ b/database/migrations/2023_10_08_144001_add_feedback.php @@ -0,0 +1,52 @@ +id(); + $table->text('feedback'); + $table->unsignedBigInteger('submitter_user_id'); + $table->unsignedBigInteger('reference_user_id')->nullable(); + $table->unsignedBigInteger('reference_position_id')->nullable(); + $table->boolean('forwarded')->default(false); + $table->timestamps(); + + $table->foreign('submitter_user_id')->references('id')->on('users')->onUpdate('CASCADE')->onDelete('CASCADE'); + $table->foreign('reference_user_id')->references('id')->on('users')->onUpdate('CASCADE')->onDelete('CASCADE'); + $table->foreign('reference_position_id')->references('id')->on('positions')->onUpdate('CASCADE')->onDelete('SET NULL'); + }); + + Schema::table('settings', function (Blueprint $table) { + DB::table(Config::get('settings.table'))->insert([ + ['key' => 'feedbackEnable', 'value' => true], + ['key' => 'feedbackForwardEmail', 'value' => false], + ]); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('feedback'); + + Schema::table('settings', function (Blueprint $table) { + DB::table(Config::get('settings.table'))->where('key', 'feedbackEnable')->delete(); + DB::table(Config::get('settings.table'))->where('key', 'feedbackForwardEmail')->delete(); + }); + } +}; diff --git a/resources/views/admin/globalsettings.blade.php b/resources/views/admin/globalsettings.blade.php index ae046ced6..00d5fe3f4 100644 --- a/resources/views/admin/globalsettings.blade.php +++ b/resources/views/admin/globalsettings.blade.php @@ -303,6 +303,35 @@ +
+
+
Feedback
+
+
+
+
+ +
+ + +
+ +
+ + + Forward feedback to the provided address. Leave blank to disable. +
+ @error('feedbackForwardEmail') + {{ $errors->first('feedbackForwardEmail') }} + @enderror + +
+
+
+
+
Telemetry
diff --git a/resources/views/feedback/create.blade.php b/resources/views/feedback/create.blade.php new file mode 100644 index 000000000..c13357d40 --- /dev/null +++ b/resources/views/feedback/create.blade.php @@ -0,0 +1,106 @@ +@extends('layouts.app') + +@section('title', 'Feedback') +@section('content') + +
+
+
+
+
+ Submit Feedback +
+
+
+
+ @csrf + +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+
+ + + + + @foreach($controllers as $controller) + @browser('isFirefox') + + @else + + @endbrowser + @endforeach + + @error('controller') + {{ $errors->first('controller') }} + @enderror +
+
+ + + + + @foreach($positions as $position) + @browser('isFirefox') + + @else + + @endbrowser + @endforeach + + @error('position') + {{ $errors->first('position') }} + @enderror +
+
+ +
+
+ + + @error('feedback') + {{ $errors->first('feedback') }} + @enderror +
+
+ + + +
+
+
+
+
+ +@endsection \ No newline at end of file diff --git a/resources/views/layouts/sidebar.blade.php b/resources/views/layouts/sidebar.blade.php index 31bc3dee0..a428d2191 100644 --- a/resources/views/layouts/sidebar.blade.php +++ b/resources/views/layouts/sidebar.blade.php @@ -166,6 +166,8 @@ @can('viewAccessReport', \App\Models\ManagementReport::class) Access @endcan + + Feedback
diff --git a/resources/views/reports/feedback.blade.php b/resources/views/reports/feedback.blade.php new file mode 100644 index 000000000..cd5e3b9a4 --- /dev/null +++ b/resources/views/reports/feedback.blade.php @@ -0,0 +1,73 @@ +@extends('layouts.app') + +@section('title', 'Feedback') + +@section('header') + @vite(['resources/sass/bootstrap-table.scss', 'resources/js/bootstrap-table.js']) +@endsection + +@section('content') + +
+
+ +
+
+
+ Feedback +
+
+
+
+ + + + + + + + + + + + @foreach($feedback as $f) + + + + + + + + @endforeach + +
ReceivedSubmitterControllerPositionFeedback
{{ $f->created_at->toEuropeanDateTime() }}{{ $f->submitter->name }} ({{ $f->submitter_user_id }}) + @isset($f->referenceUser) + {{ $f->referenceUser->name }} ({{ $f->referenceUser->id }}) + @else + N/A + @endisset + + @isset($f->referencePosition) + {{ $f->referencePosition->callsign }} + @else + N/A + @endisset + + {!! nl2br($f->feedback) !!} +
+
+
+
+
+ +
+ +@endsection \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index ad3be9cea..13220bec6 100755 --- a/routes/web.php +++ b/routes/web.php @@ -93,6 +93,7 @@ Route::get('/reports/activities/{id}', 'activities')->name('reports.activities.area'); Route::get('/reports/mentors', 'mentors')->name('reports.mentors'); Route::get('/reports/access', 'access')->name('reports.access'); + Route::get('/reports/feedback', 'feedback')->name('reports.feedback'); }); // Admin @@ -189,6 +190,11 @@ Route::get('/vote/{id}', 'show')->name('vote.show'); }); + Route::controller(FeedbackController::class)->group(function () { + Route::get('/feedback', 'create')->name('feedback'); + Route::post('/feedback/store', 'store')->name('feedback.store'); + }); + Route::controller(TaskController::class)->group(function () { Route::get('/tasks', 'index')->name('tasks'); Route::get('/tasks/{activeFilter}', 'index')->name('tasks.filtered');