Add member view

This commit is contained in:
philipp lang 2022-11-22 00:37:34 +01:00
parent bde4fc046c
commit a2b379e349
19 changed files with 405 additions and 34 deletions

View File

@ -11,4 +11,13 @@ class Course extends Model
public $timestamps = false;
public $fillable = ['name', 'nami_id'];
public function getShortNameAttribute(): string
{
return str($this->name)
->trim()
->replaceFirst('Baustein', '')
->trim()
->replaceMatches('/ - .*/', '');
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Course\Resources;
use Carbon\Carbon;
use Illuminate\Http\Resources\Json\JsonResource;
/**
* @mixin \App\Course\Models\CourseMember
*/
class CourseMemberResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
*
* @return array<string, mixed>
*/
public function toArray($request)
{
return [
'id' => $this->id,
'organizer' => $this->organizer,
'event_name' => $this->event_name,
'completed_at_human' => Carbon::parse($this->completed_at)->format('d.m.Y'),
'completed_at' => $this->completed_at,
'course_name' => $this->course->name,
'course_id' => $this->course->id,
'course' => new CourseResource($this->whenLoaded('course')),
];
}
}

View File

@ -2,11 +2,10 @@
namespace App\Course\Resources;
use Carbon\Carbon;
use Illuminate\Http\Resources\Json\JsonResource;
/**
* @mixin \App\Course\Models\CourseMember
* @mixin \App\Course\Models\Course
*/
class CourseResource extends JsonResource
{
@ -20,13 +19,10 @@ class CourseResource extends JsonResource
public function toArray($request)
{
return [
'name' => $this->name,
'nami_id' => $this->nami_id,
'id' => $this->id,
'organizer' => $this->organizer,
'event_name' => $this->event_name,
'completed_at_human' => Carbon::parse($this->completed_at)->format('d.m.Y'),
'completed_at' => $this->completed_at,
'course_name' => $this->course->name,
'course_id' => $this->course->id,
'short_name' => $this->short_name,
];
}
}

View File

@ -21,7 +21,7 @@ class MemberView
return [
'data' => MemberResource::collection(Member::select('*')
->filter($filter)->search($request->query('search', null))
->with('billKind')->with('payments.subscription')->with('memberships')->with('courses')->with('leaderMemberships')->with('ageGroupMemberships')
->with('billKind')->with('payments.subscription')->with('memberships')->with('courses')->with('subscription')->with('leaderMemberships')->with('ageGroupMemberships')
->withIsConfirmed()->withPendingPayment()
->orderByRaw('lastname, firstname')
->paginate(15)

View File

@ -23,6 +23,8 @@ class MemberShowAction
->load('payments.subscription')
->load('nationality')
->load('region')
->load('subscription')
->load('courses.course')
),
'toolbar' => [['href' => route('member.index'), 'label' => 'Zurück', 'color' => 'primary', 'icon' => 'undo']],
];

View File

@ -48,7 +48,7 @@ class Member extends Model
/**
* @var array<int, string>
*/
public $dates = ['try_created_at', 'joined_at', 'birthday'];
public $dates = ['try_created_at', 'joined_at', 'birthday', 'efz', 'ps_at', 'more_ps_at', 'without_education_at', 'without_efz_at'];
/**
* @var array<string, string>

View File

@ -2,11 +2,12 @@
namespace App\Member;
use App\Course\Resources\CourseResource;
use App\Course\Resources\CourseMemberResource;
use App\Member\Resources\NationalityResource;
use App\Member\Resources\RegionResource;
use App\Membership\MembershipResource;
use App\Payment\PaymentResource;
use App\Payment\SubscriptionResource;
use Illuminate\Http\Resources\Json\JsonResource;
/**
@ -36,7 +37,7 @@ class MemberResource extends JsonResource
'joined_at_human' => $this->joined_at->format('d.m.Y'),
'id' => $this->id,
'subscription_id' => $this->subscription_id,
'subscription_name' => $this->subscription_name,
'subscription' => new SubscriptionResource($this->whenLoaded('subscription')),
'gender_id' => $this->gender_id,
'gender_name' => $this->gender?->name ?: 'keine Angabe',
'fullname' => ($this->gender ? $this->gender?->salutation.' ' : '').$this->fullname,
@ -62,18 +63,24 @@ class MemberResource extends JsonResource
'memberships' => MembershipResource::collection($this->whenLoaded('memberships')),
'pending_payment' => $this->pending_payment ? number_format($this->pending_payment / 100, 2, ',', '.').' €' : null,
'age_group_icon' => $this->ageGroupMemberships->first()?->subactivity->slug,
'courses' => CourseResource::collection($this->whenLoaded('courses')),
'courses' => CourseMemberResource::collection($this->whenLoaded('courses')),
'nationality' => new NationalityResource($this->whenLoaded('nationality')),
'region' => new RegionResource($this->whenLoaded('region')),
'full_address' => $this->fullAddress,
'efz' => $this->efz,
'efz' => $this->efz?->format('Y-m-d'),
'efz_human' => $this->efz?->format('d.m.Y') ?: null,
'ps_at_human' => $this->ps_at?->format('d.m.Y') ?: null,
'more_ps_at_human' => $this->more_ps_at?->format('d.m.Y') ?: null,
'without_education_at_human' => $this->without_education_at?->format('d.m.Y') ?: null,
'without_efz_at_human' => $this->without_efz_at?->format('d.m.Y') ?: null,
'efz_link' => $this->getEfzLink(),
'ps_at' => $this->ps_at,
'more_ps_at' => $this->more_ps_at,
'ps_at' => $this->ps_at?->format('Y-m-d'),
'more_ps_at' => $this->more_ps_at?->format('Y-m-d'),
'has_svk' => $this->has_svk,
'nami_id' => $this->nami_id,
'has_vk' => $this->has_vk,
'without_education_at' => $this->without_education_at,
'without_efz_at' => $this->without_efz_at,
'without_education_at' => $this->without_education_at?->format('Y-m-d'),
'without_efz_at' => $this->without_efz_at?->format('Y-m-d'),
'multiply_pv' => $this->multiply_pv,
'multiply_more_pv' => $this->multiply_more_pv,
'age' => $this->getModel()->getAge(),

View File

@ -25,6 +25,11 @@ class CourseFactory extends Factory
];
}
public function name(string $name): self
{
return $this->state(['name' => $name]);
}
public function inNami(int $namiId): self
{
return $this->state(['nami_id' => $namiId]);

View File

@ -23,4 +23,9 @@ class BillKindFactory extends Factory
'name' => $this->faker->words(3, true),
];
}
public function name(string $name): self
{
return $this->state(['name' => $name]);
}
}

View File

@ -18,4 +18,9 @@ class SubscriptionFactory extends Factory
'fee_id' => Fee::factory()->createOne()->id,
];
}
public function name(string $name): self
{
return $this->state(['name' => $name]);
}
}

View File

@ -0,0 +1,35 @@
<template>
<div v-tooltip="longLabel" class="flex space-x-2 items-center">
<div
class="border-2 rounded-full w-4 h-4 flex items-center justify-center"
:class="value ? 'border-green-700' : 'border-red-700'"
>
<svg-sprite
:src="value ? 'check' : 'close'"
:class="value ? 'text-green-800' : 'text-red-800'"
class="w-3 h-3 flex-none"
></svg-sprite>
</div>
<div class="text-gray-400 text-xs" v-text="label"></div>
</div>
</template>
<script>
export default {
props: {
value: {
required: true,
type: Boolean,
},
label: {
required: true,
type: String,
},
longLabel: {
default: function () {
return null;
},
},
},
};
</script>

View File

@ -0,0 +1,28 @@
<template>
<section class="bg-gray-800 p-3 rounded-lg flex flex-col">
<heading class="col-span-full">{{ heading }}</heading>
<main :class="containerClass" class="mt-3">
<slot></slot>
</main>
</section>
</template>
<script>
export default {
props: {
heading: {
required: true,
type: String,
},
containerClass: {
default: function () {
return '';
},
},
},
components: {
heading: () => import('./Heading'),
},
};
</script>

View File

@ -1,5 +1,5 @@
<template>
<div class="text-gray-300">
<div class="font-semibold text-gray-300">
<slot></slot>
</div>
</template>

View File

@ -1,8 +1,7 @@
<template>
<div class="p-6 grid gap-6 this-grid">
<div class="p-6 grid gap-6 this-grid grow">
<!-- ****************************** Stammdaten ******************************* -->
<div class="bg-gray-800 p-3 grid grid-cols-2 justify-start gap-3 rounded-lg">
<heading class="col-span-full">Stammdaten</heading>
<box container-class="grid grid-cols-2 gap-3" heading="Stammdaten" class="area-stamm">
<key-value class="col-span-2" label="Name" :value="inner.fullname"></key-value>
<key-value class="col-span-2" label="Adresse" :value="inner.full_address"></key-value>
<key-value label="Geburtsdatum" :value="inner.birthday_human"></key-value>
@ -14,11 +13,10 @@
label="Andere Staatsangehörigkeit"
:value="inner.other_country"
></key-value>
</div>
</box>
<!-- ******************************** Kontakt ******************************** -->
<div class="bg-gray-800 p-3 grid justify-start gap-3 rounded-lg">
<heading class="col-span-full">Kontakt</heading>
<box container-class="grid gap-3" heading="Kontakt" class="area-kontakt">
<key-value
v-show="inner.main_phone"
label="Telefon Eltern"
@ -51,7 +49,135 @@
type="email"
></key-value>
<key-value v-show="inner.fax" label="Fax" :value="inner.fax" type="tel"></key-value>
</div>
</box>
<!-- ****************************** Prävention ******************************* -->
<box container-class="flex gap-3" heading="Prävention" class="area-praev">
<div class="grid gap-3">
<key-value
class="col-start-1"
label="Führungszeugnis eingesehen"
:value="inner.efz_human ? inner.efz_human : 'nie'"
></key-value>
<key-value
class="col-start-1"
label="Präventionsschulung"
:value="inner.ps_at_human ? inner.ps_at_human : 'nie'"
></key-value>
<key-value
class="col-start-1"
label="Vertiefungsschulung"
:value="inner.more_ps_at_human ? inner.more_ps_at_human : 'nie'"
></key-value>
<key-value
class="col-start-1"
label="Einsatz ohne Schulung"
:value="inner.without_education_at_human ? inner.without_education_at_human : 'nie'"
></key-value>
<key-value
class="col-start-1"
label="Einsatz ohne EFZ"
:value="inner.without_efz_at_human ? inner.without_efz_at_human : 'nie'"
></key-value>
</div>
<div class="grid gap-3 content-start">
<boolean :value="inner.has_vk" long-label="Verhaltenskodex unterschrieben" label="VK"></boolean>
<boolean :value="inner.has_svk" long-label="SVK unterschrieben" label="SVK"></boolean>
<boolean
:value="inner.multiply_pv"
long-label="Multiplikator*in Präventionsschulung"
label="Multipl. PS"
></boolean>
<boolean
:value="inner.multiply_more_pv"
long-label="Multiplikator*in Vertierungsschulung"
label="Multipl. VS"
></boolean>
</div>
</box>
<!-- ******************************** Courses ******************************** -->
<box heading="Ausbildungen" class="area-courses">
<table
cellspacing="0"
cellpadding="0"
border="0"
class="custom-table custom-table-sm text-sm"
v-if="inner.courses.length"
>
<thead>
<th>Datum</th>
<th>Baustein</th>
<th>Veranstaltung</th>
<th>Organisator</th>
</thead>
<tr v-for="(course, index) in inner.courses" :key="index">
<td v-text="course.completed_at_human"></td>
<td v-text="course.course.short_name"></td>
<td v-text="course.event_name"></td>
<td v-text="course.organizer"></td>
</tr>
</table>
<div class="py-3 text-gray-400 text-center" v-else>Keine Ausbildungen vorhanden</div>
</box>
<!-- ******************************** System ********************************* -->
<box container-class="grid gap-3" heading="System" class="area-system">
<key-value v-show="inner.nami_id" label="Nami Mitgliedsnummer" :value="inner.nami_id"></key-value>
<key-value label="Beitrag" :value="inner.subscription ? inner.subscription.name : 'kein'"></key-value>
<key-value v-if="inner.joined_at_human" label="Eintrittsdatum" :value="inner.joined_at_human"></key-value>
<key-value v-if="inner.bill_kind_name" label="Rechnung" :value="inner.bill_kind_name"></key-value>
<boolean :value="inner.send_newspaper" label="Mittendrin versenden"></boolean>
</box>
<!-- *************************** Mitgliedschaften **************************** -->
<box heading="Mitgliedschaften" class="area-memberships">
<table cellspacing="0" cellpadding="0" border="0" class="custom-table custom-table-sm text-sm">
<thead>
<th>Tätigkeit</th>
<th>Untertätigkeit</th>
<th>Datum</th>
</thead>
<tr v-for="(membership, index) in inner.memberships" :key="index">
<td v-text="membership.activity_name"></td>
<td v-text="membership.subactivity_name"></td>
<td v-text="membership.human_date"></td>
</tr>
</table>
</box>
<!-- ******************************* Zahlungen ******************************* -->
<box heading="Zahlungen" class="area-payments">
<table cellspacing="0" cellpadding="0" border="0" class="custom-table custom-table-sm text-sm">
<thead>
<th>Nr</th>
<th>Status</th>
<th>Betrag-Name</th>
<th>Betrag</th>
</thead>
<tr v-for="(payment, index) in inner.payments" :key="index">
<td v-text="payment.nr"></td>
<td v-text="payment.status_name"></td>
<td v-text="payment.subscription.name"></td>
<td v-text="payment.subscription.amount_human"></td>
</tr>
</table>
</box>
<!-- ********************************* Karte ********************************* -->
<box heading="Karte" container-class="grow" class="area-map">
<iframe
width="100%"
height="100%"
frameborder="0"
scrolling="no"
marginheight="0"
marginwidth="0"
src="https://www.openstreetmap.org/export/embed.html?bbox=9.699318408966066%2C47.484177893725764%2C9.729595184326174%2C47.49977861091604&amp;layer=mapnik&amp;marker=47.49197883161885%2C9.714467525482178"
style="border: 1px solid black"
>
</iframe>
</box>
</div>
</template>
@ -71,7 +197,8 @@ export default {
components: {
'key-value': () => import('./KeyValue'),
'heading': () => import('./Heading'),
'boolean': () => import('./Boolean'),
'box': () => import('./Box'),
},
created() {
@ -82,6 +209,34 @@ export default {
<style scoped>
.this-grid {
grid-template-columns: max-content max-content 1fr;
grid-template-areas:
'stamm kontakt prae system'
'courses courses memberships memberships'
'payments payments map map';
grid-template-columns: max-content max-content max-content 1fr;
}
.area-stamm {
grid-area: stamm;
}
.area-kontakt {
grid-area: kontakt;
}
.area-prae {
grid-area: prae;
}
.area-courses {
grid-area: courses;
}
.area-system {
grid-area: system;
}
.area-memberships {
grid-area: memberships;
}
.area-payments {
grid-area: payments;
}
.area-map {
grid-area: map;
}
</style>

View File

@ -66,7 +66,7 @@
>
</div>
</td>
<td v-text="member.subscription_name"></td>
<td v-text="member.subscription ? member.subscription.name : ''"></td>
<td v-text="`${member.birthday_human} (${member.age})`"></td>
<td v-show="hasModule('bill')">
<div class="flex justify-center">

View File

@ -28,4 +28,36 @@ class EditTest extends TestCase
$this->assertInertiaHas('edit', $response, 'mode');
$this->assertInertiaHas(false, $response, 'conflict');
}
public function testItDisplaysEducation(): void
{
$this->withoutExceptionHandling()->login()->loginNami();
$member = Member::factory()
->defaults()
->create([
'efz' => '2022-09-20',
'ps_at' => '2022-04-20',
'more_ps_at' => '2022-06-02',
'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,
]);
$response = $this->get(route('member.edit', ['member' => $member]));
$this->assertInertiaHas([
'efz' => '2022-09-20',
'ps_at' => '2022-04-20',
'more_ps_at' => '2022-06-02',
'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,
], $response, 'data');
}
}

View File

@ -139,5 +139,9 @@ class IndexTest extends TestCase
'status_name' => 'Nicht bezahlt',
'nr' => '2019',
], $response, 'data.data.0.payments.0');
$this->assertInertiaHas([
'id' => $member->subscription->id,
'name' => $member->subscription->name,
], $response, 'data.data.0.subscription');
}
}

View File

@ -2,9 +2,12 @@
namespace Tests\Feature\Member;
use App\Course\Models\Course;
use App\Course\Models\CourseMember;
use App\Fee;
use App\Gender;
use App\Group;
use App\Letter\BillKind;
use App\Member\Member;
use App\Member\Membership;
use App\Nationality;
@ -35,6 +38,10 @@ class ShowTest extends TestCase
->has(Payment::factory()->notPaid()->nr('2019')->subscription('Free', 1050))
->for(Gender::factory()->name('Männlich'))
->for(Region::factory()->name('NRW'))
->for(BillKind::factory()->name('Post'))
->inNami(123)
->for(Subscription::factory()->name('Sub')->for(Fee::factory()))
->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',
@ -50,6 +57,17 @@ class ShowTest extends TestCase
'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',
'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',
]);
$response = $this->get("/member/{$member->id}");
@ -68,17 +86,45 @@ class ShowTest extends TestCase
'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',
'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',
'subscription' => [
'name' => 'Sub',
],
], $response, 'data');
$this->assertInertiaHas([
'activity_name' => '€ LeiterIn',
'subactivity_name' => 'Jungpfadfinder',
'id' => $member->memberships->first()->id,
'human_date' => '19.11.2022',
], $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([
'subscription' => [
'name' => 'Free',
'id' => $member->payments->first()->subscription->id,
'amount' => 1050,
'amount_human' => '10,50 €',
],
'status_name' => 'Nicht bezahlt',
'nr' => '2019',
@ -103,6 +149,15 @@ class ShowTest extends TestCase
'nationality' => [
'name' => 'deutsch',
],
'efz_human' => null,
'ps_at_human' => null,
'more_ps_at_human' => null,
'without_education_at_human' => null,
'without_efz_at_human' => null,
'has_vk' => false,
'has_svk' => false,
'multiply_pv' => false,
'multiply_more_pv' => false,
], $response, 'data');
}
}

View File

@ -89,15 +89,15 @@ class UpdateTest extends TestCase
'multiply_more_pv' => true,
]));
$this->assertEquals('2021-02-01', $member->fresh()->ps_at);
$this->assertEquals('2021-02-02', $member->fresh()->more_ps_at);
$this->assertEquals('2021-02-01', $member->fresh()->ps_at->format('Y-m-d'));
$this->assertEquals('2021-02-02', $member->fresh()->more_ps_at->format('Y-m-d'));
$this->assertTrue($member->fresh()->has_svk);
$this->assertTrue($member->fresh()->has_vk);
$this->assertTrue($member->fresh()->multiply_pv);
$this->assertTrue($member->fresh()->multiply_more_pv);
$this->assertEquals('2021-02-03', $member->fresh()->efz);
$this->assertEquals('2021-02-04', $member->fresh()->without_education_at);
$this->assertEquals('2021-02-05', $member->fresh()->without_efz_at);
$this->assertEquals('2021-02-03', $member->fresh()->efz->format('Y-m-d'));
$this->assertEquals('2021-02-04', $member->fresh()->without_education_at->format('Y-m-d'));
$this->assertEquals('2021-02-05', $member->fresh()->without_efz_at->format('Y-m-d'));
}
/**