From 36c0ebced019c4206ee4b8fd9a200cb95f94d41c Mon Sep 17 00:00:00 2001 From: Philipp Lang Date: Wed, 18 Oct 2023 01:11:36 +0200 Subject: [PATCH] Update course actions --- app/Course/Actions/CourseDestroyAction.php | 45 +++++++++ app/Course/Actions/CourseIndexAction.php | 31 ++++++ app/Course/Actions/CourseStoreAction.php | 72 ++++++++++++++ app/Course/Actions/CourseUpdateAction.php | 74 ++++++++++++++ app/Course/Controllers/CourseController.php | 36 ------- app/Course/Models/Course.php | 8 ++ app/Course/Models/CourseMember.php | 9 ++ app/Course/Requests/DestroyRequest.php | 38 ------- app/Course/Requests/StoreRequest.php | 49 --------- app/Course/Requests/UpdateRequest.php | 48 --------- app/Course/Resources/CourseMemberResource.php | 25 +++++ app/Member/MemberController.php | 2 +- app/Member/MemberResource.php | 1 + resources/js/components/ui/Sidebar.vue | 3 +- resources/js/views/member/MemberCourses.vue | 71 +++---------- resources/js/views/member/VIndex.vue | 99 +++++++++---------- routes/web.php | 12 ++- tests/Feature/Course/DeleteTest.php | 4 +- tests/Feature/Course/IndexTest.php | 35 +++++++ tests/Feature/Course/UpdateTest.php | 8 +- tests/Feature/Member/IndexTest.php | 1 + 21 files changed, 380 insertions(+), 291 deletions(-) create mode 100644 app/Course/Actions/CourseDestroyAction.php create mode 100644 app/Course/Actions/CourseIndexAction.php create mode 100644 app/Course/Actions/CourseStoreAction.php create mode 100644 app/Course/Actions/CourseUpdateAction.php delete mode 100644 app/Course/Controllers/CourseController.php delete mode 100644 app/Course/Requests/DestroyRequest.php delete mode 100644 app/Course/Requests/StoreRequest.php delete mode 100644 app/Course/Requests/UpdateRequest.php create mode 100644 tests/Feature/Course/IndexTest.php diff --git a/app/Course/Actions/CourseDestroyAction.php b/app/Course/Actions/CourseDestroyAction.php new file mode 100644 index 00000000..4ee2ea15 --- /dev/null +++ b/app/Course/Actions/CourseDestroyAction.php @@ -0,0 +1,45 @@ +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')); + } +} diff --git a/app/Course/Actions/CourseIndexAction.php b/app/Course/Actions/CourseIndexAction.php new file mode 100644 index 00000000..f1188634 --- /dev/null +++ b/app/Course/Actions/CourseIndexAction.php @@ -0,0 +1,31 @@ + + */ + 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), + ]); + } +} diff --git a/app/Course/Actions/CourseStoreAction.php b/app/Course/Actions/CourseStoreAction.php new file mode 100644 index 00000000..5ea3668c --- /dev/null +++ b/app/Course/Actions/CourseStoreAction.php @@ -0,0 +1,72 @@ + $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 + */ + 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')); + } +} diff --git a/app/Course/Actions/CourseUpdateAction.php b/app/Course/Actions/CourseUpdateAction.php new file mode 100644 index 00000000..ee0e35c0 --- /dev/null +++ b/app/Course/Actions/CourseUpdateAction.php @@ -0,0 +1,74 @@ + + */ + 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 + */ + 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')); + } +} diff --git a/app/Course/Controllers/CourseController.php b/app/Course/Controllers/CourseController.php deleted file mode 100644 index c434adfa..00000000 --- a/app/Course/Controllers/CourseController.php +++ /dev/null @@ -1,36 +0,0 @@ -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'); - } -} diff --git a/app/Course/Models/Course.php b/app/Course/Models/Course.php index 610e19bb..e8f825b9 100644 --- a/app/Course/Models/Course.php +++ b/app/Course/Models/Course.php @@ -22,4 +22,12 @@ class Course extends Model ->trim() ->replaceMatches('/ - .*/', ''); } + + /** + * @return array + */ + public static function forSelect(): array + { + return static::select('name', 'id')->get()->toArray(); + } } diff --git a/app/Course/Models/CourseMember.php b/app/Course/Models/CourseMember.php index 9d603a79..7116371c 100644 --- a/app/Course/Models/CourseMember.php +++ b/app/Course/Models/CourseMember.php @@ -2,6 +2,7 @@ namespace App\Course\Models; +use App\Member\Member; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -20,4 +21,12 @@ class CourseMember extends Model { return $this->belongsTo(Course::class); } + + /** + * @return BelongsTo + */ + public function member(): BelongsTo + { + return $this->belongsTo(Member::class); + } } diff --git a/app/Course/Requests/DestroyRequest.php b/app/Course/Requests/DestroyRequest.php deleted file mode 100644 index 336d1dd6..00000000 --- a/app/Course/Requests/DestroyRequest.php +++ /dev/null @@ -1,38 +0,0 @@ - - */ - 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(); - } -} diff --git a/app/Course/Requests/StoreRequest.php b/app/Course/Requests/StoreRequest.php deleted file mode 100644 index 57d3c1a7..00000000 --- a/app/Course/Requests/StoreRequest.php +++ /dev/null @@ -1,49 +0,0 @@ - - */ - 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()); - } -} diff --git a/app/Course/Requests/UpdateRequest.php b/app/Course/Requests/UpdateRequest.php deleted file mode 100644 index 82d87c52..00000000 --- a/app/Course/Requests/UpdateRequest.php +++ /dev/null @@ -1,48 +0,0 @@ - - */ - 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()); - } -} diff --git a/app/Course/Resources/CourseMemberResource.php b/app/Course/Resources/CourseMemberResource.php index 462b06c0..b7cdb558 100644 --- a/app/Course/Resources/CourseMemberResource.php +++ b/app/Course/Resources/CourseMemberResource.php @@ -2,6 +2,8 @@ namespace App\Course\Resources; +use App\Course\Models\Course; +use App\Member\Member; use Carbon\Carbon; use Illuminate\Http\Resources\Json\JsonResource; @@ -28,6 +30,29 @@ class CourseMemberResource extends JsonResource 'course_name' => $this->course->name, 'course_id' => $this->course->id, 'course' => new CourseResource($this->whenLoaded('course')), + 'links' => [ + 'update' => route('course.update', ['course' => $this->getModel()]), + 'destroy' => route('course.destroy', ['course' => $this->getModel()]), + ] + ]; + } + + /** + * @return array + */ + 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]), + ] ]; } } diff --git a/app/Member/MemberController.php b/app/Member/MemberController.php index e0f25d93..595500c6 100644 --- a/app/Member/MemberController.php +++ b/app/Member/MemberController.php @@ -23,7 +23,7 @@ class MemberController extends Controller 'data' => MemberResource::collection(Member::search($filter->search)->query( fn ($q) => $q->select('*') ->withFilter($filter) - ->with(['courses', 'subscription', 'leaderMemberships', 'ageGroupMemberships']) + ->with(['subscription', 'leaderMemberships', 'ageGroupMemberships']) ->withPendingPayment() ->ordered() )->paginate(15)), diff --git a/app/Member/MemberResource.php b/app/Member/MemberResource.php index 23d26a89..3efe6e8e 100644 --- a/app/Member/MemberResource.php +++ b/app/Member/MemberResource.php @@ -107,6 +107,7 @@ class MemberResource extends JsonResource 'links' => [ 'membership_index' => route('member.membership.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()]), 'edit' => route('member.edit', ['member' => $this->getModel()]), ], diff --git a/resources/js/components/ui/Sidebar.vue b/resources/js/components/ui/Sidebar.vue index 0614d8d9..75284dd4 100644 --- a/resources/js/components/ui/Sidebar.vue +++ b/resources/js/components/ui/Sidebar.vue @@ -1,5 +1,6 @@ - diff --git a/resources/js/views/member/VIndex.vue b/resources/js/views/member/VIndex.vue index 05549be0..b633da48 100644 --- a/resources/js/views/member/VIndex.vue +++ b/resources/js/views/member/VIndex.vue @@ -1,17 +1,23 @@ @@ -134,14 +123,14 @@ import MemberMemberships from './MemberMemberships.vue'; import MemberCourses from './MemberCourses.vue'; import Tags from './Tags.vue'; import Actions from './index/Actions.vue'; -import {indexProps, useIndex} from '../../composables/useIndex.js'; -import {ref, defineProps} from 'vue'; +import { indexProps, useIndex } from '../../composables/useIndex.js'; +import { ref, defineProps } from 'vue'; const single = ref(null); const deleting = ref(null); 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() { window.open(`/member-export?filter=${filterString.value}`); @@ -149,7 +138,7 @@ function exportMembers() { async function remove(member) { new Promise((resolve, reject) => { - deleting.value = {resolve, reject, member}; + deleting.value = { resolve, reject, member }; }) .then(() => { router.delete(`/member/${member.id}`); diff --git a/routes/web.php b/routes/web.php index b9450f17..e98cfeef 100644 --- a/routes/web.php +++ b/routes/web.php @@ -12,7 +12,10 @@ use App\Activity\Api\SubactivityUpdateAction; use App\Contribution\Actions\FormAction as ContributionFormAction; use App\Contribution\Actions\GenerateAction as ContributionGenerateAction; 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\Efz\ShowEfzDocumentAction; use App\Group\Actions\ListAction; @@ -72,7 +75,6 @@ Route::group(['middleware' => 'auth:web'], function (): void { ->name('member.singlepdf'); Route::get('/sendpayment', [SendpaymentController::class, 'create'])->name('sendpayment.create'); 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}/resync', MemberResyncAction::class)->name('member.resync'); 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::post('/api/membership/member-list', ListForGroupAction::class)->name('membership.member-list'); 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'); }); diff --git a/tests/Feature/Course/DeleteTest.php b/tests/Feature/Course/DeleteTest.php index d637269b..a4726225 100644 --- a/tests/Feature/Course/DeleteTest.php +++ b/tests/Feature/Course/DeleteTest.php @@ -19,7 +19,7 @@ class DeleteTest extends TestCase app(CourseFake::class)->deletesSuccessfully(123, 999); $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); app(CourseFake::class)->assertDeleted(123, 999); @@ -31,7 +31,7 @@ class DeleteTest extends TestCase app(CourseFake::class)->failsDeleting(123, 999); $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); $response->assertSessionHasErrors(['id' => 'Unbekannter Fehler']); diff --git a/tests/Feature/Course/IndexTest.php b/tests/Feature/Course/IndexTest.php new file mode 100644 index 00000000..9c9806c8 --- /dev/null +++ b/tests/Feature/Course/IndexTest.php @@ -0,0 +1,35 @@ +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); + } +} diff --git a/tests/Feature/Course/UpdateTest.php b/tests/Feature/Course/UpdateTest.php index 1f05ea59..2ea3861c 100644 --- a/tests/Feature/Course/UpdateTest.php +++ b/tests/Feature/Course/UpdateTest.php @@ -57,7 +57,7 @@ class UpdateTest extends TestCase $member = Member::factory()->defaults()->inNami(123)->has(CourseMember::factory()->for(Course::factory()), 'courses')->createOne(); $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, 'completed_at' => '1999-02-03', '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(); $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, 'completed_at' => '1999-02-03', '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(); $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, 'completed_at' => '1999-02-03', '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(); $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, 'completed_at' => '2021-01-02', 'event_name' => '::event::', diff --git a/tests/Feature/Member/IndexTest.php b/tests/Feature/Member/IndexTest.php index b0964fec..a55e687a 100644 --- a/tests/Feature/Member/IndexTest.php +++ b/tests/Feature/Member/IndexTest.php @@ -38,6 +38,7 @@ class IndexTest extends TestCase $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}/payment"), $response, 'data.data.0.links.payment_index'); + $this->assertInertiaHas(url("/member/{$member->id}/course"), $response, 'data.data.0.links.course_index'); $this->assertInertiaHas([ 'id' => $member->subscription->id, 'name' => $member->subscription->name,