Add membership status to member view
continuous-integration/drone/push Build is failing Details

This commit is contained in:
Philipp Lang 2023-09-05 16:29:22 +02:00
parent 59c2c527fb
commit 0de90be8c3
10 changed files with 117 additions and 58 deletions

View File

@ -4,26 +4,26 @@ namespace App\Member;
use App\Country;
use App\Http\Controllers\Controller;
use App\Setting\GeneralSettings;
use App\Setting\NamiSettings;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Response;
use Zoomyboy\LaravelNami\Exceptions\ConflictException;
use Inertia;
class MemberController extends Controller
{
public function index(Request $request, GeneralSettings $settings): Response
public function index(Request $request): Response
{
session()->put('menu', 'member');
session()->put('title', 'Mitglieder');
$filter = FilterScope::fromRequest($request->input('filter', ''));
return \Inertia::render('member/VIndex', [
return Inertia::render('member/VIndex', [
'data' => MemberResource::collection(Member::search($filter->search)->query(
fn ($q) => $q->select('*')
->withFilter($filter)
->with('payments.subscription')->with(['memberships' => fn ($query) => $query->active()])->with('courses')->with('subscription')->with('leaderMemberships')->with('ageGroupMemberships')
->with(['payments.subscription', 'memberships', 'courses', 'subscription', 'leaderMemberships', 'ageGroupMemberships'])
->withPendingPayment()
->ordered()
)->paginate(15)),

View File

@ -61,6 +61,11 @@ class Membership extends Model
return $this->belongsTo(Member::class);
}
public function isActive(): bool
{
return $this->from->isBefore(now()) && (null === $this->to || $this->to->isAfter(now()));
}
/**
* @param Builder<Membership> $query
*
@ -68,7 +73,8 @@ class Membership extends Model
*/
public function scopeActive(Builder $query): Builder
{
return $query->whereNull('to');
return $query->where('from', '<=', now())
->where(fn ($query) => $query->whereNull('to')->orWhere('to', '>=', now()));
}
/**
@ -106,7 +112,7 @@ class Membership extends Model
*
* @return Builder<Membership>
*/
public function scopeTrying(Builder $query): Builder
public function scopeIsTrying(Builder $query): Builder
{
return $query->active()->whereHas('activity', fn ($builder) => $builder->where('is_try', true));
}

View File

@ -27,6 +27,7 @@ class MembershipResource extends JsonResource
'subactivity_name' => $this->subactivity?->name,
'human_date' => $this->from->format('d.m.Y'),
'promised_at' => $this->promised_at?->format('Y-m-d'),
'is_active' => $this->isActive(),
];
}
}

View File

@ -13,11 +13,8 @@ class TestersBlock extends Block
*/
public function query(): Builder
{
return Member::whereHas('memberships', fn ($q) => $q
->where('created_at', '<=', now()->subWeeks(7))
->trying()
)
->with(['memberships' => fn ($query) => $query->trying()]);
return Member::whereHas('memberships', fn ($q) => $q->isTrying())
->with('memberships', fn ($q) => $q->isTrying());
}
/**
@ -28,8 +25,8 @@ class TestersBlock extends Block
return [
'members' => $this->query()->get()->map(fn ($member) => [
'name' => $member->fullname,
'try_ends_at' => $member->memberships->first()->created_at->addWeeks(8)->format('d.m.Y'),
'try_ends_at_human' => $member->memberships->first()->created_at->addWeeks(8)->diffForHumans(),
'try_ends_at' => $member->memberships->first()->from->addWeeks(8)->format('d.m.Y'),
'try_ends_at_human' => $member->memberships->first()->from->addWeeks(8)->diffForHumans(),
])->toArray(),
];
}

View File

@ -1,7 +1,10 @@
<template>
<div v-tooltip="longLabel" class="flex space-x-2 items-center">
<div class="border-2 rounded-full w-5 h-5 flex items-center justify-center" :class="value ? 'border-green-700' : 'border-red-700'">
<ui-sprite :src="value ? 'check' : 'close'" :class="value ? 'text-green-800' : 'text-red-800'" class="w-3 h-3 flex-none"></ui-sprite>
<div class="border-2 rounded-full w-5 h-5 flex items-center justify-center"
:class="value ? (dark ? 'border-green-500' : 'border-green-700') : dark ? 'border-red-500' : 'border-red-700'">
<ui-sprite :src="value ? 'check' : 'close'"
:class="value ? (dark ? 'text-green-600' : 'text-green-800') : dark ? 'text-red-600' : 'text-red-800'"
class="w-3 h-3 flex-none"></ui-sprite>
</div>
<div class="text-gray-400 text-xs" v-text="label"></div>
</div>
@ -23,6 +26,10 @@ export default {
return null;
},
},
dark: {
type: Boolean,
default: () => false,
},
},
};
</script>

View File

@ -1,35 +1,35 @@
<template>
<div class="sidebar flex flex-col group is-bright">
<page-header @close="$emit('close')" title="Mitgliedschaften">
<page-header title="Mitgliedschaften" @close="$emit('close')">
<template #toolbar>
<page-toolbar-button @click.prevent="create" color="primary" icon="plus" v-if="single === null">Neue Mitgliedschaft</page-toolbar-button>
<page-toolbar-button @click.prevent="cancel" color="primary" icon="undo" v-if="single !== null">Zurück</page-toolbar-button>
<page-toolbar-button v-if="single === null" color="primary" icon="plus" @click.prevent="create">Neue
Mitgliedschaft</page-toolbar-button>
<page-toolbar-button v-if="single !== null" color="primary" icon="undo"
@click.prevent="cancel">Zurück</page-toolbar-button>
</template>
</page-header>
<form v-if="single" class="p-6 grid gap-4 justify-start" @submit.prevent="submit">
<f-select id="group_id" name="group_id" :options="groups" v-model="single.group_id" label="Gruppierung" required></f-select>
<f-select id="activity_id" name="activity_id" :options="activities" v-model="single.activity_id" label="Tätigkeit" required></f-select>
<f-select
v-if="single.activity_id"
name="subactivity_id"
:options="subactivities[single.activity_id]"
id="subactivity_id"
v-model="single.subactivity_id"
label="Untertätigkeit"
size="sm"
></f-select>
<f-switch id="has_promise" :modelValue="single.promised_at !== null" @update:modelValue="single.promised_at = $event ? '2000-02-02' : null" label="Hat Versprechen"></f-switch>
<f-text v-show="single.promised_at !== null" type="date" id="promised_at" v-model="single.promised_at" label="Versprechensdatum" size="sm"></f-text>
<f-select id="group_id" v-model="single.group_id" name="group_id" :options="groups" label="Gruppierung"
required></f-select>
<f-select id="activity_id" v-model="single.activity_id" name="activity_id" :options="activities"
label="Tätigkeit" required></f-select>
<f-select v-if="single.activity_id" id="subactivity_id" v-model="single.subactivity_id" name="subactivity_id"
:options="subactivities[single.activity_id]" label="Untertätigkeit" size="sm"></f-select>
<f-switch id="has_promise" :model-value="single.promised_at !== null" label="Hat Versprechen"
@update:modelValue="single.promised_at = $event ? '2000-02-02' : null"></f-switch>
<f-text v-show="single.promised_at !== null" id="promised_at" v-model="single.promised_at" type="date"
label="Versprechensdatum" size="sm"></f-text>
<button type="submit" class="btn btn-primary">Absenden</button>
</form>
<div class="grow" v-else>
<div v-else class="grow">
<table class="custom-table custom-table-light custom-table-sm text-sm">
<thead>
<th>Tätigkeit</th>
<th>Untertätigkeit</th>
<th>Datum</th>
<th>Aktiv</th>
<th></th>
</thead>
@ -37,17 +37,14 @@
<td v-text="membership.activity_name"></td>
<td v-text="membership.subactivity_name"></td>
<td v-text="membership.human_date"></td>
<td><ui-boolean-display :value="membership.is_active" dark></ui-boolean-display></td>
<td class="flex">
<a
href="#"
@click.prevent="
single = membership;
mode = 'edit';
"
class="inline-flex btn btn-warning btn-sm"
><ui-sprite src="pencil"></ui-sprite
></a>
<i-link href="#" @click.prevent="remove(membership)" class="inline-flex btn btn-danger btn-sm"><ui-sprite src="trash"></ui-sprite></i-link>
<a href="#" class="inline-flex btn btn-warning btn-sm" @click.prevent="
single = membership;
mode = 'edit';
"><ui-sprite src="pencil"></ui-sprite></a>
<i-link href="#" class="inline-flex btn btn-danger btn-sm"
@click.prevent="remove(membership)"><ui-sprite src="trash"></ui-sprite></i-link>
</td>
</tr>
</table>
@ -57,6 +54,12 @@
<script>
export default {
props: {
value: {},
activities: {},
subactivities: {},
groups: {},
},
data: function () {
return {
mode: null,
@ -78,7 +81,7 @@ export default {
methods: {
create() {
this.mode = 'create';
this.single = {...this.def};
this.single = { ...this.def };
},
cancel() {
this.mode = this.single = null;
@ -88,7 +91,7 @@ export default {
},
accept(payment) {
this.$inertia.patch(`/member/${this.value.id}/payment/${payment.id}`, {...payment, status_id: 3});
this.$inertia.patch(`/member/${this.value.id}/payment/${payment.id}`, { ...payment, status_id: 3 });
},
openLink(link) {
@ -114,12 +117,5 @@ export default {
: this.$inertia.patch(`/member/${this.value.id}/membership/${this.single.id}`, this.single, options);
},
},
props: {
value: {},
activities: {},
subactivities: {},
groups: {},
},
};
</script>

View File

@ -5,11 +5,13 @@
<th>Tätigkeit</th>
<th>Untertätigkeit</th>
<th>Datum</th>
<th>Aktiv</th>
</thead>
<tr v-for="(membership, index) in inner" :key="index">
<td v-text="membership.activity_name"></td>
<td v-text="membership.subactivity_name"></td>
<td v-text="membership.human_date"></td>
<td><ui-boolean-display :value="membership.is_active"></ui-boolean-display></td>
</tr>
</table>
@ -17,6 +19,7 @@
<ui-box class="relative" :heading="membership.activity_name" v-for="(membership, index) in inner" :key="index" second>
<div class="text-xs text-gray-200" v-text="membership.subactivity_name"></div>
<div class="text-xs text-gray-200" v-text="membership.human_date"></div>
<div class="text-xs text-gray-200"><ui-boolean-display :value="membership.is_active"></ui-boolean-display></div>
</ui-box>
</div>
</div>

View File

@ -8,6 +8,8 @@ use App\Member\Member;
use App\Member\Membership;
use App\Payment\Payment;
use App\Subactivity;
use Carbon\Carbon;
use Generator;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\RequestFactories\Child;
use Tests\TestCase;
@ -80,6 +82,29 @@ class IndexTest extends TestCase
$this->assertInertiaHas(false, $response, 'data.data.2.is_leader');
}
public function membershipDataProvider(): Generator
{
yield [now()->subMonth(), null, true];
yield [now()->subMonth(), now()->subDay(), false];
yield [now()->addDay(), null, false];
}
/**
* @dataProvider membershipDataProvider
*/
public function testItShowsIfMembershipIsActive(Carbon $from, ?Carbon $to, bool $isActive): void
{
$this->withoutExceptionHandling()->login()->loginNami();
$member = Member::factory()
->defaults()
->has(Membership::factory()->in('€ LeiterIn', 455, 'Pfadfinder', 15)->state(['from' => $from, 'to' => $to]))
->create();
$response = $this->get('/member');
$this->assertInertiaHas($isActive, $response, 'data.data.0.memberships.0.is_active');
}
public function testItHasNoEfzLinkWhenAddressIsMissing(): void
{
$this->withoutExceptionHandling()->login()->loginNami();
@ -187,11 +212,11 @@ class IndexTest extends TestCase
'subscription_id' => $member->payments->first()->subscription->id,
'status_name' => 'Nicht bezahlt',
'nr' => '2019',
], $response, 'data.data.0.payments.0');
], $response, 'data.data.0.payments.0');
$this->assertInertiaHas([
'id' => $member->subscription->id,
'name' => $member->subscription->name,
], $response, 'data.data.0.subscription');
], $response, 'data.data.0.subscription');
}
public function testItCanFilterForBillKinds(): void

View File

@ -14,6 +14,7 @@ use App\Payment\Payment;
use App\Payment\Subscription;
use App\Region;
use Carbon\Carbon;
use Generator;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\RequestFactories\Child;
use Tests\TestCase;
@ -119,7 +120,7 @@ class ShowTest extends TestCase
'id' => $member->memberships->first()->id,
'human_date' => '19.11.2022',
'promised_at' => now()->format('Y-m-d'),
], $response, 'data.memberships.0');
], $response, 'data.memberships.0');
$this->assertInertiaHas([
'organizer' => 'DPSG',
'event_name' => 'Wochenende',
@ -128,7 +129,7 @@ class ShowTest extends TestCase
'name' => ' Baustein 2e - Gewalt gegen Kinder und Jugendliche: Vertiefung, Prävention ',
'short_name' => '2e',
],
], $response, 'data.courses.0');
], $response, 'data.courses.0');
$this->assertInertiaHas([
'subscription' => [
'name' => 'Free',
@ -138,7 +139,7 @@ class ShowTest extends TestCase
],
'status_name' => 'Nicht bezahlt',
'nr' => '2019',
], $response, 'data.payments.0');
], $response, 'data.payments.0');
}
public function testItShowsMinimalSingleMember(): void
@ -169,4 +170,27 @@ class ShowTest extends TestCase
'multiply_more_pv' => false,
], $response, 'data');
}
public function membershipDataProvider(): Generator
{
yield [now()->subMonth(), null, true];
yield [now()->subMonth(), now()->subDay(), false];
yield [now()->addDay(), null, false];
}
/**
* @dataProvider membershipDataProvider
*/
public function testItShowsIfMembershipIsActive(Carbon $from, ?Carbon $to, bool $isActive): void
{
$this->withoutExceptionHandling()->login()->loginNami();
$member = Member::factory()
->defaults()
->has(Membership::factory()->in('€ LeiterIn', 455, 'Pfadfinder', 15)->state(['from' => $from, 'to' => $to]))
->create();
$response = $this->get("/member/{$member->id}");
$this->assertInertiaHas($isActive, $response, 'data.memberships.0.is_active');
}
}

View File

@ -18,11 +18,11 @@ class TestersBlockTest extends TestCase
Member::factory()
->defaults()
->has(Membership::factory()->in('Schnuppermitgliedschaft', 7, 'Wölfling', 8)->state(['created_at' => now()->subMonths(10)]))
->has(Membership::factory()->in('Schnuppermitgliedschaft', 7, 'Wölfling', 8)->state(['from' => now()->subMonths(10)]))
->create(['firstname' => 'Max', 'lastname' => 'Muster']);
$inactiveMember = Member::factory()
->defaults()
->has(Membership::factory()->ended()->in('Schnuppermitgliedschaft', 7, 'Wölfling', 8)->state(['created_at' => now()->subMonths(10)]))
->has(Membership::factory()->ended()->in('Schnuppermitgliedschaft', 7, 'Wölfling', 8)->state(['from' => now()->subMonths(10)]))
->create(['firstname' => 'Max', 'lastname' => 'Muster']);
$data = app(TestersBlock::class)->render()['data'];