Add membership filter
continuous-integration/drone/push Build is failing Details

This commit is contained in:
philipp lang 2024-01-29 00:13:03 +01:00
parent cc8428e6b8
commit 9985ed9e44
5 changed files with 100 additions and 10 deletions

View File

@ -23,10 +23,12 @@ class FilterScope extends Filter
* @param array<int, int> $groupIds
* @param array<int, int> $include
* @param array<int, int> $exclude
* @param array<int, array{group_ids: array<int, int>, subactivity_ids: array<int, int>, activity_ids: array<int, int>}> $memberships
*/
public function __construct(
public bool $ausstand = false,
public ?string $billKind = null,
public array $memberships = [],
public array $activityIds = [],
public array $subactivityIds = [],
public ?string $search = '',
@ -73,13 +75,16 @@ class FilterScope extends Filter
$filter->push($this->inExpression('memberships.subactivity_id', $this->subactivityIds));
}
if ($this->subactivityIds && $this->activityIds) {
$combinations = collect($this->activityIds)
->map(fn ($activityId) => collect($this->subactivityIds)->map(fn ($subactivityId) => $activityId . '|' . $subactivityId))
->flatten()
$combinations = $this->combinations($this->activityIds, $this->subactivityIds)
->map(fn ($combination) => implode('|', $combination))
->map(fn ($combination) => str($combination)->wrap('"'));
$filter->push($this->inExpression('memberships.both', $combinations));
}
foreach ($this->memberships as $membership) {
$filter->push($this->inExpression('memberships.with_group', $this->possibleValuesForMembership($membership)->map(fn ($value) => str($value)->wrap('"'))));
}
if (count($this->exclude)) {
$filter->push($this->notInExpression('id', $this->exclude));
}
@ -127,4 +132,33 @@ class FilterScope extends Filter
return "$key NOT IN [{$valueString}]";
}
protected function possibleValuesForMembership(array $membership): Collection
{
return $this->combinations($membership['group_ids'], $membership['activity_ids'], $membership['subactivity_ids'])
->map(fn ($combination) => collect($combination)->implode('|'));
}
/**
* @return Collection<int, mixed>
*/
protected function combinations(...$parts): Collection
{
$firstPart = array_shift($parts);
$otherParts = $parts;
if (!count($otherParts)) {
return collect(array_map(fn ($p) => [$p], $firstPart));
}
/** @var Collection<int, mixed> */
$results = collect([]);
foreach ($firstPart as $firstPartSegment) {
foreach ($this->combinations(...$otherParts) as $combination) {
$results->push([$firstPartSegment, ...$combination]);
}
}
return $results;
}
}

View File

@ -502,7 +502,7 @@ class Member extends Model implements Geolocatable
'bill_kind' => $this->bill_kind?->value,
'group_id' => $this->group->id,
'memberships' => $this->memberships()->active()->get()
->map(fn ($membership) => [...$membership->only('activity_id', 'subactivity_id'), 'both' => $membership->activity_id . '|' . $membership->subactivity_id]),
->map(fn ($membership) => [...$membership->only('activity_id', 'subactivity_id'), 'both' => $membership->activity_id . '|' . $membership->subactivity_id, 'with_group' => $membership->group_id . '|' . $membership->activity_id . '|' . $membership->subactivity_id]),
];
}
}

View File

@ -157,6 +157,11 @@ class MemberResource extends JsonResource
'index' => route('member.index'),
'create' => route('member.create'),
],
'default_membership_filter' => [
'group_ids' => [],
'activity_ids' => [],
'subactivity_ids' => []
],
];
}

View File

@ -23,6 +23,28 @@
</div>
</div>
</ui-popup>
<ui-popup v-if="membershipFilters !== null" heading="Nach Mitgliedschaften filtern" full
@close="membershipFilters = null">
<button class="btn btn-primary label mt-2"
@click.prevent="membershipFilters.push({ ...meta.default_membership_filter })">
<ui-sprite class="w-3 h-3 xl:mr-2" src="plus"></ui-sprite>
<span class="hidden xl:inline">Hinzufügen</span>
</button>
<div v-for="(filter, index) in membershipFilters" :key="index" class="flex space-x-2 mt-2">
<f-multipleselect id="group_id" v-model="filter.group_ids" :options="meta.groups" label="Gruppierung"
size="sm" name="group_id"></f-multipleselect>
<f-multipleselect id="activity_ids" v-model="filter.activity_ids" :options="meta.filterActivities"
label="Tätigkeiten" size="sm" name="activity_ids"></f-multipleselect>
<f-multipleselect id="subactivity_ids" v-model="filter.subactivity_ids" :options="meta.filterSubactivities"
label="Untertätigkeiten" size="sm" name="subactivity_ids"></f-multipleselect>
</div>
<button class="btn btn-primary label mt-3" @click.prevent="
setFilter('memberships', membershipFilters);
membershipFilters = null;
">
<span class="hidden xl:inline">Anwenden</span>
</button>
</ui-popup>
<page-filter breakpoint="xl">
<f-text id="search" :model-value="getFilter('search')" name="search" label="Suchen …" size="sm"
@update:model-value="setFilter('search', $event)"></f-text>
@ -34,12 +56,10 @@
<f-select v-show="hasModule('bill')" id="billKinds" name="billKinds" :options="meta.billKinds"
:model-value="getFilter('bill_kind')" label="Rechnung" size="sm"
@update:model-value="setFilter('bill_kind', $event)"></f-select>
<f-multipleselect id="activity_ids" :options="meta.filterActivities" :model-value="getFilter('activity_ids')"
label="Tätigkeiten" size="sm" name="activity_ids"
@update:model-value="setFilter('activity_ids', $event)"></f-multipleselect>
<f-multipleselect id="subactivity_ids" :options="meta.filterSubactivities"
:model-value="getFilter('subactivity_ids')" label="Untertätigkeiten" size="sm" name="subactivity_ids"
@update:model-value="setFilter('subactivity_ids', $event)"></f-multipleselect>
<button class="btn btn-primary label mr-2" @click.prevent="membershipFilters = getFilter('memberships')">
<ui-sprite class="w-3 h-3 xl:mr-2" src="filter"></ui-sprite>
<span class="hidden xl:inline">Mitgliedschaften</span>
</button>
<button class="btn btn-primary label mr-2" @click.prevent="exportMembers">
<ui-sprite class="w-3 h-3 xl:mr-2" src="save"></ui-sprite>
<span class="hidden xl:inline">Exportieren</span>
@ -126,6 +146,7 @@ import { ref, defineProps } from 'vue';
const single = ref(null);
const deleting = ref(null);
const membershipFilters = ref(null);
const props = defineProps(indexProps);
var { router, data, meta, getFilter, setFilter, filterString, reloadPage } = useIndex(props.data, 'member');

View File

@ -169,6 +169,36 @@ class MemberIndexTest extends EndToEndTestCase
->assertInertiaCount('data.data', 0);
}
public function testItFiltersForMemberships(): void
{
$this->withoutExceptionHandling()->login()->loginNami();
$mitglied = Activity::factory()->create();
$woelfling = Subactivity::factory()->create();
$juffi = Subactivity::factory()->create();
$group = Group::factory()->create();
Member::factory()->defaults()->has(Membership::factory()->for($mitglied)->for($woelfling)->for($group))->create();
Member::factory()->defaults()->has(Membership::factory()->for($mitglied)->for($juffi)->for($group))->create();
Member::factory()->defaults()
->has(Membership::factory()->for($mitglied)->for($woelfling)->for($group))
->has(Membership::factory()->for($mitglied)->for($juffi)->for($group))
->create();
sleep(1);
$this->callFilter('member.index', ['memberships' => [
['group_ids' => [$group->id], 'activity_ids' => [$mitglied->id], 'subactivity_ids' => [$woelfling->id]]
]])->assertInertiaCount('data.data', 2);
$this->callFilter('member.index', ['memberships' => [
['group_ids' => [$group->id], 'activity_ids' => [$mitglied->id], 'subactivity_ids' => [$juffi->id]]
]])->assertInertiaCount('data.data', 2);
$this->callFilter('member.index', ['memberships' => [
['group_ids' => [$group->id], 'activity_ids' => [$mitglied->id], 'subactivity_ids' => [$juffi->id, $woelfling->id]],
]])->assertInertiaCount('data.data', 3);
$this->callFilter('member.index', ['memberships' => [
['group_ids' => [$group->id], 'activity_ids' => [$mitglied->id], 'subactivity_ids' => [$woelfling->id]],
['group_ids' => [$group->id], 'activity_ids' => [$mitglied->id], 'subactivity_ids' => [$juffi->id]],
]])->assertInertiaCount('data.data', 1);
}
public function testItFiltersForSearchButNotForPayments(): void
{
$this->withoutExceptionHandling()->login()->loginNami();