Add member filter
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
This commit is contained in:
parent
210172db64
commit
bb9599dcf6
|
@ -4,6 +4,7 @@ namespace App\Http\Views;
|
|||
|
||||
use App\Activity;
|
||||
use App\Course\Models\Course;
|
||||
use App\Member\FilterScope;
|
||||
use App\Member\Member;
|
||||
use App\Member\MemberResource;
|
||||
use App\Payment\Status;
|
||||
|
@ -14,13 +15,14 @@ use Illuminate\Http\Request;
|
|||
|
||||
class MemberView
|
||||
{
|
||||
public function index(Request $request, array $filter): array
|
||||
public function index(Request $request): array
|
||||
{
|
||||
$activities = Activity::with('subactivities')->get();
|
||||
$filter = FilterScope::fromRequest($request->input('filter', ''));
|
||||
|
||||
return [
|
||||
'data' => MemberResource::collection(Member::search($request->search)->query(fn ($q) => $q->select('*')
|
||||
->filter($filter)
|
||||
->withFilter($filter)
|
||||
->with('payments.subscription')->with('memberships')->with('courses')->with('subscription')->with('leaderMemberships')->with('ageGroupMemberships')
|
||||
->withPendingPayment()
|
||||
->ordered()
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
namespace App\Lib;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
/**
|
||||
* @template T of Model
|
||||
*/
|
||||
abstract class Filter extends Data
|
||||
{
|
||||
public string $unsetReplacer = 'yoNee3ainge4eetiier9ogaiChoe0ahcaR3Hu1uzah8xaiv7ael7yahphai7ruG9';
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
abstract protected function locks(): array;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|string|null $request
|
||||
*/
|
||||
public static function fromRequest(array|string|null $request = null): static
|
||||
{
|
||||
$payload = is_string($request)
|
||||
? json_decode(base64_decode($request), true)
|
||||
: $request;
|
||||
|
||||
return static::fromPost($payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $post
|
||||
*/
|
||||
public static function fromPost(?array $post = null): static
|
||||
{
|
||||
return static::withoutMagicalCreationFrom($post ?: [])->parseLocks();
|
||||
}
|
||||
|
||||
public function parseLocks(): static
|
||||
{
|
||||
foreach ($this->locks() as $key => $value) {
|
||||
if ($value === $this->unsetReplacer) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->{$key} = $value;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function when(bool $when, $value)
|
||||
{
|
||||
return $when ? $value : $this->unsetReplacer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<T> $query
|
||||
*
|
||||
* @return Builder<T>
|
||||
*/
|
||||
protected function applyOwnOthers(Builder $query, bool $own, bool $others): Builder
|
||||
{
|
||||
if ($own && !$others) {
|
||||
$query->where('user_id', auth()->id());
|
||||
}
|
||||
|
||||
if (!$own && $others) {
|
||||
$query->where('user_id', '!=', auth()->id());
|
||||
}
|
||||
|
||||
if (!$own && !$others) {
|
||||
$query->where('id', -1);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
namespace App\Member;
|
||||
|
||||
use App\Letter\BillKind;
|
||||
use App\Lib\Filter;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Spatie\LaravelData\Attributes\MapInputName;
|
||||
use Spatie\LaravelData\Attributes\MapOutputName;
|
||||
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
|
||||
|
||||
/**
|
||||
* @extends Filter<Member>
|
||||
*/
|
||||
#[MapInputName(SnakeCaseMapper::class)]
|
||||
#[MapOutputName(SnakeCaseMapper::class)]
|
||||
class FilterScope extends Filter
|
||||
{
|
||||
public function __construct(
|
||||
public bool $ausstand = false,
|
||||
public ?string $billKind = null,
|
||||
public ?int $activityId = null,
|
||||
public ?int $subactivityId = null,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function locks(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<Member> $query
|
||||
*
|
||||
* @return Builder<Member>
|
||||
*/
|
||||
public function apply(Builder $query): Builder
|
||||
{
|
||||
if ($this->ausstand) {
|
||||
$query->whereAusstand();
|
||||
}
|
||||
|
||||
if ($this->billKind) {
|
||||
$query->where('bill_kind', BillKind::fromValue($this->billKind));
|
||||
}
|
||||
if ($this->subactivityId || $this->activityId) {
|
||||
$query->whereHas('memberships', function ($q) {
|
||||
$q->active();
|
||||
if ($this->subactivityId) {
|
||||
$q->where('subactivity_id', $this->subactivityId);
|
||||
}
|
||||
if ($this->activityId) {
|
||||
$q->where('activity_id', $this->activityId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
|
@ -381,27 +381,9 @@ class Member extends Model
|
|||
*
|
||||
* @return Builder<self>
|
||||
*/
|
||||
public function scopeFilter(Builder $query, array $filter): Builder
|
||||
public function scopeWithFilter(Builder $query, FilterScope $filter): Builder
|
||||
{
|
||||
if (true === data_get($filter, 'ausstand', false)) {
|
||||
$query->whereAusstand();
|
||||
}
|
||||
if (data_get($filter, 'bill_kind', false)) {
|
||||
$query->where('bill_kind', BillKind::fromValue($filter['bill_kind']));
|
||||
}
|
||||
if (data_get($filter, 'subactivity_id', false) || data_get($filter, 'activity_id', false)) {
|
||||
$query->whereHas('memberships', function ($q) use ($filter) {
|
||||
$q->active();
|
||||
if (data_get($filter, 'subactivity_id', false)) {
|
||||
$q->where('subactivity_id', $filter['subactivity_id']);
|
||||
}
|
||||
if (data_get($filter, 'activity_id', false)) {
|
||||
$q->where('activity_id', $filter['activity_id']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return $query;
|
||||
return $filter->apply($query);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -21,32 +21,17 @@ use Zoomyboy\LaravelNami\Exceptions\ConflictException;
|
|||
|
||||
class MemberController extends Controller
|
||||
{
|
||||
public array $filter = [
|
||||
'ausstand' => false,
|
||||
'bill_kind' => null,
|
||||
'activity_id' => null,
|
||||
'subactivity_id' => null,
|
||||
];
|
||||
|
||||
public function index(Request $request, GeneralSettings $settings): Response
|
||||
{
|
||||
session()->put('menu', 'member');
|
||||
session()->put('title', 'Mitglieder');
|
||||
|
||||
$query = [
|
||||
'filter' => array_merge(
|
||||
$this->filter,
|
||||
json_decode($request->query('filter', '{}'), true)
|
||||
),
|
||||
];
|
||||
|
||||
$payload = app(MemberView::class)->index($request, $query['filter']);
|
||||
$payload = app(MemberView::class)->index($request);
|
||||
$payload['toolbar'] = [
|
||||
['href' => route('member.create'), 'label' => 'Mitglied anlegen', 'color' => 'primary', 'icon' => 'plus'],
|
||||
['href' => route('allpayment.page'), 'label' => 'Rechnungen erstellen', 'color' => 'primary', 'icon' => 'invoice', 'show' => $settings->hasModule('bill')],
|
||||
['href' => route('sendpayment.create'), 'label' => 'Rechnungen versenden', 'color' => 'info', 'icon' => 'envelope', 'show' => $settings->hasModule('bill')],
|
||||
];
|
||||
$payload['query'] = $query;
|
||||
$payload['billKinds'] = BillKind::forSelect();
|
||||
|
||||
return \Inertia::render('member/VIndex', $payload);
|
||||
|
|
|
@ -105,6 +105,7 @@ class MemberResource extends JsonResource
|
|||
{
|
||||
return [
|
||||
'groups' => Group::select('name', 'id')->get(),
|
||||
'filter' => FilterScope::fromRequest(request()->input('filter', '')),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,73 +0,0 @@
|
|||
<template>
|
||||
<div class="px-6 py-2 flex border-b border-gray-600 space-x-3">
|
||||
<f-switch
|
||||
v-show="hasModule('bill')"
|
||||
id="ausstand"
|
||||
@input="reload"
|
||||
v-model="inner.ausstand"
|
||||
label="Nur Ausstände"
|
||||
size="sm"
|
||||
></f-switch>
|
||||
<f-select
|
||||
v-show="hasModule('bill')"
|
||||
name="billKinds"
|
||||
id="billKinds"
|
||||
@input="reload"
|
||||
:options="billKinds"
|
||||
v-model="inner.bill_kind"
|
||||
label="Rechnung"
|
||||
size="sm"
|
||||
></f-select>
|
||||
<f-select
|
||||
id="activity_id"
|
||||
@input="reload"
|
||||
:options="activities"
|
||||
v-model="inner.activity_id"
|
||||
label="Tätigkeit"
|
||||
size="sm"
|
||||
name="activity_id"
|
||||
></f-select>
|
||||
<f-select
|
||||
id="subactivity_id"
|
||||
@input="reload"
|
||||
:options="subactivities"
|
||||
v-model="inner.subactivity_id"
|
||||
label="Untertätigkeit"
|
||||
size="sm"
|
||||
name="subactivity_id"
|
||||
></f-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import mergesQueryString from '../../mixins/mergesQueryString.js';
|
||||
|
||||
export default {
|
||||
data: function () {
|
||||
return {
|
||||
inner: {},
|
||||
};
|
||||
},
|
||||
|
||||
mixins: [mergesQueryString],
|
||||
|
||||
props: {
|
||||
value: {},
|
||||
billKinds: {},
|
||||
activities: {},
|
||||
subactivities: {},
|
||||
},
|
||||
|
||||
methods: {
|
||||
reload() {
|
||||
this.$inertia.visit(this.qs({filter: JSON.stringify(this.inner)}), {
|
||||
preserveState: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.inner = this.value;
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -1,11 +1,36 @@
|
|||
<template>
|
||||
<div class="pb-6">
|
||||
<member-filter
|
||||
:value="query.filter"
|
||||
:activities="filterActivities"
|
||||
:subactivities="filterSubactivities"
|
||||
:bill-kinds="billKinds"
|
||||
></member-filter>
|
||||
<div class="px-6 py-2 flex border-b border-gray-600 space-x-3">
|
||||
<f-switch v-show="hasModule('bill')" id="ausstand" @input="setFilter('ausstand', $event)" :items="getFilter('ausstand')" label="Nur Ausstände" size="sm"></f-switch>
|
||||
<f-select
|
||||
v-show="hasModule('bill')"
|
||||
name="billKinds"
|
||||
id="billKinds"
|
||||
@input="setFilter('bill_kind', $event)"
|
||||
:options="billKinds"
|
||||
:value="getFilter('bill_kind')"
|
||||
label="Rechnung"
|
||||
size="sm"
|
||||
></f-select>
|
||||
<f-select
|
||||
id="activity_id"
|
||||
@input="setFilter('activity_id', $event)"
|
||||
:options="filterActivities"
|
||||
:value="getFilter('activity_id')"
|
||||
label="Tätigkeit"
|
||||
size="sm"
|
||||
name="activity_id"
|
||||
></f-select>
|
||||
<f-select
|
||||
id="subactivity_id"
|
||||
@input="setFilter('subactivity_id', $event)"
|
||||
:options="filterSubactivities"
|
||||
:value="getFilter('subactivity_id')"
|
||||
label="Untertätigkeit"
|
||||
size="sm"
|
||||
name="subactivity_id"
|
||||
></f-select>
|
||||
</div>
|
||||
|
||||
<table cellspacing="0" cellpadding="0" border="0" class="custom-table custom-table-sm hidden md:table">
|
||||
<thead>
|
||||
|
@ -20,7 +45,7 @@
|
|||
<th></th>
|
||||
</thead>
|
||||
|
||||
<tr v-for="(member, index) in data.data" :key="index">
|
||||
<tr v-for="(member, index) in inner.data" :key="index">
|
||||
<td><age-groups :member="member"></age-groups></td>
|
||||
<td v-text="member.lastname"></td>
|
||||
<td v-text="member.firstname"></td>
|
||||
|
@ -47,23 +72,11 @@
|
|||
<div class="text-xs text-gray-200" v-text="member.full_address"></div>
|
||||
<div class="flex items-center mt-1 space-x-4">
|
||||
<tags :member="member"></tags>
|
||||
<v-label
|
||||
class="text-gray-100 block"
|
||||
v-show="hasModule('bill')"
|
||||
:value="member.pending_payment"
|
||||
fallback=""
|
||||
></v-label>
|
||||
<v-label class="text-gray-100 block" v-show="hasModule('bill')" :value="member.pending_payment" fallback=""></v-label>
|
||||
</div>
|
||||
<actions
|
||||
class="mt-2"
|
||||
:member="member"
|
||||
@sidebar="openSidebar(index, $event)"
|
||||
@remove="remove(member)"
|
||||
></actions>
|
||||
<actions class="mt-2" :member="member" @sidebar="openSidebar(index, $event)" @remove="remove(member)"></actions>
|
||||
<div class="absolute right-0 top-0 h-full flex items-center mr-2">
|
||||
<i-link :href="member.links.show" v-tooltip="`Details`"
|
||||
><svg-sprite src="chevron-down" class="w-6 h-6 text-teal-100 -rotate-90"></svg-sprite
|
||||
></i-link>
|
||||
<i-link :href="member.links.show" v-tooltip="`Details`"><svg-sprite src="chevron-down" class="w-6 h-6 text-teal-100 -rotate-90"></svg-sprite></i-link>
|
||||
</div>
|
||||
</box>
|
||||
</div>
|
||||
|
@ -88,12 +101,7 @@
|
|||
:subactivities="subactivities"
|
||||
:value="data.data[single]"
|
||||
></member-memberships>
|
||||
<member-courses
|
||||
v-if="single !== null && sidebar === 'courses.index'"
|
||||
@close="closeSidebar"
|
||||
:courses="courses"
|
||||
:value="data.data[single]"
|
||||
></member-courses>
|
||||
<member-courses v-if="single !== null && sidebar === 'courses.index'" @close="closeSidebar" :courses="courses" :value="data.data[single]"></member-courses>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -102,8 +110,7 @@
|
|||
import MemberPayments from './MemberPayments.vue';
|
||||
import MemberMemberships from './MemberMemberships.vue';
|
||||
import MemberCourses from './MemberCourses.vue';
|
||||
import MemberFilter from './MemberFilter.vue';
|
||||
import mergesQueryString from '../../mixins/mergesQueryString.js';
|
||||
import indexHelpers from '../../mixins/indexHelpers.js';
|
||||
|
||||
export default {
|
||||
data: function () {
|
||||
|
@ -113,12 +120,11 @@ export default {
|
|||
};
|
||||
},
|
||||
|
||||
mixins: [mergesQueryString],
|
||||
mixins: [indexHelpers],
|
||||
|
||||
components: {
|
||||
MemberMemberships,
|
||||
MemberPayments,
|
||||
MemberFilter,
|
||||
MemberCourses,
|
||||
'age-groups': () => import(/* webpackChunkName: "member" */ './AgeGroups'),
|
||||
'tags': () => import(/* webpackChunkName: "member" */ './Tags'),
|
||||
|
@ -142,7 +148,6 @@ export default {
|
|||
},
|
||||
|
||||
props: {
|
||||
data: {},
|
||||
subscriptions: {},
|
||||
statuses: {},
|
||||
paymentDefaults: {},
|
||||
|
|
|
@ -160,12 +160,32 @@ class IndexTest extends TestCase
|
|||
$this->withoutExceptionHandling()->login()->loginNami();
|
||||
Member::factory()->defaults()->emailBillKind()->create();
|
||||
Member::factory()->defaults()->postBillKind()->create();
|
||||
Member::factory()->defaults()->postBillKind()->create();
|
||||
|
||||
$emailResponse = $this->call('GET', '/member', ['filter' => '{"bill_kind": "E-Mail"}']);
|
||||
$postResponse = $this->call('GET', '/member', ['filter' => '{"bill_kind": "Post"}']);
|
||||
$emailResponse = $this->callFilter('member.index', ['bill_kind' => 'E-Mail']);
|
||||
$postResponse = $this->callFilter('member.index', ['bill_kind' => 'Post']);
|
||||
|
||||
$this->assertCount(1, $this->inertia($emailResponse, 'data.data'));
|
||||
$this->assertCount(1, $this->inertia($postResponse, 'data.data'));
|
||||
$this->assertCount(2, $this->inertia($postResponse, 'data.data'));
|
||||
$this->assertInertiaHas('E-Mail', $emailResponse, 'data.meta.filter.bill_kind');
|
||||
}
|
||||
|
||||
public function testItFiltersForAusstand(): void
|
||||
{
|
||||
$this->withoutExceptionHandling()->login()->loginNami();
|
||||
Member::factory()
|
||||
->has(Payment::factory()->notPaid()->subscription('Free', [new Child('b', 50)]))
|
||||
->defaults()->create();
|
||||
Member::factory()->defaults()->create();
|
||||
Member::factory()->defaults()->create();
|
||||
|
||||
$defaultResponse = $this->callFilter('member.index', []);
|
||||
$ausstandResponse = $this->callFilter('member.index', ['ausstand' => true]);
|
||||
|
||||
$this->assertCount(3, $this->inertia($defaultResponse, 'data.data'));
|
||||
$this->assertCount(1, $this->inertia($ausstandResponse, 'data.data'));
|
||||
$this->assertInertiaHas(true, $ausstandResponse, 'data.meta.filter.ausstand');
|
||||
$this->assertInertiaHas(false, $defaultResponse, 'data.meta.filter.ausstand');
|
||||
}
|
||||
|
||||
public function testItLoadsGroups(): void
|
||||
|
|
Loading…
Reference in New Issue