Update course actions

This commit is contained in:
Philipp Lang 2023-10-18 01:11:36 +02:00
parent 6d4dda869a
commit 36c0ebced0
21 changed files with 380 additions and 291 deletions

View File

@ -0,0 +1,45 @@
<?php
namespace App\Course\Actions;
use App\Course\Models\CourseMember;
use App\Lib\JobMiddleware\JobChannels;
use App\Lib\JobMiddleware\WithJobState;
use App\Lib\Queue\TracksJob;
use App\Setting\NamiSettings;
use Illuminate\Http\JsonResponse;
use Lorisleiva\Actions\Concerns\AsAction;
class CourseDestroyAction
{
use AsAction;
use TracksJob;
public function handle(CourseMember $course): void
{
app(NamiSettings::class)->login()->deleteCourse($course->member->nami_id, $course->nami_id);
$course->delete();
}
public function asController(CourseMember $course): JsonResponse
{
$this->startJob($course);
return response()->json([]);
}
/**
* @param mixed $parameters
*/
public function jobState(WithJobState $jobState, ...$parameters): WithJobState
{
$member = $parameters[0]->member;
return $jobState
->before('Ausbildung für ' . $member->fullname . ' wird gelöscht')
->after('Ausbildung für ' . $member->fullname . ' gelöscht')
->failed('Fehler beim Löschen der Ausbildung für ' . $member->fullname)
->shouldReload(JobChannels::make()->add('member')->add('course'));
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Course\Actions;
use App\Course\Models\Course;
use App\Course\Resources\CourseMemberResource;
use App\Member\Member;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Lorisleiva\Actions\Concerns\AsAction;
class CourseIndexAction
{
use AsAction;
/**
* @return Collection<int, Course>
*/
public function handle(Member $member): Collection
{
return $member->courses()->with('course')->get();
}
public function asController(Member $member): AnonymousResourceCollection
{
return CourseMemberResource::collection($this->handle($member))
->additional([
'meta' => CourseMemberResource::memberMeta($member),
]);
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace App\Course\Actions;
use App\Course\Models\Course;
use App\Lib\JobMiddleware\JobChannels;
use App\Lib\JobMiddleware\WithJobState;
use App\Lib\Queue\TracksJob;
use App\Member\Member;
use App\Setting\NamiSettings;
use Illuminate\Http\JsonResponse;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class CourseStoreAction
{
use AsAction;
use TracksJob;
/**
* @param array<string, mixed> $attributes
*/
public function handle(Member $member, array $attributes): void
{
$course = Course::where('id', $attributes['course_id'])->firstOrFail();
$payload = collect($attributes)->only(['event_name', 'completed_at', 'organizer'])->merge([
'course_id' => $course->nami_id,
])->toArray();
$namiId = app(NamiSettings::class)->login()->createCourse($member->nami_id, $payload);
$member->courses()->create([
...$attributes,
'nami_id' => $namiId,
]);
}
/**
* @return array<string, string>
*/
public function rules(): array
{
return [
'organizer' => 'required|max:255',
'event_name' => 'required|max:255',
'completed_at' => 'required|date',
'course_id' => 'required|exists:courses,id',
];
}
public function asController(Member $member, ActionRequest $request): JsonResponse
{
$this->startJob($member, $request->validated());
return response()->json([]);
}
/**
* @param mixed $parameters
*/
public function jobState(WithJobState $jobState, ...$parameters): WithJobState
{
$member = $parameters[0];
return $jobState
->before('Ausbildung für ' . $member->fullname . ' wird gespeichert')
->after('Ausbildung für ' . $member->fullname . ' gespeichert')
->failed('Fehler beim Erstellen der Ausbildung für ' . $member->fullname)
->shouldReload(JobChannels::make()->add('member')->add('course'));
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace App\Course\Actions;
use App\Course\Models\Course;
use App\Course\Models\CourseMember;
use App\Course\Resources\CourseMemberResource;
use App\Lib\JobMiddleware\JobChannels;
use App\Lib\JobMiddleware\WithJobState;
use App\Lib\Queue\TracksJob;
use App\Member\Member;
use App\Setting\NamiSettings;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class CourseUpdateAction
{
use AsAction;
use TracksJob;
/**
* @return Collection<int, Course>
*/
public function handle(CourseMember $course, array $attributes): void
{
app(NamiSettings::class)->login()->updateCourse(
$course->member->nami_id,
$course->nami_id,
[
...$attributes,
'course_id' => Course::find($attributes['course_id'])->nami_id,
]
);
$course->update($attributes);
}
/**
* @return array<string, string>
*/
public function rules()
{
return [
'organizer' => 'required|max:255',
'event_name' => 'required|max:255',
'completed_at' => 'required|date',
'course_id' => 'required|exists:courses,id',
];
}
public function asController(CourseMember $course, ActionRequest $request): JsonResponse
{
$this->startJob($course, $request->validated());
return response()->json([]);
}
/**
* @param mixed $parameters
*/
public function jobState(WithJobState $jobState, ...$parameters): WithJobState
{
$member = $parameters[0]->member;
return $jobState
->before('Ausbildung für ' . $member->fullname . ' wird gespeichert')
->after('Ausbildung für ' . $member->fullname . ' gespeichert')
->failed('Fehler beim Erstellen der Ausbildung für ' . $member->fullname)
->shouldReload(JobChannels::make()->add('member')->add('course'));
}
}

View File

@ -1,36 +0,0 @@
<?php
namespace App\Course\Controllers;
use App\Course\Models\CourseMember;
use App\Course\Requests\DestroyRequest;
use App\Course\Requests\StoreRequest;
use App\Course\Requests\UpdateRequest;
use App\Http\Controllers\Controller;
use App\Member\Member;
use App\Setting\NamiSettings;
use Illuminate\Http\RedirectResponse;
class CourseController extends Controller
{
public function store(Member $member, StoreRequest $request, NamiSettings $settings): RedirectResponse
{
$request->persist($member, $settings);
return redirect()->back()->success('Ausbildung erstellt');
}
public function update(Member $member, CourseMember $course, UpdateRequest $request, NamiSettings $settings): RedirectResponse
{
$request->persist($member, $course, $settings);
return redirect()->back()->success('Ausbildung aktualisiert');
}
public function destroy(Member $member, CourseMember $course, DestroyRequest $request, NamiSettings $settings): RedirectResponse
{
$request->persist($member, $course, $settings);
return redirect()->back()->success('Ausbildung gelöscht');
}
}

View File

@ -22,4 +22,12 @@ class Course extends Model
->trim() ->trim()
->replaceMatches('/ - .*/', ''); ->replaceMatches('/ - .*/', '');
} }
/**
* @return array<int, array{id: int, name: string}>
*/
public static function forSelect(): array
{
return static::select('name', 'id')->get()->toArray();
}
} }

View File

@ -2,6 +2,7 @@
namespace App\Course\Models; namespace App\Course\Models;
use App\Member\Member;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -20,4 +21,12 @@ class CourseMember extends Model
{ {
return $this->belongsTo(Course::class); return $this->belongsTo(Course::class);
} }
/**
* @return BelongsTo<Member, self>
*/
public function member(): BelongsTo
{
return $this->belongsTo(Member::class);
}
} }

View File

@ -1,38 +0,0 @@
<?php
namespace App\Course\Requests;
use App\Course\Models\CourseMember;
use App\Member\Member;
use App\Setting\NamiSettings;
use Illuminate\Foundation\Http\FormRequest;
class DestroyRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, string>
*/
public function rules()
{
return [];
}
public function persist(Member $member, CourseMember $course, NamiSettings $settings): void
{
$settings->login()->deleteCourse($member->nami_id, $course->nami_id);
$course->delete();
}
}

View File

@ -1,49 +0,0 @@
<?php
namespace App\Course\Requests;
use App\Course\Models\Course;
use App\Member\Member;
use App\Setting\NamiSettings;
use Illuminate\Foundation\Http\FormRequest;
class StoreRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, string>
*/
public function rules()
{
return [
'organizer' => 'required|max:255',
'event_name' => 'required|max:255',
'completed_at' => 'required|date',
'course_id' => 'required|exists:courses,id',
];
}
public function persist(Member $member, NamiSettings $settings): void
{
$course = Course::where('id', $this->input('course_id'))->firstOrFail();
$payload = collect($this->input())->only(['event_name', 'completed_at', 'organizer'])->merge([
'course_id' => $course->nami_id,
])->toArray();
$namiId = $settings->login()->createCourse($member->nami_id, $payload);
$member->courses()->create($this->safe()->collect()->put('nami_id', $namiId)->toArray());
}
}

View File

@ -1,48 +0,0 @@
<?php
namespace App\Course\Requests;
use App\Course\Models\Course;
use App\Course\Models\CourseMember;
use App\Member\Member;
use App\Setting\NamiSettings;
use Illuminate\Foundation\Http\FormRequest;
class UpdateRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, string>
*/
public function rules()
{
return [
'organizer' => 'required|max:255',
'event_name' => 'required|max:255',
'completed_at' => 'required|date',
'course_id' => 'required|exists:courses,id',
];
}
public function persist(Member $member, CourseMember $course, NamiSettings $settings): void
{
$settings->login()->updateCourse(
$member->nami_id,
$course->nami_id,
$this->safe()->merge(['course_id' => Course::find($this->input('course_id'))->nami_id])->toArray()
);
$course->update($this->safe()->toArray());
}
}

View File

@ -2,6 +2,8 @@
namespace App\Course\Resources; namespace App\Course\Resources;
use App\Course\Models\Course;
use App\Member\Member;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
@ -28,6 +30,29 @@ class CourseMemberResource extends JsonResource
'course_name' => $this->course->name, 'course_name' => $this->course->name,
'course_id' => $this->course->id, 'course_id' => $this->course->id,
'course' => new CourseResource($this->whenLoaded('course')), 'course' => new CourseResource($this->whenLoaded('course')),
'links' => [
'update' => route('course.update', ['course' => $this->getModel()]),
'destroy' => route('course.destroy', ['course' => $this->getModel()]),
]
];
}
/**
* @return array<string, mixed>
*/
public static function memberMeta(Member $member): array
{
return [
'default' => [
'event_name' => '',
'completed_at' => null,
'course_id' => null,
'organizer' => ''
],
'courses' => Course::forSelect(),
'links' => [
'store' => route('member.course.store', ['member' => $member]),
]
]; ];
} }
} }

View File

@ -23,7 +23,7 @@ class MemberController extends Controller
'data' => MemberResource::collection(Member::search($filter->search)->query( 'data' => MemberResource::collection(Member::search($filter->search)->query(
fn ($q) => $q->select('*') fn ($q) => $q->select('*')
->withFilter($filter) ->withFilter($filter)
->with(['courses', 'subscription', 'leaderMemberships', 'ageGroupMemberships']) ->with(['subscription', 'leaderMemberships', 'ageGroupMemberships'])
->withPendingPayment() ->withPendingPayment()
->ordered() ->ordered()
)->paginate(15)), )->paginate(15)),

View File

@ -107,6 +107,7 @@ class MemberResource extends JsonResource
'links' => [ 'links' => [
'membership_index' => route('member.membership.index', ['member' => $this->getModel()]), 'membership_index' => route('member.membership.index', ['member' => $this->getModel()]),
'payment_index' => route('member.payment.index', ['member' => $this->getModel()]), 'payment_index' => route('member.payment.index', ['member' => $this->getModel()]),
'course_index' => route('member.course.index', ['member' => $this->getModel()]),
'show' => route('member.show', ['member' => $this->getModel()]), 'show' => route('member.show', ['member' => $this->getModel()]),
'edit' => route('member.edit', ['member' => $this->getModel()]), 'edit' => route('member.edit', ['member' => $this->getModel()]),
], ],

View File

@ -1,5 +1,6 @@
<template> <template>
<div class="fixed w-full w-[80vw] max-w-[30rem] shadow-2xl bg-gray-600 right-0 top-0 h-full flex flex-col group is-bright"> <div
class="fixed w-full w-[80vw] max-w-[40rem] shadow-2xl bg-gray-600 right-0 top-0 h-full flex flex-col group is-bright">
<suspense> <suspense>
<slot></slot> <slot></slot>

View File

@ -11,7 +11,7 @@
<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-text>
<f-select id="course_id" v-model="single.course_id" name="course_id" :options="courses" label="Baustein" <f-select id="course_id" v-model="single.course_id" name="course_id" :options="meta.courses" label="Baustein"
required></f-select> required></f-select>
<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>
<f-text id="organizer" v-model="single.organizer" label="Veranstalter" required></f-text> <f-text id="organizer" v-model="single.organizer" label="Veranstalter" required></f-text>
@ -28,18 +28,16 @@
<th></th> <th></th>
</thead> </thead>
<tr v-for="(course, index) in value.courses" :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>
<td v-text="course.event_name"></td> <td v-text="course.event_name"></td>
<td v-text="course.organizer"></td> <td v-text="course.organizer"></td>
<td v-text="course.completed_at_human"></td> <td v-text="course.completed_at_human"></td>
<td class="flex"> <td class="flex">
<a href="#" class="inline-flex btn btn-warning btn-sm" @click.prevent=" <a href="#" class="inline-flex btn btn-warning btn-sm" @click.prevent="edit(course)"><ui-sprite
single = course; src="pencil"></ui-sprite></a>
mode = 'edit'; <a href="#" class="inline-flex btn btn-danger btn-sm" @click.prevent="remove(course)"><ui-sprite
"><ui-sprite src="pencil"></ui-sprite></a> src="trash"></ui-sprite></a>
<i-link href="#" class="inline-flex btn btn-danger btn-sm"
@click.prevent="remove(course)"><ui-sprite src="trash"></ui-sprite></i-link>
</td> </td>
</tr> </tr>
</table> </table>
@ -47,55 +45,18 @@
</div> </div>
</template> </template>
<script> <script setup>
export default { defineEmits(['close']);
import { useApiIndex } from '../../composables/useApiIndex.js';
props: { const props = defineProps({
courses: {}, url: {
value: {}, type: String,
}, required: true,
data: function () {
return {
mode: null,
single: null,
};
}, },
});
methods: { const { data, meta, reload, cancel, single, create, edit, submit, remove } = useApiIndex(props.url, 'course');
create() {
this.mode = 'create';
this.single = {};
},
cancel() {
this.mode = this.single = null;
},
remove(payment) {
this.$inertia.delete(`/member/${this.value.id}/course/${payment.id}`);
},
openLink(link) { await reload();
if (link.disabled) {
return;
}
window.open(link.href);
},
submit() {
var _self = this;
this.mode === 'create'
? this.$inertia.post(`/member/${this.value.id}/course`, this.single, {
onFinish() {
_self.single = null;
},
})
: this.$inertia.patch(`/member/${this.value.id}/course/${this.single.id}`, this.single, {
onFinish() {
_self.single = null;
},
});
},
},
};
</script> </script>

View File

@ -1,17 +1,23 @@
<template> <template>
<page-layout page-class="pb-6"> <page-layout page-class="pb-6">
<template #toolbar> <template #toolbar>
<page-toolbar-button :href="meta.links.create" color="primary" icon="plus">Mitglied anlegen</page-toolbar-button> <page-toolbar-button :href="meta.links.create" color="primary" icon="plus">Mitglied
<page-toolbar-button v-if="hasModule('bill')" :href="meta.links.allpayment" color="primary" icon="invoice">Rechnungen erstellen</page-toolbar-button> anlegen</page-toolbar-button>
<page-toolbar-button v-if="hasModule('bill')" :href="meta.links.sendpayment" color="info" icon="envelope">Rechnungen versenden</page-toolbar-button> <page-toolbar-button v-if="hasModule('bill')" :href="meta.links.allpayment" color="primary"
icon="invoice">Rechnungen erstellen</page-toolbar-button>
<page-toolbar-button v-if="hasModule('bill')" :href="meta.links.sendpayment" color="info"
icon="envelope">Rechnungen versenden</page-toolbar-button>
</template> </template>
<ui-popup v-if="deleting !== null" heading="Mitglied löschen?" @close="deleting.reject()"> <ui-popup v-if="deleting !== null" heading="Mitglied löschen?" @close="deleting.reject()">
<div> <div>
<p class="mt-4">Das Mitglied "{{ deleting.member.fullname }}" löschen?</p> <p class="mt-4">Das Mitglied "{{ deleting.member.fullname }}" löschen?</p>
<p class="mt-2">Alle Zuordnungen (Ausbildungen, Rechnungen, Zahlungen, Tätigkeiten) werden ebenfalls entfernt.</p> <p class="mt-2">Alle Zuordnungen (Ausbildungen, Rechnungen, Zahlungen, Tätigkeiten) werden ebenfalls
<ui-note v-if="!deleting.member.has_nami" class="mt-5" type="warning"> Dieses Mitglied ist nicht in NaMi vorhanden und wird daher nur in der AdReMa gelöscht werden. </ui-note> entfernt.</p>
<ui-note v-if="!deleting.member.has_nami" class="mt-5" type="warning"> Dieses Mitglied ist nicht in NaMi
vorhanden und wird daher nur in der AdReMa gelöscht werden. </ui-note>
<ui-note v-if="deleting.member.has_nami" class="mt-5" type="danger"> <ui-note v-if="deleting.member.has_nami" class="mt-5" type="danger">
Dieses Mitglied ist in NaMi vorhanden und wird daher in NaMi abgemeldet werden. Sofern "Datenweiterverwendung" eingeschaltet ist, wird das Mitglied auf inaktiv gesetzt. Dieses Mitglied ist in NaMi vorhanden und wird daher in NaMi abgemeldet werden. Sofern
"Datenweiterverwendung" eingeschaltet ist, wird das Mitglied auf inaktiv gesetzt.
</ui-note> </ui-note>
<div class="grid grid-cols-2 gap-3 mt-6"> <div class="grid grid-cols-2 gap-3 mt-6">
<a href="#" class="text-center btn btn-danger" @click.prevent="deleting.resolve">Mitglied loschen</a> <a href="#" class="text-center btn btn-danger" @click.prevent="deleting.resolve">Mitglied loschen</a>
@ -20,45 +26,22 @@
</div> </div>
</ui-popup> </ui-popup>
<page-filter breakpoint="xl"> <page-filter breakpoint="xl">
<f-text id="search" :model-value="getFilter('search')" name="search" label="Suchen …" size="sm" @update:model-value="setFilter('search', $event)"></f-text> <f-text id="search" :model-value="getFilter('search')" name="search" label="Suchen …" size="sm"
<f-switch v-show="hasModule('bill')" id="ausstand" :model-value="getFilter('ausstand')" label="Nur Ausstände" size="sm" @update:model-value="setFilter('ausstand', $event)"></f-switch> @update:model-value="setFilter('search', $event)"></f-text>
<f-multipleselect <f-switch v-show="hasModule('bill')" id="ausstand" :model-value="getFilter('ausstand')" label="Nur Ausstände"
id="group_ids" size="sm" @update:model-value="setFilter('ausstand', $event)"></f-switch>
:options="meta.groups" <f-multipleselect id="group_ids" :options="meta.groups" :model-value="getFilter('group_ids')"
:model-value="getFilter('group_ids')" label="Gruppierungen" size="sm" name="group_ids"
label="Gruppierungen" @update:model-value="setFilter('group_ids', $event)"></f-multipleselect>
size="sm" <f-select v-show="hasModule('bill')" id="billKinds" name="billKinds" :options="meta.billKinds"
name="group_ids" :model-value="getFilter('bill_kind')" label="Rechnung" size="sm"
@update:model-value="setFilter('group_ids', $event)" @update:model-value="setFilter('bill_kind', $event)"></f-select>
></f-multipleselect> <f-multipleselect id="activity_ids" :options="meta.filterActivities" :model-value="getFilter('activity_ids')"
<f-select label="Tätigkeiten" size="sm" name="activity_ids"
v-show="hasModule('bill')" @update:model-value="setFilter('activity_ids', $event)"></f-multipleselect>
id="billKinds" <f-multipleselect id="subactivity_ids" :options="meta.filterSubactivities"
name="billKinds" :model-value="getFilter('subactivity_ids')" label="Untertätigkeiten" size="sm" name="subactivity_ids"
:options="meta.billKinds" @update:model-value="setFilter('subactivity_ids', $event)"></f-multipleselect>
:model-value="getFilter('bill_kind')"
label="Rechnung"
size="sm"
@update:model-value="setFilter('bill_kind', $event)"
></f-select>
<f-multipleselect
id="activity_ids"
:options="meta.filterActivities"
:model-value="getFilter('activity_ids')"
label="Tätigkeiten"
size="sm"
name="activity_ids"
@update:model-value="setFilter('activity_ids', $event)"
></f-multipleselect>
<f-multipleselect
id="subactivity_ids"
:options="meta.filterSubactivities"
:model-value="getFilter('subactivity_ids')"
label="Untertätigkeiten"
size="sm"
name="subactivity_ids"
@update:model-value="setFilter('subactivity_ids', $event)"
></f-multipleselect>
<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"></ui-sprite>
<span class="hidden xl:inline">Exportieren</span> <span class="hidden xl:inline">Exportieren</span>
@ -107,11 +90,14 @@
<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>
<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"></tags>
<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=""></ui-label>
</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)">
</actions>
<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"></ui-sprite></i-link>
</div> </div>
</ui-box> </ui-box>
</div> </div>
@ -121,9 +107,12 @@
</div> </div>
<ui-sidebar v-if="single !== null" @close="closeSidebar"> <ui-sidebar v-if="single !== null" @close="closeSidebar">
<member-payments v-if="single.type === 'payment'" :url="single.model.links.payment_index" @close="closeSidebar"></member-payments> <member-payments v-if="single.type === 'payment'" :url="single.model.links.payment_index"
<member-memberships v-if="single.type === 'membership'" :url="single.model.links.membership_index" @close="closeSidebar"></member-memberships> @close="closeSidebar"></member-payments>
<member-courses v-if="single.type === 'courses'" :courses="meta.courses" :value="single.model" @close="closeSidebar"></member-courses> <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>
</ui-sidebar> </ui-sidebar>
</page-layout> </page-layout>
</template> </template>
@ -134,14 +123,14 @@ import MemberMemberships from './MemberMemberships.vue';
import MemberCourses from './MemberCourses.vue'; import MemberCourses from './MemberCourses.vue';
import Tags from './Tags.vue'; import Tags from './Tags.vue';
import Actions from './index/Actions.vue'; import Actions from './index/Actions.vue';
import {indexProps, useIndex} from '../../composables/useIndex.js'; import { indexProps, useIndex } from '../../composables/useIndex.js';
import {ref, defineProps} from 'vue'; import { ref, defineProps } from 'vue';
const single = ref(null); const single = ref(null);
const deleting = ref(null); const deleting = ref(null);
const props = defineProps(indexProps); const props = defineProps(indexProps);
var {router, data, meta, getFilter, setFilter, filterString} = useIndex(props.data, 'member'); var { router, data, meta, getFilter, setFilter, filterString } = useIndex(props.data, 'member');
function exportMembers() { function exportMembers() {
window.open(`/member-export?filter=${filterString.value}`); window.open(`/member-export?filter=${filterString.value}`);
@ -149,7 +138,7 @@ function exportMembers() {
async function remove(member) { async function remove(member) {
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
deleting.value = {resolve, reject, member}; deleting.value = { resolve, reject, member };
}) })
.then(() => { .then(() => {
router.delete(`/member/${member.id}`); router.delete(`/member/${member.id}`);

View File

@ -12,7 +12,10 @@ use App\Activity\Api\SubactivityUpdateAction;
use App\Contribution\Actions\FormAction as ContributionFormAction; use App\Contribution\Actions\FormAction as ContributionFormAction;
use App\Contribution\Actions\GenerateAction as ContributionGenerateAction; use App\Contribution\Actions\GenerateAction as ContributionGenerateAction;
use App\Contribution\Actions\ValidateAction as ContributionValidateAction; use App\Contribution\Actions\ValidateAction as ContributionValidateAction;
use App\Course\Controllers\CourseController; use App\Course\Actions\CourseDestroyAction;
use App\Course\Actions\CourseIndexAction;
use App\Course\Actions\CourseStoreAction;
use App\Course\Actions\CourseUpdateAction;
use App\Dashboard\Actions\IndexAction as DashboardIndexAction; use App\Dashboard\Actions\IndexAction as DashboardIndexAction;
use App\Efz\ShowEfzDocumentAction; use App\Efz\ShowEfzDocumentAction;
use App\Group\Actions\ListAction; use App\Group\Actions\ListAction;
@ -72,7 +75,6 @@ Route::group(['middleware' => 'auth:web'], function (): void {
->name('member.singlepdf'); ->name('member.singlepdf');
Route::get('/sendpayment', [SendpaymentController::class, 'create'])->name('sendpayment.create'); Route::get('/sendpayment', [SendpaymentController::class, 'create'])->name('sendpayment.create');
Route::get('/sendpayment/pdf', [SendpaymentController::class, 'send'])->name('sendpayment.pdf'); Route::get('/sendpayment/pdf', [SendpaymentController::class, 'send'])->name('sendpayment.pdf');
Route::resource('member.course', CourseController::class);
Route::get('/member/{member}/efz', ShowEfzDocumentAction::class)->name('efz'); Route::get('/member/{member}/efz', ShowEfzDocumentAction::class)->name('efz');
Route::get('/member/{member}/resync', MemberResyncAction::class)->name('member.resync'); Route::get('/member/{member}/resync', MemberResyncAction::class)->name('member.resync');
Route::get('member-export', ExportAction::class)->name('member-export'); Route::get('member-export', ExportAction::class)->name('member-export');
@ -118,4 +120,10 @@ Route::group(['middleware' => 'auth:web'], function (): void {
Route::delete('/membership/{membership}', MembershipDestroyAction::class)->name('membership.destroy'); Route::delete('/membership/{membership}', MembershipDestroyAction::class)->name('membership.destroy');
Route::post('/api/membership/member-list', ListForGroupAction::class)->name('membership.member-list'); Route::post('/api/membership/member-list', ListForGroupAction::class)->name('membership.member-list');
Route::post('/api/membership/sync', StoreForGroupAction::class)->name('membership.sync'); Route::post('/api/membership/sync', StoreForGroupAction::class)->name('membership.sync');
// ----------------------------------- course ----------------------------------
Route::get('/member/{member}/course', CourseIndexAction::class)->name('member.course.index');
Route::post('/member/{member}/course', CourseStoreAction::class)->name('member.course.store');
Route::patch('/course/{course}', CourseUpdateAction::class)->name('course.update');
Route::delete('/course/{course}', CourseDestroyAction::class)->name('course.destroy');
}); });

View File

@ -19,7 +19,7 @@ class DeleteTest extends TestCase
app(CourseFake::class)->deletesSuccessfully(123, 999); app(CourseFake::class)->deletesSuccessfully(123, 999);
$member = Member::factory()->defaults()->inNami(123)->has(CourseMember::factory()->inNami(999)->for(Course::factory()), 'courses')->createOne(); $member = Member::factory()->defaults()->inNami(123)->has(CourseMember::factory()->inNami(999)->for(Course::factory()), 'courses')->createOne();
$this->delete("/member/{$member->id}/course/{$member->courses->first()->id}"); $this->delete("//course/{$member->courses->first()->id}");
$this->assertDatabaseCount('course_members', 0); $this->assertDatabaseCount('course_members', 0);
app(CourseFake::class)->assertDeleted(123, 999); app(CourseFake::class)->assertDeleted(123, 999);
@ -31,7 +31,7 @@ class DeleteTest extends TestCase
app(CourseFake::class)->failsDeleting(123, 999); app(CourseFake::class)->failsDeleting(123, 999);
$member = Member::factory()->defaults()->inNami(123)->has(CourseMember::factory()->inNami(999)->for(Course::factory()), 'courses')->createOne(); $member = Member::factory()->defaults()->inNami(123)->has(CourseMember::factory()->inNami(999)->for(Course::factory()), 'courses')->createOne();
$response = $this->delete("/member/{$member->id}/course/{$member->courses->first()->id}"); $response = $this->delete("/course/{$member->courses->first()->id}");
$this->assertDatabaseCount('course_members', 1); $this->assertDatabaseCount('course_members', 1);
$response->assertSessionHasErrors(['id' => 'Unbekannter Fehler']); $response->assertSessionHasErrors(['id' => 'Unbekannter Fehler']);

View File

@ -0,0 +1,35 @@
<?php
namespace Tests\Feature\Course;
use App\Course\Models\Course;
use App\Course\Models\CourseMember;
use App\Member\Member;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;
class IndexTest extends TestCase
{
use DatabaseTransactions;
public function testItShowsCourses(): void
{
$this->login()->withNamiSettings();
$member = Member::factory()->defaults()->has(CourseMember::factory()->for(Course::factory()->state(['name' => '2a']))->state(['event_name' => 'WE', 'organizer' => 'DPSG', 'completed_at' => now()->subDays(2)]), 'courses')->create();
$this->get("/member/{$member->id}/course")
->assertJsonPath('data.0.course_name', '2a')
->assertJsonPath('data.0.event_name', 'WE')
->assertJsonPath('data.0.organizer', 'DPSG')
->assertJsonPath('data.0.links.update', route('course.update', ['course' => $member->courses->first()->id]))
->assertJsonPath('data.0.links.destroy', route('course.destroy', ['course' => $member->courses->first()->id]))
->assertJsonPath('data.0.completed_at_human', now()->subDays(2)->format('d.m.Y'))
->assertJsonPath('meta.links.store', route('member.course.store', ['member' => $member]))
->assertJsonPath('meta.default.completed_at', null)
->assertJsonPath('meta.default.course_id', null)
->assertJsonPath('meta.default.event_name', '')
->assertJsonPath('meta.default.organizer', '')
->assertJsonPath('meta.courses.0.name', '2a')
->assertJsonPath('meta.courses.0.id', Course::first()->id);
}
}

View File

@ -57,7 +57,7 @@ class UpdateTest extends TestCase
$member = Member::factory()->defaults()->inNami(123)->has(CourseMember::factory()->for(Course::factory()), 'courses')->createOne(); $member = Member::factory()->defaults()->inNami(123)->has(CourseMember::factory()->for(Course::factory()), 'courses')->createOne();
$newCourse = Course::factory()->inNami(789)->create(); $newCourse = Course::factory()->inNami(789)->create();
$response = $this->patch("/member/{$member->id}/course/{$member->courses->first()->id}", array_merge([ $response = $this->patch("/course/{$member->courses->first()->id}", array_merge([
'course_id' => $newCourse->id, 'course_id' => $newCourse->id,
'completed_at' => '1999-02-03', 'completed_at' => '1999-02-03',
'event_name' => '::newevent::', 'event_name' => '::newevent::',
@ -75,7 +75,7 @@ class UpdateTest extends TestCase
$member = Member::factory()->defaults()->inNami(123)->has(CourseMember::factory()->inNami(999)->for(Course::factory()), 'courses')->createOne(); $member = Member::factory()->defaults()->inNami(123)->has(CourseMember::factory()->inNami(999)->for(Course::factory()), 'courses')->createOne();
$newCourse = Course::factory()->inNami(789)->create(); $newCourse = Course::factory()->inNami(789)->create();
$this->patch("/member/{$member->id}/course/{$member->courses->first()->id}", array_merge([ $this->patch("/course/{$member->courses->first()->id}", array_merge([
'course_id' => $newCourse->id, 'course_id' => $newCourse->id,
'completed_at' => '1999-02-03', 'completed_at' => '1999-02-03',
'event_name' => '::newevent::', 'event_name' => '::newevent::',
@ -104,7 +104,7 @@ class UpdateTest extends TestCase
$member = Member::factory()->defaults()->inNami(123)->has(CourseMember::factory()->inNami(999)->for(Course::factory()), 'courses')->createOne(); $member = Member::factory()->defaults()->inNami(123)->has(CourseMember::factory()->inNami(999)->for(Course::factory()), 'courses')->createOne();
$newCourse = Course::factory()->inNami(789)->create(); $newCourse = Course::factory()->inNami(789)->create();
$response = $this->patch("/member/{$member->id}/course/{$member->courses->first()->id}", array_merge([ $response = $this->patch("/course/{$member->courses->first()->id}", array_merge([
'course_id' => $newCourse->id, 'course_id' => $newCourse->id,
'completed_at' => '1999-02-03', 'completed_at' => '1999-02-03',
'event_name' => '::newevent::', 'event_name' => '::newevent::',
@ -121,7 +121,7 @@ class UpdateTest extends TestCase
$member = Member::factory()->defaults()->inNami(123)->has(CourseMember::factory()->inNami(999)->for(Course::factory()), 'courses')->createOne(); $member = Member::factory()->defaults()->inNami(123)->has(CourseMember::factory()->inNami(999)->for(Course::factory()), 'courses')->createOne();
$newCourse = Course::factory()->inNami(789)->create(); $newCourse = Course::factory()->inNami(789)->create();
$response = $this->patch("/member/{$member->id}/course/{$member->courses->first()->id}", [ $response = $this->patch("/course/{$member->courses->first()->id}", [
'course_id' => $newCourse->id, 'course_id' => $newCourse->id,
'completed_at' => '2021-01-02', 'completed_at' => '2021-01-02',
'event_name' => '::event::', 'event_name' => '::event::',

View File

@ -38,6 +38,7 @@ class IndexTest extends TestCase
$this->assertInertiaHas(null, $response, 'data.data.0.memberships'); $this->assertInertiaHas(null, $response, 'data.data.0.memberships');
$this->assertInertiaHas(url("/member/{$member->id}/membership"), $response, 'data.data.0.links.membership_index'); $this->assertInertiaHas(url("/member/{$member->id}/membership"), $response, 'data.data.0.links.membership_index');
$this->assertInertiaHas(url("/member/{$member->id}/payment"), $response, 'data.data.0.links.payment_index'); $this->assertInertiaHas(url("/member/{$member->id}/payment"), $response, 'data.data.0.links.payment_index');
$this->assertInertiaHas(url("/member/{$member->id}/course"), $response, 'data.data.0.links.course_index');
$this->assertInertiaHas([ $this->assertInertiaHas([
'id' => $member->subscription->id, 'id' => $member->subscription->id,
'name' => $member->subscription->name, 'name' => $member->subscription->name,