From fce6e92d9d3e2eb2aef4fc75281448efb4d1546e Mon Sep 17 00:00:00 2001 From: Miguel N Date: Wed, 3 Nov 2021 00:37:47 +0000 Subject: [PATCH] Added an appointment cancellation button --- .../Controllers/AppointmentController.php | 25 +- .../Requests/CancelAppointmentRequest.php | 30 ++ app/Notifications/AppointmentCancelled.php | 78 +++++ app/Notifications/AppointmentScheduled.php | 4 +- app/Services/AppointmentService.php | 36 +++ .../views/dashboard/user/viewapp.blade.php | 34 +- .../views/vendor/mail/html/button.blade.php | 19 ++ .../views/vendor/mail/html/footer.blade.php | 11 + .../views/vendor/mail/html/header.blade.php | 11 + .../views/vendor/mail/html/layout.blade.php | 56 ++++ .../views/vendor/mail/html/message.blade.php | 27 ++ .../views/vendor/mail/html/panel.blade.php | 14 + .../views/vendor/mail/html/subcopy.blade.php | 7 + .../views/vendor/mail/html/table.blade.php | 3 + .../views/vendor/mail/html/themes/default.css | 290 ++++++++++++++++++ .../views/vendor/mail/text/button.blade.php | 1 + .../views/vendor/mail/text/footer.blade.php | 1 + .../views/vendor/mail/text/header.blade.php | 1 + .../views/vendor/mail/text/layout.blade.php | 9 + .../views/vendor/mail/text/message.blade.php | 27 ++ .../views/vendor/mail/text/panel.blade.php | 1 + .../views/vendor/mail/text/subcopy.blade.php | 1 + .../views/vendor/mail/text/table.blade.php | 1 + .../vendor/notifications/email.blade.php | 62 ++++ routes/web.php | 4 + 25 files changed, 746 insertions(+), 7 deletions(-) create mode 100644 app/Http/Requests/CancelAppointmentRequest.php create mode 100644 app/Notifications/AppointmentCancelled.php create mode 100644 resources/views/vendor/mail/html/button.blade.php create mode 100644 resources/views/vendor/mail/html/footer.blade.php create mode 100644 resources/views/vendor/mail/html/header.blade.php create mode 100644 resources/views/vendor/mail/html/layout.blade.php create mode 100644 resources/views/vendor/mail/html/message.blade.php create mode 100644 resources/views/vendor/mail/html/panel.blade.php create mode 100644 resources/views/vendor/mail/html/subcopy.blade.php create mode 100644 resources/views/vendor/mail/html/table.blade.php create mode 100644 resources/views/vendor/mail/html/themes/default.css create mode 100644 resources/views/vendor/mail/text/button.blade.php create mode 100644 resources/views/vendor/mail/text/footer.blade.php create mode 100644 resources/views/vendor/mail/text/header.blade.php create mode 100644 resources/views/vendor/mail/text/layout.blade.php create mode 100644 resources/views/vendor/mail/text/message.blade.php create mode 100644 resources/views/vendor/mail/text/panel.blade.php create mode 100644 resources/views/vendor/mail/text/subcopy.blade.php create mode 100644 resources/views/vendor/mail/text/table.blade.php create mode 100644 resources/views/vendor/notifications/email.blade.php diff --git a/app/Http/Controllers/AppointmentController.php b/app/Http/Controllers/AppointmentController.php index 18efe1d..2a8f19b 100755 --- a/app/Http/Controllers/AppointmentController.php +++ b/app/Http/Controllers/AppointmentController.php @@ -25,6 +25,7 @@ use App\Application; use App\Appointment; use App\Exceptions\InvalidAppointmentException; use App\Exceptions\InvalidAppointmentStatusException; +use App\Http\Requests\CancelAppointmentRequest; use App\Http\Requests\SaveNotesRequest; use App\Services\AppointmentService; use App\Services\MeetingNoteService; @@ -61,7 +62,7 @@ class AppointmentController extends Controller /** * @throws AuthorizationException */ - public function updateAppointment(Application $application, $status): RedirectResponse + public function updateAppointment(Application $application, string $status): RedirectResponse { $this->authorize('update', $application->appointment); @@ -78,10 +79,32 @@ class AppointmentController extends Controller ->back() ->with('error', $ex->getMessage()); } + } + + public function deleteAppointment(CancelAppointmentRequest $request, Application $application) + { + $this->authorize('update', $application->appointment); + + try { + + $this->appointmentService->deleteAppointment($application, $request->reason); + + return redirect() + ->back() + ->with('success', __('Appointment cancelled.')); + + } + catch (\Exception $ex) { + return redirect() + ->back() + ->with('error', $ex->getMessage()); + } } + + public function saveNotes(SaveNotesRequest $request, Application $application) { try { diff --git a/app/Http/Requests/CancelAppointmentRequest.php b/app/Http/Requests/CancelAppointmentRequest.php new file mode 100644 index 0000000..4ba920e --- /dev/null +++ b/app/Http/Requests/CancelAppointmentRequest.php @@ -0,0 +1,30 @@ + 'string|required' + ]; + } +} diff --git a/app/Notifications/AppointmentCancelled.php b/app/Notifications/AppointmentCancelled.php new file mode 100644 index 0000000..6b7f8e5 --- /dev/null +++ b/app/Notifications/AppointmentCancelled.php @@ -0,0 +1,78 @@ +application = $app; + $this->reason = $reason; + $this->appointmentDate = $appointmentDate; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['mail']; + } + + /** + * Get the mail representation of the notification. + * + * @param mixed $notifiable + * @return \Illuminate\Notifications\Messages\MailMessage + */ + public function toMail($notifiable) + { + // TODO: Switch to HTML & Blade. + + return (new MailMessage) + ->from(config('notification.sender.address'), config('notification.sender.name')) + ->subject(config('app.name').' - Interview Cancelled') + ->greeting("Hi " . explode(' ', $this->application->user->name, 2)[0] . ",") + ->line('The interview that was previously scheduled with you has been cancelled by a staff member.') + ->line('Date & time of the old appointment: '.$this->appointmentDate) + ->line('Your appointment was cancelled for the following reason: ' . $this->reason) + ->line('A staff member may contact you to reschedule within a new timeframe - you may also let us know of a date and time that suits you.') + ->line('Your application will be automatically rejected within 7 days if an interview is not scheduled.') + ->action('View ongoing applications', url(route('showUserApps'))) + ->line('Thank you!'); + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + // + ]; + } +} diff --git a/app/Notifications/AppointmentScheduled.php b/app/Notifications/AppointmentScheduled.php index e77cd5d..62dbd57 100755 --- a/app/Notifications/AppointmentScheduled.php +++ b/app/Notifications/AppointmentScheduled.php @@ -64,11 +64,11 @@ class AppointmentScheduled extends Notification implements ShouldQueue { return (new MailMessage) ->from(config('notification.sender.address'), config('notification.sender.name')) - ->subject(config('app.name').' - Interview scheduled') + ->subject(config('app.name').' - Interview Scheduled') ->line('A voice interview has been scheduled for you @ '.$this->appointment->appointmentDate.'.') ->line('With the following details: '.$this->appointment->appointmentDescription) ->line('This meeting will take place @ '.$this->appointment->appointmentLocation.'. You will receive an email soon with details on how to join this meeting.') - ->line('You are expected to show up at least 5 minutes before the scheduled date.') + ->line('Please join a public voice channel (or another platform if specified) at around this time.') ->action('Sign in', url(route('login'))) ->line('Thank you!'); } diff --git a/app/Services/AppointmentService.php b/app/Services/AppointmentService.php index c00c666..e5a6a65 100644 --- a/app/Services/AppointmentService.php +++ b/app/Services/AppointmentService.php @@ -8,6 +8,7 @@ use App\Application; use App\Appointment; use App\Exceptions\InvalidAppointmentStatusException; use App\Notifications\ApplicationMoved; +use App\Notifications\AppointmentCancelled; use App\Notifications\AppointmentScheduled; use Carbon\Carbon; use Illuminate\Support\Facades\Auth; @@ -25,6 +26,15 @@ class AppointmentService ]; + /** + * Schedules an appointment for the provided application. + * + * @param Application $application The target application. + * @param Carbon $appointmentDate The appointment's date and time. + * @param string $appointmentDescription The appointment description. + * @param string $appointmentLocation The appointment location. + * @return bool Whether the appointment was scheduled. + */ public function createAppointment(Application $application, Carbon $appointmentDate, $appointmentDescription, $appointmentLocation) { $appointment = Appointment::create([ @@ -46,6 +56,32 @@ class AppointmentService return true; } + + /** + * Cancels an appointment for the provided application. + * + * @param Application $application The target application. + * @param string $reason The reason for cancelling the appointment. + * @throws \Exception Thrown when there's no appointment to cancel + */ + public function deleteAppointment(Application $application, string $reason): bool + { + if (!empty($application->appointment)) + { + $application->user->notify(new AppointmentCancelled($application, Carbon::parse($application->appointment->appointmentDate), $reason)); + $application->appointment->delete(); + + $application->setStatus('STAGE_INTERVIEW'); + + Log::info('User '.Auth::user()->name.' cancelled an appointment with '.$application->user->name.' for application ID'.$application->id); + + return true; + } + + throw new \Exception("This application doesn't have an appointment!"); + + } + /** * Updates the appointment with the new $status. * It also sets the application's status to peer approval. diff --git a/resources/views/dashboard/user/viewapp.blade.php b/resources/views/dashboard/user/viewapp.blade.php index a38fabf..981a06d 100755 --- a/resources/views/dashboard/user/viewapp.blade.php +++ b/resources/views/dashboard/user/viewapp.blade.php @@ -67,12 +67,37 @@ @method('PATCH') - - + + + +
+

{{ __('Caution') }}

+ +

{{__('Are you sure you want to cancel this appointment? The user will be notified of this via email, and you will be able to reschedule.')}}

+

{{ __('Before you can cancel this appointment, you\'ll need to provide a reason in writing. ') }}

+
+ +
+ @method('DELETE') + @csrf +
+
+ +
+ +
+
+ + + + + +
+ @endhasrole
@@ -290,12 +315,13 @@ + @endcan @can('applications.vote') - + @endcan diff --git a/resources/views/vendor/mail/html/button.blade.php b/resources/views/vendor/mail/html/button.blade.php new file mode 100644 index 0000000..e74fe55 --- /dev/null +++ b/resources/views/vendor/mail/html/button.blade.php @@ -0,0 +1,19 @@ + + + + + diff --git a/resources/views/vendor/mail/html/footer.blade.php b/resources/views/vendor/mail/html/footer.blade.php new file mode 100644 index 0000000..3ff41f8 --- /dev/null +++ b/resources/views/vendor/mail/html/footer.blade.php @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/resources/views/vendor/mail/html/header.blade.php b/resources/views/vendor/mail/html/header.blade.php new file mode 100644 index 0000000..a2d0e7e --- /dev/null +++ b/resources/views/vendor/mail/html/header.blade.php @@ -0,0 +1,11 @@ + + + +@if (trim($slot) === 'Laravel') + +@else +{{ $slot }} +@endif + + + diff --git a/resources/views/vendor/mail/html/layout.blade.php b/resources/views/vendor/mail/html/layout.blade.php new file mode 100644 index 0000000..21d349b --- /dev/null +++ b/resources/views/vendor/mail/html/layout.blade.php @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/views/vendor/mail/html/message.blade.php b/resources/views/vendor/mail/html/message.blade.php new file mode 100644 index 0000000..9ab3d7c --- /dev/null +++ b/resources/views/vendor/mail/html/message.blade.php @@ -0,0 +1,27 @@ +@component('mail::layout') +{{-- Header --}} +@slot('header') +@component('mail::header', ['url' => config('app.url')]) + gamesclub official logo +@endcomponent +@endslot + +{{-- Body --}} +{{ $slot }} + +{{-- Subcopy --}} +@isset($subcopy) +@slot('subcopy') +@component('mail::subcopy') +{{ $subcopy }} +@endcomponent +@endslot +@endisset + +{{-- Footer --}} +@slot('footer') +@component('mail::footer') +© {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.') +@endcomponent +@endslot +@endcomponent diff --git a/resources/views/vendor/mail/html/panel.blade.php b/resources/views/vendor/mail/html/panel.blade.php new file mode 100644 index 0000000..2975a60 --- /dev/null +++ b/resources/views/vendor/mail/html/panel.blade.php @@ -0,0 +1,14 @@ + + + + + + diff --git a/resources/views/vendor/mail/html/subcopy.blade.php b/resources/views/vendor/mail/html/subcopy.blade.php new file mode 100644 index 0000000..790ce6c --- /dev/null +++ b/resources/views/vendor/mail/html/subcopy.blade.php @@ -0,0 +1,7 @@ + + + + + diff --git a/resources/views/vendor/mail/html/table.blade.php b/resources/views/vendor/mail/html/table.blade.php new file mode 100644 index 0000000..a5f3348 --- /dev/null +++ b/resources/views/vendor/mail/html/table.blade.php @@ -0,0 +1,3 @@ +
+{{ Illuminate\Mail\Markdown::parse($slot) }} +
diff --git a/resources/views/vendor/mail/html/themes/default.css b/resources/views/vendor/mail/html/themes/default.css new file mode 100644 index 0000000..2483b11 --- /dev/null +++ b/resources/views/vendor/mail/html/themes/default.css @@ -0,0 +1,290 @@ +/* Base */ + +body, +body *:not(html):not(style):not(br):not(tr):not(code) { + box-sizing: border-box; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, + 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + position: relative; +} + +body { + -webkit-text-size-adjust: none; + background-color: #ffffff; + color: #718096; + height: 100%; + line-height: 1.4; + margin: 0; + padding: 0; + width: 100% !important; +} + +p, +ul, +ol, +blockquote { + line-height: 1.4; + text-align: left; +} + +a { + color: #3869d4; +} + +a img { + border: none; +} + +/* Typography */ + +h1 { + color: #3d4852; + font-size: 18px; + font-weight: bold; + margin-top: 0; + text-align: left; +} + +h2 { + font-size: 16px; + font-weight: bold; + margin-top: 0; + text-align: left; +} + +h3 { + font-size: 14px; + font-weight: bold; + margin-top: 0; + text-align: left; +} + +p { + font-size: 16px; + line-height: 1.5em; + margin-top: 0; + text-align: left; +} + +p.sub { + font-size: 12px; +} + +img { + max-width: 100%; +} + +/* Layout */ + +.wrapper { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + background-color: #edf2f7; + margin: 0; + padding: 0; + width: 100%; +} + +.content { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + margin: 0; + padding: 0; + width: 100%; +} + +/* Header */ + +.header { + padding: 25px 0; + text-align: center; +} + +.header a { + color: #3d4852; + font-size: 19px; + font-weight: bold; + text-decoration: none; +} + +/* Logo */ + +.logo { + height: 75px; + max-height: 75px; + width: 75px; +} + +/* Body */ + +.body { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + background-color: #edf2f7; + border-bottom: 1px solid #edf2f7; + border-top: 1px solid #edf2f7; + margin: 0; + padding: 0; + width: 100%; +} + +.inner-body { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 570px; + background-color: #ffffff; + border-color: #e8e5ef; + border-radius: 2px; + border-width: 1px; + box-shadow: 0 2px 0 rgba(0, 0, 150, 0.025), 2px 4px 0 rgba(0, 0, 150, 0.015); + margin: 0 auto; + padding: 0; + width: 570px; +} + +/* Subcopy */ + +.subcopy { + border-top: 1px solid #e8e5ef; + margin-top: 25px; + padding-top: 25px; +} + +.subcopy p { + font-size: 14px; +} + +/* Footer */ + +.footer { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 570px; + margin: 0 auto; + padding: 0; + text-align: center; + width: 570px; +} + +.footer p { + color: #b0adc5; + font-size: 12px; + text-align: center; +} + +.footer a { + color: #b0adc5; + text-decoration: underline; +} + +/* Tables */ + +.table table { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + margin: 30px auto; + width: 100%; +} + +.table th { + border-bottom: 1px solid #edeff2; + margin: 0; + padding-bottom: 8px; +} + +.table td { + color: #74787e; + font-size: 15px; + line-height: 18px; + margin: 0; + padding: 10px 0; +} + +.content-cell { + max-width: 100vw; + padding: 32px; +} + +/* Buttons */ + +.action { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + margin: 30px auto; + padding: 0; + text-align: center; + width: 100%; +} + +.button { + -webkit-text-size-adjust: none; + border-radius: 4px; + color: #fff; + display: inline-block; + overflow: hidden; + text-decoration: none; +} + +.button-blue, +.button-primary { + background-color: #2d3748; + border-bottom: 8px solid #2d3748; + border-left: 18px solid #2d3748; + border-right: 18px solid #2d3748; + border-top: 8px solid #2d3748; +} + +.button-green, +.button-success { + background-color: #48bb78; + border-bottom: 8px solid #48bb78; + border-left: 18px solid #48bb78; + border-right: 18px solid #48bb78; + border-top: 8px solid #48bb78; +} + +.button-red, +.button-error { + background-color: #e53e3e; + border-bottom: 8px solid #e53e3e; + border-left: 18px solid #e53e3e; + border-right: 18px solid #e53e3e; + border-top: 8px solid #e53e3e; +} + +/* Panels */ + +.panel { + border-left: #2d3748 solid 4px; + margin: 21px 0; +} + +.panel-content { + background-color: #edf2f7; + color: #718096; + padding: 16px; +} + +.panel-content p { + color: #718096; +} + +.panel-item { + padding: 0; +} + +.panel-item p:last-of-type { + margin-bottom: 0; + padding-bottom: 0; +} + +/* Utilities */ + +.break-all { + word-break: break-all; +} diff --git a/resources/views/vendor/mail/text/button.blade.php b/resources/views/vendor/mail/text/button.blade.php new file mode 100644 index 0000000..97444eb --- /dev/null +++ b/resources/views/vendor/mail/text/button.blade.php @@ -0,0 +1 @@ +{{ $slot }}: {{ $url }} diff --git a/resources/views/vendor/mail/text/footer.blade.php b/resources/views/vendor/mail/text/footer.blade.php new file mode 100644 index 0000000..3338f62 --- /dev/null +++ b/resources/views/vendor/mail/text/footer.blade.php @@ -0,0 +1 @@ +{{ $slot }} diff --git a/resources/views/vendor/mail/text/header.blade.php b/resources/views/vendor/mail/text/header.blade.php new file mode 100644 index 0000000..aaa3e57 --- /dev/null +++ b/resources/views/vendor/mail/text/header.blade.php @@ -0,0 +1 @@ +[{{ $slot }}]({{ $url }}) diff --git a/resources/views/vendor/mail/text/layout.blade.php b/resources/views/vendor/mail/text/layout.blade.php new file mode 100644 index 0000000..9378baa --- /dev/null +++ b/resources/views/vendor/mail/text/layout.blade.php @@ -0,0 +1,9 @@ +{!! strip_tags($header) !!} + +{!! strip_tags($slot) !!} +@isset($subcopy) + +{!! strip_tags($subcopy) !!} +@endisset + +{!! strip_tags($footer) !!} diff --git a/resources/views/vendor/mail/text/message.blade.php b/resources/views/vendor/mail/text/message.blade.php new file mode 100644 index 0000000..1ae9ed8 --- /dev/null +++ b/resources/views/vendor/mail/text/message.blade.php @@ -0,0 +1,27 @@ +@component('mail::layout') + {{-- Header --}} + @slot('header') + @component('mail::header', ['url' => config('app.url')]) + {{ config('app.name') }} + @endcomponent + @endslot + + {{-- Body --}} + {{ $slot }} + + {{-- Subcopy --}} + @isset($subcopy) + @slot('subcopy') + @component('mail::subcopy') + {{ $subcopy }} + @endcomponent + @endslot + @endisset + + {{-- Footer --}} + @slot('footer') + @component('mail::footer') + © {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.') + @endcomponent + @endslot +@endcomponent diff --git a/resources/views/vendor/mail/text/panel.blade.php b/resources/views/vendor/mail/text/panel.blade.php new file mode 100644 index 0000000..3338f62 --- /dev/null +++ b/resources/views/vendor/mail/text/panel.blade.php @@ -0,0 +1 @@ +{{ $slot }} diff --git a/resources/views/vendor/mail/text/subcopy.blade.php b/resources/views/vendor/mail/text/subcopy.blade.php new file mode 100644 index 0000000..3338f62 --- /dev/null +++ b/resources/views/vendor/mail/text/subcopy.blade.php @@ -0,0 +1 @@ +{{ $slot }} diff --git a/resources/views/vendor/mail/text/table.blade.php b/resources/views/vendor/mail/text/table.blade.php new file mode 100644 index 0000000..3338f62 --- /dev/null +++ b/resources/views/vendor/mail/text/table.blade.php @@ -0,0 +1 @@ +{{ $slot }} diff --git a/resources/views/vendor/notifications/email.blade.php b/resources/views/vendor/notifications/email.blade.php new file mode 100644 index 0000000..bcf39f0 --- /dev/null +++ b/resources/views/vendor/notifications/email.blade.php @@ -0,0 +1,62 @@ +@component('mail::message') +{{-- Greeting --}} +@if (! empty($greeting)) +# {{ $greeting }} +@else +@if ($level === 'error') +# @lang('Whoops!') +@else +# @lang('Hello!') +@endif +@endif + +{{-- Intro Lines --}} +@foreach ($introLines as $line) +{{ $line }} + +@endforeach + +{{-- Action Button --}} +@isset($actionText) + +@component('mail::button', ['url' => $actionUrl, 'color' => $color]) +{{ $actionText }} +@endcomponent +@endisset + +{{-- Outro Lines --}} +@foreach ($outroLines as $line) +{{ $line }} + +@endforeach + +{{-- Salutation --}} +@if (! empty($salutation)) +{{ $salutation }} +@else +@lang('Regards'),
+{{ config('app.name') }} +@endif + +{{-- Subcopy --}} +@isset($actionText) +@slot('subcopy') +@lang( + "If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\n". + 'into your web browser:', + [ + 'actionText' => $actionText, + ] +) [{{ $displayableActionUrl }}]({{ $actionUrl }}) +@endslot +@endisset +@endcomponent diff --git a/routes/web.php b/routes/web.php index 1857364..61069b7 100755 --- a/routes/web.php +++ b/routes/web.php @@ -166,6 +166,9 @@ Route::group(['prefix' => LaravelLocalization::setLocale(), 'middleware' => ['lo Route::patch('update/appointments/{application}/{status}', [AppointmentController::class, 'updateAppointment']) ->name('updateAppointment'); + + Route::delete('delete/appointments/{application}', [AppointmentController::class, 'deleteAppointment']) + ->name('deleteAppointment'); }); Route::group(['prefix' => 'apply', 'middleware' => ['eligibility', 'passwordredirect']], function () { @@ -310,6 +313,7 @@ Route::group(['prefix' => LaravelLocalization::setLocale(), 'middleware' => ['lo ->name('devTools'); + Route::post('/applications/force-approval', [DevToolsController::class, 'forceApprovalEvent']); Route::post('/applications/force-approval', [DevToolsController::class, 'forceApprovalEvent']) ->name('devForceApprovalEvent');