Add activity index filter
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
This commit is contained in:
parent
a18214b803
commit
067cbe6d9d
|
@ -2,8 +2,10 @@
|
|||
|
||||
namespace App;
|
||||
|
||||
use App\Http\Views\ActivityFilterScope;
|
||||
use App\Nami\HasNamiField;
|
||||
use Cviebrock\EloquentSluggable\Sluggable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
|
@ -30,6 +32,16 @@ class Activity extends Model
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<self> $query
|
||||
*
|
||||
* @return Builder<self>
|
||||
*/
|
||||
public function scopeWithFilter(Builder $query, ActivityFilterScope $filter): Builder
|
||||
{
|
||||
return $filter->apply($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsToMany<Subactivity>
|
||||
*/
|
||||
|
@ -37,4 +49,5 @@ class Activity extends Model
|
|||
{
|
||||
return $this->belongsToMany(Subactivity::class);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace App\Activity\Actions;
|
|||
|
||||
use App\Activity;
|
||||
use App\Activity\Resources\ActivityResource;
|
||||
use App\Http\Views\ActivityFilterScope;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
@ -14,15 +15,17 @@ class IndexAction
|
|||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(): AnonymousResourceCollection
|
||||
public function handle(ActivityFilterScope $filter): AnonymousResourceCollection
|
||||
{
|
||||
return ActivityResource::collection(Activity::local()->paginate(20));
|
||||
return ActivityResource::collection(Activity::local()->withFilter($filter)->paginate(20));
|
||||
}
|
||||
|
||||
public function asController(ActionRequest $request): Response
|
||||
{
|
||||
return Inertia::render('activity/Index', [
|
||||
'data' => $this->handle(),
|
||||
$filter = ActivityFilterScope::fromRequest($request->input('filter'));
|
||||
|
||||
return Inertia::render('activity/VIndex', [
|
||||
'data' => $this->handle($filter),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,9 @@
|
|||
namespace App\Activity\Resources;
|
||||
|
||||
use App\Activity;
|
||||
use App\Http\Views\ActivityFilterScope;
|
||||
use App\Lib\HasMeta;
|
||||
use App\Subactivity;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
|
@ -10,6 +13,8 @@ use Illuminate\Http\Resources\Json\JsonResource;
|
|||
*/
|
||||
class ActivityResource extends JsonResource
|
||||
{
|
||||
use HasMeta;
|
||||
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
|
@ -24,4 +29,15 @@ class ActivityResource extends JsonResource
|
|||
'id' => $this->id,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function meta(): array
|
||||
{
|
||||
return [
|
||||
'subactivities' => Subactivity::pluck('name', 'id'),
|
||||
'filter' => ActivityFilterScope::fromRequest(request()->input('filter')),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Views;
|
||||
|
||||
use App\Activity;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Spatie\LaravelData\Attributes\MapInputName;
|
||||
use Spatie\LaravelData\Attributes\MapOutputName;
|
||||
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
|
||||
|
||||
/**
|
||||
* @extends Filter<Activity>
|
||||
*/
|
||||
#[MapInputName(SnakeCaseMapper::class)]
|
||||
#[MapOutputName(SnakeCaseMapper::class)]
|
||||
class ActivityFilterScope extends Filter
|
||||
{
|
||||
public function __construct(
|
||||
public ?int $subactivityId = null,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function locks(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<Activity> $query
|
||||
*
|
||||
* @return Builder<Activity>
|
||||
*/
|
||||
public function apply(Builder $query): Builder
|
||||
{
|
||||
if ($this->subactivityId) {
|
||||
$query->whereHas('subactivities', fn ($query) => $query->where('id', $this->subactivityId));
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Views;
|
||||
|
||||
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;
|
||||
|
||||
public static function fromRequest(?string $request = null): static
|
||||
{
|
||||
$parameters = json_decode(base64_decode($request), true);
|
||||
|
||||
return static::from($parameters ?: [])->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,33 @@
|
|||
<?php
|
||||
|
||||
namespace App\Lib;
|
||||
|
||||
/** @mixin \Illuminate\Http\Resources\Json\JsonResource */
|
||||
trait HasMeta
|
||||
{
|
||||
/**
|
||||
* Create a new anonymous resource collection.
|
||||
*
|
||||
* @param mixed $resource
|
||||
*
|
||||
* @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection
|
||||
*/
|
||||
public static function collection($resource)
|
||||
{
|
||||
$meta = self::meta();
|
||||
|
||||
if (!count($meta)) {
|
||||
return parent::collection($resource);
|
||||
}
|
||||
|
||||
return parent::collection($resource)->additional([
|
||||
'meta' => $meta,
|
||||
]);
|
||||
}
|
||||
|
||||
public static function meta(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<svg height="512" viewBox="0 0 60 60" width="512" xmlns="http://www.w3.org/2000/svg"><circle cx="34.131" cy="13.505" transform="rotate(-45 34.131 13.513)" r="4.916"/><path d="m17.885 50.17 3.614-.772a10.307 10.307 0 0 0 7.305-6.012l.865-2.03 2.587.883a5.744 5.744 0 0 1 3.682 3.845l.968 3.289a2.811 2.811 0 0 0 2.707 2.038c.11 0 .222-.008.342-.026a2.821 2.821 0 0 0 2.398-3.528l-1.053-4a11.993 11.993 0 0 0-4.291-6.44l-3.092-2.364 1.182-6.535a8.532 8.532 0 0 0 8.933.043l.548-.325a2.351 2.351 0 0 0 .796-3.22 2.358 2.358 0 0 0-3.057-.891 3.403 3.403 0 0 1-4.154-.925l-.488-.617a6.864 6.864 0 0 0-4.376-2.526l-3.709-.549a6.871 6.871 0 0 0-3.794.514l-.6.257c-4.436 1.962-7.22 6.526-6.92 11.365.035.617.334 1.19.814 1.585.48.385 1.096.565 1.713.48a2.234 2.234 0 0 0 1.91-2.125 6.573 6.573 0 0 1 2.997-5.267l-3.288 15.725-6.244 2.449a2.951 2.951 0 0 0-1.893 2.766c0 .9.403 1.748 1.105 2.313.703.565 1.61.788 2.493.6z"/></svg>
|
After Width: | Height: | Size: 921 B |
|
@ -17,6 +17,7 @@
|
|||
>Beiträge</v-link
|
||||
>
|
||||
<v-link href="/contribution" menu="contribution" icon="contribution">Zuschüsse</v-link>
|
||||
<v-link href="/activity" menu="activity" icon="activity">Tätigkeiten</v-link>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<v-link href="/setting" menu="setting" icon="setting">Einstellungen</v-link>
|
||||
|
|
|
@ -0,0 +1,157 @@
|
|||
<template>
|
||||
<div class="pb-6">
|
||||
<member-filter
|
||||
:value="query.filter"
|
||||
:activities="filterActivities"
|
||||
:subactivities="filterSubactivities"
|
||||
:bill-kinds="billKinds"
|
||||
></member-filter>
|
||||
|
||||
<table cellspacing="0" cellpadding="0" border="0" class="custom-table custom-table-sm hidden md:table">
|
||||
<thead>
|
||||
<th></th>
|
||||
<th>Nachname</th>
|
||||
<th>Vorname</th>
|
||||
<th class="hidden 2xl:table-cell">Ort</th>
|
||||
<th>Tags</th>
|
||||
<th class="hidden xl:table-cell">Alter</th>
|
||||
<th class="hidden xl:table-cell" v-show="hasModule('bill')">Rechnung</th>
|
||||
<th v-show="hasModule('bill')">Ausstand</th>
|
||||
<th></th>
|
||||
</thead>
|
||||
|
||||
<tr v-for="(member, index) in data.data" :key="index">
|
||||
<td><age-groups :member="member"></age-groups></td>
|
||||
<td v-text="member.lastname"></td>
|
||||
<td v-text="member.firstname"></td>
|
||||
<td class="hidden 2xl:table-cell" v-text="member.full_address"></td>
|
||||
<td><tags :member="member"></tags></td>
|
||||
<td class="hidden xl:table-cell" v-text="member.age"></td>
|
||||
<td class="hidden xl:table-cell" v-show="hasModule('bill')">
|
||||
<v-label :value="member.bill_kind_name" fallback="kein"></v-label>
|
||||
</td>
|
||||
<td v-show="hasModule('bill')">
|
||||
<v-label :value="member.pending_payment" fallback="---"></v-label>
|
||||
</td>
|
||||
<td>
|
||||
<actions :member="member" @sidebar="openSidebar(index, $event)" @remove="remove(member)"></actions>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="md:hidden p-3 grid gap-3">
|
||||
<box class="relative" :heading="member.fullname" v-for="(member, index) in data.data" :key="index">
|
||||
<div slot="in-title">
|
||||
<age-groups class="ml-2" :member="member" icon-class="w-4 h-4"></age-groups>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</box>
|
||||
</div>
|
||||
|
||||
<div class="px-6">
|
||||
<v-pages class="mt-4" :value="data.meta" :only="['data']"></v-pages>
|
||||
</div>
|
||||
|
||||
<transition name="sidebar">
|
||||
<member-payments
|
||||
v-if="single !== null && sidebar === 'payment.index'"
|
||||
@close="closeSidebar"
|
||||
:subscriptions="subscriptions"
|
||||
:statuses="statuses"
|
||||
:value="data.data[single]"
|
||||
></member-payments>
|
||||
<member-memberships
|
||||
v-if="single !== null && sidebar === 'membership.index'"
|
||||
@close="closeSidebar"
|
||||
:activities="activities"
|
||||
: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>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
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';
|
||||
|
||||
export default {
|
||||
data: function () {
|
||||
return {
|
||||
sidebar: null,
|
||||
single: null,
|
||||
};
|
||||
},
|
||||
|
||||
mixins: [mergesQueryString],
|
||||
|
||||
components: {
|
||||
MemberMemberships,
|
||||
MemberPayments,
|
||||
MemberFilter,
|
||||
MemberCourses,
|
||||
'age-groups': () => import(/* webpackChunkName: "member" */ './AgeGroups'),
|
||||
'tags': () => import(/* webpackChunkName: "member" */ './Tags'),
|
||||
'actions': () => import(/* webpackChunkName: "member" */ './index/Actions'),
|
||||
},
|
||||
|
||||
methods: {
|
||||
remove(member) {
|
||||
if (window.confirm('Mitglied löschen?')) {
|
||||
this.$inertia.delete(`/member/${member.id}`);
|
||||
}
|
||||
},
|
||||
openSidebar(index, name) {
|
||||
this.single = index;
|
||||
this.sidebar = name;
|
||||
},
|
||||
closeSidebar() {
|
||||
this.single = null;
|
||||
this.sidebar = null;
|
||||
},
|
||||
},
|
||||
|
||||
props: {
|
||||
data: {},
|
||||
subscriptions: {},
|
||||
statuses: {},
|
||||
paymentDefaults: {},
|
||||
query: {},
|
||||
billKinds: {},
|
||||
activities: {},
|
||||
subactivities: {},
|
||||
filterActivities: {},
|
||||
filterSubactivities: {},
|
||||
courses: {},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -3,6 +3,7 @@
|
|||
namespace Tests\Feature\Activity;
|
||||
|
||||
use App\Activity;
|
||||
use App\Subactivity;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Tests\TestCase;
|
||||
|
||||
|
@ -13,12 +14,34 @@ class IndexTest extends TestCase
|
|||
public function testItDisplaysLocalActivities(): void
|
||||
{
|
||||
$this->login()->loginNami()->withoutExceptionHandling();
|
||||
$local = Activity::factory()->name('Local')->create();
|
||||
$remote = Activity::factory()->name('Remote')->inNami(123)->create();
|
||||
Activity::factory()->name('Local')->create();
|
||||
Activity::factory()->name('Remote')->inNami(123)->create();
|
||||
|
||||
$response = $this->get('/activity');
|
||||
|
||||
$this->assertInertiaHas('Local', $response, 'data.data.0.name');
|
||||
$this->assertCount(1, $this->inertia($response, 'data.data'));
|
||||
}
|
||||
|
||||
public function testItDisplaysDefaultFilter(): void
|
||||
{
|
||||
$this->login()->loginNami()->withoutExceptionHandling();
|
||||
|
||||
$response = $this->callFilter('activity.index', []);
|
||||
|
||||
$this->assertInertiaHas(null, $response, 'data.meta.filter.subactivity');
|
||||
}
|
||||
|
||||
public function testItFiltersActivityBySubactivity(): void
|
||||
{
|
||||
$this->login()->loginNami()->withoutExceptionHandling();
|
||||
$subactivity = Subactivity::factory()->name('jjon')->create();
|
||||
Activity::factory()->name('Local')->hasAttached($subactivity)->create();
|
||||
Activity::factory()->count(2)->name('Local')->create();
|
||||
|
||||
$response = $this->callFilter('activity.index', ['subactivity_id' => $subactivity->id]);
|
||||
|
||||
$this->assertInertiaHas($subactivity->id, $response, 'data.meta.filter.subactivity_id');
|
||||
$this->assertCount(1, $this->inertia($response, 'data.data'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Lib;
|
||||
|
||||
use Illuminate\Testing\TestResponse;
|
||||
|
||||
trait MakesHttpCalls
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $filter
|
||||
*/
|
||||
public function callFilter(string $routeName, array $filter): TestResponse
|
||||
{
|
||||
return $this->call('GET', $this->filterUrl($routeName, $filter));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $filter
|
||||
*/
|
||||
public function filterUrl(string $routeName, array $filter): string
|
||||
{
|
||||
$params = [
|
||||
'filter' => base64_encode(json_encode($filter)),
|
||||
];
|
||||
|
||||
return route($routeName, $params);
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ use Illuminate\Http\RedirectResponse;
|
|||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Testing\TestResponse;
|
||||
use Phake;
|
||||
use Tests\Lib\MakesHttpCalls;
|
||||
use Tests\Lib\TestsInertia;
|
||||
use Zoomyboy\LaravelNami\Authentication\Auth;
|
||||
|
||||
|
@ -18,6 +19,7 @@ abstract class TestCase extends BaseTestCase
|
|||
{
|
||||
use CreatesApplication;
|
||||
use TestsInertia;
|
||||
use MakesHttpCalls;
|
||||
|
||||
protected User $me;
|
||||
|
||||
|
|
Loading…
Reference in New Issue