Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(frontdesk): allow searching by name or keyword/category #88

Merged
merged 3 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 74 additions & 7 deletions app/Http/Controllers/FrontdeskController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
use App\Http\Requests\CheckInRequest;
use App\Http\Requests\CheckOutRequest;
use App\Http\Requests\CommentRequest;
use App\Models\Category;
use App\Models\Comment;
use App\Models\Keyword;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
Expand All @@ -23,16 +25,79 @@ public function __invoke(Request $request)
$this->authorize('view-any', Application::class);

$search = $request->get('search');
$type = $request->get('type') ?? 'default';

if (empty($search)) {
$categories = null;
if ($type === 'keyword') {
$categories = Category::orderBy('name', 'asc')->get();
}
return view('frontdesk', [
'user' => \Auth::user(),
'search' => null,
'type' => $type,
'applications' => null,
'application' => null,
'applicant' => null
'applicant' => null,
'categories' => $categories,
]);
}

$searchResult = [];
switch ($type) {
case 'name':
$searchResult = $this->nameSearch($search);
break;
case 'keyword':
$searchResult = $this->keywordSearch($search);
break;
case 'default':
default:
$searchResult = $this->defaultSearch($search);
}

return view('frontdesk', array_merge([
'user' => \Auth::user(),
'search' => $search,
'type' => $type,
'showAdditional' => $request->has('show_additional'),
'applications' => null,
'application' => null,
'applicant' => null,
], $searchResult));
}

private function keywordSearch($search)
{
$keywordId = '-1';
$searchString = $search;
if (str_starts_with($search, 'k::')) {
$keywordId = intval(substr($search, 3));
$searchString = Keyword::where('id', $keywordId)?->first()?->name ?? $search;
}
$categoryId = '-1';
if (str_starts_with($search, 'c::')) {
$categoryId = intval(substr($search, 3));
$searchString = Category::where('id', $categoryId)?->first()?->name ?? $search;
}
$applications = Application::whereHas('profile.keywords', function ($query) use ($search, $keywordId) {
$query->where('name', 'like', "%$search%")->orWhere('id', '=', $keywordId);
})->orWhereHas('profile.keywords.category', function ($query) use ($search, $categoryId) {
$query->where('name', 'like', "%$search%")->orWhere('id', '=', $categoryId);
})->get();
return ['applications' => $applications, 'search' => $searchString];
}

private function nameSearch($search)
{
$applications = Application::where('display_name', 'like', "%$search%")->orWhereHas('user', function ($query) use ($search) {
$query->where('name', 'like', "%$search%");
})->get();
return ['applications' => $applications];
}

private function defaultSearch($search)
{
// 1. search by user user_id
// 2. search by user name
$user = User::where('reg_id', $search)->orWhere('name', 'like', $search)->first();
Expand All @@ -41,7 +106,12 @@ public function __invoke(Request $request)
// 2. search by application table_number
// 4. search by dealership display_name
if ($application === null) {
$application = Application::where('table_number', strtoupper($search))->orWhere('display_name', 'like', $search)->first();
$tableNumberVariation1 = preg_replace('/^([a-zA-Z]{1,2}[0-9])([0-9]+)$/', '\1/\2', $search);
$tableNumberVariation2 = preg_replace('/^([a-zA-Z]{1,2})([0-9]+)$/', '\1/\2', $search);
$application = Application::where('table_number', strtoupper($search))
->orWhere('table_number', strtoupper($tableNumberVariation1))
->orWhere('table_number', strtoupper($tableNumberVariation2))
->orWhere('display_name', 'like', $search)->first();
$user = $application ? $application->user : null;
}

Expand All @@ -65,9 +135,7 @@ public function __invoke(Request $request)

$profile = $application ? $application->profile : null;

return view('frontdesk', [
'user' => \Auth::user(),
'search' => $search,
return [
'application' => $application,
'applicant' => $user,
'table' => $table,
Expand All @@ -76,8 +144,7 @@ public function __invoke(Request $request)
'shares' => $shares,
'assistants' => $assistants,
'profile' => $profile,
'showAdditional' => $request->has('show_additional'),
]);
];
}

public function comment(CommentRequest $request)
Expand Down
31 changes: 31 additions & 0 deletions resources/views/components/frontdesk/keywords.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
@props(['categories'])
<div class="accordion" id="keywords">
@foreach ($categories as $category)
<div class="accordion-item">
<h2 class="accordion-header d-flex align-items-center">
<a class="flex-shrink-1 btn btn-outline-primary fs-3" href="?type=keyword&search=c::{{ $category->id }}">
🔎
</a>
<button class="accordion-button collapsed w-100 fs-3" type="button" data-bs-toggle="collapse"
data-bs-target="#category-{{ $loop->index }}" aria-expanded="false"
aria-controls="category-{{ $loop->index }}">
{{ $category->name }}
</button>
</h2>
<div id="category-{{ $loop->index }}" class="accordion-collapse collapse">
<div class="accordion-body">
<div class="list-group">
@foreach ($category->keywords()->get() as $keyword)
<a class="list-group-item" href="?type=keyword&search=k::{{ $keyword->id }}">
🔎 {{ $keyword->name }}
</a>
@endforeach
</div>
<div class="form-text">
{{ $category->description }}
</div>
</div>
</div>
</div>
@endforeach
</div>
95 changes: 83 additions & 12 deletions resources/views/frontdesk.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,27 @@ class="btn btn-secondary">Dealer Profiles ⎋</a>
<!-- Numpad & Search Column -->
<div class="col-md-3 mh-100">
<form method="get" action="{{ route('frontdesk') }}" name="search" autocomplete="off">
<div class="container" style="height: 8% !important">
<div class="row row-cols-3 h-100">
<button type="button"
onclick="document.forms.search.elements.type.value = 'default'; document.forms.search.submit();"
class="btn h-100 border align-self-center @if ($type === 'default') btn-primary @else btn-secondary @endif"
tabindex="-1">Def</button>
<button type="button"
onclick="document.forms.search.elements.type.value = 'name'; document.forms.search.submit();"
class="btn h-100 border align-self-center @if ($type === 'name') btn-primary @else btn-secondary @endif"
tabindex="-1">Name</button>
<button type="button"
onclick="document.forms.search.elements.type.value = 'keyword'; document.forms.search.submit();"
class="btn h-100 border align-self-center @if ($type === 'keyword') btn-primary @else btn-secondary @endif"
tabindex="-1">Key</button>
</div>
</div>
<input type="text" class="form-control my-2 text-center fs-1" id="search" name="search"
tabindex="1" autofocus>
<input type="hidden" name="type" value="{{ app('request')->input('type') ?? 'default' }}">
</form>
<div class="container text-center h-75">
<div class="container text-center" style="height: 68% !important">
<div class="row row-cols-3 h-25">
<button type="button"
onclick="document.forms.search.elements.search.value = document.forms.search.elements.search.value + this.innerHTML;"
Expand Down Expand Up @@ -140,7 +157,7 @@ class="btn btn-primary align-self-center h-100 border fs-1" tabindex="-1">3</but
</div>
<div class="row row-cols-3 h-25">
<button type="button" class="btn btn-danger align-self-center h-100 border fs-1"
onclick="document.forms.search.reset();document.forms.search.submit()"
onclick="document.forms.search.reset();document.forms.search.elements.type.value='default';document.forms.search.submit()"
tabindex="-1">✗</button>
<button type="button"
onclick="document.forms.search.elements.search.value = document.forms.search.elements.search.value + this.innerHTML;"
Expand All @@ -153,34 +170,45 @@ class="btn btn-success align-self-center h-100 border fs-1" tabindex="-1">↵</b

<!-- Application Column -->
<div class="col-md-6 fs-3 mh-100 overflow-auto">
@if (empty($search))
@if (empty($search) && $type !== 'keyword')
<div class="card my-2">
<div class="card-header text-center fs-3">
Welcome to the Dealers' Den Frontdesk!
</div>
<div class="card-body fs-4">
You can search Dealers, Shares or Assistants by:
<div class="fs-3"><strong>Def</strong>ault Mode</div>
Search Dealers, Shares or Assistants by:
<ul>
<li><strong>registration ID</strong> (exact match; no checksum!),</li>
<li><strong>attendee nickname</strong> (supports <code>%</code> as wildcard; not
case-sensitive),</li>
<li><strong>table number</strong> (exact match) or</li>
<li><strong>table number</strong> (exact match; slash (/) can be omitted) or</li>
<li><strong>display name</strong> (supports <code>%</code> as wildcard; not
case-sensitive).</li>
</ul>
<div class="fs-3"><strong>Name</strong> Mode</div>
Search for any part of a name or display name.
<div class="fs-3"><strong>Key</strong>word & Category Mode</div>
Search for part of a keyword/category name or select one from the list if
you provide no search text.
</div>
<div class="card-footer fs-5">
The first matching result will be loaded automatically. If it's not who you were looking
for, please try making your search more specific.
<strong>Def</strong>ault mode will always load the first matching result. If it's not
who you
were looking for, please try making your search more specific or use one of the other
search modes.
</div>
</div>
@elseif ($applicant === null)
@elseif(empty($search) && $type === 'keyword')
<x-frontdesk.keywords :categories="$categories"></x-frontdesk.keywords>
@elseif ($applicant === null && ($applications === null || count($applications) === 0))
<div class="card text-bg-danger text-center my-2">
<div class="card-body">
No dealer, share or assistant found for search query <em>"{{ $search }}"</em>.
No dealers, shares or assistants found for search query <em>"{{ $search }}"</em>
in mode <em>{{ $type }}</em>.
</div>
</div>
@else
@elseif ($applicant !== null)
@if ($application->status === \App\Enums\ApplicationStatus::TableAccepted)
<div class="card text-bg-success text-center my-2">
<div class="card-body">
Expand Down Expand Up @@ -264,7 +292,7 @@ class="btn btn-success align-self-center h-100 border fs-1" tabindex="-1">↵</b
<div class="mb-3">
<label for="table" class="form-label">Table</label>
<input type="text" readonly class="form-control fs-4" id="table"
value="{{ $application->table_number }}{{ $table ? ' – ' . $table->name : '' }}"
value="{{ $application->parent?->table_number ?? $application->table_number }}{{ $table ? ' – ' . $table->name : '' }}"
tabindex="-1">
</div>

Expand Down Expand Up @@ -354,7 +382,8 @@ class="btn btn-success align-self-center h-100 border fs-1" tabindex="-1">↵</b
'check-button',
'btn-primary' => $application->is_afterdark,
'btn-secondary' => !$application->is_afterdark,
])>After Dark</button>
])>After
Dark</button>
<button type="button" @class([
'btn',
'fs-4',
Expand Down Expand Up @@ -547,6 +576,48 @@ class="form-check-input @error('power_strip', 'check-out') is-invalid @enderror"
</div>
@endif
</div>
@else
<div class="card my-2">
<div class="card-header text-center fs-3">
Search results for "{{ $search }}"
</div>
<div class="card-body fs-4">
<div class="list-group">
@foreach ($applications as $applicationResult)
<a class="list-group-item list-group-item-action"
href="?type=default&search={{ $applicationResult->user->reg_id }}">
{{ $applicationResult->user->name }}
({{ $applicationResult->user->reg_id }})
[{{ $applicationResult->parent?->table_number ?? $applicationResult->table_number }}]
@switch($applicationResult->type)
@case(\App\Enums\ApplicationType::Dealer)
<span
class="badge text-bg-success mx-2">{{ $applicationResult->type->name }}</span>
@break

@case(\App\Enums\ApplicationType::Share)
<span
class="badge text-bg-warning mx-2">{{ $applicationResult->type->name }}</span>
@break

@case(\App\Enums\ApplicationType::Assistant)
<span
class="badge text-bg-info mx-2">{{ $applicationResult->type->name }}</span>
@break

@default
<span class="badge text-bg-danger mx-2">Unknown Type!</span>
@endswitch
@if ($applicationResult->is_afterdark)
<span class="badge text-bg-dark">ADD</span>
@endif
<span
class="badge text-bg-secondary">{{ $applicationResult->status->name }}</span>
</a>
@endforeach
</div>
</div>
</div>
@endif
</div>

Expand Down
Loading