Compare commits

...

13 Commits
master ... ts

Author SHA1 Message Date
philipp lang 33149e8b79 --wip-- [skip ci] 2025-06-12 21:18:43 +02:00
philipp lang 50878a9a3c Add ts for Tabs 2025-06-12 18:31:46 +02:00
philipp lang 63799c87ec Fix table structure 2025-06-12 18:30:44 +02:00
philipp lang 7b00d1d3ee Fix tests
continuous-integration/drone/push Build is passing Details
2025-06-12 17:20:46 +02:00
philipp lang d837bb48a4 lint 2025-06-12 17:06:19 +02:00
philipp lang 55b3bc7fe9 Fix tests
continuous-integration/drone/push Build is failing Details
2025-06-12 16:55:16 +02:00
philipp lang bf067d7352 Mod Membership IndexTest
continuous-integration/drone/push Build is failing Details
2025-06-12 02:17:26 +02:00
philipp lang ef4cf07647 Lint Member ShowTest
continuous-integration/drone/push Build is failing Details
2025-06-12 01:48:21 +02:00
philipp lang 7b54d29345 Fix ShowTest 2025-06-12 00:17:04 +02:00
philipp lang fd8c0f1085 Lint
continuous-integration/drone/push Build is failing Details
2025-06-11 23:58:00 +02:00
philipp lang 286148de2e Remove TypeScript attribute 2025-06-11 23:55:42 +02:00
philipp lang 0c4e12132a Add types for membership
continuous-integration/drone/push Build is failing Details
2025-06-11 23:25:50 +02:00
philipp lang 2e89002641 Move Membership to data
continuous-integration/drone/push Build is failing Details
2025-06-11 22:33:03 +02:00
36 changed files with 1474 additions and 538 deletions

34
app/Lib/Data/DateData.php Normal file
View File

@ -0,0 +1,34 @@
<?php
namespace App\Lib\Data;
use Spatie\LaravelData\Normalizers\Normalizer;
use App\Lib\Normalizers\DateNormalizer;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Attributes\WithTransformer;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
use App\Lib\Transformers\DateTransformer;
use Carbon\Carbon;
#[MapInputName(SnakeCaseMapper::class)]
#[MapOutputName(SnakeCaseMapper::class)]
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,
];
}
}

View File

@ -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'),
];
}
}

View File

@ -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');
}
}

View File

@ -17,16 +17,19 @@ class MemberShowAction
*/ */
public function handle(Member $member): array public function handle(Member $member): array
{ {
$member = Member::withPendingPayment()->with([
'memberships.activity',
'memberships.subactivity',
'invoicePositions.invoice',
'nationality',
'region',
'subscription',
'courses.course',
'bankAccount',
])->find($member->id);
return [ return [
'data' => new MemberResource( 'data' => new MemberResource($member),
$member
->load('memberships')
->load('invoicePositions.invoice')
->load('nationality')
->load('region')
->load('subscription')
->load('courses.course')
),
'meta' => MemberResource::meta(), 'meta' => MemberResource::meta(),
]; ];
} }

View File

@ -0,0 +1,19 @@
<?php
namespace App\Member\Data;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
#[MapInputName(SnakeCaseMapper::class)]
#[MapOutputName(SnakeCaseMapper::class)]
class ActivityData extends Data {
public function __construct(
public int $id,
public string $name,
) {}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Member\Data;
use App\Group;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
#[MapInputName(SnakeCaseMapper::class)]
#[MapOutputName(SnakeCaseMapper::class)]
class GroupData extends Data {
public function __construct(
public int $id,
public string $name,
) {}
public static function fromId(int $id): static {
$group = Group::findOrFail($id);
return static::from([
'name' => $group->name,
'id' => $group->id,
]);
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace App\Member\Data;
use Spatie\LaravelData\Data;
use App\Member\Membership;
use App\Member\Member;
use App\Lib\Data\DateData;
class MembershipData extends Data
{
public function __construct(
public ?ActivityData $activity = null,
public ?SubactivityData $subactivity = null,
public ?GroupData $group = null,
public ?DateData $promisedAt = null,
public ?DateData $from = null,
public bool $isActive = false,
public array $links = [],
) {}
public static function fromModel(Membership $membership): static
{
return static::factory()->withoutMagicalCreation()->from([
'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),
]
]);
}
/**
* @return array<string, mixed>
*/
public static function memberMeta(Member $member): MembershipMeta
{
return MembershipMeta::fromMember($member);
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace App\Member\Data;
use App\Activity;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
use App\Member\Member;
use Illuminate\Support\Collection;
#[MapInputName(SnakeCaseMapper::class)]
#[MapOutputName(SnakeCaseMapper::class)]
class MembershipMeta extends Data
{
public function __construct(
public array $links,
public Collection $groups,
public Collection $activities,
public Collection $subactivities,
public MembershipData $default,
) {}
public static function fromMember(Member $member): static
{
$activities = Activity::with('subactivities')->get();
return static::factory()->withoutMagicalCreation()->from([
'links' => [
'store' => route('member.membership.store', ['member' => $member]),
],
'groups' => NestedGroup::cacheForSelect(),
'activities' => $activities->map(fn($activity) => ['id' => $activity->id, 'name' => $activity->name]),
'subactivities' => $activities->mapWithKeys(fn($activity) => [$activity->id => $activity->subactivities->map(fn($subactivity) => ['id' => $subactivity->id, 'name' => $subactivity->name, 'is_age_group' => $subactivity->is_age_group])]),
'default' => MembershipData::from(['group' => $member->group_id]),
]);
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Member\Data;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
#[MapInputName(SnakeCaseMapper::class)]
#[MapOutputName(SnakeCaseMapper::class)]
class SubactivityData extends Data {
public function __construct(
public int $id,
public string $name,
) {}
}

View File

@ -14,7 +14,7 @@ use App\Member\Data\NestedGroup;
use App\Member\Resources\BankAccountResource; use App\Member\Resources\BankAccountResource;
use App\Member\Resources\NationalityResource; use App\Member\Resources\NationalityResource;
use App\Member\Resources\RegionResource; use App\Member\Resources\RegionResource;
use App\Membership\MembershipResource; use App\Member\Data\MembershipData;
use App\Nationality; use App\Nationality;
use App\Payment\Subscription; use App\Payment\Subscription;
use App\Payment\SubscriptionResource; use App\Payment\SubscriptionResource;
@ -75,7 +75,7 @@ class MemberResource extends JsonResource
'pending_payment' => $this->pending_payment ? number_format($this->pending_payment / 100, 2, ',', '.') . ' €' : null, 'pending_payment' => $this->pending_payment ? number_format($this->pending_payment / 100, 2, ',', '.') . ' €' : null,
'age_group_icon' => $this->ageGroupMemberships->first()?->subactivity->slug, 'age_group_icon' => $this->ageGroupMemberships->first()?->subactivity->slug,
'courses' => CourseMemberResource::collection($this->whenLoaded('courses')), 'courses' => CourseMemberResource::collection($this->whenLoaded('courses')),
'memberships' => MembershipResource::collection($this->whenLoaded('memberships')), 'memberships' => $this->relationLoaded('memberships') ? MembershipData::collect($this->memberships) : null,
'invoicePositions' => InvoicePositionResource::collection($this->whenLoaded('invoicePositions')), 'invoicePositions' => InvoicePositionResource::collection($this->whenLoaded('invoicePositions')),
'nationality' => new NationalityResource($this->whenLoaded('nationality')), 'nationality' => new NationalityResource($this->whenLoaded('nationality')),
'region' => new RegionResource($this->whenLoaded('region')), 'region' => new RegionResource($this->whenLoaded('region')),
@ -138,7 +138,7 @@ class MemberResource extends JsonResource
return [ return [
'filterActivities' => Activity::where('is_filterable', true)->get()->map(fn($a) => ['id' => $a->id, 'name' => $a->name]), 'filterActivities' => Activity::where('is_filterable', true)->get()->map(fn($a) => ['id' => $a->id, 'name' => $a->name]),
'filterSubactivities' => Subactivity::where('is_filterable', true)->get()->map(fn($a) => ['id' => $a->id, 'name' => $a->name]), 'filterSubactivities' => Subactivity::where('is_filterable', true)->get()->map(fn($a) => ['id' => $a->id, 'name' => $a->name]),
'formActivities' => $activities->pluck('name', 'id'), 'formActivities' => $activities->map(fn($a) => ['id' => $a->id, 'name' => $a->name]),
'formSubactivities' => $activities->map(function (Activity $activity) { 'formSubactivities' => $activities->map(function (Activity $activity) {
return ['subactivities' => $activity->subactivities->pluck('name', 'id'), 'id' => $activity->id]; return ['subactivities' => $activity->subactivities->pluck('name', 'id'), 'id' => $activity->id];
})->pluck('subactivities', 'id'), })->pluck('subactivities', 'id'),
@ -148,11 +148,11 @@ class MemberResource extends JsonResource
})->pluck('subactivities', 'id'), })->pluck('subactivities', 'id'),
'groups' => NestedGroup::cacheForSelect(), 'groups' => NestedGroup::cacheForSelect(),
'filter' => FilterScope::fromRequest(request()->input('filter', '')), 'filter' => FilterScope::fromRequest(request()->input('filter', '')),
'courses' => Course::pluck('name', 'id'), 'courses' => Course::get()->map(fn($a) => ['id' => $a->id, 'name' => $a->name]),
'regions' => Region::forSelect(), 'regions' => Region::get()->map(fn($a) => ['id' => $a->id, 'name' => $a->name]),
'subscriptions' => Subscription::pluck('name', 'id'), 'subscriptions' => Subscription::get()->map(fn($a) => ['id' => $a->id, 'name' => $a->name]),
'countries' => Country::pluck('name', 'id'), 'countries' => Country::get()->map(fn($c) => ['id' => $c->id, 'name' => $c->name]),
'genders' => Gender::pluck('name', 'id'), 'genders' => Gender::get()->map(fn($c) => ['id' => $c->id, 'name' => $c->name]),
'billKinds' => BillKind::forSelect(), 'billKinds' => BillKind::forSelect(),
'nationalities' => Nationality::pluck('name', 'id'), 'nationalities' => Nationality::pluck('name', 'id'),
'members' => Member::ordered()->get()->map(fn($member) => ['id' => $member->id, 'name' => $member->fullname]), 'members' => Member::ordered()->get()->map(fn($member) => ['id' => $member->id, 'name' => $member->fullname]),

View File

@ -4,10 +4,10 @@ namespace App\Membership\Actions;
use App\Member\Member; use App\Member\Member;
use App\Member\Membership; use App\Member\Membership;
use App\Membership\MembershipResource;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
use App\Member\Data\MembershipData;
use Illuminate\Http\JsonResponse;
class IndexAction class IndexAction
{ {
@ -21,11 +21,11 @@ class IndexAction
return $member->memberships; return $member->memberships;
} }
public function asController(Member $member): AnonymousResourceCollection public function asController(Member $member): JsonResponse
{ {
return MembershipResource::collection($this->handle($member)) return response()->json([
->additional([ 'data' => MembershipData::collect($this->handle($member)),
'meta' => MembershipResource::memberMeta($member) 'meta' => MembershipData::memberMeta($member),
]); ]);
} }
} }

View File

@ -1,64 +0,0 @@
<?php
namespace App\Membership;
use App\Activity;
use App\Lib\HasMeta;
use App\Member\Data\NestedGroup;
use App\Member\Member;
use Illuminate\Http\Resources\Json\JsonResource;
/**
* @mixin \App\Member\Membership
*/
class MembershipResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
*
* @return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'group_id' => $this->group_id,
'activity_id' => $this->activity_id,
'activity_name' => $this->activity->name,
'subactivity_id' => $this->subactivity_id,
'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(),
'links' => [
'update' => route('membership.update', ['membership' => $this->getModel()]),
'destroy' => route('membership.destroy', ['membership' => $this->getModel()]),
]
];
}
/**
* @return array<string, mixed>
*/
public static function memberMeta(Member $member): array
{
$activities = Activity::with('subactivities')->get();
return [
'links' => [
'store' => route('member.membership.store', ['member' => $member]),
],
'groups' => NestedGroup::cacheForSelect(),
'activities' => $activities->map(fn ($activity) => ['id' => $activity->id, 'name' => $activity->name]),
'subactivities' => $activities->mapWithKeys(fn ($activity) => [$activity->id => $activity->subactivities->map(fn ($subactivity) => ['id' => $subactivity->id, 'name' => $subactivity->name, 'is_age_group' => $subactivity->is_age_group])]),
'default' => [
'group_id' => $member->group_id,
'activity_id' => null,
'subactivity_id' => null,
'promised_at' => null,
],
];
}
}

View File

@ -70,6 +70,7 @@
"spatie/laravel-data": "^4.0", "spatie/laravel-data": "^4.0",
"spatie/laravel-ignition": "^2.0", "spatie/laravel-ignition": "^2.0",
"spatie/laravel-settings": "^3.0", "spatie/laravel-settings": "^3.0",
"spatie/laravel-typescript-transformer": "^2.5",
"worksome/request-factories": "^3.0", "worksome/request-factories": "^3.0",
"zoomyboy/laravel-nami": "dev-master", "zoomyboy/laravel-nami": "dev-master",
"zoomyboy/medialibrary-helper": "dev-master as 1.0", "zoomyboy/medialibrary-helper": "dev-master as 1.0",

186
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "b901e66375d81c04e5067a7352780f13", "content-hash": "e5a34643b9d5b3f0fe8e4cc9c7e9570d",
"packages": [ "packages": [
{ {
"name": "amphp/amp", "name": "amphp/amp",
@ -11389,6 +11389,87 @@
], ],
"time": "2025-02-14T14:40:11+00:00" "time": "2025-02-14T14:40:11+00:00"
}, },
{
"name": "spatie/laravel-typescript-transformer",
"version": "2.5.2",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-typescript-transformer.git",
"reference": "a268a08341f5a5d8f80a79493642a43275d219a5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-typescript-transformer/zipball/a268a08341f5a5d8f80a79493642a43275d219a5",
"reference": "a268a08341f5a5d8f80a79493642a43275d219a5",
"shasum": ""
},
"require": {
"illuminate/console": "^8.83|^9.30|^10.0|^11.0|^12.0",
"php": "^8.1",
"spatie/laravel-package-tools": "^1.12",
"spatie/typescript-transformer": "^2.4"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.0",
"mockery/mockery": "^1.4",
"nesbot/carbon": "^2.63|^3.0",
"orchestra/testbench": "^6.0|^7.0|^8.0|^9.0|^10.0",
"pestphp/pest": "^1.22|^2.0|^3.0",
"phpunit/phpunit": "^9.0|^10.0|^11.0|^12.0",
"spatie/data-transfer-object": "^2.0",
"spatie/enum": "^3.0",
"spatie/laravel-model-states": "^1.6|^2.0",
"spatie/pest-plugin-snapshots": "^1.1|^2.0",
"spatie/phpunit-snapshot-assertions": "^4.2|^5.0",
"spatie/temporary-directory": "^1.2"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Spatie\\LaravelTypeScriptTransformer\\TypeScriptTransformerServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Spatie\\LaravelTypeScriptTransformer\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ruben Van Assche",
"email": "ruben@spatie.be",
"homepage": "https://spatie.be",
"role": "Developer"
}
],
"description": "Transform your PHP structures to TypeScript types",
"homepage": "https://github.com/spatie/typescript-transformer",
"keywords": [
"spatie",
"typescript-transformer"
],
"support": {
"issues": "https://github.com/spatie/laravel-typescript-transformer/issues",
"source": "https://github.com/spatie/laravel-typescript-transformer/tree/2.5.2"
},
"funding": [
{
"url": "https://spatie.be/open-source/support-us",
"type": "custom"
},
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2025-04-25T14:09:28+00:00"
},
{ {
"name": "spatie/php-structure-discoverer", "name": "spatie/php-structure-discoverer",
"version": "2.3.1", "version": "2.3.1",
@ -11529,6 +11610,78 @@
], ],
"time": "2025-01-13T13:04:43+00:00" "time": "2025-01-13T13:04:43+00:00"
}, },
{
"name": "spatie/typescript-transformer",
"version": "2.5.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/typescript-transformer.git",
"reference": "dd7cbb90b6b8c34f2aee68701cf39c5432400c0d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/typescript-transformer/zipball/dd7cbb90b6b8c34f2aee68701cf39c5432400c0d",
"reference": "dd7cbb90b6b8c34f2aee68701cf39c5432400c0d",
"shasum": ""
},
"require": {
"nikic/php-parser": "^4.18|^5.0",
"php": "^8.1",
"phpdocumentor/type-resolver": "^1.6.2",
"symfony/process": "^5.2|^6.0|^7.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.40",
"larapack/dd": "^1.1",
"myclabs/php-enum": "^1.7",
"pestphp/pest": "^1.22",
"phpstan/extension-installer": "^1.1",
"phpunit/phpunit": "^9.0",
"spatie/data-transfer-object": "^2.0",
"spatie/enum": "^3.0",
"spatie/pest-plugin-snapshots": "^1.1",
"spatie/temporary-directory": "^1.2|^2.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Spatie\\TypeScriptTransformer\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ruben Van Assche",
"email": "ruben@spatie.be",
"homepage": "https://spatie.be",
"role": "Developer"
}
],
"description": "Transform your PHP structures to TypeScript types",
"homepage": "https://github.com/spatie/typescript-transformer",
"keywords": [
"spatie",
"typescript-transformer"
],
"support": {
"issues": "https://github.com/spatie/typescript-transformer/issues",
"source": "https://github.com/spatie/typescript-transformer/tree/2.5.0"
},
"funding": [
{
"url": "https://spatie.be/open-source/support-us",
"type": "custom"
},
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2025-04-25T13:53:57+00:00"
},
{ {
"name": "staabm/side-effects-detector", "name": "staabm/side-effects-detector",
"version": "1.0.5", "version": "1.0.5",
@ -14702,7 +14855,7 @@
"dist": { "dist": {
"type": "path", "type": "path",
"url": "./packages/tex", "url": "./packages/tex",
"reference": "48251272de62e3fea044a7ad31e1a411c15eb4c6" "reference": "ed283d97ca7680b3c27b2d75da9937f4f379e321"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
@ -15792,32 +15945,7 @@
"time": "2025-03-03T07:12:39+00:00" "time": "2025-03-03T07:12:39+00:00"
} }
], ],
"aliases": [ "aliases": [],
{
"package": "league/flysystem-webdav",
"version": "9999999-dev",
"alias": "3.28.0",
"alias_normalized": "3.28.0.0"
},
{
"package": "zoomyboy/medialibrary-helper",
"version": "9999999-dev",
"alias": "1.0",
"alias_normalized": "1.0.0.0"
},
{
"package": "zoomyboy/table-document",
"version": "9999999-dev",
"alias": "1.0",
"alias_normalized": "1.0.0.0"
},
{
"package": "zoomyboy/tex",
"version": "dev-main",
"alias": "1.0",
"alias_normalized": "1.0.0.0"
}
],
"minimum-stability": "dev", "minimum-stability": "dev",
"stability-flags": { "stability-flags": {
"league/flysystem-webdav": 20, "league/flysystem-webdav": 20,
@ -15832,5 +15960,5 @@
"php": "^8.3" "php": "^8.3"
}, },
"platform-dev": [], "platform-dev": [],
"plugin-api-version": "2.6.0" "plugin-api-version": "2.2.0"
} }

View File

@ -0,0 +1,88 @@
<?php
return [
/*
* The paths where typescript-transformer will look for PHP classes
* to transform, this will be the `app` path by default.
*/
'auto_discover_types' => [
app_path(),
],
/*
* Collectors will search for classes in the `auto_discover_types` paths and choose the correct
* transformer to transform them. By default, we include a DefaultCollector which will search
* for @typescript annotated and #[TypeScript] attributed classes to transform.
*/
'collectors' => [
Spatie\TypeScriptTransformer\Collectors\DefaultCollector::class,
Spatie\TypeScriptTransformer\Collectors\EnumCollector::class,
Spatie\LaravelData\Support\TypeScriptTransformer\DataTypeScriptCollector::class,
],
/*
* Transformers take PHP classes(e.g., enums) as an input and will output
* a TypeScript representation of the PHP class.
*/
'transformers' => [
Spatie\LaravelTypeScriptTransformer\Transformers\SpatieStateTransformer::class,
Spatie\TypeScriptTransformer\Transformers\EnumTransformer::class,
Spatie\TypeScriptTransformer\Transformers\SpatieEnumTransformer::class,
Spatie\LaravelData\Support\TypeScriptTransformer\DataTypeScriptTransformer::class,
Spatie\LaravelTypeScriptTransformer\Transformers\DtoTransformer::class,
],
/*
* In your classes, you sometimes have types that should always be replaced
* by the same TypeScript representations. For example, you can replace a
* Datetime always with a string. You define these replacements here.
*/
'default_type_replacements' => [
DateTime::class => 'string',
DateTimeImmutable::class => 'string',
Carbon\CarbonInterface::class => 'string',
Carbon\CarbonImmutable::class => 'string',
Carbon\Carbon::class => 'string',
],
/*
* The package will write the generated TypeScript to this file.
*/
'output_file' => resource_path('types/generated.d.ts'),
/*
* When the package is writing types to the output file, a writer is used to
* determine the format. By default, this is the `TypeDefinitionWriter`.
* But you can also use the `ModuleWriter` or implement your own.
*/
'writer' => Spatie\TypeScriptTransformer\Writers\TypeDefinitionWriter::class,
/*
* The generated TypeScript file can be formatted. We ship a Prettier formatter
* out of the box: `PrettierFormatter` but you can also implement your own one.
* The generated TypeScript will not be formatted when no formatter was set.
*/
'formatter' => null,
/*
* Enums can be transformed into types or native TypeScript enums, by default
* the package will transform them to types.
*/
'transform_to_native_enums' => false,
/*
* By default, this package will convert PHP nullable properties to TypeScript
* types using a `null` type union. Setting `transform_null_to_optional` will
* make them optional instead.
*/
'transform_null_to_optional' => false,
];

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;
} }
@ -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;
} }

View File

@ -1,17 +1,17 @@
<template> <template>
<label class="flex flex-col group" :for="id" :class="sizeClass(size)"> <label class="flex flex-col group" :for="id" :class="sizeClass(size)">
<f-label v-if="label" :required="required" :value="label"></f-label> <f-label v-if="label" :required="required" :value="label" />
<div class="relative flex-none flex"> <div class="relative flex-none flex">
<select v-model="inner" :disabled="disabled" :name="name" :class="[fieldHeight, fieldAppearance, selectAppearance]"> <select v-model="inner" :disabled="disabled" :name="name" :class="[fieldHeight, fieldAppearance, selectAppearance]">
<option v-if="placeholder" :value="def">{{ placeholder }}</option> <option v-if="placeholder" :value="def">{{ placeholder }}</option>
<option v-for="option in parsedOptions" :key="option.id" :value="option.id">{{ option.name }}</option> <option v-for="option in parsedOptions" :key="option.id" :value="option.id">{{ option.name }}</option>
</select> </select>
<f-hint v-if="hint" :value="hint"></f-hint> <f-hint v-if="hint" :value="hint" />
</div> </div>
</label> </label>
</template> </template>
<script setup> <script lang="ts" setup>
import {computed, ref} from 'vue'; import {computed, ref} from 'vue';
import useFieldSize from '../../composables/useFieldSize.js'; import useFieldSize from '../../composables/useFieldSize.js';
import map from 'lodash/map'; import map from 'lodash/map';
@ -59,11 +59,6 @@ const props = defineProps({
default: '--kein--', default: '--kein--',
type: String, type: String,
}, },
def: {
required: false,
type: Number,
default: -1,
},
name: { name: {
required: true, required: true,
}, },
@ -79,8 +74,8 @@ const parsedOptions = computed(() => {
return Array.isArray(props.options) return Array.isArray(props.options)
? props.options ? props.options
: map(props.options, (value, key) => { : map(props.options, (value, key) => {
return {name: value, id: key}; return {name: value, id: key};
}); });
}); });
const def = ref('iu1Feixah5AeKai3ewooJahjeaegee0eiD4maeth1oul4Hei7u'); const def = ref('iu1Feixah5AeKai3ewooJahjeaegee0eiD4maeth1oul4Hei7u');

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,84 +1,73 @@
import {ref, inject, onBeforeUnmount} from 'vue'; import {ref, inject, onBeforeUnmount} from 'vue';
import {router} from '@inertiajs/vue3'; import {router} from '@inertiajs/vue3';
import type {Ref} from 'vue';
import useQueueEvents from './useQueueEvents.js'; import useQueueEvents from './useQueueEvents.js';
import { Axios } from 'axios';
export function useApiIndex(firstUrl, siteName = null) { export function useApiIndex<D, M extends Custom.PageMetadata>(firstUrl, siteName = null) {
const axios = inject('axios'); const axios = inject<Axios>('axios');
if (siteName !== null) { if (siteName !== null) {
var {startListener, stopListener} = useQueueEvents(siteName, () => reload()); var {startListener, stopListener} = useQueueEvents(siteName, () => reload());
} }
const single = ref(null); const single: Ref<D|null> = ref(null);
const url = ref(firstUrl); const url = ref(firstUrl);
const inner = { const inner: {data: Ref<D[]|null>, meta: Ref<M|null>} = {
data: ref([]), data: ref(null),
meta: ref({}), meta: ref(null),
}; };
async function reload(resetPage = true, p = {}) { async function reload(resetPage = true, p = {}) {
var params = { const params = {
page: resetPage ? 1 : inner.meta.value.current_page, page: resetPage ? 1 : inner.meta.value?.current_page,
...p, ...p,
}; };
var response = (await axios.get(url.value, {params})).data; const response = (await axios.get(url.value, {params})).data;
inner.data.value = response.data; inner.data.value = response.data;
inner.meta.value = response.meta; inner.meta.value = response.meta;
} }
async function reloadPage(page, p = {}) { async function reloadPage(page: number, p = {}) {
inner.meta.value.current_page = page; if (inner.meta.value?.current_page) {
inner.meta.value.current_page = page;
}
await reload(false, p); await reload(false, p);
} }
function create() { function create() {
single.value = JSON.parse(JSON.stringify(inner.meta.value.default)); single.value = JSON.parse(JSON.stringify(inner.meta.value?.default));
} }
function edit(model) { function edit(model: D) {
single.value = JSON.parse(JSON.stringify(model)); single.value = JSON.parse(JSON.stringify(model));
} }
async function submit() { async function submit() {
if (single.value === null) {
return;
}
single.value.id ? await axios.patch(single.value.links.update, single.value) : await axios.post(inner.meta.value.links.store, single.value); single.value.id ? await axios.patch(single.value.links.update, single.value) : await axios.post(inner.meta.value.links.store, single.value);
await reload(); await reload();
single.value = null; single.value = null;
} }
async function remove(model) { async function remove(model: D) {
await axios.delete(model.links.destroy); await axios.delete(model.links.destroy);
await reload(); await reload();
} }
function can(permission) {
return inner.meta.value.can[permission];
}
function toFilterString(data) { function toFilterString(data) {
return btoa(encodeURIComponent(JSON.stringify(data))); return btoa(encodeURIComponent(JSON.stringify(data)));
} }
function requestCallback(successMessage, failureMessage) {
return {
onSuccess: () => {
this.$success(successMessage);
reload(false);
},
onFailure: () => {
this.$error(failureMessage);
reload(false);
},
preserveState: true,
};
}
function cancel() { function cancel() {
single.value = null; single.value = null;
} }
function updateUrl(newUrl) { function updateUrl(newUrl: string) {
url.value = newUrl; url.value = newUrl;
} }
@ -95,8 +84,6 @@ export function useApiIndex(firstUrl, siteName = null) {
edit, edit,
reload, reload,
reloadPage, reloadPage,
can,
requestCallback,
router, router,
submit, submit,
remove, remove,

View File

@ -8,10 +8,10 @@
</page-header> </page-header>
<form v-if="single" class="p-6 grid gap-4 justify-start" @submit.prevent="submit"> <form v-if="single" class="p-6 grid gap-4 justify-start" @submit.prevent="submit">
<f-text id="completed_at" v-model="single.completed_at" type="date" label="Datum" required></f-text> <f-text id="completed_at" v-model="single.completed_at" type="date" label="Datum" required />
<f-select id="course_id" v-model="single.course_id" name="course_id" :options="meta.courses" label="Baustein" required></f-select> <f-select id="course_id" v-model="single.course_id" name="course_id" :options="meta.courses" label="Baustein" required />
<f-text id="event_name" v-model="single.event_name" label="Veranstaltung" required></f-text> <f-text id="event_name" v-model="single.event_name" label="Veranstaltung" required />
<f-text id="organizer" v-model="single.organizer" label="Veranstalter" required></f-text> <f-text id="organizer" v-model="single.organizer" label="Veranstalter" required />
<button type="submit" class="btn btn-primary">Absenden</button> <button type="submit" class="btn btn-primary">Absenden</button>
</form> </form>
@ -23,18 +23,18 @@
<th>Veranstaltung</th> <th>Veranstaltung</th>
<th>Veranstalter</th> <th>Veranstalter</th>
<th>Datum</th> <th>Datum</th>
<th></th> <th />
</tr> </tr>
</thead> </thead>
<tr v-for="(course, index) in data" :key="index"> <tr v-for="(course, index) in data" :key="index">
<td v-text="course.course_name"></td> <td v-text="course.course_name" />
<td v-text="course.event_name"></td> <td v-text="course.event_name" />
<td v-text="course.organizer"></td> <td v-text="course.organizer" />
<td v-text="course.completed_at_human"></td> <td v-text="course.completed_at_human" />
<td class="flex"> <td class="flex">
<a href="#" class="inline-flex btn btn-warning btn-sm" @click.prevent="edit(course)"><ui-sprite src="pencil"></ui-sprite></a> <a href="#" class="inline-flex btn btn-warning btn-sm" @click.prevent="edit(course)"><ui-sprite src="pencil" /></a>
<a href="#" class="inline-flex btn btn-danger btn-sm" @click.prevent="remove(course)"><ui-sprite src="trash"></ui-sprite></a> <a href="#" class="inline-flex btn btn-danger btn-sm" @click.prevent="remove(course)"><ui-sprite src="trash" /></a>
</td> </td>
</tr> </tr>
</table> </table>
@ -44,7 +44,7 @@
<script lang="js" setup> <script lang="js" setup>
defineEmits(['close']); defineEmits(['close']);
import { useApiIndex } from '../../composables/useApiIndex.js'; import { useApiIndex } from '../../composables/useApiIndex.ts';
const props = defineProps({ const props = defineProps({
url: { url: {

View File

@ -1,5 +1,5 @@
<template> <template>
<page-header title="Zahlungen" @close="$emit('close')"> </page-header> <page-header title="Zahlungen" @close="$emit('close')" />
<div class="grow"> <div class="grow">
<table class="custom-table custom-table-light custom-table-sm text-sm"> <table class="custom-table custom-table-light custom-table-sm text-sm">
@ -10,15 +10,15 @@
</thead> </thead>
<tr v-for="(position, index) in data" :key="index"> <tr v-for="(position, index) in data" :key="index">
<td v-text="position.description"></td> <td v-text="position.description" />
<td v-text="position.invoice.status"></td> <td v-text="position.invoice.status" />
<td v-text="position.price_human"></td> <td v-text="position.price_human" />
</tr> </tr>
</table> </table>
</div> </div>
</template> </template>
<script lang="js" setup> <script lang="ts" setup>
defineEmits(['close']); defineEmits(['close']);
import { useApiIndex } from '../../composables/useApiIndex.js'; import { useApiIndex } from '../../composables/useApiIndex.js';

View File

@ -6,60 +6,62 @@
</template> </template>
</page-header> </page-header>
<form v-if="single" class="p-6 grid gap-4 justify-start" @submit.prevent="submit"> <form v-if="single && meta !== null" class="p-6 grid gap-4 justify-start" @submit.prevent="submit">
<f-select id="group_id" v-model="single.group_id" name="group_id" :options="meta.groups" label="Gruppierung" required></f-select> <f-select id="group_id" v-model="single.group" name="group_id" :options="meta.groups" label="Gruppierung" required />
<f-select id="activity_id" v-model="single.activity_id" name="activity_id" :options="meta.activities" label="Tätigkeit" required></f-select> <f-select id="activity_id" v-model="single.activity" name="activity_id" :options="meta.activities" label="Tätigkeit" required />
<f-select <f-select v-if="single.activity"
v-if="single.activity_id" id="subactivity_id"
id="subactivity_id" :model-value="single.subactivity"
:model-value="single.subactivity_id" name="subactivity_id"
name="subactivity_id" :options="meta.subactivities[single.activity.id]"
:options="meta.subactivities[single.activity_id]" label="Untertätigkeit"
label="Untertätigkeit" @update:model-value="setSubactivityId(single, $event)"
@update:modelValue="setSubactivityId(single, $event)" />
></f-select> <f-switch v-if="displayPromisedAt"
<f-switch id="has_promise"
v-if="displayPromisedAt" name="has_promise"
id="has_promise" :model-value="single.promisedAt !== null"
name="has_promise" label="Hat Versprechen"
:model-value="single.promised_at !== null" @update:model-value="setPromisedAtSwitch(single, $event)"
label="Hat Versprechen" />
@update:modelValue="setPromisedAtSwitch(single, $event)" <f-text v-show="displayPromisedAt && single.promisedAt !== null" id="promised_at" v-model="single.promisedAt" type="date" label="Versprechensdatum" size="sm" />
></f-switch>
<f-text v-show="displayPromisedAt && 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> <button type="submit" class="btn btn-primary">Absenden</button>
</form> </form>
<div v-else class="grow"> <div v-else class="grow">
<table class="custom-table custom-table-light custom-table-sm text-sm"> <table class="custom-table custom-table-light 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></th> <th>Aktiv</th>
<th />
</tr>
</thead> </thead>
<tr v-for="(membership, index) in data" :key="index"> <tbody>
<td v-text="membership.activity_name"></td> <tr v-for="(membership, index) in data" :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" dark></ui-boolean-display></td> <td v-text="membership.from?.human" />
<td class="flex space-x-1"> <td><ui-boolean-display :value="membership.isActive" dark /></td>
<a href="#" class="inline-flex btn btn-warning btn-sm" @click.prevent="edit(membership)"><ui-sprite src="pencil"></ui-sprite></a> <td class="flex space-x-1">
<a href="#" class="inline-flex btn btn-danger btn-sm" @click.prevent="remove(membership)"><ui-sprite src="trash"></ui-sprite></a> <a href="#" class="inline-flex btn btn-warning btn-sm" @click.prevent="edit(membership)"><ui-sprite src="pencil" /></a>
</td> <a href="#" class="inline-flex btn btn-danger btn-sm" @click.prevent="remove(membership)"><ui-sprite src="trash" /></a>
</tr> </td>
</tr>
</tbody>
</table> </table>
</div> </div>
</template> </template>
<script lang="js" setup> <script lang="ts" setup>
import { computed } from 'vue'; import { computed } from 'vue';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
defineEmits(['close']); defineEmits(['close']);
import { useApiIndex } from '../../composables/useApiIndex.js'; import { useApiIndex } from '../../composables/useApiIndex.ts';
const props = defineProps({ const props = defineProps({
url: { url: {
@ -67,14 +69,14 @@ const props = defineProps({
required: true, required: true,
}, },
}); });
const { data, meta, reload, single, create, edit, submit, remove } = useApiIndex(props.url, 'membership'); const { data, meta, reload, single, create, edit, submit, remove } = useApiIndex<App.Member.Data.MembershipData, App.Member.Data.MembershipMeta>(props.url, 'membership');
function setPromisedAtSwitch(single, value) { function setPromisedAtSwitch(single, value) {
single.promised_at = value ? dayjs().format('YYYY-MM-DD') : null; single.promised_at = value ? dayjs().format('YYYY-MM-DD') : null;
} }
const displayPromisedAt = computed(function () { const displayPromisedAt = computed(function () {
if (!single.value || !single.value.activity_id || !single.value.subactivity_id) { if (!single.value || !single.value.activity || !single.value.subactivity) {
return false; return false;
} }

View File

@ -22,9 +22,9 @@
<member-filter-fields :model-value="filter" @update:model-value="setFilterObject($event)" /> <member-filter-fields :model-value="filter" @update:model-value="setFilterObject($event)" />
</template> </template>
<template #buttons> <template #buttons>
<f-text id="search" :model-value="filter.search" label="Suchen …" size="sm" @update:model-value="setFilterObject({...filter, search: $event})"></f-text> <f-text id="search" :model-value="filter.search" label="Suchen …" size="sm" @update:model-value="setFilterObject({...filter, search: $event})" />
<button class="btn btn-primary label mr-2" @click.prevent="exportMembers"> <button class="btn btn-primary label mr-2" @click.prevent="exportMembers">
<ui-sprite class="w-3 h-3 xl:mr-2" src="save"></ui-sprite> <ui-sprite class="w-3 h-3 xl:mr-2" src="save" />
<span class="hidden xl:inline">Exportieren</span> <span class="hidden xl:inline">Exportieren</span>
</button> </button>
</template> </template>
@ -32,7 +32,7 @@
<table cellspacing="0" cellpadding="0" border="0" class="custom-table custom-table-sm hidden md:table"> <table cellspacing="0" cellpadding="0" border="0" class="custom-table custom-table-sm hidden md:table">
<thead> <thead>
<th></th> <th />
<th>Nachname</th> <th>Nachname</th>
<th>Vorname</th> <th>Vorname</th>
<th class="!hidden 2xl:!table-cell">Ort</th> <th class="!hidden 2xl:!table-cell">Ort</th>
@ -40,26 +40,26 @@
<th class="!hidden xl:!table-cell">Alter</th> <th class="!hidden xl:!table-cell">Alter</th>
<th v-if="hasModule('bill')" class="!hidden xl:!table-cell">Rechnung</th> <th v-if="hasModule('bill')" class="!hidden xl:!table-cell">Rechnung</th>
<th v-if="hasModule('bill')">Ausstand</th> <th v-if="hasModule('bill')">Ausstand</th>
<th></th> <th />
</thead> </thead>
<tr v-for="(member, index) in data" :key="index"> <tr v-for="(member, index) in data" :key="index">
<td><ui-age-groups :member="member"></ui-age-groups></td> <td><ui-age-groups :member="member" /></td>
<td v-text="member.lastname"></td> <td v-text="member.lastname" />
<td v-text="member.firstname"></td> <td v-text="member.firstname" />
<td class="!hidden 2xl:!table-cell" v-text="member.full_address"></td> <td class="!hidden 2xl:!table-cell" v-text="member.full_address" />
<td> <td>
<tags :member="member"></tags> <tags :member="member" />
</td> </td>
<td class="!hidden xl:!table-cell" v-text="member.age"></td> <td class="!hidden xl:!table-cell" v-text="member.age" />
<td v-if="hasModule('bill')" class="!hidden xl:!table-cell"> <td v-if="hasModule('bill')" class="!hidden xl:!table-cell">
<ui-label :value="member.bill_kind_name" fallback="kein"></ui-label> <ui-label :value="member.bill_kind_name" fallback="kein" />
</td> </td>
<td v-if="hasModule('bill')"> <td v-if="hasModule('bill')">
<ui-label :value="member.pending_payment" fallback="---"></ui-label> <ui-label :value="member.pending_payment" fallback="---" />
</td> </td>
<td> <td>
<actions :member="member" @sidebar="openSidebar($event, member)" @remove="remove(member)"></actions> <actions :member="member" @sidebar="openSidebar($event, member)" @remove="remove(member)" />
</td> </td>
</tr> </tr>
</table> </table>
@ -67,28 +67,28 @@
<div class="md:hidden p-3 grid gap-3"> <div class="md:hidden p-3 grid gap-3">
<ui-box v-for="(member, index) in data" :key="index" class="relative" :heading="member.fullname"> <ui-box v-for="(member, index) in data" :key="index" class="relative" :heading="member.fullname">
<template #in-title> <template #in-title>
<ui-age-groups class="ml-2" :member="member" icon-class="w-4 h-4"></ui-age-groups> <ui-age-groups class="ml-2" :member="member" icon-class="w-4 h-4" />
</template> </template>
<div class="text-xs text-gray-200" v-text="member.full_address"></div> <div class="text-xs text-gray-200" v-text="member.full_address" />
<div class="flex items-center mt-1 space-x-4"> <div class="flex items-center mt-1 space-x-4">
<tags :member="member"></tags> <tags :member="member" />
<ui-label v-show="hasModule('bill')" class="text-gray-100 block" :value="member.pending_payment" fallback=""></ui-label> <ui-label v-show="hasModule('bill')" class="text-gray-100 block" :value="member.pending_payment" fallback="" />
</div> </div>
<actions class="mt-2" :member="member" @sidebar="openSidebar($event, member)" @remove="remove(member)"> </actions> <actions class="mt-2" :member="member" @sidebar="openSidebar($event, member)" @remove="remove(member)" />
<div class="absolute right-0 top-0 h-full flex items-center mr-2"> <div class="absolute right-0 top-0 h-full flex items-center mr-2">
<i-link v-tooltip="`Details`" :href="member.links.show"><ui-sprite src="chevron" class="w-6 h-6 text-teal-100 -rotate-90"></ui-sprite></i-link> <i-link v-tooltip="`Details`" :href="member.links.show"><ui-sprite src="chevron" class="w-6 h-6 text-teal-100 -rotate-90" /></i-link>
</div> </div>
</ui-box> </ui-box>
</div> </div>
<div class="px-6"> <div class="px-6">
<ui-pagination class="mt-4" :value="meta" @reload="reloadPage"></ui-pagination> <ui-pagination class="mt-4" :value="meta" @reload="reloadPage" />
</div> </div>
<ui-sidebar v-if="single !== null" @close="closeSidebar"> <ui-sidebar v-if="single !== null" @close="closeSidebar">
<member-invoice-positions v-if="single.type === 'invoicePosition'" :url="single.model.links.invoiceposition_index" @close="closeSidebar"></member-invoice-positions> <member-invoice-positions v-if="single.type === 'invoicePosition'" :url="single.model.links.invoiceposition_index" @close="closeSidebar" />
<member-memberships v-if="single.type === 'membership'" :url="single.model.links.membership_index" @close="closeSidebar"></member-memberships> <member-memberships v-if="single.type === 'membership'" :url="single.model.links.membership_index" @close="closeSidebar" />
<member-courses v-if="single.type === 'courses'" :url="single.model.links.course_index" @close="closeSidebar"></member-courses> <member-courses v-if="single.type === 'courses'" :url="single.model.links.course_index" @close="closeSidebar" />
</ui-sidebar> </ui-sidebar>
</page-layout> </page-layout>
</template> </template>
@ -107,7 +107,7 @@ const single = ref(null);
const deleting = ref(null); const deleting = ref(null);
const props = defineProps(indexProps); const props = defineProps(indexProps);
var {router, data, meta, filter, setFilterObject, filterString, reloadPage} = useIndex(props.data, 'member'); const {router, data, meta, filter, setFilterObject, filterString, reloadPage} = useIndex(props.data, 'member');
function exportMembers() { function exportMembers() {
window.open(`/member-export?filter=${filterString.value}`); window.open(`/member-export?filter=${filterString.value}`);

6
resources/types/custom.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
declare namespace Custom {
export type PageMetadata = {
current_page: number;
default: object;
};
}

487
resources/types/generated.d.ts vendored Normal file
View File

@ -0,0 +1,487 @@
declare namespace App.Contribution.Data {
export type MemberData = {
firstname: string;
lastname: string;
address: string;
zip: string;
location: string;
birthday: string;
gender: any | null;
isLeader: boolean;
};
}
declare namespace App.Contribution.Documents {
export type BdkjHesse = {
dateFrom: string;
dateUntil: string;
zipLocation: string;
country: any | null;
members: any;
eventName: string;
filename: string | null;
type: string;
};
export type CityFrankfurtMainDocument = {
fromName: string;
dateFrom: string;
dateUntil: string;
zipLocation: string;
country: any | null;
members: any;
eventName: string;
filename: string | null;
type: string;
};
export type CityRemscheidDocument = {
dateFrom: string;
dateUntil: string;
zipLocation: string;
country: any | null;
leaders: any;
children: any;
filename: string | null;
type: string;
eventName: string;
};
export type CitySolingenDocument = {
fromName: string;
dateFrom: string;
dateUntil: string;
zipLocation: string;
members: Array<App.Contribution.Data.MemberData>;
eventName: string;
type: string;
};
export type ContributionDocument = {
};
export type RdpNrwDocument = {
dateFrom: string;
dateUntil: string;
zipLocation: string;
country: any | null;
members: any;
filename: string | null;
type: string;
eventName: string;
};
export type WuppertalDocument = {
dateFrom: string;
dateUntil: string;
zipLocation: string;
country: any | null;
members: any;
filename: string | null;
type: string;
eventName: string;
};
}
declare namespace App.Efz {
export type EfzDocument = {
name: string;
slug: string;
secondLine: string;
now: string;
sender: App.Pdf.Sender;
member: any;
};
}
declare namespace App.Fileshare.ConnectionTypes {
export type ConnectionType = {
};
export type NextcloudConnection = {
user: string;
password: string;
base_url: string;
};
export type OwncloudConnection = {
user: string;
password: string;
base_url: string;
};
}
declare namespace App.Fileshare.Data {
export type FileshareResourceData = {
connection_id: number;
resource: string;
};
export type ResourceData = {
name: string;
path: string;
parent: string;
};
}
declare namespace App.Form.Data {
export type ColumnData = {
mobile: number;
tablet: number;
desktop: number;
};
export type ExportData = {
root: App.Fileshare.Data.FileshareResourceData | null;
group_by: string | null;
to_group_field: string | null;
};
export type FormConfigData = {
sections: Array<App.Form.Data.SectionData>;
};
export type SectionData = {
name: string;
fields: Array<App.Form.Fields.Field>;
intro: string | null;
};
}
declare namespace App.Form.Enums {
export type NamiType = 'Vorname' | 'Nachname' | 'Spitzname' | 'Geburtstag' | 'Bezirk' | 'Stamm' | 'E-Mail-Adresse' | 'Adresse' | 'PLZ' | 'Ort' | 'Geschlecht' | 'Handynummer' | 'Alter (zum Zeitpunkt der Anmeldung)' | 'Alter (zum Zeitpunkt der Veranstaltung)';
export type SpecialType = 'Vorname' | 'Nachname' | 'E-Mail-Adresse';
}
declare namespace App.Form.Fields {
export type CheckboxField = {
required: boolean;
description: string;
key: string;
name: string;
nami_type: App.Form.Enums.NamiType | null;
columns: App.Form.Data.ColumnData;
for_members: boolean;
special_type: App.Form.Enums.SpecialType | null;
hint: string | null;
intro: string | null;
value: any;
};
export type CheckboxesField = {
options: { [key: number]: string };
min: number | null;
max: number | null;
key: string;
name: string;
nami_type: App.Form.Enums.NamiType | null;
columns: App.Form.Data.ColumnData;
for_members: boolean;
special_type: App.Form.Enums.SpecialType | null;
hint: string | null;
intro: string | null;
value: any;
};
export type DateField = {
required: boolean;
max_today: boolean;
key: string;
name: string;
nami_type: App.Form.Enums.NamiType | null;
columns: App.Form.Data.ColumnData;
for_members: boolean;
special_type: App.Form.Enums.SpecialType | null;
hint: string | null;
intro: string | null;
value: any;
};
export type DropdownField = {
required: boolean;
options: { [key: number]: string };
allowcustom: boolean;
key: string;
name: string;
nami_type: App.Form.Enums.NamiType | null;
columns: App.Form.Data.ColumnData;
for_members: boolean;
special_type: App.Form.Enums.SpecialType | null;
hint: string | null;
intro: string | null;
value: any;
};
export type EmailField = {
required: boolean;
key: string;
name: string;
nami_type: App.Form.Enums.NamiType | null;
columns: App.Form.Data.ColumnData;
for_members: boolean;
special_type: App.Form.Enums.SpecialType | null;
hint: string | null;
intro: string | null;
value: any;
};
export type Field = {
key: string;
name: string;
nami_type: App.Form.Enums.NamiType | null;
columns: App.Form.Data.ColumnData;
for_members: boolean;
special_type: App.Form.Enums.SpecialType | null;
hint: string | null;
intro: string | null;
value: any;
};
export type GroupField = {
required: boolean;
parent_field: string | null;
parent_group: number | null;
has_empty_option: boolean;
empty_option_value: string | null;
key: string;
name: string;
nami_type: App.Form.Enums.NamiType | null;
columns: App.Form.Data.ColumnData;
for_members: boolean;
special_type: App.Form.Enums.SpecialType | null;
hint: string | null;
intro: string | null;
value: any;
};
export type NamiField = {
key: string;
name: string;
nami_type: App.Form.Enums.NamiType | null;
columns: App.Form.Data.ColumnData;
for_members: boolean;
special_type: App.Form.Enums.SpecialType | null;
hint: string | null;
intro: string | null;
value: any;
};
export type NumberField = {
required: boolean;
min: number | null;
max: number | null;
key: string;
name: string;
nami_type: App.Form.Enums.NamiType | null;
columns: App.Form.Data.ColumnData;
for_members: boolean;
special_type: App.Form.Enums.SpecialType | null;
hint: string | null;
intro: string | null;
value: any;
};
export type RadioField = {
required: boolean;
options: { [key: number]: string };
allowcustom: boolean;
key: string;
name: string;
nami_type: App.Form.Enums.NamiType | null;
columns: App.Form.Data.ColumnData;
for_members: boolean;
special_type: App.Form.Enums.SpecialType | null;
hint: string | null;
intro: string | null;
value: any;
};
export type TextField = {
required: boolean;
key: string;
name: string;
nami_type: App.Form.Enums.NamiType | null;
columns: App.Form.Data.ColumnData;
for_members: boolean;
special_type: App.Form.Enums.SpecialType | null;
hint: string | null;
intro: string | null;
value: any;
};
export type TextareaField = {
required: boolean;
rows: number;
key: string;
name: string;
nami_type: App.Form.Enums.NamiType | null;
columns: App.Form.Data.ColumnData;
for_members: boolean;
special_type: App.Form.Enums.SpecialType | null;
hint: string | null;
intro: string | null;
value: any;
};
}
declare namespace App.Form.Scopes {
export type FormFilterScope = {
search: string | null;
past: boolean;
inactive: boolean;
};
export type ParticipantFilterScope = {
data: Array<any>;
search: string;
options: Array<any>;
parent: number | null;
sort: App.Lib.Sorting | null;
};
}
declare namespace App.Group.Enums {
export type Level = 'Diözese' | 'Bezirk' | 'Stamm';
}
declare namespace App.Invoice {
export type BillKind = 'E-Mail' | 'Post';
export type BillDocument = {
until: string;
filename: string;
toName: string;
toAddress: string;
toZip: string;
toLocation: string;
greeting: string;
positions: Array<any>;
usage: string;
};
export type InvoiceDocument = {
until: string;
filename: string;
toName: string;
toAddress: string;
toZip: string;
toLocation: string;
greeting: string;
positions: Array<any>;
usage: string;
};
export type RememberDocument = {
until: string;
filename: string;
toName: string;
toAddress: string;
toZip: string;
toLocation: string;
greeting: string;
positions: Array<any>;
usage: string;
};
}
declare namespace App.Invoice.Enums {
export type InvoiceStatus = 'Neu' | 'Rechnung gestellt' | 'Rechnung beglichen';
}
declare namespace App.Invoice.Scopes {
export type InvoiceFilterScope = {
statuses: Array<any> | null;
search: string | null;
};
}
declare namespace App.Lib {
export type Filter = {
};
export type ScoutFilter = {
};
export type Sorting = {
by: string;
direction: boolean;
};
}
declare namespace App.Lib.Data {
export type DateData = {
raw: string;
human: string;
};
}
declare namespace App.Lib.Editor {
export type Comparator = 'isEqual' | 'isNotEqual' | 'isIn' | 'isNotIn';
export type ConditionMode = 'all' | 'any';
export type Condition = {
mode: App.Lib.Editor.ConditionMode;
ifs: Array<App.Lib.Editor.Statement>;
};
export type EditorData = {
version: string;
blocks: Array<any>;
time: number;
};
export type Statement = {
field: string;
value: any;
comparator: App.Lib.Editor.Comparator;
};
}
declare namespace App.Maildispatcher.Data {
export type MailEntry = {
email: string;
};
}
declare namespace App.Mailman.Data {
export type MailingList = {
description: string;
display_name: string;
name: string;
id: string;
list_name: string;
mail_host: string;
member_count: number;
self_link: string;
volume: number;
};
export type Member = {
email: string;
member_id: string;
};
}
declare namespace App.Member {
export type FilterScope = {
options: { [key: string]: any };
ausstand: boolean;
bill_kind: string | null;
memberships: Array<any>;
activity_ids: Array<any>;
subactivity_ids: Array<any>;
search: string | null;
group_ids: Array<any>;
include: Array<any>;
exclude: Array<any>;
has_full_address: boolean | null;
has_birthday: boolean | null;
has_svk: boolean | null;
has_vk: boolean | null;
};
}
declare namespace App.Member.Data {
export type ActivityData = {
id: number;
name: string;
};
export type FullMember = {
member: any;
courses: Array<any>;
memberships: Array<any>;
};
export type GroupData = {
id: number;
name: string;
};
export type MembershipData = {
activity: App.Member.Data.ActivityData | null;
subactivity: App.Member.Data.SubactivityData | null;
group: App.Member.Data.GroupData | null;
promisedAt: App.Lib.Data.DateData | null;
from: App.Lib.Data.DateData | null;
isActive: boolean;
links: Array<any>;
};
export type MembershipMeta = {
links: Array<any>;
groups: any;
activities: any;
subactivities: any;
default: App.Member.Data.MembershipData;
};
export type NestedGroup = {
id: number;
name: string;
};
export type SubactivityData = {
id: number;
name: string;
};
}
declare namespace App.Module {
export type Module = 'bill' | 'course' | 'event';
}
declare namespace App.Pdf {
export type Sender = {
name: string;
address: string;
zipLocation: string;
mglnr?: string;
};
}
declare namespace App.Prevention.Data {
export type PreventionData = {
type: any;
expires: string;
};
}

View File

@ -130,20 +130,6 @@ class IndexTest extends EndToEndTestCase
$this->assertInertiaHas(null, $response, 'data.data.0.birthday'); $this->assertInertiaHas(null, $response, 'data.data.0.birthday');
} }
public function testItShowsActivitiesAndSubactivities(): void
{
$this->withoutExceptionHandling()->login()->loginNami();
$activity = Activity::factory()->hasAttached(Subactivity::factory()->name('Biber'))->name('€ Mitglied')->create();
$subactivity = $activity->subactivities->first();
sleep(1);
$this->get('/member')
->assertInertiaPath("data.meta.formSubactivities.{$activity->id}.{$subactivity->id}", 'Biber')
->assertInertiaPath("data.meta.filterSubactivities.0.name", 'Biber')
->assertInertiaPath("data.meta.filterSubactivities.0.id", $activity->id)
->assertInertiaPath("data.meta.formActivities.{$activity->id}", '€ Mitglied');
}
public function testItCanFilterForBillKinds(): void public function testItCanFilterForBillKinds(): void
{ {
$this->withoutExceptionHandling()->login()->loginNami(); $this->withoutExceptionHandling()->login()->loginNami();

View File

@ -23,23 +23,8 @@ class CreateTest extends TestCase
public function testItDisplaysCreatePage(): void public function testItDisplaysCreatePage(): void
{ {
$activity = Activity::factory()->inNami(5)->hasAttached(Subactivity::factory()->inNami(23)->name('Biber'))->name('€ Mitglied')->create(); $this->get(route('member.create'))
$subactivity = $activity->subactivities->first(); ->assertInertiaPath('meta.default.address', '');
$response = $this->get(route('member.create'));
$this->assertInertiaHas('Biber', $response, "meta.formSubactivities.{$activity->id}.{$subactivity->id}");
$this->assertInertiaHas('€ Mitglied', $response, "meta.formActivities.{$activity->id}");
$this->assertInertiaHas(['name' => 'E-Mail', 'id' => 'E-Mail'], $response, 'meta.billKinds.0');
$this->assertInertiaHas([
'efz' => null,
'ps_at' => null,
'more_ps_at' => null,
'without_education_at' => null,
'without_efz_at' => null,
'address' => '',
], $response, 'meta.default');
} }
public function testItDoesntDisplayActivitiesAndSubactivitiesNotInNami(): void public function testItDoesntDisplayActivitiesAndSubactivitiesNotInNami(): void

View File

@ -30,8 +30,6 @@ class EditTest extends TestCase
$response = $this->get(route('member.edit', ['member' => $member])); $response = $this->get(route('member.edit', ['member' => $member]));
$this->assertInertiaHas('Biber', $response, "meta.formSubactivities.{$activity->id}.{$subactivity->id}");
$this->assertInertiaHas('€ Mitglied', $response, "meta.formActivities.{$activity->id}");
$this->assertInertiaHas('Max', $response, 'data.firstname'); $this->assertInertiaHas('Max', $response, 'data.firstname');
$this->assertInertiaHas(false, $response, 'data.keepdata'); $this->assertInertiaHas(false, $response, 'data.keepdata');
$this->assertInertiaHas('Doktor', $response, 'data.salutation'); $this->assertInertiaHas('Doktor', $response, 'data.salutation');

View File

@ -2,193 +2,266 @@
namespace Tests\Feature\Member; namespace Tests\Feature\Member;
use App\Activity;
use App\Confession;
use App\Country; use App\Country;
use App\Course\Models\Course; 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\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;
use App\Region; use App\Region;
use App\Subactivity;
use Carbon\Carbon; use Carbon\Carbon;
use Generator;
use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Foundation\Testing\DatabaseTransactions;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
class ShowTest extends TestCase uses(DatabaseTransactions::class);
{
use DatabaseTransactions;
public function setUp(): void beforeEach(function () {
{ Country::factory()->create(['name' => 'Deutschland']);
parent::setUp(); });
Country::factory()->create(['name' => 'Deutschland']);
}
public function testItShowsSingleMember(): void mutates(MemberResource::class);
{
Carbon::setTestNow(Carbon::parse('2006-01-01 15:00:00'));
$this->withoutExceptionHandling()->login()->loginNami(); it('shows courses', function () {
$member = Member::factory() $this->withoutExceptionHandling()->login()->loginNami();
->defaults() $member = Member::factory()
->for(Group::factory()->name('Stamm Beispiel')) ->defaults()
->has(Membership::factory()->promise(now())->in('€ LeiterIn', 5, 'Jungpfadfinder', 88)->from('2022-11-19')) ->has(
->has(InvoicePosition::factory()->for(Invoice::factory())->price(1050)->description('uu')) CourseMember::factory()
->for(Gender::factory()->male()) ->for(Course::factory()->name(' Baustein 2e - Gewalt gegen Kinder und Jugendliche: Vertiefung, Prävention '))
->for(Region::factory()->name('NRW')) ->state(['organizer' => 'DPSG', 'event_name' => 'Wochenende', 'completed_at' => '2022-03-03']),
->postBillKind() 'courses'
->inNami(123) )
->for(Subscription::factory()->name('Sub')->forFee()) ->create();
->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.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');
});
$this->assertInertiaHas([ it('shows default', function () {
'birthday_human' => '20.04.1991', $this->withoutExceptionHandling()->login()->loginNami();
'age' => 14, $activity = Activity::factory()->name('€ Mitglied')->create();
'group_name' => 'Stamm Beispiel', $subactivity = Subactivity::factory()->name('Jungpfadfinder')->create();
'full_address' => 'Itterstr 3, 42719 Solingen', $activity->subactivities()->attach($subactivity);
'region' => ['name' => 'NRW'], $course = Course::factory()->name('LL')->create();
'other_country' => 'other', $region = Region::factory()->name('LLa')->create();
'main_phone' => '+49 212 1266775', $member = Member::factory()->male()->defaults()->create();
'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([
'activity_name' => '€ LeiterIn',
'subactivity_name' => 'Jungpfadfinder',
'id' => $member->memberships->first()->id,
'human_date' => '19.11.2022',
'promised_at' => now()->format('Y-m-d'),
], $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');
}
public function testItShowsMinimalSingleMember(): void $this->get("/member/{$member->id}")
{ ->assertInertiaPath('meta.default.address', '')
$this->withoutExceptionHandling()->login()->loginNami(); ->assertInertiaPath('meta.filterActivities.0.name', '€ Mitglied')
$member = Member::factory() ->assertInertiaPath('meta.filterActivities.0.id', $activity->id)
->for(Group::factory()) ->assertInertiaPath('meta.filterSubactivities.0.name', 'Jungpfadfinder')
->for(Nationality::factory()->name('deutsch')) ->assertInertiaPath('meta.filterSubactivities.0.id', $subactivity->id)
->for(Subscription::factory()->forFee()) ->assertInertiaPath('meta.formActivities.0.name', '€ Mitglied')
->create(['firstname' => 'Max', 'lastname' => 'Muster', 'has_vk' => false, 'has_svk' => false]); ->assertInertiaPath('meta.formActivities.0.id', $activity->id)
->assertInertiaPath("meta.formSubactivities.{$activity->id}.{$subactivity->id}", "Jungpfadfinder")
->assertInertiaPath('meta.default.has_nami', false)
->assertInertiaPath('meta.default.send_newspaper', false)
->assertInertiaPath('meta.groups.0.id', $member->group->id)
->assertInertiaPath('meta.default.fax', '')
->assertInertiaPath('meta.filter.search', '')
->assertInertiaPath('meta.billKinds.0.id', 'E-Mail')
->assertInertiaPath('meta.courses.0.id', $course->id)
->assertInertiaPath('meta.courses.0.name', 'LL')
->assertInertiaPath('meta.regions.0.name', 'LLa')
->assertInertiaPath('meta.regions.0.id', $region->id)
->assertInertiaPath('meta.subscriptions.0.id', $member->subscription->id)
->assertInertiaPath('meta.subscriptions.0.name', $member->subscription->name)
->assertInertiaPath('meta.links.create', route('member.create'))
->assertInertiaPath('meta.links.index', route('member.index'))
->assertInertiaPath('meta.countries.0.id', $member->country->id)
->assertInertiaPath('meta.countries.0.name', $member->country->name)
->assertInertiaPath('meta.genders.0.id', $member->gender->id)
->assertInertiaPath('meta.genders.0.name', $member->gender->name)
->assertInertiaPath('meta.default.bank_account.iban', '')
->assertInertiaPath('meta.default_membership_filter.group_ids', [])
->assertInertiaPath('meta.default_membership_filter.activity_ids', [])
->assertInertiaPath('meta.default_membership_filter.subactivity_ids', [])
->assertInertiaPath('meta.default.bank_account.bic', '')
->assertInertiaPath('meta.default.bank_account.blz', '')
->assertInertiaPath('meta.default.bank_account.bank_name', '')
->assertInertiaPath('meta.default.bank_account.person', '')
->assertInertiaPath('meta.default.bank_account.account_number', '')
->assertInertiaPath('meta.default.letter_address', '')
->assertInertiaPath('meta.default.email', '')
->assertInertiaPath('meta.default.has_vk', false)
->assertInertiaPath('meta.default.has_svk', false)
->assertInertiaPath('meta.default.multiply_pv', false)
->assertInertiaPath('meta.default.multiply_more_pv', false)
->assertInertiaPath('meta.default.email_parents', '')
->assertInertiaPath('meta.default.children_phone', '')
->assertInertiaPath('meta.default.work_phone', '')
->assertInertiaPath('meta.default.mobile_phone', '')
->assertInertiaPath('meta.default.main_phone', '')
->assertInertiaPath('meta.default.other_country', '')
->assertInertiaPath('meta.default.birthday', '')
->assertInertiaPath('meta.default.location', '')
->assertInertiaPath('meta.default.zip', '')
->assertInertiaPath('meta.default.address', '')
->assertInertiaPath('meta.default.further_address', '')
->assertInertiaPath('meta.default.firstname', '')
->assertInertiaPath('meta.default.comment', '')
->assertInertiaPath('meta.default.joined_at', now()->format('Y-m-d'))
->assertInertiaPath('meta.default.lastname', '')
->assertInertiaPath('meta.default.ps_at', null)
->assertInertiaPath('meta.default.more_ps_at', null)
->assertInertiaPath('meta.default.without_education_at', null)
->assertInertiaPath('meta.default.without_efz_at', null)
->assertInertiaPath('meta.default.efz', null);
});
$response = $this->get("/member/{$member->id}"); it('shows efz link', function () {
$this->withoutExceptionHandling()->login()->loginNami();
$member = Member::factory()->defaults()->create();
$this->assertInertiaHas([ $this->get("/member/{$member->id}")
'region' => ['name' => '-- kein --'], ->assertInertiaPath('data.efz_link', route('efz', $member));
'fullname' => 'Max Muster', });
'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');
}
public static function membershipDataProvider(): Generator it('shows memberships', function () {
{ Carbon::setTestNow(Carbon::parse('2006-01-01 15:00:00'));
yield [now()->subMonths(2), null, true];
yield [now()->subMonths(2), now()->subDay(), false];
yield [now()->addDays(2), null, false];
}
#[DataProvider('membershipDataProvider')] $this->withoutExceptionHandling()->login()->loginNami();
public function testItShowsIfMembershipIsActive(Carbon $from, ?Carbon $to, bool $isActive): void $member = Member::factory()
{ ->defaults()
$this->withoutExceptionHandling()->login()->loginNami(); ->for(Group::factory()->name('Stamm Beispiel'))
$member = Member::factory() ->has(Membership::factory()->promise(now())->in('€ LeiterIn', 5, 'Jungpfadfinder', 88)->from('2005-11-19'))
->defaults() ->create();
->has(Membership::factory()->in('€ LeiterIn', 455, 'Pfadfinder', 15)->state(['from' => $from, 'to' => $to]))
->create();
$response = $this->get("/member/{$member->id}"); $this->get("/member/{$member->id}")
->assertInertiaPath('data.age_group_icon', 'jungpfadfinder')
->assertInertiaPath('data.is_leader', true)
->assertInertiaPath('data.memberships.0.id', $member->memberships->first()->id)
->assertInertiaPath('data.memberships.0.from.human', '19.11.2005')
->assertInertiaPath('data.memberships.0.from.raw', '2005-11-19')
->assertInertiaPath('data.memberships.0.promised_at.human', now()->format('d.m.Y'))
->assertInertiaPath('data.memberships.0.promised_at.raw', now()->format('Y-m-d'))
->assertInertiaPath('data.memberships.0.activity.name', '€ LeiterIn')
->assertInertiaPath('data.memberships.0.activity.id', $member->memberships->first()->activity->id)
->assertInertiaPath('data.memberships.0.subactivity.name', 'Jungpfadfinder')
->assertInertiaPath('data.memberships.0.subactivity.id', $member->memberships->first()->subactivity->id);
});
$this->assertInertiaHas($isActive, $response, 'data.memberships.0.is_active'); it('shows that member is not a leader', function () {
} $this->withoutExceptionHandling()->login()->loginNami();
} $member = Member::factory()->defaults()->create();
$this->get("/member/{$member->id}")
->assertInertiaPath('data.is_leader', false);
});
it('shows links', function () {
$this->withoutExceptionHandling()->login()->loginNami();
$member = Member::factory()->defaults()->create();
$this->get("/member/{$member->id}")
->assertInertiaPath('data.links.edit', route('member.edit', $member))
->assertInertiaPath('data.links.show', route('member.show', $member))
->assertInertiaPath('data.links.invoiceposition_index', route('member.invoice-position.index', $member))
->assertInertiaPath('data.links.membership_index', route('member.membership.index', $member))
->assertInertiaPath('data.links.course_index', route('member.course.index', $member));
});
it('shows invoice positions', function () {
Carbon::setTestNow(Carbon::parse('2006-01-01 15:00:00'));
$this->withoutExceptionHandling()->login()->loginNami();
$member = Member::factory()
->defaults()
->has(InvoicePosition::factory()->for(Invoice::factory())->price(1050)->description('uu'))
->create();
$this->get("/member/{$member->id}")
->assertInertiaPath('data.pending_payment', '10,50 €')
->assertInertiaPath('data.invoicePositions.0.description', 'uu')
->assertInertiaPath('data.invoicePositions.0.price_human', '10,50 €')
->assertInertiaPath('data.invoicePositions.0.invoice.status', 'Neu');
});
it('shows member single', function (array $attributes, array $expect) {
Carbon::setTestNow(Carbon::parse('2006-01-01 15:00:00'));
$this->withoutExceptionHandling()->login()->loginNami();
$member = Member::factory()
->for(Group::factory())
->for(Nationality::factory()->name('deutsch'))
->for(Subscription::factory()->forFee())
->create($attributes);
$this->get("/member/{$member->id}")
->assertInertiaPath('data.id', $member->id)
->assertInertiaPathArray($expect);
})->with([
fn() => [['region_id' => Region::factory()->name('UUU')->create()->id], ['data.region.name' => 'UUU', 'data.region.id' => Region::first()->id, 'data.region_id' => Region::first()->id]],
fn() => [['confession_id' => Confession::factory()->create(['name' => 'UUU'])->id], ['data.confession_id' => Confession::firstWhere('name', 'UUU')->id]],
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]],
fn() => [['group_id' => Group::factory()->name('UUU')->create()->id], ['data.group_name' => 'UUU', 'data.group_id' => Group::firstWhere('name', 'UUU')->id]],
fn() => [['bill_kind' => BillKind::EMAIL->value], ['data.bill_kind_name' => 'E-Mail', 'data.bill_kind' => 'E-Mail']],
fn() => [['subscription_id' => Subscription::factory()->name('Sub')->forFee()->create()], ['data.subscription.name' => 'Sub', 'data.subscription_id' => Subscription::first()->id]],
fn() => [['country_id' => Country::factory()->create(['name' => 'Sub'])->id], ['data.country_id' => Country::firstWhere('name', 'Sub')->id]],
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]],
[['firstname' => 'Max', 'lastname' => 'Muster', 'gender_id' => null], ['data.fullname' => 'Max Muster']],
[['other_country' => 'other', 'further_address' => 'other', 'letter_address' => 'A'], ['data.other_country' => 'other', 'data.further_address' => 'other', 'data.letter_address' => 'A']],
[['gender_id' => null], ['data.gender_name' => 'keine Angabe']],
[['salutation' => 'Dr'], ['data.salutation' => 'Dr']],
[['comment' => 'Com'], ['data.comment' => 'Com']],
[['birthday' => null], ['data.birthday' => null, 'data.birthday_human' => null]],
[[], ['data.bank_account.iban' => null]],
[
['efz' => null, 'ps_at' => null, 'ps_at' => null, 'more_ps_at' => null, 'has_svk' => false, 'has_vk' => false, 'has_svk' => true],
['data.efz_human' => null, 'data.ps_at_human' => null, 'data.ps_at_human' => null, 'data.more_ps_at_human' => null, 'data.has_svk' => false, 'data.has_vk' => false, 'data.has_svk' => true]
],
[
['has_vk' => true, 'multiply_more_pv' => false, 'without_efz_at' => null, 'without_education_at' => null],
['data.has_vk' => true, 'data.multiply_more_pv' => false, 'data.without_efz_at_human' => null, 'data.without_education_at_human' => null]
],
[
['main_phone' => '+49 212 1266775', 'mobile_phone' => '+49 212 1266776', 'work_phone' => '+49 212 1266777', 'children_phone' => '+49 212 1266778'],
['data.main_phone' => '+49 212 1266775', 'data.mobile_phone' => '+49 212 1266776', 'data.work_phone' => '+49 212 1266777', 'data.children_phone' => '+49 212 1266778']
],
[
['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'],
['data.efz' => '2022-09-20', 'data.efz_human' => '20.09.2022', 'data.ps_at' => '2022-04-20', 'data.more_ps_at' => '2022-06-02', 'data.without_education_at' => '2022-06-03', 'data.without_efz_at' => '2022-06-04', 'data.ps_at_human' => '20.04.2022', 'data.more_ps_at_human' => '02.06.2022', 'data.without_education_at_human' => '03.06.2022', 'data.without_efz_at_human' => '04.06.2022']
],
[['recertified_at' => '2022-06-13'], ['data.recertified_at_human' => '13.06.2022', 'data.recertified_at' => '2022-06-13']],
[['multiply_pv' => true, 'multiply_more_pv' => true], ['data.multiply_pv' => true, 'data.multiply_more_pv' => true]],
[
['email' => 'a@b.de', 'email_parents' => 'b@c.de', 'fax' => '+49 212 1255674'],
['data.email' => 'a@b.de', 'data.email_parents' => 'b@c.de', '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, 'lat' => 14.053], ['data.lon' => 19.05, '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, 'keepdata' => true], ['data.mitgliedsnr' => 998, 'data.keepdata' => true]],
]);
it('testItShowsIfMembershipIsActive', function (Carbon $from, ?Carbon $to, bool $isActive) {
$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');
})->with([
[now()->subMonths(2), null, true],
[now()->subMonths(2), now()->subDay(), false],
[now()->addDays(2), null, false],
]);

View File

@ -3,66 +3,106 @@
namespace Tests\Feature\Membership; namespace Tests\Feature\Membership;
use App\Group; use App\Group;
use App\Lib\Data\DateData;
use App\Member\Data\ActivityData;
use App\Member\Data\GroupData;
use App\Member\Data\MembershipData;
use App\Member\Data\SubactivityData;
use App\Member\Member; use App\Member\Member;
use App\Member\Membership; use App\Member\Membership;
use Generator;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Foundation\Testing\DatabaseTransactions;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
class IndexTest extends TestCase covers(MembershipData::class);
{ covers(DateData::class);
covers(GroupData::class);
covers(ActivityData::class);
covers(SubactivityData::class);
uses(DatabaseTransactions::class);
use DatabaseTransactions; it('testItShowsActivityAndSubactivityNamesOfMember', function () {
$this->withoutExceptionHandling()->login()->loginNami();
$group = Group::factory()->create(['name' => 'aaaaaaaa']);
$member = Member::factory()
->defaults()
->for($group)
->has(Membership::factory()->for($group)->in('€ Mitglied', 122, 'Wölfling', 234)->from('2022-11-02')->promise(now()))
->create();
$membership = $member->memberships->first();
public function testItShowsActivityAndSubactivityNamesOfMember(): void $this->get("/member/{$member->id}/membership")
{ ->assertJsonPath('data.0.activity.id', $membership->activity_id)
$this->withoutExceptionHandling()->login()->loginNami(); ->assertJsonPath('data.0.subactivity.id', $membership->subactivity_id)
$group = Group::factory()->create(['name' => 'aaaaaaaa']); ->assertJsonPath('data.0.activity.name', '€ Mitglied')
$member = Member::factory() ->assertJsonPath('data.0.subactivity.name', 'Wölfling')
->defaults() ->assertJsonPath('data.0.from.human', '02.11.2022')
->for($group) ->assertJsonPath('data.0.from.raw', '2022-11-02')
->has(Membership::factory()->for($group)->in('€ Mitglied', 122, 'Wölfling', 234)->from('2022-11-02')) ->assertJsonPath('data.0.promisedAt.raw', now()->format('Y-m-d'))
->create(); ->assertJsonPath('data.0.promisedAt.human', now()->format('d.m.Y'))
$membership = $member->memberships->first(); ->assertJsonPath('data.0.group.id', $group->id)
->assertJsonPath('data.0.links.update', route('membership.update', ['membership' => $membership]))
->assertJsonPath('data.0.links.destroy', route('membership.destroy', ['membership' => $membership]));
});
$this->get("/member/{$member->id}/membership") it('activity and subactivity can be null', function () {
->assertJsonPath('data.0.activity_id', $membership->activity_id) $this->withoutExceptionHandling()->login()->loginNami();
->assertJsonPath('data.0.subactivity_id', $membership->subactivity_id) $group = Group::factory()->create(['name' => 'aaaaaaaa']);
->assertJsonPath('data.0.activity_name', '€ Mitglied') $member = Member::factory()
->assertJsonPath('data.0.subactivity_name', 'Wölfling') ->defaults()
->assertJsonPath('data.0.human_date', '02.11.2022') ->for($group)
->assertJsonPath('data.0.group_id', $group->id) ->has(Membership::factory()->for($group)->in('€ Mitglied', 122)->from('2022-11-02')->promise(now()))
->assertJsonPath('data.0.id', $membership->id) ->create();
->assertJsonPath('data.0.links.update', route('membership.update', ['membership' => $membership]))
->assertJsonPath('data.0.links.destroy', route('membership.destroy', ['membership' => $membership]))
->assertJsonPath('meta.default.activity_id', null)
->assertJsonPath('meta.default.group_id', $group->id)
->assertJsonPath('meta.groups.0.id', $group->id)
->assertJsonPath('meta.activities.0.id', $membership->activity_id)
->assertJsonPath("meta.subactivities.{$membership->activity_id}.0.id", $membership->subactivity_id)
->assertJsonPath("meta.subactivities.{$membership->activity_id}.0.is_age_group", true)
->assertJsonPath('meta.links.store', route('member.membership.store', ['member' => $member]));
}
public static function membershipDataProvider(): Generator $this->get("/member/{$member->id}/membership")->assertNull('data.0.subactivity');
{ });
yield [now()->subMonths(2), null, true];
yield [now()->subMonths(2), now()->subDay(), false];
yield [now()->addDays(2), null, false];
}
#[DataProvider('membershipDataProvider')] it('returns meta', function () {
public function testItShowsIfMembershipIsActive(Carbon $from, ?Carbon $to, bool $isActive): void $this->withoutExceptionHandling()->login()->loginNami();
{ $group = Group::factory()->create(['name' => 'aaaaaaaa']);
$this->withoutExceptionHandling()->login()->loginNami(); $membership = Membership::factory()
$member = Member::factory() ->for(Member::factory()->defaults()->for($group))
->defaults() ->for($group)
->has(Membership::factory()->in('€ LeiterIn', 455, 'Pfadfinder', 15)->state(['from' => $from, 'to' => $to])) ->in('€ Mitglied', 122, 'Wölfling', 234)->from('2022-11-02')
->create(); ->create();
$this->get("/member/{$member->id}/membership") $this->get("/member/{$membership->member->id}/membership")
->assertJsonPath('data.0.is_active', $isActive); ->assertNull('meta.default.activity')
} ->assertNull('meta.default.subactivity')
} ->assertNull('meta.default.promisedAt')
->assertJsonPath('meta.default.group.id', $group->id)
->assertJsonPath('meta.default.group.name', $group->name)
->assertJsonPath('meta.groups.0.id', $group->id)
->assertJsonPath('meta.activities.0.id', $membership->activity->id)
->assertJsonPath('meta.activities.0.name', $membership->activity->name)
->assertJsonPath("meta.subactivities.{$membership->activity_id}.0.id", $membership->subactivity_id)
->assertJsonPath("meta.subactivities.{$membership->activity_id}.0.name", $membership->subactivity->name)
->assertJsonPath("meta.subactivities.{$membership->activity_id}.0.is_age_group", true)
->assertJsonPath('meta.links.store', route('member.membership.store', ['member' => $membership->member]));
});
it('promised at can be null', function () {
$this->withoutExceptionHandling()->login()->loginNami();
$member = Member::factory()
->defaults()
->has(Membership::factory()->in('€ Mitglied', 122, 'Wölfling', 234))
->create();
$this->get("/member/{$member->id}/membership")
->assertJsonPath('data.0.promisedAt', null);
});
it('testItShowsIfMembershipIsActive', function (Carbon $from, ?Carbon $to, bool $isActive) {
$this->withoutExceptionHandling()->login()->loginNami();
$member = Member::factory()
->defaults()
->has(Membership::factory()->in('€ LeiterIn', 455, 'Pfadfinder', 15)->state(['from' => $from, 'to' => $to]))
->create();
$this->get("/member/{$member->id}/membership")
->assertJsonPath('data.0.isActive', $isActive);
})->with([
[now()->subMonths(2), null, true],
[now()->subMonths(2), now()->subDay(), false],
[now()->addDays(2), null, false],
]);

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;
@ -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;
});
} }
} }

View File

@ -59,5 +59,5 @@
"@/*": ["./resources/js/*"] "@/*": ["./resources/js/*"]
} }
}, },
"include": ["**/*", "resources/js/components/components.d.ts"] "include": ["**/*", "resources/types/generated.d.ts", "resources/types/custom.d.ts"]
} }