Add invoice full text search
continuous-integration/drone/push Build is failing Details

This commit is contained in:
philipp lang 2024-12-15 16:05:38 +01:00
parent c3a0381a0a
commit 784b49c3dd
8 changed files with 111 additions and 32 deletions

View File

@ -19,9 +19,9 @@ class InvoiceIndexAction
/**
* @return LengthAwarePaginator<Invoice>
*/
public function handle(InvoiceFilterScope $filter): LengthAwarePaginator
public function handle(InvoiceFilterScope $filter)
{
return Invoice::withFilter($filter)->with('positions')->paginate(15);
return $filter->getQuery()->query(fn ($q) => $q->with('positions'));
}
public function asController(ActionRequest $request): Response
@ -32,7 +32,7 @@ class InvoiceIndexAction
$filter = InvoiceFilterScope::fromRequest($request->input('filter', ''));
return Inertia::render('invoice/Index', [
'data' => InvoiceResource::collection($this->handle($filter)),
'data' => InvoiceResource::collection($this->handle($filter)->paginate(15)),
]);
}
}

View File

@ -17,12 +17,14 @@ use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Laravel\Scout\Searchable;
use stdClass;
class Invoice extends Model
{
/** @use HasFactory<InvoiceFactory> */
use HasFactory;
use Searchable;
public $guarded = [];
@ -154,4 +156,20 @@ class Invoice extends Model
]);
}
}
/**
* Get the indexable data array for the model.
*
* @return array<string, mixed>
*/
public function toSearchableArray(): array
{
return [
'to' => implode(', ', $this->to),
'usage' => $this->usage,
'greeting' => $this->greeting,
'mail_email' => $this->mail_email,
'status' => $this->status->value,
];
}
}

View File

@ -5,7 +5,8 @@ namespace App\Invoice\Scopes;
use App\Invoice\Enums\InvoiceStatus;
use App\Invoice\Models\Invoice;
use App\Lib\Filter;
use Illuminate\Database\Eloquent\Builder;
use App\Lib\ScoutFilter;
use Laravel\Scout\Builder;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
@ -15,32 +16,32 @@ use Spatie\LaravelData\Mappers\SnakeCaseMapper;
*/
#[MapInputName(SnakeCaseMapper::class)]
#[MapOutputName(SnakeCaseMapper::class)]
class InvoiceFilterScope extends Filter
class InvoiceFilterScope extends ScoutFilter
{
/**
* @param array<int, string> $statuses
*/
public function __construct(
public ?array $statuses = null,
public ?string $search = null
) {
}
/**
* @inheritdoc
*/
public function apply(Builder $query): Builder
{
$query = $query->whereIn('status', $this->statuses);
return $query;
}
/**
* @inheritdoc
*/
public function toDefault(): self
{
$this->statuses = $this->statuses === null ? InvoiceStatus::defaultVisibleValues()->toArray() : $this->statuses;
return $this;
}
/**
* @inheritdoc
*/
public function getQuery(): Builder
{
$this->search = $this->search ?: '';
return Invoice::search($this->search, function ($engine, string $query, array $options) {
if (empty($this->statuses)) {
$filter = 'status = "asa6aeruuni4BahC7Wei6ahm1"';
} else {
$filter = collect($this->statuses)->map(fn (string $status) => "status = \"$status\"")->join(' OR ');
}
return $engine->search($query, [...$options, 'filter' => $filter]);
});
}
}

View File

@ -1,6 +1,7 @@
<?php
use App\Form\Models\Form;
use App\Invoice\Models\Invoice;
use App\Member\Member;
return [
@ -153,8 +154,17 @@ return [
'pagination' => [
'maxTotalHits' => 1000000,
]
],
Invoice::class => [
'filterableAttributes' => ['to', 'usage', 'greeting', 'mail_email', 'status', 'id'],
'searchableAttributes' => ['to', 'usage', 'greeting', 'mail_email', 'status', 'id'],
'sortableAttributes' => [],
'displayedAttributes' => ['to', 'usage', 'greeting', 'mail_email', 'status', 'id'],
'pagination' => [
'maxTotalHits' => 1000000,
]
],
],
],
];

View File

@ -0,0 +1,27 @@
<?php
use App\Invoice\Models\Invoice;
use Illuminate\Database\Migrations\Migration;
use Laravel\Scout\Console\SyncIndexSettingsCommand;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Artisan::call(SyncIndexSettingsCommand::class);
foreach (Invoice::get() as $invoice) {
$invoice->searchable();
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
//
}
};

View File

@ -74,6 +74,7 @@
</ui-popup>
<page-filter breakpoint="xl" :filterable="false">
<template #buttons>
<f-text id="search" :model-value="getFilter('search')" label="Suchen …" size="sm" @update:model-value="setFilter('search', $event)"></f-text>
<f-multipleselect
id="statuses"
:options="meta.statuses"

View File

@ -1,6 +1,6 @@
<?php
namespace Tests\Feature\Invoice;
namespace Tests\Feature\EndToEnd;
use App\Invoice\BillKind;
use App\Invoice\Enums\InvoiceStatus;
@ -9,8 +9,11 @@ use App\Invoice\Models\InvoicePosition;
use App\Member\Member;
use App\Payment\Subscription;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\EndToEndTestCase;
use Tests\Feature\Invoice\ReceiverRequestFactory;
uses(DatabaseTransactions::class);
uses(EndToEndTestCase::class);
it('testItDisplaysInvoices', function () {
$this->login()->loginNami()->withoutExceptionHandling();
@ -25,6 +28,7 @@ it('testItDisplaysInvoices', function () {
->status(InvoiceStatus::SENT)
->create(['usage' => 'Usa', 'mail_email' => 'a@b.de']);
sleep(2);
test()->get(route('invoice.index'))
->assertInertiaPath('data.data.0.to.name', 'Familie Blabla')
->assertInertiaPath('data.data.0.id', $invoice->id)
@ -78,19 +82,35 @@ it('testValuesCanBeNull', function () {
test()->login()->loginNami()->withoutExceptionHandling();
Invoice::factory()->create();
sleep(2);
test()->get(route('invoice.index'))
->assertInertiaPath('data.data.0.sent_at_human', '');
});
it('testItFiltersForInvoiceStatus', function () {
it('filters for invoice status', function (array $filter, int $count) {
test()->login()->loginNami()->withoutExceptionHandling();
Invoice::factory()->status(InvoiceStatus::NEW)->create();
Invoice::factory()->status(InvoiceStatus::SENT)->count(2)->create();
Invoice::factory()->status(InvoiceStatus::PAID)->count(3)->create();
test()->callFilter('invoice.index', [])->assertInertiaCount('data.data', 3);
test()->callFilter('invoice.index', ['statuses' => []])->assertInertiaCount('data.data', 0);
test()->callFilter('invoice.index', ['statuses' => ['Neu']])->assertInertiaCount('data.data', 1);
test()->callFilter('invoice.index', ['statuses' => ['Neu', 'Rechnung beglichen']])->assertInertiaCount('data.data', 4);
test()->callFilter('invoice.index', ['statuses' => ['Neu', 'Rechnung beglichen', 'Rechnung gestellt']])->assertInertiaCount('data.data', 6);
});
sleep(2);
test()->callFilter('invoice.index', $filter)->assertInertiaCount('data.data', $count);
})->with([
[[], 3],
[['statuses' => []], 0],
[['statuses' => ['Neu']], 1],
[['statuses' => ['Neu', 'Rechnung beglichen']], 4],
[['statuses' => ['Neu', 'Rechnung beglichen', 'Rechnung gestellt']], 6],
]);
it('searches invoice usage', function (array $filter, int $count) {
test()->login()->loginNami()->withoutExceptionHandling();
Invoice::factory()->status(InvoiceStatus::NEW)->create(['usage' => 'Kein Zweck']);
Invoice::factory()->status(InvoiceStatus::NEW)->create(['usage' => 'Mitgliedsbeitrag']);
sleep(2);
test()->callFilter('invoice.index', $filter)->assertInertiaCount('data.data', $count);
})->with([
[['search' => 'Mitgliedsbeitrag'], 1],
[['search' => 'Kein'], 1],
]);

View File

@ -3,6 +3,7 @@
namespace Tests;
use App\Form\Models\Form;
use App\Invoice\Models\Invoice;
use App\Member\Member;
use Laravel\Scout\Console\FlushCommand;
use Laravel\Scout\Console\SyncIndexSettingsCommand;
@ -26,6 +27,7 @@ abstract class EndToEndTestCase extends TestCase
config()->set('scout.driver', 'meilisearch');
Artisan::call(FlushCommand::class, ['model' => Member::class]);
Artisan::call(FlushCommand::class, ['model' => Form::class]);
Artisan::call(FlushCommand::class, ['model' => Invoice::class]);
Artisan::call(SyncIndexSettingsCommand::class);
return $this;