Add participant overview
This commit is contained in:
parent
7361dcccff
commit
c7b23df01e
|
@ -6,7 +6,6 @@ use App\Form\Models\Form;
|
|||
use App\Form\Resources\ParticipantResource;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class ParticipantIndexAction
|
||||
|
@ -18,7 +17,7 @@ class ParticipantIndexAction
|
|||
return $form->participants()->with('form')->paginate(15);
|
||||
}
|
||||
|
||||
public function asController(ActionRequest $request, Form $form): AnonymousResourceCollection
|
||||
public function asController(Form $form): AnonymousResourceCollection
|
||||
{
|
||||
return ParticipantResource::collection($this->handle($form))
|
||||
->additional(['meta' => ParticipantResource::meta($form)]);
|
||||
|
|
|
@ -7,8 +7,6 @@ use Illuminate\Validation\Rule;
|
|||
|
||||
class CheckboxField extends Field
|
||||
{
|
||||
public string $name;
|
||||
public string $key;
|
||||
public bool $required;
|
||||
public string $description;
|
||||
|
||||
|
|
|
@ -7,8 +7,6 @@ use Illuminate\Validation\Rule;
|
|||
|
||||
class CheckboxesField extends Field
|
||||
{
|
||||
public string $name;
|
||||
public string $key;
|
||||
/** @var array<int, string> */
|
||||
public array $options;
|
||||
|
||||
|
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
namespace App\Form\Fields;
|
||||
|
||||
use App\Form\Contracts\Displayable;
|
||||
use Carbon\Carbon;
|
||||
use Faker\Generator;
|
||||
|
||||
class DateField extends Field
|
||||
{
|
||||
|
||||
public string $name;
|
||||
public string $key;
|
||||
public bool $required;
|
||||
public bool $maxToday;
|
||||
|
||||
|
@ -73,4 +73,21 @@ class DateField extends Field
|
|||
$this->key . '.before_or_equal' => $this->name . ' muss ein Datum vor oder gleich dem ' . now()->format('d.m.Y') . ' sein.',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @return mixed
|
||||
*/
|
||||
public function presentValue($value)
|
||||
{
|
||||
return [
|
||||
$this->key => $value,
|
||||
$this->key . '_human' => $value ? Carbon::parse($value)->format('d.m.Y') : null,
|
||||
];
|
||||
}
|
||||
|
||||
public function displayAttribute(): string
|
||||
{
|
||||
return $this->key . '_human';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,8 +7,6 @@ use Illuminate\Validation\Rule;
|
|||
|
||||
class DropdownField extends Field
|
||||
{
|
||||
public string $name;
|
||||
public string $key;
|
||||
public bool $required;
|
||||
/** @var array<int, string> */
|
||||
public array $options;
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
namespace App\Form\Fields;
|
||||
|
||||
use Faker\Generator;
|
||||
use Illuminate\Support\Collection;
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\LaravelData\Attributes\MapInputName;
|
||||
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
|
||||
|
@ -12,6 +11,9 @@ use Spatie\LaravelData\Mappers\SnakeCaseMapper;
|
|||
abstract class Field extends Data
|
||||
{
|
||||
|
||||
public string $key;
|
||||
public string $name;
|
||||
|
||||
abstract public static function name(): string;
|
||||
|
||||
/** @return array<int, array{key: string, default: mixed, label: string, rules: array<string, mixed>}> */
|
||||
|
@ -71,6 +73,15 @@ abstract class Field extends Data
|
|||
return static::classFromType($config['type'])::withoutMagicalCreationFrom($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @return mixed
|
||||
*/
|
||||
public function presentValue($value)
|
||||
{
|
||||
return [$this->key => $value];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
|
@ -117,4 +128,9 @@ abstract class Field extends Data
|
|||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function displayAttribute(): string
|
||||
{
|
||||
return $this->key;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,8 +9,6 @@ use Illuminate\Validation\Rule;
|
|||
|
||||
class GroupField extends Field
|
||||
{
|
||||
public string $name;
|
||||
public string $key;
|
||||
public bool $required;
|
||||
public ?string $parentField = null;
|
||||
public ?int $parentGroup = null;
|
||||
|
@ -79,4 +77,21 @@ class GroupField extends Field
|
|||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @return mixed
|
||||
*/
|
||||
public function presentValue($value)
|
||||
{
|
||||
return [
|
||||
$this->key => $value,
|
||||
$this->key . '_name' => Group::find($value)?->display() ?: ''
|
||||
];
|
||||
}
|
||||
|
||||
public function displayAttribute(): string
|
||||
{
|
||||
return $this->key . '_name';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,8 +7,6 @@ use Illuminate\Validation\Rule;
|
|||
|
||||
class RadioField extends Field
|
||||
{
|
||||
public string $name;
|
||||
public string $key;
|
||||
public bool $required;
|
||||
/** @var array<int, string> */
|
||||
public array $options;
|
||||
|
|
|
@ -7,8 +7,6 @@ use Faker\Generator;
|
|||
class TextField extends Field
|
||||
{
|
||||
|
||||
public string $name;
|
||||
public string $key;
|
||||
public bool $required;
|
||||
|
||||
public static function name(): string
|
||||
|
|
|
@ -6,8 +6,6 @@ use Faker\Generator;
|
|||
|
||||
class TextareaField extends Field
|
||||
{
|
||||
public string $name;
|
||||
public string $key;
|
||||
public bool $required;
|
||||
|
||||
public static function name(): string
|
||||
|
|
|
@ -27,6 +27,7 @@ class Form extends Model implements HasMedia
|
|||
|
||||
public $casts = [
|
||||
'config' => 'json',
|
||||
'active_columns' => 'json',
|
||||
'description' => 'json',
|
||||
];
|
||||
|
||||
|
@ -133,4 +134,13 @@ class Form extends Model implements HasMedia
|
|||
'name' => $this->name,
|
||||
];
|
||||
}
|
||||
|
||||
public static function booted(): void
|
||||
{
|
||||
static::saving(function (self $model) {
|
||||
if (is_null($model->active_columns)) {
|
||||
$model->setAttribute('active_columns', $model->getFields()->take(4)->pluck('key')->toArray());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Form\Resources;
|
||||
|
||||
use App\Form\Fields\Field;
|
||||
use App\Form\Models\Form;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
|
@ -15,19 +16,28 @@ class ParticipantResource extends JsonResource
|
|||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
return $this->form->getFields()->mapWithKeys(function ($field) {
|
||||
return [$field['key'] => $this->data[$field['key']]];
|
||||
})->toArray();
|
||||
$attributes = collect([]);
|
||||
|
||||
foreach ($this->form->getFields() as $field) {
|
||||
$attributes = $attributes->merge(Field::fromConfig($field)->presentValue($this->data[$field['key']]));
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
public static function meta(Form $form): array
|
||||
{
|
||||
return [
|
||||
'columns' => $form->getFields()->map(fn ($field) => [
|
||||
'name' => $field['name'],
|
||||
'base_type' => class_basename($field['type']),
|
||||
'id' => $field['key'],
|
||||
])
|
||||
'active_columns' => $form->active_columns,
|
||||
'columns' => $form->getFields()->map(function ($field) {
|
||||
$field = Field::fromConfig($field);
|
||||
return [
|
||||
'name' => $field->name,
|
||||
'base_type' => class_basename($field),
|
||||
'id' => $field->key,
|
||||
'display_attribute' => $field->displayAttribute(),
|
||||
];
|
||||
})
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,10 +55,15 @@ class Group extends Model
|
|||
|
||||
return $result
|
||||
->reduce(
|
||||
fn ($before, $group) => $before->concat([['id' => $group->id, 'name' => $prefix . ($group->inner_name ?: $group->name)]])
|
||||
fn ($before, $group) => $before->concat([['id' => $group->id, 'name' => $prefix . ($group->display())]])
|
||||
->concat($group->children_count > 0 ? self::forSelect($group, $prefix . '-- ') : []),
|
||||
collect([])
|
||||
)
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function display(): string
|
||||
{
|
||||
return $this->inner_name ?: $this->name;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,4 +37,9 @@ class GroupFactory extends Factory
|
|||
{
|
||||
return $this->state(['name' => $name]);
|
||||
}
|
||||
|
||||
public function innerName(string $name): self
|
||||
{
|
||||
return $this->state(['inner_name' => $name]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ return new class extends Migration
|
|||
$table->dateTime('registration_until')->nullable();
|
||||
$table->text('mail_top')->nullable();
|
||||
$table->text('mail_bottom')->nullable();
|
||||
$table->json('active_columns');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -29,4 +29,14 @@ return new class extends Migration
|
|||
$table->nullableTimestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('media');
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
<template>
|
||||
<div class="fixed z-40 top-0 left-0 w-full h-full flex items-center justify-center p-6">
|
||||
<div class="relative rounded-lg p-8 bg-zinc-800 shadow-2xl shadow-black border border-zinc-700 border-solid w-full max-h-full overflow-auto" :class="full ? 'h-full' : innerWidth">
|
||||
<div
|
||||
class="relative rounded-lg p-8 bg-zinc-800 shadow-2xl shadow-black border border-zinc-700 border-solid w-full max-h-full flex flex-col overflow-auto"
|
||||
:class="full ? 'h-full' : innerWidth"
|
||||
>
|
||||
<div class="absolute top-0 right-0 mt-6 mr-6 flex space-x-6">
|
||||
<slot name="actions"></slot>
|
||||
<a href="#" @click.prevent="$emit('close')">
|
||||
|
@ -8,8 +11,18 @@
|
|||
</a>
|
||||
</div>
|
||||
<h3 v-if="heading" class="font-semibold text-primary-200 text-xl" v-html="heading"></h3>
|
||||
<div class="text-primary-100 group is-popup">
|
||||
<slot></slot>
|
||||
<div class="text-primary-100 group is-popup grow flex flex-col">
|
||||
<suspense>
|
||||
<div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
<template #fallback>
|
||||
<div class="flex flex-col items-center justify-center h-full">
|
||||
<ui-spinner class="border-primary-400 w-32 h-32"></ui-spinner>
|
||||
<div class="text-3xl mt-10">Lade …</div>
|
||||
</div>
|
||||
</template>
|
||||
</suspense>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -31,6 +31,10 @@
|
|||
</div>
|
||||
</ui-popup>
|
||||
|
||||
<ui-popup v-if="showing !== null" :heading="`Teilnehmende für ${showing.name}`" full @close="showing = null">
|
||||
<participants :url="showing.links.participant_index"></participants>
|
||||
</ui-popup>
|
||||
|
||||
<ui-popup v-if="single !== null && single.config !== null" :heading="`Veranstaltung ${single.id ? 'bearbeiten' : 'erstellen'}`" full @close="cancel">
|
||||
<div class="flex flex-col mt-3">
|
||||
<ui-tabs v-model="active" :entries="tabs"></ui-tabs>
|
||||
|
@ -109,6 +113,7 @@
|
|||
</td>
|
||||
<td>
|
||||
<a v-tooltip="`Bearbeiten`" href="#" class="ml-2 inline-flex btn btn-warning btn-sm" @click.prevent="edit(form)"><ui-sprite src="pencil"></ui-sprite></a>
|
||||
<a v-tooltip="`Teilnehmende anzeigen`" href="#" class="ml-2 inline-flex btn btn-info btn-sm" @click.prevent="showParticipants(form)"><ui-sprite src="user"></ui-sprite></a>
|
||||
<a v-tooltip="`Löschen`" href="#" class="ml-2 inline-flex btn btn-danger btn-sm" @click.prevent="deleting = form"><ui-sprite src="trash"></ui-sprite></a>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -123,12 +128,14 @@
|
|||
import {ref} from 'vue';
|
||||
import {indexProps, useIndex} from '../../composables/useInertiaApiIndex.js';
|
||||
import FormBuilder from '../formtemplate/FormBuilder.vue';
|
||||
import Participants from './Participants.vue';
|
||||
|
||||
const props = defineProps(indexProps);
|
||||
var {meta, data, reloadPage, create, single, edit, cancel, submit, remove, getFilter, setFilter} = useIndex(props.data, 'form');
|
||||
|
||||
const active = ref(0);
|
||||
const deleting = ref(null);
|
||||
const showing = ref(null);
|
||||
|
||||
const tabs = [{title: 'Allgemeines'}, {title: 'Formular'}, {title: 'E-Mail'}, {title: 'Export'}];
|
||||
|
||||
|
@ -136,4 +143,8 @@ function setTemplate(template) {
|
|||
active.value = 0;
|
||||
single.value.config = template.config;
|
||||
}
|
||||
|
||||
function showParticipants(form) {
|
||||
showing.value = form;
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
<template>
|
||||
<div class="mt-5">
|
||||
<table cellspacing="0" cellpadding="0" border="0" class="custom-table custom-table-sm">
|
||||
<thead>
|
||||
<th v-for="column in activeColumns" :key="column.id" v-text="column.name"></th>
|
||||
<th></th>
|
||||
</thead>
|
||||
|
||||
<tr v-for="(form, index) in data" :key="index">
|
||||
<td v-for="column in activeColumns" :key="column.id">
|
||||
<div v-text="form[column.display_attribute]"></div>
|
||||
</td>
|
||||
<td>
|
||||
<a v-tooltip="`Bearbeiten`" href="#" class="ml-2 inline-flex btn btn-warning btn-sm" @click.prevent="edit(form)"><ui-sprite src="pencil"></ui-sprite></a>
|
||||
<a v-tooltip="`Löschen`" href="#" class="ml-2 inline-flex btn btn-danger btn-sm" @click.prevent="deleting = form"><ui-sprite src="trash"></ui-sprite></a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="px-6">
|
||||
<ui-pagination class="mt-4" :value="meta" @reload="reloadPage"></ui-pagination>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, computed} from 'vue';
|
||||
import {useApiIndex} from '../../composables/useApiIndex.js';
|
||||
import FormBuilder from '../formtemplate/FormBuilder.vue';
|
||||
|
||||
const props = defineProps({
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
validator: (value) => value.startsWith('http'),
|
||||
},
|
||||
});
|
||||
|
||||
var {meta, data, reload, reloadPage} = useApiIndex(props.url, 'participant');
|
||||
|
||||
await reload();
|
||||
|
||||
const activeColumns = computed(() => meta.value.columns.filter((c) => meta.value.active_columns.includes(c.id)));
|
||||
</script>
|
|
@ -3,10 +3,13 @@
|
|||
namespace Tests\Feature\Form;
|
||||
|
||||
use App\Form\Fields\CheckboxesField;
|
||||
use App\Form\Fields\DateField;
|
||||
use App\Form\Fields\DropdownField;
|
||||
use App\Form\Fields\GroupField;
|
||||
use App\Form\Fields\TextField;
|
||||
use App\Form\Models\Form;
|
||||
use App\Form\Models\Participant;
|
||||
use App\Group;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Tests\TestCase;
|
||||
|
||||
|
@ -18,13 +21,19 @@ class ParticipantIndexActionTest extends TestCase
|
|||
public function testItShowsParticipantsAndColumns(): void
|
||||
{
|
||||
$this->login()->loginNami()->withoutExceptionHandling();
|
||||
$group = Group::factory()->innerName('Stamm')->create();
|
||||
$form = Form::factory()
|
||||
->has(Participant::factory()->data(['vorname' => 'Max', 'select' => 'A', 'stufe' => 'Pfadfinder']))
|
||||
->has(Participant::factory()->data(['vorname' => 'Max', 'select' => 'A', 'stufe' => 'Pfadfinder', 'test1' => '', 'test2' => '', 'test3' => '', 'birthday' => '1991-04-20', 'bezirk' => $group->id]))
|
||||
->sections([
|
||||
FormtemplateSectionRequest::new()->fields([
|
||||
FormtemplateFieldRequest::type(TextField::class)->name('Vorname')->key('vorname'),
|
||||
FormtemplateFieldRequest::type(CheckboxesField::class)->key('select')->options(['A', 'B', 'C']),
|
||||
FormtemplateFieldRequest::type(DropdownField::class)->key('stufe')->options(['Wölfling', 'Jungpfadfinder', 'Pfadfinder']),
|
||||
FormtemplateFieldRequest::type(TextField::class)->name('Test 1')->key('test1'),
|
||||
FormtemplateFieldRequest::type(TextField::class)->name('Test 2')->key('test2'),
|
||||
FormtemplateFieldRequest::type(TextField::class)->name('Test 3')->key('test3'),
|
||||
FormtemplateFieldRequest::type(DateField::class)->name('Geburtsdatum')->key('birthday'),
|
||||
FormtemplateFieldRequest::type(GroupField::class)->name('bezirk')->key('bezirk'),
|
||||
]),
|
||||
])
|
||||
->create();
|
||||
|
@ -33,8 +42,15 @@ class ParticipantIndexActionTest extends TestCase
|
|||
->assertOk()
|
||||
->assertJsonPath('data.0.vorname', 'Max')
|
||||
->assertJsonPath('data.0.stufe', 'Pfadfinder')
|
||||
->assertJsonPath('data.0.bezirk', $group->id)
|
||||
->assertJsonPath('data.0.bezirk_name', 'Stamm')
|
||||
->assertJsonPath('data.0.birthday_human', '20.04.1991')
|
||||
->assertJsonPath('data.0.birthday', '1991-04-20')
|
||||
->assertJsonPath('meta.columns.0.name', 'Vorname')
|
||||
->assertJsonPath('meta.columns.0.base_type', class_basename(TextField::class))
|
||||
->assertJsonPath('meta.columns.0.id', 'vorname');
|
||||
->assertJsonPath('meta.columns.0.id', 'vorname')
|
||||
->assertJsonPath('meta.columns.6.display_attribute', 'birthday_human')
|
||||
->assertJsonPath('meta.columns.0.display_attribute', 'vorname')
|
||||
->assertJsonPath('meta.active_columns', ['vorname', 'select', 'stufe', 'test1']);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue