Add store Action for subactivity
This commit is contained in:
parent
9baf5eac6b
commit
7db9e10200
|
@ -0,0 +1,52 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Activity\Api;
|
||||||
|
|
||||||
|
use App\Subactivity;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Lorisleiva\Actions\ActionRequest;
|
||||||
|
use Lorisleiva\Actions\Concerns\AsAction;
|
||||||
|
|
||||||
|
class SubactivityStoreAction
|
||||||
|
{
|
||||||
|
use AsAction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, string|array<int, int>> $payload
|
||||||
|
*/
|
||||||
|
public function handle(array $payload): Subactivity
|
||||||
|
{
|
||||||
|
$subactivity = Subactivity::create(Arr::except($payload, 'activities'));
|
||||||
|
$subactivity->activities()->sync($payload['activities']);
|
||||||
|
|
||||||
|
return $subactivity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => 'required|unique:subactivities,name',
|
||||||
|
'activities' => 'present|array|min:1',
|
||||||
|
'is_filterable' => 'present|boolean',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function getValidationAttributes(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'activities' => 'Tätigkeiten',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function asController(ActionRequest $request): JsonResponse
|
||||||
|
{
|
||||||
|
return response()->json($this->handle($request->validated()));
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,7 @@
|
||||||
"query-string": "^7.0.0",
|
"query-string": "^7.0.0",
|
||||||
"svg-sprite": "^2.0.2",
|
"svg-sprite": "^2.0.2",
|
||||||
"v-tooltip": "^2.1.3",
|
"v-tooltip": "^2.1.3",
|
||||||
|
"vue-toasted": "^1.1.28",
|
||||||
"wnumb": "^1.2.0"
|
"wnumb": "^1.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -10318,6 +10319,11 @@
|
||||||
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
|
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-toasted": {
|
||||||
|
"version": "1.1.28",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-toasted/-/vue-toasted-1.1.28.tgz",
|
||||||
|
"integrity": "sha512-UUzr5LX51UbbiROSGZ49GOgSzFxaMHK6L00JV8fir/CYNJCpIIvNZ5YmS4Qc8Y2+Z/4VVYRpeQL2UO0G800Raw=="
|
||||||
|
},
|
||||||
"node_modules/watchpack": {
|
"node_modules/watchpack": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz",
|
||||||
|
@ -18728,6 +18734,11 @@
|
||||||
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
|
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"vue-toasted": {
|
||||||
|
"version": "1.1.28",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-toasted/-/vue-toasted-1.1.28.tgz",
|
||||||
|
"integrity": "sha512-UUzr5LX51UbbiROSGZ49GOgSzFxaMHK6L00JV8fir/CYNJCpIIvNZ5YmS4Qc8Y2+Z/4VVYRpeQL2UO0G800Raw=="
|
||||||
|
},
|
||||||
"watchpack": {
|
"watchpack": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz",
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
"query-string": "^7.0.0",
|
"query-string": "^7.0.0",
|
||||||
"svg-sprite": "^2.0.2",
|
"svg-sprite": "^2.0.2",
|
||||||
"v-tooltip": "^2.1.3",
|
"v-tooltip": "^2.1.3",
|
||||||
|
"vue-toasted": "^1.1.28",
|
||||||
"wnumb": "^1.2.0"
|
"wnumb": "^1.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,16 +8,20 @@ import VLabel from './components/VLabel.vue';
|
||||||
import VBool from './components/VBool.vue';
|
import VBool from './components/VBool.vue';
|
||||||
import Box from './components/Box.vue';
|
import Box from './components/Box.vue';
|
||||||
import Heading from './components/Heading.vue';
|
import Heading from './components/Heading.vue';
|
||||||
|
import IconButton from './components/Ui/IconButton.vue';
|
||||||
import AppLayout from './layouts/AppLayout.vue';
|
import AppLayout from './layouts/AppLayout.vue';
|
||||||
import VTooltip from 'v-tooltip';
|
import VTooltip from 'v-tooltip';
|
||||||
import hasModule from './mixins/hasModule.js';
|
import hasModule from './mixins/hasModule.js';
|
||||||
|
import hasFlash from './mixins/hasFlash.js';
|
||||||
import PortalVue from 'portal-vue';
|
import PortalVue from 'portal-vue';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import VueAxios from 'vue-axios';
|
import VueAxios from 'vue-axios';
|
||||||
|
import Toasted from 'vue-toasted';
|
||||||
|
|
||||||
Vue.use(plugin);
|
Vue.use(plugin);
|
||||||
Vue.use(PortalVue);
|
Vue.use(PortalVue);
|
||||||
Vue.use(VTooltip);
|
Vue.use(VTooltip);
|
||||||
|
Vue.use(Toasted);
|
||||||
Vue.use(VueAxios, axios);
|
Vue.use(VueAxios, axios);
|
||||||
Vue.component('f-text', () => import(/* webpackChunkName: "form" */ './components/FText'));
|
Vue.component('f-text', () => import(/* webpackChunkName: "form" */ './components/FText'));
|
||||||
Vue.component('f-switch', () => import(/* webpackChunkName: "form" */ './components/FSwitch'));
|
Vue.component('f-switch', () => import(/* webpackChunkName: "form" */ './components/FSwitch'));
|
||||||
|
@ -29,11 +33,13 @@ Vue.component('v-bool', VBool);
|
||||||
Vue.component('v-label', VLabel);
|
Vue.component('v-label', VLabel);
|
||||||
Vue.component('box', Box);
|
Vue.component('box', Box);
|
||||||
Vue.component('heading', Heading);
|
Vue.component('heading', Heading);
|
||||||
|
Vue.component('icon-button', IconButton);
|
||||||
Vue.component('save-button', () => import(/* webpackChunkName: "form" */ './components/SaveButton'));
|
Vue.component('save-button', () => import(/* webpackChunkName: "form" */ './components/SaveButton'));
|
||||||
|
|
||||||
const el = document.getElementById('app');
|
const el = document.getElementById('app');
|
||||||
|
|
||||||
Vue.mixin(hasModule);
|
Vue.mixin(hasModule);
|
||||||
|
Vue.mixin(hasFlash);
|
||||||
Vue.component('ILink', ILink);
|
Vue.component('ILink', ILink);
|
||||||
|
|
||||||
Inertia.on('start', (event) => window.dispatchEvent(new Event('inertiaStart')));
|
Inertia.on('start', (event) => window.dispatchEvent(new Event('inertiaStart')));
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
<template>
|
||||||
|
<button v-on="$listeners" type="button" class="btn label btn-primary">
|
||||||
|
<svg-sprite class="w-3 h-3 mr-2" :src="icon"></svg-sprite>
|
||||||
|
<span><slot></slot></span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
icon: {
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -0,0 +1,18 @@
|
||||||
|
export default {
|
||||||
|
methods: {
|
||||||
|
['$success'](message) {
|
||||||
|
this.$toasted.show(message, {
|
||||||
|
position: 'bottom-right',
|
||||||
|
duration: 2000,
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
['$error'](message) {
|
||||||
|
this.$toasted.show(message, {
|
||||||
|
position: 'bottom-right',
|
||||||
|
duration: 2000,
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,38 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<icon-button class="mt-4" icon="plus" v-show="model === null" @click.prevent="model = {name: '', is_filterable: false, activities: [activityId]}">Neue Untertätigkeit</icon-button>
|
||||||
|
<icon-button class="mt-4" icon="close" v-show="model !== null" @click.prevent="model = null">Schließen</icon-button>
|
||||||
|
<div class="mt-2 border border-primary-700 rounded-lg p-5" v-if="model !== null">
|
||||||
|
<div class="flex space-x-3">
|
||||||
|
<f-text size="sm" id="name" v-model="model.name" label="Name" required></f-text>
|
||||||
|
<f-switch size="sm" v-model="model.is_filterable" name="is_filterable" id="is_filterable" label="Filterbar"></f-switch>
|
||||||
|
</div>
|
||||||
|
<icon-button class="mt-3" icon="save" @click.prevent="store">Speichern</icon-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
visible: false,
|
||||||
|
model: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
activityId: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async store() {
|
||||||
|
var response = await this.axios.post('/subactivity', this.model);
|
||||||
|
this.model = null;
|
||||||
|
this.$emit('stored', response.data);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -1,11 +1,23 @@
|
||||||
<template>
|
<template>
|
||||||
<form id="actionform" class="grow p-3" @submit.prevent="submit">
|
<form id="actionform" class="grow p-3" @submit.prevent="submit">
|
||||||
<f-text id="name" v-model="inner.name" label="Name" required></f-text>
|
<f-text id="name" v-model="inner.name" label="Name" required></f-text>
|
||||||
<checkboxes-label class="mt-4">Untertätigkeiten</checkboxes-label>
|
<checkboxes-label class="mt-6">Untertätigkeiten</checkboxes-label>
|
||||||
<div class="grid gap-2 sm:grid-cols-2 md:grid-cols-4">
|
<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>
|
<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 subactivities"
|
||||||
|
></f-switch>
|
||||||
</div>
|
</div>
|
||||||
<save-button form="actionform"></save-button>
|
<save-button form="actionform"></save-button>
|
||||||
|
|
||||||
|
<new-subactivity @stored="reloadSubactivities" :activity-id="inner.id"></new-subactivity>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -13,6 +25,7 @@
|
||||||
export default {
|
export default {
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
|
subactivities: [...this.meta.subactivities],
|
||||||
inner: {...this.data},
|
inner: {...this.data},
|
||||||
mode: this.data.name === '' ? 'create' : 'edit',
|
mode: this.data.name === '' ? 'create' : 'edit',
|
||||||
};
|
};
|
||||||
|
@ -25,15 +38,25 @@ export default {
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
'checkboxes-label': () => import('../../components/Form/CheckboxesLabel'),
|
'checkboxes-label': () => import('../../components/Form/CheckboxesLabel'),
|
||||||
|
'new-subactivity': () => import('./NewSubactivity.vue'),
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
submit() {
|
submit() {
|
||||||
this.mode === 'create'
|
this.mode === 'create' ? this.$inertia.post('/activity', this.inner) : this.$inertia.patch(`/activity/${this.inner.id}`, this.inner);
|
||||||
? this.$inertia.post('/activity', this.inner)
|
|
||||||
: this.$inertia.patch(`/activity/${this.inner.id}`, this.inner);
|
|
||||||
},
|
},
|
||||||
}
|
|
||||||
|
|
||||||
|
reloadSubactivities(model) {
|
||||||
|
var _self = this;
|
||||||
|
|
||||||
|
this.$inertia.reload({
|
||||||
|
onSuccess(page) {
|
||||||
|
_self.subactivities = page.props.meta.subactivities;
|
||||||
|
_self.inner.subactivities.push(model.id);
|
||||||
|
_self.$success('Untertätigkeit gespeichert.');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -2,10 +2,11 @@
|
||||||
|
|
||||||
use App\Activity\Actions\ActivityStoreAction;
|
use App\Activity\Actions\ActivityStoreAction;
|
||||||
use App\Activity\Actions\ActivityUpdateAction;
|
use App\Activity\Actions\ActivityUpdateAction;
|
||||||
use App\Activity\Actions\IndexAction as ActivityIndexAction;
|
|
||||||
use App\Activity\Actions\CreateAction as ActivityCreateAction;
|
use App\Activity\Actions\CreateAction as ActivityCreateAction;
|
||||||
use App\Activity\Actions\EditAction as ActivityEditAction;
|
|
||||||
use App\Activity\Actions\DestroyAction as ActivityDestroyAction;
|
use App\Activity\Actions\DestroyAction as ActivityDestroyAction;
|
||||||
|
use App\Activity\Actions\EditAction as ActivityEditAction;
|
||||||
|
use App\Activity\Actions\IndexAction as ActivityIndexAction;
|
||||||
|
use App\Activity\Api\SubactivityStoreAction;
|
||||||
use App\Contribution\Actions\FormAction as ContributionFormAction;
|
use App\Contribution\Actions\FormAction as ContributionFormAction;
|
||||||
use App\Contribution\ContributionController;
|
use App\Contribution\ContributionController;
|
||||||
use App\Course\Controllers\CourseController;
|
use App\Course\Controllers\CourseController;
|
||||||
|
@ -60,4 +61,5 @@ Route::group(['middleware' => 'auth:web'], function (): void {
|
||||||
Route::post('/activity', ActivityStoreAction::class)->name('activity.store');
|
Route::post('/activity', ActivityStoreAction::class)->name('activity.store');
|
||||||
Route::patch('/activity/{activity}', ActivityUpdateAction::class)->name('activity.update');
|
Route::patch('/activity/{activity}', ActivityUpdateAction::class)->name('activity.update');
|
||||||
Route::delete('/activity/{activity}', ActivityDestroyAction::class)->name('activity.destroy');
|
Route::delete('/activity/{activity}', ActivityDestroyAction::class)->name('activity.destroy');
|
||||||
|
Route::post('/subactivity', SubactivityStoreAction::class)->name('api.subactivity.store');
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Activity;
|
||||||
|
|
||||||
|
use App\Activity;
|
||||||
|
use App\Subactivity;
|
||||||
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class SubactivityTest extends TestCase
|
||||||
|
{
|
||||||
|
use DatabaseTransactions;
|
||||||
|
|
||||||
|
public function testItStoresASubactivity(): void
|
||||||
|
{
|
||||||
|
$this->login()->loginNami()->withoutExceptionHandling();
|
||||||
|
$activity = Activity::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->postJson(route('api.subactivity.store'), [
|
||||||
|
'name' => 'Leiter Lost',
|
||||||
|
'activities' => [$activity->id],
|
||||||
|
'is_filterable' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$subactivity = Subactivity::where('name', 'Leiter Lost')->firstOrFail();
|
||||||
|
$response->assertJsonPath('id', $subactivity->id);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('subactivities', [
|
||||||
|
'name' => 'Leiter Lost',
|
||||||
|
'nami_id' => null,
|
||||||
|
'is_age_group' => 0,
|
||||||
|
'is_filterable' => 1,
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseHas('activity_subactivity', ['activity_id' => $activity->id, 'subactivity_id' => $subactivity->id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNameIsRequired(): void
|
||||||
|
{
|
||||||
|
$this->login()->loginNami();
|
||||||
|
$activity = Activity::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->postJson(route('api.subactivity.store'), [
|
||||||
|
'name' => '',
|
||||||
|
'activities' => [$activity->id],
|
||||||
|
'is_filterable' => true,
|
||||||
|
]);
|
||||||
|
$response->assertJsonValidationErrors(['name' => 'Name ist erforderlich.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNameIsUnique(): void
|
||||||
|
{
|
||||||
|
$this->login()->loginNami();
|
||||||
|
$activity = Activity::factory()->create();
|
||||||
|
$subactivity = Subactivity::factory()->name('Lott')->create();
|
||||||
|
|
||||||
|
$response = $this->postJson(route('api.subactivity.store'), [
|
||||||
|
'name' => 'Lott',
|
||||||
|
'activities' => [$activity->id],
|
||||||
|
'is_filterable' => true,
|
||||||
|
]);
|
||||||
|
$response->assertJsonValidationErrors(['name' => 'Name ist bereits vergeben.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testItNeedsAtLeasttOneActivity(): void
|
||||||
|
{
|
||||||
|
$this->login()->loginNami();
|
||||||
|
|
||||||
|
$response = $this->postJson(route('api.subactivity.store'), [
|
||||||
|
'name' => '',
|
||||||
|
'activities' => [],
|
||||||
|
'is_filterable' => true,
|
||||||
|
]);
|
||||||
|
$response->assertJsonValidationErrors(['activities' => 'Tätigkeiten muss mindestens 1 Elemente haben.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNamiIdIsNotSet(): void
|
||||||
|
{
|
||||||
|
$this->login()->loginNami();
|
||||||
|
$activity = Activity::factory()->create();
|
||||||
|
|
||||||
|
$this->postJson(route('api.subactivity.store'), [
|
||||||
|
'name' => 'aaaa',
|
||||||
|
'nami_id' => 556,
|
||||||
|
'activities' => [$activity->id],
|
||||||
|
'is_filterable' => true,
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseMissing('subactivities', ['nami_id' => 556]);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue