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
{
$member = Member::withPendingPayment()->with([
'memberships.activity',
'memberships.subactivity',
'invoicePositions.invoice',
'nationality',
'region',
'subscription',
'courses.course',
'bankAccount',
])->find($member->id);
return [
'data' => new MemberResource(
$member
->load('memberships')
->load('invoicePositions.invoice')
->load('nationality')
->load('region')
->load('subscription')
->load('courses.course')
),
'data' => new MemberResource($member),
'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\NationalityResource;
use App\Member\Resources\RegionResource;
use App\Membership\MembershipResource;
use App\Member\Data\MembershipData;
use App\Nationality;
use App\Payment\Subscription;
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,
'age_group_icon' => $this->ageGroupMemberships->first()?->subactivity->slug,
'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')),
'nationality' => new NationalityResource($this->whenLoaded('nationality')),
'region' => new RegionResource($this->whenLoaded('region')),
@ -138,7 +138,7 @@ class MemberResource extends JsonResource
return [
'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]),
'formActivities' => $activities->pluck('name', 'id'),
'formActivities' => $activities->map(fn($a) => ['id' => $a->id, 'name' => $a->name]),
'formSubactivities' => $activities->map(function (Activity $activity) {
return ['subactivities' => $activity->subactivities->pluck('name', 'id'), 'id' => $activity->id];
})->pluck('subactivities', 'id'),
@ -148,11 +148,11 @@ class MemberResource extends JsonResource
})->pluck('subactivities', 'id'),
'groups' => NestedGroup::cacheForSelect(),
'filter' => FilterScope::fromRequest(request()->input('filter', '')),
'courses' => Course::pluck('name', 'id'),
'regions' => Region::forSelect(),
'subscriptions' => Subscription::pluck('name', 'id'),
'countries' => Country::pluck('name', 'id'),
'genders' => Gender::pluck('name', 'id'),
'courses' => Course::get()->map(fn($a) => ['id' => $a->id, 'name' => $a->name]),
'regions' => Region::get()->map(fn($a) => ['id' => $a->id, 'name' => $a->name]),
'subscriptions' => Subscription::get()->map(fn($a) => ['id' => $a->id, 'name' => $a->name]),
'countries' => Country::get()->map(fn($c) => ['id' => $c->id, 'name' => $c->name]),
'genders' => Gender::get()->map(fn($c) => ['id' => $c->id, 'name' => $c->name]),
'billKinds' => BillKind::forSelect(),
'nationalities' => Nationality::pluck('name', 'id'),
'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\Membership;
use App\Membership\MembershipResource;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Lorisleiva\Actions\Concerns\AsAction;
use App\Member\Data\MembershipData;
use Illuminate\Http\JsonResponse;
class IndexAction
{
@ -21,11 +21,11 @@ class IndexAction
return $member->memberships;
}
public function asController(Member $member): AnonymousResourceCollection
public function asController(Member $member): JsonResponse
{
return MembershipResource::collection($this->handle($member))
->additional([
'meta' => MembershipResource::memberMeta($member)
]);
return response()->json([
'data' => MembershipData::collect($this->handle($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-ignition": "^2.0",
"spatie/laravel-settings": "^3.0",
"spatie/laravel-typescript-transformer": "^2.5",
"worksome/request-factories": "^3.0",
"zoomyboy/laravel-nami": "dev-master",
"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",
"This file is @generated automatically"
],
"content-hash": "b901e66375d81c04e5067a7352780f13",
"content-hash": "e5a34643b9d5b3f0fe8e4cc9c7e9570d",
"packages": [
{
"name": "amphp/amp",
@ -11389,6 +11389,87 @@
],
"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",
"version": "2.3.1",
@ -11529,6 +11610,78 @@
],
"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",
"version": "1.0.5",
@ -14702,7 +14855,7 @@
"dist": {
"type": "path",
"url": "./packages/tex",
"reference": "48251272de62e3fea044a7ad31e1a411c15eb4c6"
"reference": "ed283d97ca7680b3c27b2d75da9937f4f379e321"
},
"type": "library",
"extra": {
@ -15792,32 +15945,7 @@
"time": "2025-03-03T07:12:39+00:00"
}
],
"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"
}
],
"aliases": [],
"minimum-stability": "dev",
"stability-flags": {
"league/flysystem-webdav": 20,
@ -15832,5 +15960,5 @@
"php": "^8.3"
},
"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
meilisearch:
ports:
- "7700:7700"
image: getmeili/meilisearch:v1.6
volumes:
- ./data/meilisearch:/meili_data

View File

@ -68,11 +68,6 @@ parameters:
count: 1
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\\.$#"
count: 1

View File

@ -1,10 +1,10 @@
.custom-table {
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;
}
& > tr {
& > tr, & > tbody > tr {
@apply text-gray-200 transition-all duration-300 rounded hover:bg-gray-800;
& > td {
@apply py-1 px-6;
@ -12,10 +12,10 @@
}
&.custom-table-sm {
& > thead > th {
& > thead > th, & > thead > tr > th {
@apply px-3 py-2;
}
& > tr {
& > tr, & > tbody > tr {
& > td {
@apply py-1 px-3;
}
@ -23,20 +23,11 @@
}
&.custom-table-light {
& > thead > th {
& > thead > th, & > thead > tr > th {
@apply border-gray-500;
}
& > td {
&:hover {
@apply bg-gray-700;
}
& > tr, & > tbody > tr {
@apply hover:bg-gray-700;
}
}
}
.custom-table > * {
display: table-row;
}
.custom-table > * > * {
display: table-cell;
}

View File

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

View File

@ -1,28 +1,14 @@
<template>
<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>
</template>
<script setup>
defineProps({
tooltip: {
required: true,
type: String,
},
href: {
type: String,
default: () => '#',
required: false,
},
blank: {
type: Boolean,
default: () => false,
required: false,
},
icon: {
type: String,
required: true,
},
});
<script lang="ts" setup>
const {tooltip, icon, blank = false, href = '#'} = defineProps<{
tooltip: string,
href?: string,
blank?: boolean,
icon: string,
}>();
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,9 +22,9 @@
<member-filter-fields :model-value="filter" @update:model-value="setFilterObject($event)" />
</template>
<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">
<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>
</button>
</template>
@ -32,7 +32,7 @@
<table cellspacing="0" cellpadding="0" border="0" class="custom-table custom-table-sm hidden md:table">
<thead>
<th></th>
<th />
<th>Nachname</th>
<th>Vorname</th>
<th class="!hidden 2xl:!table-cell">Ort</th>
@ -40,26 +40,26 @@
<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')">Ausstand</th>
<th></th>
<th />
</thead>
<tr v-for="(member, index) in data" :key="index">
<td><ui-age-groups :member="member"></ui-age-groups></td>
<td v-text="member.lastname"></td>
<td v-text="member.firstname"></td>
<td class="!hidden 2xl:!table-cell" v-text="member.full_address"></td>
<td><ui-age-groups :member="member" /></td>
<td v-text="member.lastname" />
<td v-text="member.firstname" />
<td class="!hidden 2xl:!table-cell" v-text="member.full_address" />
<td>
<tags :member="member"></tags>
<tags :member="member" />
</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">
<ui-label :value="member.bill_kind_name" fallback="kein"></ui-label>
<ui-label :value="member.bill_kind_name" fallback="kein" />
</td>
<td v-if="hasModule('bill')">
<ui-label :value="member.pending_payment" fallback="---"></ui-label>
<ui-label :value="member.pending_payment" fallback="---" />
</td>
<td>
<actions :member="member" @sidebar="openSidebar($event, member)" @remove="remove(member)"></actions>
<actions :member="member" @sidebar="openSidebar($event, member)" @remove="remove(member)" />
</td>
</tr>
</table>
@ -67,28 +67,28 @@
<div class="md:hidden p-3 grid gap-3">
<ui-box v-for="(member, index) in data" :key="index" class="relative" :heading="member.fullname">
<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>
<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">
<tags :member="member"></tags>
<ui-label v-show="hasModule('bill')" class="text-gray-100 block" :value="member.pending_payment" fallback=""></ui-label>
<tags :member="member" />
<ui-label v-show="hasModule('bill')" class="text-gray-100 block" :value="member.pending_payment" fallback="" />
</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">
<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>
</ui-box>
</div>
<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>
<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-memberships v-if="single.type === 'membership'" :url="single.model.links.membership_index" @close="closeSidebar"></member-memberships>
<member-courses v-if="single.type === 'courses'" :url="single.model.links.course_index" @close="closeSidebar"></member-courses>
<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-courses v-if="single.type === 'courses'" :url="single.model.links.course_index" @close="closeSidebar" />
</ui-sidebar>
</page-layout>
</template>
@ -107,7 +107,7 @@ const single = ref(null);
const deleting = ref(null);
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() {
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');
}
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
{
$this->withoutExceptionHandling()->login()->loginNami();

View File

@ -23,23 +23,8 @@ class CreateTest extends TestCase
public function testItDisplaysCreatePage(): void
{
$activity = Activity::factory()->inNami(5)->hasAttached(Subactivity::factory()->inNami(23)->name('Biber'))->name('€ Mitglied')->create();
$subactivity = $activity->subactivities->first();
$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');
$this->get(route('member.create'))
->assertInertiaPath('meta.default.address', '');
}
public function testItDoesntDisplayActivitiesAndSubactivitiesNotInNami(): void

View File

@ -30,8 +30,6 @@ class EditTest extends TestCase
$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(false, $response, 'data.keepdata');
$this->assertInertiaHas('Doktor', $response, 'data.salutation');

View File

@ -2,193 +2,266 @@
namespace Tests\Feature\Member;
use App\Activity;
use App\Confession;
use App\Country;
use App\Course\Models\Course;
use App\Course\Models\CourseMember;
use App\Gender;
use App\Group;
use App\Invoice\BillKind;
use App\Invoice\Models\Invoice;
use App\Invoice\Models\InvoicePosition;
use App\Member\Member;
use App\Member\MemberResource;
use App\Member\Membership;
use App\Nationality;
use App\Payment\Subscription;
use App\Region;
use App\Subactivity;
use Carbon\Carbon;
use Generator;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
class ShowTest extends TestCase
{
use DatabaseTransactions;
uses(DatabaseTransactions::class);
public function setUp(): void
{
parent::setUp();
Country::factory()->create(['name' => 'Deutschland']);
}
beforeEach(function () {
Country::factory()->create(['name' => 'Deutschland']);
});
public function testItShowsSingleMember(): void
{
Carbon::setTestNow(Carbon::parse('2006-01-01 15:00:00'));
mutates(MemberResource::class);
$this->withoutExceptionHandling()->login()->loginNami();
$member = Member::factory()
->defaults()
->for(Group::factory()->name('Stamm Beispiel'))
->has(Membership::factory()->promise(now())->in('€ LeiterIn', 5, 'Jungpfadfinder', 88)->from('2022-11-19'))
->has(InvoicePosition::factory()->for(Invoice::factory())->price(1050)->description('uu'))
->for(Gender::factory()->male())
->for(Region::factory()->name('NRW'))
->postBillKind()
->inNami(123)
->for(Subscription::factory()->name('Sub')->forFee())
->has(CourseMember::factory()->for(Course::factory()->name(' Baustein 2e - Gewalt gegen Kinder und Jugendliche: Vertiefung, Prävention '))->state(['organizer' => 'DPSG', 'event_name' => 'Wochenende', 'completed_at' => '2022-03-03']), 'courses')
->create([
'birthday' => '1991-04-20',
'address' => 'Itterstr 3',
'zip' => '42719',
'location' => 'Solingen',
'firstname' => 'Max',
'lastname' => 'Muster',
'other_country' => 'other',
'main_phone' => '+49 212 1266775',
'mobile_phone' => '+49 212 1266776',
'work_phone' => '+49 212 1266777',
'children_phone' => '+49 212 1266778',
'email' => 'a@b.de',
'email_parents' => 'b@c.de',
'fax' => '+49 212 1255674',
'efz' => '2022-09-20',
'ps_at' => '2022-04-20',
'more_ps_at' => '2022-06-02',
'recertified_at' => '2022-06-13',
'without_education_at' => '2022-06-03',
'without_efz_at' => '2022-06-04',
'has_vk' => true,
'has_svk' => true,
'multiply_pv' => true,
'multiply_more_pv' => true,
'send_newspaper' => true,
'joined_at' => '2022-06-11',
'mitgliedsnr' => 998,
'lon' => 19.05,
'lat' => 14.053,
]);
it('shows courses', function () {
$this->withoutExceptionHandling()->login()->loginNami();
$member = Member::factory()
->defaults()
->has(
CourseMember::factory()
->for(Course::factory()->name(' Baustein 2e - Gewalt gegen Kinder und Jugendliche: Vertiefung, Prävention '))
->state(['organizer' => 'DPSG', 'event_name' => 'Wochenende', 'completed_at' => '2022-03-03']),
'courses'
)
->create();
$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([
'birthday_human' => '20.04.1991',
'age' => 14,
'group_name' => 'Stamm Beispiel',
'full_address' => 'Itterstr 3, 42719 Solingen',
'region' => ['name' => 'NRW'],
'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',
'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');
}
it('shows default', function () {
$this->withoutExceptionHandling()->login()->loginNami();
$activity = Activity::factory()->name('€ Mitglied')->create();
$subactivity = Subactivity::factory()->name('Jungpfadfinder')->create();
$activity->subactivities()->attach($subactivity);
$course = Course::factory()->name('LL')->create();
$region = Region::factory()->name('LLa')->create();
$member = Member::factory()->male()->defaults()->create();
public function testItShowsMinimalSingleMember(): void
{
$this->withoutExceptionHandling()->login()->loginNami();
$member = Member::factory()
->for(Group::factory())
->for(Nationality::factory()->name('deutsch'))
->for(Subscription::factory()->forFee())
->create(['firstname' => 'Max', 'lastname' => 'Muster', 'has_vk' => false, 'has_svk' => false]);
$this->get("/member/{$member->id}")
->assertInertiaPath('meta.default.address', '')
->assertInertiaPath('meta.filterActivities.0.name', '€ Mitglied')
->assertInertiaPath('meta.filterActivities.0.id', $activity->id)
->assertInertiaPath('meta.filterSubactivities.0.name', 'Jungpfadfinder')
->assertInertiaPath('meta.filterSubactivities.0.id', $subactivity->id)
->assertInertiaPath('meta.formActivities.0.name', '€ Mitglied')
->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([
'region' => ['name' => '-- kein --'],
'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');
}
$this->get("/member/{$member->id}")
->assertInertiaPath('data.efz_link', route('efz', $member));
});
public static function membershipDataProvider(): Generator
{
yield [now()->subMonths(2), null, true];
yield [now()->subMonths(2), now()->subDay(), false];
yield [now()->addDays(2), null, false];
}
it('shows memberships', function () {
Carbon::setTestNow(Carbon::parse('2006-01-01 15:00:00'));
#[DataProvider('membershipDataProvider')]
public function testItShowsIfMembershipIsActive(Carbon $from, ?Carbon $to, bool $isActive): void
{
$this->withoutExceptionHandling()->login()->loginNami();
$member = Member::factory()
->defaults()
->has(Membership::factory()->in('€ LeiterIn', 455, 'Pfadfinder', 15)->state(['from' => $from, 'to' => $to]))
->create();
$this->withoutExceptionHandling()->login()->loginNami();
$member = Member::factory()
->defaults()
->for(Group::factory()->name('Stamm Beispiel'))
->has(Membership::factory()->promise(now())->in('€ LeiterIn', 5, 'Jungpfadfinder', 88)->from('2005-11-19'))
->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;
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\Membership;
use Generator;
use Carbon\Carbon;
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->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'))
->create();
$membership = $member->memberships->first();
$this->get("/member/{$member->id}/membership")
->assertJsonPath('data.0.activity.id', $membership->activity_id)
->assertJsonPath('data.0.subactivity.id', $membership->subactivity_id)
->assertJsonPath('data.0.activity.name', '€ Mitglied')
->assertJsonPath('data.0.subactivity.name', 'Wölfling')
->assertJsonPath('data.0.from.human', '02.11.2022')
->assertJsonPath('data.0.from.raw', '2022-11-02')
->assertJsonPath('data.0.promisedAt.raw', now()->format('Y-m-d'))
->assertJsonPath('data.0.promisedAt.human', now()->format('d.m.Y'))
->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")
->assertJsonPath('data.0.activity_id', $membership->activity_id)
->assertJsonPath('data.0.subactivity_id', $membership->subactivity_id)
->assertJsonPath('data.0.activity_name', '€ Mitglied')
->assertJsonPath('data.0.subactivity_name', 'Wölfling')
->assertJsonPath('data.0.human_date', '02.11.2022')
->assertJsonPath('data.0.group_id', $group->id)
->assertJsonPath('data.0.id', $membership->id)
->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]));
}
it('activity and subactivity can be null', 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)->from('2022-11-02')->promise(now()))
->create();
public static function membershipDataProvider(): Generator
{
yield [now()->subMonths(2), null, true];
yield [now()->subMonths(2), now()->subDay(), false];
yield [now()->addDays(2), null, false];
}
$this->get("/member/{$member->id}/membership")->assertNull('data.0.subactivity');
});
#[DataProvider('membershipDataProvider')]
public function testItShowsIfMembershipIsActive(Carbon $from, ?Carbon $to, bool $isActive): void
{
$this->withoutExceptionHandling()->login()->loginNami();
$member = Member::factory()
->defaults()
->has(Membership::factory()->in('€ LeiterIn', 455, 'Pfadfinder', 15)->state(['from' => $from, 'to' => $to]))
->create();
it('returns meta', function () {
$this->withoutExceptionHandling()->login()->loginNami();
$group = Group::factory()->create(['name' => 'aaaaaaaa']);
$membership = Membership::factory()
->for(Member::factory()->defaults()->for($group))
->for($group)
->in('€ Mitglied', 122, 'Wölfling', 234)->from('2022-11-02')
->create();
$this->get("/member/{$member->id}/membership")
->assertJsonPath('data.0.is_active', $isActive);
}
}
$this->get("/member/{$membership->member->id}/membership")
->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 */
$response = $this;
$props = data_get($response->viewData('page'), 'props');
Assert::assertTrue(Arr::has($props, $path), 'Failed that key ' . $path . ' is in Response.');
Assert::assertNotNull($props);
$json = new AssertableJsonString($props);
$json->assertPath($path, $value);
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) {
/** @var TestResponse */
$response = $this;
@ -171,5 +183,13 @@ class TestCase extends BaseTestCase
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/*"]
}
},
"include": ["**/*", "resources/js/components/components.d.ts"]
"include": ["**/*", "resources/types/generated.d.ts", "resources/types/custom.d.ts"]
}