--wip-- [skip ci]

This commit is contained in:
philipp lang 2025-06-12 03:33:50 +02:00
parent bf067d7352
commit d5f6d18f10
10 changed files with 178 additions and 218 deletions

View File

@ -103,6 +103,8 @@ services:
- ./data/redis:/data - ./data/redis:/data
meilisearch: meilisearch:
ports:
- "7700:7700"
image: getmeili/meilisearch:v1.6 image: getmeili/meilisearch:v1.6
volumes: volumes:
- ./data/meilisearch:/meili_data - ./data/meilisearch:/meili_data

View File

@ -68,11 +68,6 @@ parameters:
count: 1 count: 1
path: app/Member/MemberRequest.php path: app/Member/MemberRequest.php
-
message: "#^Method App\\\\Membership\\\\MembershipResource\\:\\:toArray\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: app/Membership/MembershipResource.php
- -
message: "#^Method App\\\\Payment\\\\SubscriptionResource\\:\\:toArray\\(\\) return type has no value type specified in iterable type array\\.$#" message: "#^Method App\\\\Payment\\\\SubscriptionResource\\:\\:toArray\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1 count: 1

View File

@ -1,10 +1,10 @@
.custom-table { .custom-table {
width: 100%; width: 100%;
& > thead > th { & > thead > th, & > thead > tr > th {
@apply text-left px-6 text-gray-200 font-semibold py-3 border-gray-600 border-b; @apply text-left px-6 text-gray-200 font-semibold py-3 border-gray-600 border-b;
} }
& > tr { & > tr, & > tbody > tr {
@apply text-gray-200 transition-all duration-300 rounded hover:bg-gray-800; @apply text-gray-200 transition-all duration-300 rounded hover:bg-gray-800;
& > td { & > td {
@apply py-1 px-6; @apply py-1 px-6;
@ -12,10 +12,10 @@
} }
&.custom-table-sm { &.custom-table-sm {
& > thead > th { & > thead > th, & > thead > tr > th {
@apply px-3 py-2; @apply px-3 py-2;
} }
& > tr { & > tr, & > tbody > tr {
& > td { & > td {
@apply py-1 px-3; @apply py-1 px-3;
} }
@ -32,11 +32,4 @@
} }
} }
} }
}
.custom-table > * {
display: table-row;
}
.custom-table > * > * {
display: table-cell;
} }

View File

@ -1,28 +1,14 @@
<template> <template>
<a v-tooltip="tooltip" :href="href" :target="blank ? '_BLANK' : '_SELF'" class="inline-flex btn btn-sm"> <a v-tooltip="tooltip" :href="href" :target="blank ? '_BLANK' : '_SELF'" class="inline-flex btn btn-sm">
<ui-sprite :src="icon"></ui-sprite> <ui-sprite :src="icon" />
</a> </a>
</template> </template>
<script setup> <script lang="ts" setup>
defineProps({ const {tooltip, icon, blank = false, href = '#'} = defineProps<{
tooltip: { tooltip: string,
required: true, href?: string,
type: String, blank?: boolean,
}, icon: string,
href: { }>();
type: String,
default: () => '#',
required: false,
},
blank: {
type: Boolean,
default: () => false,
required: false,
},
icon: {
type: String,
required: true,
},
});
</script> </script>

View File

@ -2,6 +2,5 @@
<svg v-bind="$attrs" class="fill-current"><use :xlink:href="`/sprite.svg#${$attrs.src}`" /></svg> <svg v-bind="$attrs" class="fill-current"><use :xlink:href="`/sprite.svg#${$attrs.src}`" /></svg>
</template> </template>
<script> <script lang="ts" setup>
export default {};
</script> </script>

View File

@ -9,7 +9,7 @@
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
defineProps<{ defineProps<{
modelValue: number, modelValue: number,
entries: {title: string}[] entries: {title: string}[]

View File

@ -1,25 +1,29 @@
<template> <template>
<div> <div>
<table cellspacing="0" cellpadding="0" border="0" class="hidden md:table custom-table overflow-auto custom-table-sm text-sm" v-if="inner.length"> <table v-if="inner.length" cellspacing="0" cellpadding="0" border="0" class="hidden md:table custom-table overflow-auto custom-table-sm">
<thead> <thead>
<th>Datum</th> <tr>
<th>Baustein</th> <th>Datum</th>
<th>Veranstaltung</th> <th>Baustein</th>
<th>Organisator</th> <th>Veranstaltung</th>
<th>Organisator</th>
</tr>
</thead> </thead>
<tr v-for="(course, index) in inner" :key="index"> <tbody>
<td v-text="course.completed_at_human"></td> <tr v-for="(course, index) in inner" :key="index">
<td v-text="course.course.short_name"></td> <td v-text="course.completed_at_human" />
<td v-text="course.event_name"></td> <td v-text="course.course.short_name" />
<td v-text="course.organizer"></td> <td v-text="course.event_name" />
</tr> <td v-text="course.organizer" />
</tr>
</tbody>
</table> </table>
<div class="py-3 text-gray-400 text-center" v-else>Keine Ausbildungen vorhanden</div> <div v-else class="py-3 text-gray-400 text-center">Keine Ausbildungen vorhanden</div>
<div class="md:hidden grid gap-3"> <div class="md:hidden grid gap-3">
<ui-box class="relative" :heading="course.course.short_name" v-for="(course, index) in inner" :key="index" second> <ui-box v-for="(course, index) in inner" :key="index" class="relative" :heading="course.course.short_name" second>
<div class="text-xs text-gray-200" v-text="course.event_name"></div> <div class="text-xs text-gray-200" v-text="course.event_name" />
<div class="text-xs text-gray-200" v-text="course.completed_at_human"></div> <div class="text-xs text-gray-200" v-text="course.completed_at_human" />
<div class="text-xs text-gray-200" v-text="course.organizer"></div> <div class="text-xs text-gray-200" v-text="course.organizer" />
</ui-box> </ui-box>
</div> </div>
</div> </div>
@ -27,14 +31,14 @@
<script> <script>
export default { export default {
props: {
value: {},
},
data: function () { data: function () {
return { return {
inner: [], inner: [],
}; };
}, },
props: {
value: {},
},
created() { created() {
this.inner = this.value; this.inner = this.value;
}, },

View File

@ -2,42 +2,35 @@
<div> <div>
<table cellspacing="0" cellpadding="0" border="0" class="hidden md:table custom-table overflow-auto custom-table-sm text-sm"> <table cellspacing="0" cellpadding="0" border="0" class="hidden md:table custom-table overflow-auto custom-table-sm text-sm">
<thead> <thead>
<th>Tätigkeit</th> <tr>
<th>Untertätigkeit</th> <th>Tätigkeit</th>
<th>Datum</th> <th>Untertätigkeit</th>
<th>Aktiv</th> <th>Datum</th>
<th>Aktiv</th>
</tr>
</thead> </thead>
<tr v-for="(membership, index) in inner" :key="index"> <tbody>
<td v-text="membership.activity_name"></td> <tr v-for="(membership, index) in value" :key="index">
<td v-text="membership.subactivity_name"></td> <td v-text="membership.activity_name" />
<td v-text="membership.human_date"></td> <td v-text="membership.subactivity_name" />
<td><ui-boolean-display :value="membership.is_active"></ui-boolean-display></td> <td v-text="membership.human_date" />
</tr> <td><ui-boolean-display :value="membership.is_active" /></td>
</tr>
</tbody>
</table> </table>
<div class="md:hidden grid gap-3"> <div class="md:hidden grid gap-3">
<ui-box class="relative" :heading="membership.activity_name" v-for="(membership, index) in inner" :key="index" second> <ui-box v-for="(membership, index) in value" :key="index" class="relative" :heading="membership.activity_name" second>
<div class="text-xs text-gray-200" v-text="membership.subactivity_name"></div> <div class="text-xs text-gray-200" v-text="membership.subactivity_name" />
<div class="text-xs text-gray-200" v-text="membership.human_date"></div> <div class="text-xs text-gray-200" v-text="membership.human_date" />
<div class="text-xs text-gray-200"><ui-boolean-display :value="membership.is_active"></ui-boolean-display></div> <div class="text-xs text-gray-200"><ui-boolean-display :value="membership.is_active" /></div>
</ui-box> </ui-box>
</div> </div>
</div> </div>
</template> </template>
<script> <script lang="ts" setup>
export default { defineProps<{
data: function () { value: Record<number, {activity_name: string, subactivity_name: string, human_date: string, is_active: boolean}>,
return { }>();
inner: [],
};
},
props: {
value: {},
},
created() {
this.inner = this.value;
},
};
</script> </script>

View File

@ -7,10 +7,12 @@ use App\Course\Models\Course;
use App\Course\Models\CourseMember; use App\Course\Models\CourseMember;
use App\Gender; use App\Gender;
use App\Group; use App\Group;
use App\Invoice\BillKind;
use App\Invoice\Models\Invoice; use App\Invoice\Models\Invoice;
use App\Invoice\Models\InvoicePosition; use App\Invoice\Models\InvoicePosition;
use App\Member\Data\MembershipData; use App\Member\Data\MembershipData;
use App\Member\Member; use App\Member\Member;
use App\Member\MemberResource;
use App\Member\Membership; use App\Member\Membership;
use App\Nationality; use App\Nationality;
use App\Payment\Subscription; use App\Payment\Subscription;
@ -25,7 +27,28 @@ beforeEach(function () {
Country::factory()->create(['name' => 'Deutschland']); Country::factory()->create(['name' => 'Deutschland']);
}); });
it('testItShowsSingleMember', function () { covers(MemberResource::class);
covers(MembershipData::class);
it('shows courses', function () {
$this->withoutExceptionHandling()->login()->loginNami();
$member = Member::factory()
->defaults()
->has(CourseMember::factory()
->for(Course::factory()->name(' Baustein 2e - Gewalt gegen Kinder und Jugendliche: Vertiefung, Prävention '))
->state(['organizer' => 'DPSG', 'event_name' => 'Wochenende', 'completed_at' => '2022-03-03']),
'courses')
->create();
$this->get("/member/{$member->id}")
->assertInertiaPath('data.courses.0.organizer', 'DPSG')
->assertInertiaPath('data.courses.0.event_name', 'Wochenende')
->assertInertiaPath('data.courses.0.completed_at_human', '03.03.2022')
->assertInertiaPath('data.courses.0.course.name', ' Baustein 2e - Gewalt gegen Kinder und Jugendliche: Vertiefung, Prävention ')
->assertInertiaPath('data.courses.0.course.short_name', '2e');
});
it('shows memberships', function () {
Carbon::setTestNow(Carbon::parse('2006-01-01 15:00:00')); Carbon::setTestNow(Carbon::parse('2006-01-01 15:00:00'));
$this->withoutExceptionHandling()->login()->loginNami(); $this->withoutExceptionHandling()->login()->loginNami();
@ -33,143 +56,96 @@ it('testItShowsSingleMember', function () {
->defaults() ->defaults()
->for(Group::factory()->name('Stamm Beispiel')) ->for(Group::factory()->name('Stamm Beispiel'))
->has(Membership::factory()->promise(now())->in('€ LeiterIn', 5, 'Jungpfadfinder', 88)->from('2022-11-19')) ->has(Membership::factory()->promise(now())->in('€ LeiterIn', 5, 'Jungpfadfinder', 88)->from('2022-11-19'))
->has(InvoicePosition::factory()->for(Invoice::factory())->price(1050)->description('uu')) ->create();
->for(Gender::factory()->male())
->for(Region::factory()->name('NRW'))
->postBillKind()
->inNami(123)
->for(Subscription::factory()->name('Sub')->forFee())
->has(CourseMember::factory()->for(Course::factory()->name(' Baustein 2e - Gewalt gegen Kinder und Jugendliche: Vertiefung, Prävention '))->state(['organizer' => 'DPSG', 'event_name' => 'Wochenende', 'completed_at' => '2022-03-03']), 'courses')
->create([
'birthday' => '1991-04-20',
'address' => 'Itterstr 3',
'zip' => '42719',
'location' => 'Solingen',
'firstname' => 'Max',
'lastname' => 'Muster',
'other_country' => 'other',
'main_phone' => '+49 212 1266775',
'mobile_phone' => '+49 212 1266776',
'work_phone' => '+49 212 1266777',
'children_phone' => '+49 212 1266778',
'email' => 'a@b.de',
'email_parents' => 'b@c.de',
'fax' => '+49 212 1255674',
'efz' => '2022-09-20',
'ps_at' => '2022-04-20',
'more_ps_at' => '2022-06-02',
'recertified_at' => '2022-06-13',
'without_education_at' => '2022-06-03',
'without_efz_at' => '2022-06-04',
'has_vk' => true,
'has_svk' => true,
'multiply_pv' => true,
'multiply_more_pv' => true,
'send_newspaper' => true,
'joined_at' => '2022-06-11',
'mitgliedsnr' => 998,
'lon' => 19.05,
'lat' => 14.053,
]);
$response = $this->get("/member/{$member->id}"); $this->get("/member/{$member->id}")
->assertInertiaPath('data.memberships.0.id', $member->memberships->first()->id)
$this->assertInertiaHas([ ->assertInertiaPath('data.memberships.0.from.human', '19.11.2022')
'birthday_human' => '20.04.1991', ->assertInertiaPath('data.memberships.0.from.raw', '2022-11-19')
'age' => 14, ->assertInertiaPath('data.memberships.0.promised_at.human', now()->format('d.m.Y'))
'group_name' => 'Stamm Beispiel', ->assertInertiaPath('data.memberships.0.promised_at.raw', now()->format('Y-m-d'))
'full_address' => 'Itterstr 3, 42719 Solingen', ->assertInertiaPath('data.memberships.0.activity.name', '€ LeiterIn')
'region' => ['name' => 'NRW'], ->assertInertiaPath('data.memberships.0.activity.id',$member->memberships->first()->activity->id)
'other_country' => 'other', ->assertInertiaPath('data.memberships.0.subactivity.name', 'Jungpfadfinder')
'main_phone' => '+49 212 1266775', ->assertInertiaPath('data.memberships.0.subactivity.id',$member->memberships->first()->subactivity->id);
'mobile_phone' => '+49 212 1266776',
'work_phone' => '+49 212 1266777',
'children_phone' => '+49 212 1266778',
'email' => 'a@b.de',
'email_parents' => 'b@c.de',
'fax' => '+49 212 1255674',
'fullname' => 'Herr Max Muster',
'efz_human' => '20.09.2022',
'ps_at_human' => '20.04.2022',
'more_ps_at_human' => '02.06.2022',
'without_education_at_human' => '03.06.2022',
'without_efz_at_human' => '04.06.2022',
'recertified_at_human' => '13.06.2022',
'has_vk' => true,
'has_svk' => true,
'multiply_pv' => true,
'multiply_more_pv' => true,
'has_nami' => true,
'nami_id' => 123,
'send_newspaper' => true,
'joined_at_human' => '11.06.2022',
'bill_kind_name' => 'Post',
'mitgliedsnr' => 998,
'lon' => 19.05,
'lat' => 14.053,
'subscription' => [
'name' => 'Sub',
],
], $response, 'data');
$this->assertInertiaHas([
'id' => $member->memberships->first()->id,
'from' => ['human' => '19.11.2022', 'raw' => '2022-11-19'],
'promised_at' => ['human' => now()->format('d.m.Y'), 'raw' => now()->format('Y-m-d')],
'activity' => [
'name' => '€ LeiterIn',
'id' => $member->memberships->first()->activity->id,
],
'subactivity' => [
'name' => 'Jungpfadfinder',
'id' => $member->memberships->first()->subactivity->id,
]
], $response, 'data.memberships.0');
$this->assertInertiaHas([
'organizer' => 'DPSG',
'event_name' => 'Wochenende',
'completed_at_human' => '03.03.2022',
'course' => [
'name' => ' Baustein 2e - Gewalt gegen Kinder und Jugendliche: Vertiefung, Prävention ',
'short_name' => '2e',
],
], $response, 'data.courses.0');
$this->assertInertiaHas([
'description' => 'uu',
'price_human' => '10,50 €',
'invoice' => [
'status' => 'Neu',
]
], $response, 'data.invoicePositions.0');
}); });
it('testItShowsMinimalSingleMember', function () { it('shows invoice positions', function () {
Carbon::setTestNow(Carbon::parse('2006-01-01 15:00:00'));
$this->withoutExceptionHandling()->login()->loginNami();
$member = Member::factory()
->defaults()
->for(Group::factory()->name('Stamm Beispiel'))
->has(InvoicePosition::factory()->for(Invoice::factory())->price(1050)->description('uu'))
->create();
$this->get("/member/{$member->id}")
->assertInertiaPath('data.invoicePositions.0.description', 'uu')
->assertInertiaPath('data.invoicePositions.0.price_human', '10,50 €')
->assertInertiaPath('data.invoicePositions.0.invoice.status', 'Neu');
});
it('shows member', function (array $attributes, array $expect) {
Carbon::setTestNow(Carbon::parse('2006-01-01 15:00:00'));
$this->withoutExceptionHandling()->login()->loginNami(); $this->withoutExceptionHandling()->login()->loginNami();
$member = Member::factory() $member = Member::factory()
->for(Group::factory()) ->for(Group::factory())
->for(Nationality::factory()->name('deutsch')) ->for(Nationality::factory()->name('deutsch'))
->for(Subscription::factory()->forFee()) ->for(Subscription::factory()->forFee())
->create(['firstname' => 'Max', 'lastname' => 'Muster', 'has_vk' => false, 'has_svk' => false]); ->create($attributes);
$response = $this->get("/member/{$member->id}"); $this->get("/member/{$member->id}")
->assertInertiaPath('data.id', $member->id)
$this->assertInertiaHas([ ->assertInertiaPathArray($expect);
'region' => ['name' => '-- kein --'], })->with([
'fullname' => 'Max Muster', fn() => [['region_id' => Region::factory()->name('UUU')->create()->id], ['data.region.name' => 'UUU', 'data.region.id' => Region::first()->id]],
'nationality' => [ fn() => [['nationality_id' => Nationality::factory()->name('UUU')->create()->id], ['data.nationality.name' => 'UUU', 'data.nationality_id' => Nationality::first()->id, 'data.nationality.id' => Nationality::first()->id]],
'name' => 'deutsch', fn() => [['group_id' => Group::factory()->name('UUU')->create()->id], ['data.group_name' => 'UUU']],
], fn() => [['bill_kind' => BillKind::EMAIL->value], ['data.bill_kind_name' => 'E-Mail']],
'efz_human' => null, fn() => [['subscription_id' => Subscription::factory()->name('Sub')->forFee()->create()], ['data.subscription.name' => 'Sub', 'data.subscription_id' => Subscription::first()->id]],
'ps_at_human' => null, fn() => [['country_id' => Country::factory()->create(['name' => 'Sub'])->id], ['data.country_id' => Country::firstWhere('name', 'Sub')->id]],
'more_ps_at_human' => null, fn () => [['firstname' => 'Max', 'lastname' => 'Muster', 'gender_id' => Gender::factory()->male()->create()->id], ['data.firstname' => 'Max', 'data.lastname' => 'Muster', 'data.fullname' => 'Herr Max Muster', 'data.gender_id' => Gender::first()->id]],
'without_education_at_human' => null, [['firstname' => 'Max', 'lastname' => 'Muster', 'gender_id' => null], ['data.fullname' => 'Max Muster']],
'without_efz_at_human' => null, [['other_country' => 'other'], ['data.other_country' => 'other']],
'has_vk' => false, [['further_address' => 'other'], ['data.further_address' => 'other']],
'has_svk' => false, [['gender_id' => null], ['data.gender_name' => 'keine Angabe']],
'multiply_pv' => false, [['birthday' => null], ['data.birthday' => null, 'data.birthday_human' => null]],
'multiply_more_pv' => false, [['efz' => null], ['data.efz_human' => null]],
], $response, 'data'); [['ps_at' => null], ['data.ps_at_human' => null]],
}); [['ps_at' => null], ['data.ps_at_human' => null]],
[['more_ps_at' => null], ['data.more_ps_at_human' => null]],
[['has_svk' => false], ['data.has_svk' => false]],
[['has_vk' => false], ['data.has_vk' => false]],
[['has_svk' => true], ['data.has_svk' => true]],
[['has_vk' => true], ['data.has_vk' => true]],
[['multiply_more_pv' => false], ['data.multiply_more_pv' => false]],
[['without_efz_at' => null], ['data.without_efz_at_human' => null]],
[['without_education_at' => null], ['data.without_education_at_human' => null]],
[['main_phone' => '+49 212 1266775'], ['data.main_phone' => '+49 212 1266775']],
[['mobile_phone' => '+49 212 1266776'], ['data.mobile_phone' => '+49 212 1266776']],
[['work_phone' => '+49 212 1266777'], ['data.work_phone' => '+49 212 1266777']],
[['children_phone' => '+49 212 1266778'], ['data.children_phone' => '+49 212 1266778']],
[['efz' => '2022-09-20'], ['data.efz_human' => '20.09.2022']],
[['ps_at' => '2022-04-20'], ['data.ps_at_human' => '20.04.2022']],
[['more_ps_at' => '2022-06-02'], ['data.more_ps_at_human' => '02.06.2022']],
[['without_education_at' => '03.06.2022'], ['data.without_education_at_human' => '03.06.2022']],
[['without_efz_at' => '2022-06-04'], ['data.without_efz_at_human' => '04.06.2022']],
[['recertified_at' => '2022-06-13'], ['data.recertified_at_human' => '13.06.2022']],
[['multiply_pv' => true], ['data.multiply_pv' => true]],
[['multiply_more_pv' => true], ['data.multiply_more_pv' => true]],
[['email' => 'a@b.de'], ['data.email' => 'a@b.de']],
[['email_parents' => 'b@c.de'], ['data.email_parents' => 'b@c.de']],
[['fax' => '+49 212 1255674'], ['data.fax' => '+49 212 1255674']],
[['nami_id' => 123], ['data.nami_id' => 123, 'data.has_nami' => true]],
[['send_newspaper' => true], ['data.send_newspaper' => true]],
[['address' => 'Itterstr 3', 'location' => 'Solingen', 'zip' => '42719'], ['data.location' => 'Solingen', 'data.address' => 'Itterstr 3', 'data.zip' => '42719', 'data.full_address' => 'Itterstr 3, 42719 Solingen']],
[['lon' => 19.05], ['data.lon' => 19.05]],
[['lat' => 14.053], ['data.lat' => 14.053]],
[['birthday' => '1991-04-20'], ['data.birthday' => '1991-04-20', 'data.birthday_human' => '20.04.1991', 'data.age' => 14]],
[['joined_at' => '2022-06-11'], ['data.joined_at' => '2022-06-11', 'data.joined_at_human' => '11.06.2022']],
[['mitgliedsnr' => 998], ['data.mitgliedsnr' => 998]],
]);
it('testItShowsIfMembershipIsActive', function (Carbon $from, ?Carbon $to, bool $isActive) { it('testItShowsIfMembershipIsActive', function (Carbon $from, ?Carbon $to, bool $isActive) {
$this->withoutExceptionHandling()->login()->loginNami(); $this->withoutExceptionHandling()->login()->loginNami();

View File

@ -119,12 +119,24 @@ class TestCase extends BaseTestCase
/** @var TestResponse */ /** @var TestResponse */
$response = $this; $response = $this;
$props = data_get($response->viewData('page'), 'props'); $props = data_get($response->viewData('page'), 'props');
Assert::assertTrue(Arr::has($props, $path), 'Failed that key ' . $path . ' is in Response.');
Assert::assertNotNull($props); Assert::assertNotNull($props);
$json = new AssertableJsonString($props); $json = new AssertableJsonString($props);
$json->assertPath($path, $value); $json->assertPath($path, $value);
return $this; return $this;
}); });
TestResponse::macro('assertInertiaPathArray', function ($arr) {
/** @var TestResponse */
$response = $this;
foreach ($arr as $key => $value) {
$response->assertInertiaPath($key, $value);
}
return $response;
});
TestResponse::macro('assertInertiaCount', function ($path, $count) { TestResponse::macro('assertInertiaCount', function ($path, $count) {
/** @var TestResponse */ /** @var TestResponse */
$response = $this; $response = $this;