Skip to content

Commit

Permalink
Improve user experience for table assignment and status notification (#3
Browse files Browse the repository at this point in the history
)

* prevent unnecessary updates to status timestamps

* notify admin user about sent status notifications

* auto update status on table type change in list view

* prevent calling update on accepted tables and delete in general

* prevent shares/assistants from accepting a table

* prevent cancellation once table has been accepted

* prevent modification of shares after registration period

* change styling of buttons on table confirmation

* fix issue with automatic status change

* update child applications to TableAccepted on parent accepting their offered table

* update table number for assistants with parent

* only send status notifications to Dealers

* show status information on Dasboard

* show reg status on dashboard

* rework status flow during acceptance phase dropping is_notified
add notifications for shares
improve status messages in dasboard

* allow sorting and searching applications by dealership

* disable placeholder selection to avoid error in applications list
  • Loading branch information
Fenrikur authored May 17, 2023
1 parent bdf789f commit 564a851
Show file tree
Hide file tree
Showing 27 changed files with 727 additions and 182 deletions.
1 change: 1 addition & 0 deletions app/Enums/ApplicationStatus.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ enum ApplicationStatus: string
case Canceled = 'canceled';
case Open = 'open';
case Waiting = 'waiting';
case TableAssigned = 'table_assigned';
case TableOffered = 'table_offered';
case TableAccepted = 'table_accepted';
case CheckedIn = 'checked_in';
Expand Down
13 changes: 13 additions & 0 deletions app/Enums/StatusNotificationResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace App\Enums;

enum StatusNotificationResult: string
{
case Accepted = 'accepted';
case OnHold = 'on-hold';
case WaitingList = 'waiting list';
case SharesInvalid = 'shares invalid';
case StatusNotApplicable = 'status not applicable';
case NotDealer = 'not dealer';
}
135 changes: 102 additions & 33 deletions app/Filament/Resources/ApplicationResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,25 @@

use App\Enums\ApplicationStatus;
use App\Enums\ApplicationType;
use App\Enums\StatusNotificationResult;
use App\Filament\Resources\ApplicationResource\Pages;
use App\Filament\Resources\ApplicationResource\RelationManagers;
use App\Http\Controllers\Applications\ApplicationController;
use App\Models\Application;
use App\Models\TableType;
use App\Notifications\AcceptedNotification;
use App\Notifications\OnHoldNotification;
use App\Notifications\WaitingListNotification;
use Filament\Forms;
use Filament\Notifications\Notification;
use Filament\Resources\Form;
use Filament\Resources\Resource;
use Filament\Resources\Table;
use Filament\Tables;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

class ApplicationResource extends Resource
{
Expand Down Expand Up @@ -48,7 +53,7 @@ public static function form(Form $form): Form

Forms\Components\Grid::make()->columns()->schema([
Forms\Components\Textarea::make('wanted_neighbors')
->label('Wanted')
->label('Wanted Neighbors')
->maxLength(65535),
]),
Forms\Components\Textarea::make('comment')
Expand All @@ -63,13 +68,13 @@ public static function form(Form $form): Form
"canceled" => "Canceled",
"open" => "Open",
"waiting" => "Waiting",
"table_assigned" => "Table assigned (Open)",
"table_offered" => "Table offered",
"table_accepted" => "Table accepted",
"checked_in" => "Checked in (Onsite)"
])->required()->reactive(),
"checked_in" => "Checked in (on-site)"
])->disablePlaceholderSelection()->required()->reactive(),
Forms\Components\TextInput::make('table_number')
->maxLength(255),
Forms\Components\Toggle::make('is_notified')->label('Notification sent'),
]),

Forms\Components\Fieldset::make('Relationships')->inlineLabel()->columns(1)->schema([
Expand All @@ -79,24 +84,24 @@ public static function form(Form $form): Form
Forms\Components\Select::make('user_id')->searchable()->relationship('user', 'name')
->required(),
Forms\Components\Select::make('parent')->searchable()->options(Application::getEligibleParents()->pluck('name', 'id'))
->hidden(fn(\Closure $get) => $get('type') === ApplicationType::Dealer->value)
->required(fn(\Closure $get) => $get('type') !== ApplicationType::Dealer->value),
->hidden(fn (\Closure $get) => $get('type') === ApplicationType::Dealer->value)
->required(fn (\Closure $get) => $get('type') !== ApplicationType::Dealer->value),
Forms\Components\Select::make('table_type_requested')->relationship('requestedTable', 'name')
->hidden(fn(\Closure $get) => $get('type') !== ApplicationType::Dealer->value)
->required(fn(\Closure $get) => $get('type') === ApplicationType::Dealer->value),
->hidden(fn (\Closure $get) => $get('type') !== ApplicationType::Dealer->value)
->required(fn (\Closure $get) => $get('type') === ApplicationType::Dealer->value),
Forms\Components\Select::make('table_type_assigned')->relationship('assignedTable', 'name')
->hidden(fn(\Closure $get) => $get('type') !== ApplicationType::Dealer->value)
->nullable(fn(\Closure $get) => $get('status') !== ApplicationStatus::TableOffered->value),
->hidden(fn (\Closure $get) => $get('type') !== ApplicationType::Dealer->value)
->nullable(fn (\Closure $get) => $get('status') !== ApplicationStatus::TableOffered->value),
]),

Forms\Components\Fieldset::make('Dates')->inlineLabel()->columns(1)->schema([
Forms\Components\Placeholder::make('offer_sent_at')->content(fn(?Application $record): string => $record?->offer_sent_at?->diffForHumans() ?? '-'),
Forms\Components\Placeholder::make('offer_accepted_at')->content(fn(?Application $record): string => $record?->offer_accepted_at?->diffForHumans() ?? '-'),
Forms\Components\Placeholder::make('waiting_at')->content(fn(?Application $record): string => $record?->waiting_at?->diffForHumans() ?? '-'),
Forms\Components\Placeholder::make('checked_in_at')->content(fn(?Application $record): string => $record?->checked_in_at?->diffForHumans() ?? '-'),
Forms\Components\Placeholder::make('canceled_at')->content(fn(?Application $record): string => $record?->canceled_at?->diffForHumans() ?? '-'),
Forms\Components\Placeholder::make('updated_at')->content(fn(?Application $record): string => $record?->updated_at?->diffForHumans() ?? '-'),
Forms\Components\Placeholder::make('created_at')->content(fn(?Application $record): string => $record?->created_at?->diffForHumans() ?? '-'),
Forms\Components\Placeholder::make('offer_sent_at')->content(fn (?Application $record): string => $record?->offer_sent_at?->diffForHumans() ?? '-'),
Forms\Components\Placeholder::make('offer_accepted_at')->content(fn (?Application $record): string => $record?->offer_accepted_at?->diffForHumans() ?? '-'),
Forms\Components\Placeholder::make('waiting_at')->content(fn (?Application $record): string => $record?->waiting_at?->diffForHumans() ?? '-'),
Forms\Components\Placeholder::make('checked_in_at')->content(fn (?Application $record): string => $record?->checked_in_at?->diffForHumans() ?? '-'),
Forms\Components\Placeholder::make('canceled_at')->content(fn (?Application $record): string => $record?->canceled_at?->diffForHumans() ?? '-'),
Forms\Components\Placeholder::make('updated_at')->content(fn (?Application $record): string => $record?->updated_at?->diffForHumans() ?? '-'),
Forms\Components\Placeholder::make('created_at')->content(fn (?Application $record): string => $record?->created_at?->diffForHumans() ?? '-'),
]),
]),
]);
Expand All @@ -111,17 +116,31 @@ public static function table(Table $table): Table
Tables\Columns\BadgeColumn::make('status')->enum(ApplicationStatus::cases())->formatStateUsing(function (Application $record) {
return $record->status->name;
})->colors([
'secondary',
'success' => ApplicationStatus::TableAccepted->value,
'danger' => ApplicationStatus::Canceled->value
]),
'secondary',
'success' => ApplicationStatus::TableAccepted->value,
'danger' => ApplicationStatus::Canceled->value
]),
Tables\Columns\TextColumn::make('requestedTable.name'),
Tables\Columns\TextColumn::make('assignedTable.name'),
// FIXME: It is currently not possible to select 'null' to clear the assigned table here, therefore placeholder selection has been disabled.
Tables\Columns\SelectColumn::make('table_type_assigned')->options(TableType::pluck('name', 'id')->toArray())->disablePlaceholderSelection(),
Tables\Columns\TextColumn::make('type')->formatStateUsing(function (string $state) {
return ucfirst($state);
})->sortable(),
Tables\Columns\TextColumn::make('display_name')->searchable(),
Tables\Columns\TextInputColumn::make('table_number')->sortable()->searchable(),
Tables\Columns\IconColumn::make('is_ready')->getStateUsing(function (Application $record) {
return $record->isReady();
})->boolean(),
Tables\Columns\TextColumn::make('dlrshp')->getStateUsing(function (Application $record) {
return $record->parent ?: $record->id;
})->sortable(query: function (Builder $query, string $direction): Builder {
return $query
->orderBy(DB::raw('IF(`type` = \'dealer\', `id`,`parent`)'), $direction);
})->searchable(query: function (Builder $query, string $search): Builder {
return $query
->where('id', '=', $search)
->orWhere('parent', '=', $search);
}),
Tables\Columns\TextColumn::make('display_name')->searchable(),
Tables\Columns\IconColumn::make('wanted_neighbors')->label('N Wanted')->default(false)->boolean(),
Tables\Columns\IconColumn::make('comment')->default(false)->boolean(),
Tables\Columns\IconColumn::make('is_afterdark')
Expand All @@ -136,19 +155,15 @@ public static function table(Table $table): Table
->label('Wallseat')
->sortable()
->boolean(),
Tables\Columns\IconColumn::make('is_notified')
->label('Notification sent')
->sortable()
->boolean(),
Tables\Columns\TextColumn::make('created_at')
->dateTime(),
])
->filters([
Tables\Filters\Filter::make('parent')->query(fn(Builder $query): Builder => $query->where('type', 'dealer'))->label('Only Dealerships'),
Tables\Filters\Filter::make('assignedTable')->query(fn(Builder $query): Builder => $query->whereNull('table_type_assigned'))->label('Missing assigned table'),
Tables\Filters\Filter::make('table_number')->query(fn(Builder $query): Builder => $query->whereNull('table_number'))->label('Missing table number'),
Tables\Filters\Filter::make('is_afterdark')->query(fn(Builder $query): Builder => $query->where('is_afterdark', '=', '1'))->label('Is Afterdark'),
Tables\Filters\Filter::make('table_assigned')->query(fn(Builder $query): Builder => $query->whereNotNull('offer_sent_at'))->label('Table assigned'),
Tables\Filters\Filter::make('parent')->query(fn (Builder $query): Builder => $query->where('type', 'dealer'))->label('Only Dealerships'),
Tables\Filters\Filter::make('assignedTable')->query(fn (Builder $query): Builder => $query->whereNull('table_type_assigned'))->label('Missing assigned table'),
Tables\Filters\Filter::make('table_number')->query(fn (Builder $query): Builder => $query->whereNull('table_number'))->label('Missing table number'),
Tables\Filters\Filter::make('is_afterdark')->query(fn (Builder $query): Builder => $query->where('is_afterdark', '=', '1'))->label('Is Afterdark'),
Tables\Filters\Filter::make('table_assigned')->query(fn (Builder $query): Builder => $query->whereNotNull('offer_sent_at'))->label('Table assigned'),
])
->actions([
Tables\Actions\EditAction::make(),
Expand All @@ -157,9 +172,63 @@ public static function table(Table $table): Table
Tables\Actions\DeleteBulkAction::make(),
Tables\Actions\BulkAction::make('Send status notification')
->action(function (Collection $records): void {
$resultsType = StatusNotificationResult::class;
$results = array_fill_keys(
array_map(
fn (StatusNotificationResult $r) => $r->name,
$resultsType::cases()
),
0
);
foreach ($records as $record) {
ApplicationController::sendStatusNotification($record);
$result = ApplicationController::sendStatusNotification($record);
$results[$result->name] += 1;
}

$statusCounts = '';
$totalSentCount = 0;
$frontendNotification = Notification::make();

foreach ($results as $statusName => $count) {
switch ($statusName) {
case StatusNotificationResult::Accepted->name:
$statusCounts .= "<li>{$count} notified about being accepted with requested table</li>";
$totalSentCount += $count;
break;
case StatusNotificationResult::OnHold->name:
$statusCounts .= "<li>{$count} notified about offered table differing from requested table (on-hold)</li>";
$totalSentCount += $count;
break;
case StatusNotificationResult::WaitingList->name:
$statusCounts .= "<li>{$count} notified about waiting list</li>";
$totalSentCount += $count;
break;
case StatusNotificationResult::SharesInvalid->name:
$statusCounts .= "<li>{$count} not notified because shares/assistants not assigned to same table</li>";
break;
case StatusNotificationResult::StatusNotApplicable->name:
$statusCounts .= "<li>{$count} not notified because status not applicable</li>";
break;
case StatusNotificationResult::NotDealer->name:
$statusCounts .= "<li>{$count} not directly notified because share/assistant</li>";
break;
default:
$statusCounts .= "<li>{$count} with unknown status {$statusName}</li>";
break;
}
}

if ($totalSentCount > 0) {
$frontendNotification->title("{$totalSentCount} bulk notifications sent")
->success();
} else {
$frontendNotification->title("No bulk notifications sent")
->warning();
}

$frontendNotification->body("<ul>
{$statusCounts}
</ul>")->persistent()->send();
})
->requiresConfirmation()
->icon('heroicon-o-mail'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

namespace App\Filament\Resources\ApplicationResource\Pages;

use App\Enums\StatusNotificationResult;
use App\Filament\Resources\ApplicationResource;
use App\Http\Controllers\Applications\ApplicationController;
use App\Models\Application;
use Filament\Notifications\Notification;
use Filament\Pages\Actions;
use Filament\Resources\Pages\EditRecord;

Expand All @@ -22,9 +25,57 @@ protected function getActions(): array
];
}

public function getRecord(): Application
{
return parent::getRecord();
}

public function sendStatusNotification()
{
ApplicationController::sendStatusNotification($this->getRecord());
$this->refreshFormData(['is_notified']);
$application = $this->getRecord();
$user = $application->user()->first();
$result = ApplicationController::sendStatusNotification($application);
$frontendNotification = Notification::make();

switch ($result) {
case StatusNotificationResult::Accepted:
$frontendNotification->title('Notification sent')
->body("Notified application {$application->id} of user {$user->name} about being accepted with their requested table type.")
->success();
break;
case StatusNotificationResult::OnHold:
$frontendNotification->title('Notification sent')
->body("Notified application {$application->id} of user {$user->name} about being offered a different table type than they requested (on-hold).")
->success();
break;
case StatusNotificationResult::WaitingList:
$frontendNotification->title('Notification sent')
->body("Notified application {$application->id} of user {$user->name} about being put on the waiting list.")
->success();
break;
case StatusNotificationResult::SharesInvalid->name:
$frontendNotification->title('Notification not sent')
->body("Application not notified because some uncanceled shares have not been assigned to the same table number!")
->danger();
break;
case StatusNotificationResult::StatusNotApplicable:
$frontendNotification->title('Notification not sent')
->body("No applicable notification for current status '{$application->status->value}' or type '{$application->type->value}' of application {$application->id} of user {$user->name}.")
->warning();
break;
case StatusNotificationResult::NotDealer:
$frontendNotification->title('Notification not sent')
->body("Did not notify application {$application->id} of user {$user->name} because they are of type {$application->type->value}.")
->danger();
break;
default:
$frontendNotification->title('Error')
->body("Unexpected return value from ApplicationController::sendStatusNotification! Please inform the developers: [application={$application->id},result={$result->name}]")
->danger();
break;
}

$frontendNotification->persistent()->send();
$this->refreshFormData(['status']);
}
}
2 changes: 1 addition & 1 deletion app/Filament/Resources/UserResource/Pages/EditUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@ public function bookPackage()
public function removePackage()
{
RegSysClientController::removePackage($this->getRecord()->reg_id, $this->getRecord()->application()->first()->assignedTable()->first());
$this->refreshFormData(['is_notified']);
$this->refreshFormData(['packages booked']);
}
}
Loading

0 comments on commit 564a851

Please sign in to comment.