Add activity edit

This commit is contained in:
philipp lang 2023-02-25 17:19:17 +01:00
parent 067cbe6d9d
commit 197690069a
12 changed files with 202 additions and 145 deletions

View File

@ -0,0 +1,15 @@
<?php
namespace App\Activity\Actions;
use Lorisleiva\Actions\Concerns\AsAction;
class DestroyAction
{
use AsAction;
public function handle()
{
// ...
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Activity\Actions;
use App\Activity;
use App\Activity\Resources\ActivityResource;
use Inertia\Inertia;
use Inertia\Response;
use Lorisleiva\Actions\Concerns\AsAction;
class EditAction
{
use AsAction;
public function handle(Activity $activity): Response
{
return Inertia::render('activity/VForm', [
'meta' => ActivityResource::meta(),
'data' => new ActivityResource($activity->load('subactivities')),
]);
}
}

View File

@ -22,6 +22,8 @@ class IndexAction
public function asController(ActionRequest $request): Response
{
session()->put('menu', 'activity');
session()->put('title', 'Tätigkeiten');
$filter = ActivityFilterScope::fromRequest($request->input('filter'));
return Inertia::render('activity/VIndex', [

View File

@ -27,6 +27,12 @@ class ActivityResource extends JsonResource
return [
'name' => $this->name,
'id' => $this->id,
'subactivities' => $this->subactivities->pluck('id')->toArray(),
'links' => [
'edit' => route('activity.edit', ['activity' => $this->getModel()]),
'update' => route('activity.update', ['activity' => $this->getModel()]),
'destroy' => route('activity.destroy', ['activity' => $this->getModel()]),
]
];
}
@ -36,7 +42,7 @@ class ActivityResource extends JsonResource
public static function meta(): array
{
return [
'subactivities' => Subactivity::pluck('name', 'id'),
'subactivities' => Subactivity::select('name', 'id')->get(),
'filter' => ActivityFilterScope::fromRequest(request()->input('filter')),
];
}

View File

@ -1,5 +1,9 @@
<template>
<label class="flex flex-col relative field-switch cursor-pointer" :for="id" :class="sizes[size].wrap">
<label class="flex relative field-switch cursor-pointer" :for="id" :class="{
'items-center flex-row-reverse space-x-3 space-x-reverse justify-end': inline,
'flex-col': !inline,
[sizes[size].wrap]: true,
}">
<span
v-if="label"
class="font-semibold leading-none text-gray-400"
@ -69,9 +73,9 @@ export default {
event: 'input',
},
props: {
inset: {
type: Boolean,
inline: {
default: false,
type: Boolean,
},
size: {
default: 'base',
@ -132,16 +136,7 @@ export default {
var sizes = ['xxs', 'xs', 'sm', 'md', 'lg'];
var sizeIndex = sizes.findIndex((s) => s === this.size);
return sizes[this.inset ? sizeIndex : sizeIndex - 1];
},
outerSize() {
var sizes = ['xxs', 'xs', 'sm', 'md', 'lg'];
var sizeIndex = sizes.findIndex((s) => s === this.size);
if (!this.label || this.inset) {
sizeIndex--;
}
return sizes[sizeIndex];
return sizes[sizeIndex - 1];
},
},
};

View File

@ -0,0 +1,6 @@
<template>
<div class="font-semibold text-gray-400 leading-none mb-2">
<slot></slot>
</div>
</template>

53
resources/js/mixins/indexHelpers.js vendored Normal file
View File

@ -0,0 +1,53 @@
export default {
data: function () {
return {
inner: {...this.data},
};
},
props: {
data: {},
},
methods: {
reload(resetPage = true) {
var _self = this;
var data = {
filter: btoa(JSON.stringify(this.inner.meta.filter)),
page: 1,
};
data['page'] = resetPage ? 1 : this.inner.meta.current_page;
this.$inertia.visit(window.location.pathname, {
data,
preserveState: true,
onSuccess(page) {
_self.inner = page.props.data;
},
});
},
can(permission) {
return this.inner.meta.can[permission];
},
getFilter(value) {
return this.inner.meta.filter[value];
},
setFilter(key, value) {
this.inner.meta.filter[key] = value;
this.reload();
},
requestCallback(successMessage, failureMessage) {
return {
onSuccess: () => {
this.$success(successMessage);
this.reload(false);
},
onFailure: () => {
this.$error(failureMessage);
this.reload(false);
},
preserveState: true,
};
},
},
};

View File

@ -0,0 +1,36 @@
<template>
<form id="actionform" class="grow p-3" @submit.prevent="submit">
<f-text id="name" v-model="inner.name" label="Name" required></f-text>
<checkboxes-label class="mt-4">Untertätigkeiten</checkboxes-label>
<div class="grid gap-2 sm:grid-cols-2 md:grid-cols-4">
<f-switch inline size="sm" :key="option.id" v-model="inner.subactivities" name="subactivities[]" :id="`subactivities-${option.id}`" :value="option.id" :label="option.name" v-for="option in meta.subactivities"></f-switch>
</div>
<save-button form="actionform"></save-button>
</form>
</template>
<script>
export default {
data: function () {
return {
inner: {...this.data},
};
},
props: {
data: {},
meta: {},
},
components: {
'checkboxes-label': () => import('../../components/Form/CheckboxesLabel'),
},
methods: {
submit() {
this.$inertia.patch(`/activity/${this.inner.id}`, this.inner);
},
}
};
</script>

View File

@ -1,157 +1,42 @@
<template>
<div class="pb-6">
<member-filter
:value="query.filter"
:activities="filterActivities"
:subactivities="filterSubactivities"
:bill-kinds="billKinds"
></member-filter>
<table cellspacing="0" cellpadding="0" border="0" class="custom-table custom-table-sm hidden md:table">
<thead>
<th></th>
<th>Nachname</th>
<th>Vorname</th>
<th class="hidden 2xl:table-cell">Ort</th>
<th>Tags</th>
<th class="hidden xl:table-cell">Alter</th>
<th class="hidden xl:table-cell" v-show="hasModule('bill')">Rechnung</th>
<th v-show="hasModule('bill')">Ausstand</th>
<th>Name</th>
<th></th>
</thead>
<tr v-for="(member, index) in data.data" :key="index">
<td><age-groups :member="member"></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><tags :member="member"></tags></td>
<td class="hidden xl:table-cell" v-text="member.age"></td>
<td class="hidden xl:table-cell" v-show="hasModule('bill')">
<v-label :value="member.bill_kind_name" fallback="kein"></v-label>
</td>
<td v-show="hasModule('bill')">
<v-label :value="member.pending_payment" fallback="---"></v-label>
</td>
<tr v-for="(activity, index) in inner.data" :key="index">
<td v-text="activity.name"></td>
<td>
<actions :member="member" @sidebar="openSidebar(index, $event)" @remove="remove(member)"></actions>
<div class="flex space-x-1">
<i-link :href="activity.links.edit" class="inline-flex btn btn-warning btn-sm" v-tooltip="`bearbeiten`"><svg-sprite src="pencil"></svg-sprite></i-link>
<i-link
href="#"
@click.prevent="$emit('remove')"
class="inline-flex btn btn-danger btn-sm"
v-tooltip="`Entfernen`"
><svg-sprite src="trash"></svg-sprite
></i-link>
</div>
</td>
</tr>
</table>
<div class="md:hidden p-3 grid gap-3">
<box class="relative" :heading="member.fullname" v-for="(member, index) in data.data" :key="index">
<div slot="in-title">
<age-groups class="ml-2" :member="member" icon-class="w-4 h-4"></age-groups>
</div>
<div class="text-xs text-gray-200" v-text="member.full_address"></div>
<div class="flex items-center mt-1 space-x-4">
<tags :member="member"></tags>
<v-label
class="text-gray-100 block"
v-show="hasModule('bill')"
:value="member.pending_payment"
fallback=""
></v-label>
</div>
<actions
class="mt-2"
:member="member"
@sidebar="openSidebar(index, $event)"
@remove="remove(member)"
></actions>
<div class="absolute right-0 top-0 h-full flex items-center mr-2">
<i-link :href="member.links.show" v-tooltip="`Details`"
><svg-sprite src="chevron-down" class="w-6 h-6 text-teal-100 -rotate-90"></svg-sprite
></i-link>
</div>
</box>
</div>
<div class="px-6">
<v-pages class="mt-4" :value="data.meta" :only="['data']"></v-pages>
</div>
<transition name="sidebar">
<member-payments
v-if="single !== null && sidebar === 'payment.index'"
@close="closeSidebar"
:subscriptions="subscriptions"
:statuses="statuses"
:value="data.data[single]"
></member-payments>
<member-memberships
v-if="single !== null && sidebar === 'membership.index'"
@close="closeSidebar"
:activities="activities"
:subactivities="subactivities"
:value="data.data[single]"
></member-memberships>
<member-courses
v-if="single !== null && sidebar === 'courses.index'"
@close="closeSidebar"
:courses="courses"
:value="data.data[single]"
></member-courses>
</transition>
</div>
</template>
<script>
import MemberPayments from './MemberPayments.vue';
import MemberMemberships from './MemberMemberships.vue';
import MemberCourses from './MemberCourses.vue';
import MemberFilter from './MemberFilter.vue';
import mergesQueryString from '../../mixins/mergesQueryString.js';
import indexHelpers from '../../mixins/indexHelpers';
export default {
data: function () {
return {
sidebar: null,
single: null,
};
},
mixins: [mergesQueryString],
mixins: [indexHelpers],
components: {
MemberMemberships,
MemberPayments,
MemberFilter,
MemberCourses,
'age-groups': () => import(/* webpackChunkName: "member" */ './AgeGroups'),
'tags': () => import(/* webpackChunkName: "member" */ './Tags'),
'actions': () => import(/* webpackChunkName: "member" */ './index/Actions'),
},
methods: {
remove(member) {
if (window.confirm('Mitglied löschen?')) {
this.$inertia.delete(`/member/${member.id}`);
}
},
openSidebar(index, name) {
this.single = index;
this.sidebar = name;
},
closeSidebar() {
this.single = null;
this.sidebar = null;
},
},
props: {
data: {},
subscriptions: {},
statuses: {},
paymentDefaults: {},
query: {},
billKinds: {},
activities: {},
subactivities: {},
filterActivities: {},
filterSubactivities: {},
courses: {},
},
};
</script>

View File

@ -3,6 +3,8 @@
use App\Activity\Actions\ActivityStoreAction;
use App\Activity\Actions\ActivityUpdateAction;
use App\Activity\Actions\IndexAction as ActivityIndexAction;
use App\Activity\Actions\EditAction as ActivityEditAction;
use App\Activity\Actions\DestroyAction as ActivityDestroyAction;
use App\Contribution\Actions\FormAction as ContributionFormAction;
use App\Contribution\ContributionController;
use App\Course\Controllers\CourseController;
@ -50,6 +52,8 @@ Route::group(['middleware' => 'auth:web'], function (): void {
Route::get('/contribution', ContributionFormAction::class)->name('contribution.form');
Route::get('/contribution/generate', [ContributionController::class, 'generate'])->name('contribution.generate');
Route::get('/activity', ActivityIndexAction::class)->name('activity.index');
Route::get('/activity/{activity}/edit', ActivityEditAction::class)->name('activity.edit');
Route::post('/activity', ActivityStoreAction::class)->name('activity.store');
Route::patch('/activity/{activity}', ActivityUpdateAction::class)->name('activity.update');
Route::delete('/activity/{activity}', ActivityDestroyAction::class)->name('activity.destroy');
});

View File

@ -0,0 +1,31 @@
<?php
namespace Tests\Feature\Activity;
use App\Activity;
use App\Subactivity;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;
class EditTest extends TestCase
{
use DatabaseTransactions;
public function testItEditsAnActivity(): void
{
$this->login()->loginNami()->withoutExceptionHandling();
$activity = Activity::factory()->name('Asas')->hasAttached(Subactivity::factory()->name('Pupu'))->create();
$response = $this->get(route('activity.edit', ['activity' => $activity]));
$this->assertInertiaHas([
'name' => 'Asas',
'subactivities' => [$activity->subactivities->first()->id],
], $response, 'data');
$this->assertInertiaHas([
'id' => $activity->subactivities->first()->id,
'name' => 'Pupu',
], $response, 'meta.subactivities.0');
}
}

View File

@ -14,12 +14,14 @@ class IndexTest extends TestCase
public function testItDisplaysLocalActivities(): void
{
$this->login()->loginNami()->withoutExceptionHandling();
Activity::factory()->name('Local')->create();
$first = Activity::factory()->name('Local')->create();
Activity::factory()->name('Remote')->inNami(123)->create();
$response = $this->get('/activity');
$this->assertInertiaHas('Local', $response, 'data.data.0.name');
$this->assertInertiaHas(route('activity.update', ['activity' => $first]), $response, 'data.data.0.links.update');
$this->assertInertiaHas(route('activity.destroy', ['activity' => $first]), $response, 'data.data.0.links.destroy');
$this->assertCount(1, $this->inertia($response, 'data.data'));
}