Compare commits
4 Commits
adf9341dd7
...
66b6a549c3
Author | SHA1 | Date |
---|---|---|
|
66b6a549c3 | |
|
878de3f566 | |
|
aa55b56df7 | |
|
31c582e81d |
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Lib\Data;
|
||||||
|
|
||||||
|
use Spatie\LaravelData\Normalizers\Normalizer;
|
||||||
|
use App\Lib\Normalizers\DateNormalizer;
|
||||||
|
use Spatie\LaravelData\Data;
|
||||||
|
use Spatie\LaravelData\Attributes\WithTransformer;
|
||||||
|
use App\Lib\Transformers\DateTransformer;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
class DateData extends Data
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
#[WithTransformer(DateTransformer::class)]
|
||||||
|
public Carbon $raw,
|
||||||
|
public string $human,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, class-string<Normalizer>>
|
||||||
|
*/
|
||||||
|
public static function normalizers(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
DateNormalizer::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Lib\Data;
|
||||||
|
|
||||||
|
use Spatie\LaravelData\Data;
|
||||||
|
|
||||||
|
class RecordData extends Data {
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public int $id,
|
||||||
|
public string $name,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Lib\Normalizers;
|
||||||
|
|
||||||
|
use Spatie\LaravelData\Normalizers\Normalizer;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
class DateNormalizer implements Normalizer
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function normalize(mixed $value): ?array
|
||||||
|
{
|
||||||
|
if (!$value instanceof Carbon) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'raw' => $value,
|
||||||
|
'human' => $value->format('d.m.Y'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Lib\Transformers;
|
||||||
|
|
||||||
|
use Spatie\LaravelData\Transformers\Transformer;
|
||||||
|
use Spatie\LaravelData\Support\DataProperty;
|
||||||
|
use Spatie\LaravelData\Support\Transformation\TransformationContext;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
class DateTransformer implements Transformer
|
||||||
|
{
|
||||||
|
public function transform(DataProperty $property, mixed $value, TransformationContext $context): string
|
||||||
|
{
|
||||||
|
return Carbon::parse($value)->format('Y-m-d');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Member\Data;
|
||||||
|
|
||||||
|
use Spatie\LaravelData\Data;
|
||||||
|
use App\Lib\Data\DateData;
|
||||||
|
use App\Lib\Data\RecordData;
|
||||||
|
use App\Member\Membership;
|
||||||
|
|
||||||
|
class MembershipData extends Data
|
||||||
|
{
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public int $id,
|
||||||
|
public RecordData $activity,
|
||||||
|
public ?RecordData $subactivity,
|
||||||
|
public RecordData $group,
|
||||||
|
public ?DateData $promisedAt,
|
||||||
|
public DateData $from,
|
||||||
|
public bool $isActive,
|
||||||
|
public array $links,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static function fromModel(Membership $membership): static
|
||||||
|
{
|
||||||
|
return static::factory()->withoutMagicalCreation()->from([
|
||||||
|
'id' => $membership->id,
|
||||||
|
'activity' => $membership->activity,
|
||||||
|
'subactivity' => $membership->subactivity,
|
||||||
|
'isActive' => $membership->isActive(),
|
||||||
|
'from' => $membership->from,
|
||||||
|
'group' => $membership->group,
|
||||||
|
'promisedAt' => $membership->promised_at,
|
||||||
|
'links' => [
|
||||||
|
'update' => route('membership.update', $membership),
|
||||||
|
'destroy' => route('membership.destroy', $membership),
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ use Illuminate\Database\Eloquent\Collection;
|
||||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||||
use Lorisleiva\Actions\Concerns\AsAction;
|
use Lorisleiva\Actions\Concerns\AsAction;
|
||||||
|
|
||||||
class IndexAction
|
class MemberIndexAction
|
||||||
{
|
{
|
||||||
use AsAction;
|
use AsAction;
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Membership\Actions;
|
||||||
|
|
||||||
|
use App\Member\Data\MembershipData;
|
||||||
|
use App\Member\Membership;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Inertia\Response;
|
||||||
|
use Lorisleiva\Actions\Concerns\AsAction;
|
||||||
|
use Spatie\LaravelData\PaginatedDataCollection;
|
||||||
|
|
||||||
|
class MembershipIndexAction
|
||||||
|
{
|
||||||
|
use AsAction;
|
||||||
|
|
||||||
|
public function asController(): Response
|
||||||
|
{
|
||||||
|
return Inertia::render(
|
||||||
|
'membership/Index',
|
||||||
|
MembershipData::collect(Membership::orderByRaw('member_id, activity_id, subactivity_id')->paginate(20), PaginatedDataCollection::class)->wrap('data')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -98,6 +98,12 @@ class MemberFactory extends Factory
|
||||||
return $this->state(['nami_id' => null]);
|
return $this->state(['nami_id' => null]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function name(string $name): self
|
||||||
|
{
|
||||||
|
[$firstname, $lastname] = explode(' ', $name);
|
||||||
|
return $this->state(compact('firstname', 'lastname'));
|
||||||
|
}
|
||||||
|
|
||||||
public function withBankAccount(BankAccountFactory $factory): self
|
public function withBankAccount(BankAccountFactory $factory): self
|
||||||
{
|
{
|
||||||
return $this->afterCreating(function ($member) use ($factory) {
|
return $this->afterCreating(function ($member) use ($factory) {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -23,20 +23,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&.custom-table-light {
|
&.custom-table-light {
|
||||||
& > thead > th {
|
& > thead > th, & > thead > tr > th {
|
||||||
@apply border-gray-500;
|
@apply border-gray-500;
|
||||||
}
|
}
|
||||||
& > td {
|
& > tr, & > tbody > tr {
|
||||||
&:hover {
|
@apply hover:bg-gray-700;
|
||||||
@apply bg-gray-700;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
.custom-table > * {
|
|
||||||
display: table-row;
|
|
||||||
}
|
|
||||||
.custom-table > * > * {
|
|
||||||
display: table-cell;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,9 @@ use App\Membership\Actions\IndexAction as MembershipIndexAction;
|
||||||
use App\Membership\Actions\ListForGroupAction;
|
use App\Membership\Actions\ListForGroupAction;
|
||||||
use App\Membership\Actions\MassListAction;
|
use App\Membership\Actions\MassListAction;
|
||||||
use App\Membership\Actions\MassStoreAction;
|
use App\Membership\Actions\MassStoreAction;
|
||||||
|
use App\Membership\Actions\MemberIndexAction;
|
||||||
use App\Membership\Actions\MembershipDestroyAction;
|
use App\Membership\Actions\MembershipDestroyAction;
|
||||||
|
use App\Membership\Actions\MembershipIndexAction as ActionsMembershipIndexAction;
|
||||||
use App\Membership\Actions\MembershipStoreAction;
|
use App\Membership\Actions\MembershipStoreAction;
|
||||||
use App\Membership\Actions\MembershipUpdateAction;
|
use App\Membership\Actions\MembershipUpdateAction;
|
||||||
use App\Payment\SubscriptionController;
|
use App\Payment\SubscriptionController;
|
||||||
|
@ -140,13 +142,14 @@ Route::group(['middleware' => 'auth:web'], function (): void {
|
||||||
Route::get('/member/{member}/invoice-position', PaymentPositionIndexAction::class)->name('member.invoice-position.index');
|
Route::get('/member/{member}/invoice-position', PaymentPositionIndexAction::class)->name('member.invoice-position.index');
|
||||||
|
|
||||||
// --------------------------------- membership --------------------------------
|
// --------------------------------- membership --------------------------------
|
||||||
Route::get('/member/{member}/membership', MembershipIndexAction::class)->name('member.membership.index');
|
Route::get('/member/{member}/membership', MemberIndexAction::class)->name('member.membership.index');
|
||||||
Route::post('/member/{member}/membership', MembershipStoreAction::class)->name('member.membership.store');
|
Route::post('/member/{member}/membership', MembershipStoreAction::class)->name('member.membership.store');
|
||||||
Route::patch('/membership/{membership}', MembershipUpdateAction::class)->name('membership.update');
|
Route::patch('/membership/{membership}', MembershipUpdateAction::class)->name('membership.update');
|
||||||
Route::delete('/membership/{membership}', MembershipDestroyAction::class)->name('membership.destroy');
|
Route::delete('/membership/{membership}', MembershipDestroyAction::class)->name('membership.destroy');
|
||||||
Route::post('/api/membership/member-list', ListForGroupAction::class)->name('membership.member-list');
|
Route::post('/api/membership/member-list', ListForGroupAction::class)->name('membership.member-list');
|
||||||
Route::post('/api/membership/masslist', MassStoreAction::class)->name('membership.masslist.store');
|
Route::post('/api/membership/masslist', MassStoreAction::class)->name('membership.masslist.store');
|
||||||
Route::get('/membership/masslist', MassListAction::class)->name('membership.masslist.index');
|
Route::get('/membership/masslist', MassListAction::class)->name('membership.masslist.index');
|
||||||
|
Route::get('/membership', ActionsMembershipIndexAction::class)->name('membership.index');
|
||||||
|
|
||||||
// ----------------------------------- group ----------------------------------
|
// ----------------------------------- group ----------------------------------
|
||||||
Route::get('/group', GroupIndexAction::class)->name('group.index');
|
Route::get('/group', GroupIndexAction::class)->name('group.index');
|
||||||
|
|
|
@ -63,7 +63,7 @@ it('cannot create membership when activity and subactivity doesnt belong togethe
|
||||||
])->assertJsonValidationErrors(['activity_id' => 'Tätigkeit ist nicht vorhanden.']);
|
])->assertJsonValidationErrors(['activity_id' => 'Tätigkeit ist nicht vorhanden.']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('testItDeletesAMembership', function() {
|
it('deletes a membership', function() {
|
||||||
MembershipDestroyAction::partialMock()->shouldReceive('handle')->once();
|
MembershipDestroyAction::partialMock()->shouldReceive('handle')->once();
|
||||||
MembershipStoreAction::partialMock()->shouldReceive('handle')->never();
|
MembershipStoreAction::partialMock()->shouldReceive('handle')->never();
|
||||||
ResyncAction::partialMock()->shouldReceive('handle')->once();
|
ResyncAction::partialMock()->shouldReceive('handle')->once();
|
||||||
|
@ -73,7 +73,7 @@ it('testItDeletesAMembership', function() {
|
||||||
MassStoreAction::run($member->memberships->first()->group, $member->memberships->first()->activity, $member->memberships->first()->subactivity, []);
|
MassStoreAction::run($member->memberships->first()->group, $member->memberships->first()->activity, $member->memberships->first()->subactivity, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('testItRollsbackWhenDeletionFails', function() {
|
it('rolls back when deletion fails', function() {
|
||||||
app(MembershipFake::class)
|
app(MembershipFake::class)
|
||||||
->shows(3, ['id' => 55])
|
->shows(3, ['id' => 55])
|
||||||
->shows(3, ['id' => 56])
|
->shows(3, ['id' => 56])
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Membership;
|
||||||
|
|
||||||
|
use App\Activity;
|
||||||
|
use App\Group;
|
||||||
|
use App\Member\Data\MembershipData;
|
||||||
|
use App\Member\Member;
|
||||||
|
use App\Member\Membership;
|
||||||
|
use App\Subactivity;
|
||||||
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||||
|
use Inertia\Testing\AssertableInertia as Assert;
|
||||||
|
|
||||||
|
uses(DatabaseTransactions::class);
|
||||||
|
|
||||||
|
mutates(MembershipData::class);
|
||||||
|
|
||||||
|
it('lists memberships of users', function () {
|
||||||
|
$this->login()->loginNami()->withoutExceptionHandling();
|
||||||
|
$activity = Activity::factory()
|
||||||
|
->hasAttached(Subactivity::factory()->name('SubAct'))
|
||||||
|
->name('Act')
|
||||||
|
->create();
|
||||||
|
$group = Group::factory()->name('GG')->create();
|
||||||
|
$member = Member::factory()->defaults()
|
||||||
|
->for($group)
|
||||||
|
->has(Membership::factory()->for($activity)->for($activity->subactivities->first())->for($group))
|
||||||
|
->male()
|
||||||
|
->name('Max Muster')
|
||||||
|
->create();
|
||||||
|
|
||||||
|
$activity->subactivities()->first();
|
||||||
|
$this->callFilter('membership.index', [])
|
||||||
|
->assertInertia(fn(Assert $page) => $page
|
||||||
|
->has('data.0', fn(Assert $page) => $page
|
||||||
|
->where('activity.name', 'Act')
|
||||||
|
->where('subactivity.name', 'SubAct')
|
||||||
|
->where('group.name', 'GG')
|
||||||
|
->where('promisedAt', null)
|
||||||
|
->where('links.update', route('membership.update', $member->memberships->first()))
|
||||||
|
->where('links.destroy', route('membership.destroy', $member->memberships->first()))
|
||||||
|
->etc()
|
||||||
|
)->has('meta', fn (Assert $page) => $page
|
||||||
|
->where('current_page', 1)
|
||||||
|
->etc()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -171,5 +183,13 @@ class TestCase extends BaseTestCase
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
TestResponse::macro('assertNull', function (string $path) {
|
||||||
|
/** @var TestResponse */
|
||||||
|
$response = $this;
|
||||||
|
$response->assertHasJsonPath($path)->assertJsonPath($path, null);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue