Implemented additional event information
This commit is contained in:
+8
-4
@@ -27,7 +27,7 @@ class SetParticipationOptionsCommand
|
||||
* Geltungsbereichs eindeutig, damit Antworten robust referenzieren können.
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $questions
|
||||
* @return array<int, array{key: string, label: string, options: array<int, array{value: string, label: string}>}>
|
||||
* @return array<int, array{key: string, title: string, label: string, options: array<int, array{value: string, label: string}>}>
|
||||
*/
|
||||
private function normalize(array $questions): array
|
||||
{
|
||||
@@ -35,21 +35,25 @@ class SetParticipationOptionsCommand
|
||||
$usedKeys = [];
|
||||
|
||||
foreach ($questions as $question) {
|
||||
$label = trim((string)($question['label'] ?? ''));
|
||||
if ($label === '') {
|
||||
// Titel ist Pflicht (Überschrift/Feldbeschriftung); die Frage ist optionaler Erklärtext.
|
||||
$title = trim((string)($question['title'] ?? ''));
|
||||
if ($title === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$label = trim((string)($question['label'] ?? ''));
|
||||
|
||||
$options = $this->normalizeOptions($question['options'] ?? []);
|
||||
if ($options === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = $this->uniqueSlug((string)($question['key'] ?? ''), $label, $usedKeys);
|
||||
$key = $this->uniqueSlug((string)($question['key'] ?? ''), $title, $usedKeys);
|
||||
$usedKeys[] = $key;
|
||||
|
||||
$normalized[] = [
|
||||
'key' => $key,
|
||||
'title' => $title,
|
||||
'label' => $label,
|
||||
'options' => $options,
|
||||
];
|
||||
|
||||
@@ -72,11 +72,63 @@ class UpdateParticipantCommand {
|
||||
|
||||
|
||||
$p->save();
|
||||
|
||||
if ($this->request->participationOptions !== null) {
|
||||
$this->syncParticipationOptions($p);
|
||||
}
|
||||
|
||||
$this->response->success = true;
|
||||
$this->response->participant = $p;
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ersetzt die zugeordneten Teilnahmeoptionen anhand der übergebenen Antworten. Nachträgliche Zuordnung im
|
||||
* Back-Office ist lückenlos möglich: nur gegen die Event-Definition gültige Antworten werden gespeichert,
|
||||
* leere/entfallene Antworten werden abgeräumt. Frage-/Options-Label werden als Snapshot mitgeschrieben.
|
||||
*/
|
||||
private function syncParticipationOptions(EventParticipant $participant): void
|
||||
{
|
||||
$participant->selectedOptions()->delete();
|
||||
|
||||
$answers = $this->request->participationOptions ?? [];
|
||||
|
||||
foreach ($participant->event?->participation_options ?? [] as $question) {
|
||||
$key = $question['key'] ?? null;
|
||||
if ($key === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $answers[$key] ?? null;
|
||||
if ($value === null || $value === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$option = null;
|
||||
foreach ($question['options'] ?? [] as $candidate) {
|
||||
if (($candidate['value'] ?? null) === $value) {
|
||||
$option = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($option === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$label = trim((string)($question['label'] ?? ''));
|
||||
|
||||
$participant->selectedOptions()->create([
|
||||
'tenant' => $participant->tenant,
|
||||
'event_id' => $participant->event_id,
|
||||
'question_key' => $key,
|
||||
'question_label' => $label !== '' ? $label : ($question['title'] ?? $key),
|
||||
'value' => $value,
|
||||
'value_label' => $option['label'] ?? $value,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function handleAmountChanges(EventParticipant $participant) {
|
||||
$this->response->amountPaid = $participant->amount_paid;
|
||||
$this->response->amountExpected = $participant->amount;
|
||||
|
||||
@@ -36,5 +36,7 @@ class UpdateParticipantRequest {
|
||||
public string $amountPaid,
|
||||
public string $amountExpected,
|
||||
public string $cocStatus,
|
||||
/** Antworten auf die Teilnahmeoptionen: [ question_key => value ]. null = unverändert lassen. */
|
||||
public ?array $participationOptions = null,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -208,6 +208,9 @@ class DetailsController extends CommonController {
|
||||
case 'by-participation-group':
|
||||
$participants = $this->eventParticipants->groupByParticipationType($event, $request);
|
||||
break;
|
||||
case 'by-participation-option':
|
||||
$participants = $this->eventParticipants->groupByParticipationOption($event, $request);
|
||||
break;
|
||||
case 'signed-off':
|
||||
$participants = $this->eventParticipants->getSignedOffParticipants($event, $request);
|
||||
break;
|
||||
|
||||
@@ -19,6 +19,10 @@ class ByGroupController extends CommonController
|
||||
$participants = $this->eventParticipants->groupByParticipationType($event, $request, $request->input('groupName'));
|
||||
$recipients = $this->eventParticipants->getMailAddresses($participants[$request->input('groupName')]);
|
||||
break;
|
||||
case 'by-participation-option':
|
||||
$participants = $this->eventParticipants->groupByParticipationOption($event, $request, $request->input('groupName'));
|
||||
$recipients = $this->eventParticipants->getMailAddresses($participants[$request->input('groupName')] ?? []);
|
||||
break;
|
||||
case 'signed-off':
|
||||
$participants = $this->eventParticipants->getSignedOffParticipants($event, $request, $request->input('groupName'));
|
||||
$recipients = $this->eventParticipants->getMailAddresses($participants[$request->input('groupName')]);
|
||||
|
||||
@@ -44,6 +44,7 @@ class ParticipantUpdateController extends CommonController {
|
||||
amountPaid: $request->input('amountPaid'),
|
||||
amountExpected: $request->input('amountExpected'),
|
||||
cocStatus: $request->input('cocStatus'),
|
||||
participationOptions: $request->has('participationOptions') ? (array)$request->input('participationOptions') : null,
|
||||
);
|
||||
|
||||
$command = new UpdateParticipantCommand($updateRequest);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script setup>
|
||||
import {reactive, inject, onMounted} from 'vue';
|
||||
import {onMounted} from 'vue';
|
||||
import AppLayout from "../../../../resources/js/layouts/AppLayout.vue";
|
||||
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
|
||||
import TabbedPage from "../../../Views/Components/TabbedPage.vue";
|
||||
import ParticipantsList from "./Partials/ParticipantsList.vue";
|
||||
import ParticipantsByOption from "./Partials/ParticipantsByOption.vue";
|
||||
import Overview from "./Partials/Overview.vue";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -31,6 +32,11 @@ const tabs = [
|
||||
component: ParticipantsList,
|
||||
endpoint: "/api/v1/event/details/" + props.event.identifier + '/participants/by-participation-group',
|
||||
},
|
||||
{
|
||||
title: 'Teilis nach Teilnahmeoption',
|
||||
component: ParticipantsByOption,
|
||||
endpoint: "/api/v1/event/details/" + props.event.identifier + '/participants/by-participation-option',
|
||||
},
|
||||
{
|
||||
title: 'Abgemeldete Teilis',
|
||||
component: ParticipantsList,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import {onMounted, reactive, watch} from "vue";
|
||||
import {computed, onMounted, reactive, watch} from "vue";
|
||||
import AmountInput from "../../../../Views/Components/AmountInput.vue";
|
||||
import DialableTelephoneNumber from "../../../../Views/Components/DialableTelephoneNumber.vue";
|
||||
|
||||
@@ -60,8 +60,18 @@ const form = reactive({
|
||||
amountPaid: '',
|
||||
amountExpected: '',
|
||||
cocStatus: '',
|
||||
participationOptions: {},
|
||||
});
|
||||
|
||||
// Teilnahmeoptionen des Events (Definition mit Titel/Frage/Optionen).
|
||||
const participationQuestions = computed(() => staticProps.event?.participationOptions ?? []);
|
||||
|
||||
// Gewählte Antwort (Options-Label) eines Teilnehmers zu einer Frage – für die Ansicht „Titel: Antwort".
|
||||
function answerLabelFor(questionKey) {
|
||||
const answer = (props.participant?.participationOptions ?? []).find(o => o.questionKey === questionKey);
|
||||
return answer?.valueLabel ?? '—';
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.participant,
|
||||
(participant) => {
|
||||
@@ -94,6 +104,9 @@ watch(
|
||||
form.amountPaid = participant?.amountPaid.short ?? '';
|
||||
form.amountExpected = participant?.amountExpected.short ?? '';
|
||||
form.cocStatus = participant?.efz_status ?? '';
|
||||
form.participationOptions = Object.fromEntries(
|
||||
(participant?.participationOptions ?? []).map(o => [o.questionKey, o.value])
|
||||
);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
@@ -388,6 +401,27 @@ function saveParticipant() {
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="participationData" v-if="participationQuestions.length > 0">
|
||||
<div>
|
||||
<h3>Teilnahmeoptionen</h3>
|
||||
<table>
|
||||
<tr v-for="question in participationQuestions" :key="question.key">
|
||||
<th>{{ question.title || question.label }}</th>
|
||||
<td>
|
||||
<span v-if="!staticProps.editMode">{{ answerLabelFor(question.key) }}</span>
|
||||
<select v-else v-model="form.participationOptions[question.key]">
|
||||
<option value="">— keine —</option>
|
||||
<option v-for="option in question.options" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button v-if="!staticProps.editMode" class="button" @click="enableEditMode">Bearbeiten</button>
|
||||
@@ -407,6 +441,10 @@ function saveParticipant() {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.participationData table tr th {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.participationData div {
|
||||
flex: 1;
|
||||
vertical-align: top;
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<script setup>
|
||||
import {computed, inject} from "vue";
|
||||
import ParticipantsList from "./ParticipantsList.vue";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({participants: {}, event: {}, listType: ''}),
|
||||
},
|
||||
});
|
||||
|
||||
// Von TabbedPage bereitgestellt: erlaubt den Sprung zum Übersicht-Tab (Index 0), wo die
|
||||
// Teilnahmeoptionen konfiguriert werden.
|
||||
const selectTab = inject('selectTab', null);
|
||||
|
||||
const hasOptions = computed(() => (props.data?.event?.participationOptions?.length ?? 0) > 0);
|
||||
|
||||
function goToOverview() {
|
||||
if (selectTab) {
|
||||
selectTab(0);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ParticipantsList v-if="hasOptions" :data="data" :show-group-mail="false"/>
|
||||
|
||||
<div v-else class="no-options">
|
||||
<h3>Keine Teilnahmeoptionen angelegt</h3>
|
||||
<p>
|
||||
Für diese Veranstaltung sind noch keine Teilnahmeoptionen angelegt. Sobald du unter
|
||||
<strong>Übersicht → Teilnahmeoptionen</strong> Fragen definierst, werden die Teilnehmenden hier nach
|
||||
ihrer Auswahl gruppiert.
|
||||
</p>
|
||||
<button v-if="selectTab" type="button" class="button" @click="goToOverview">
|
||||
Zu den Teilnahmeoptionen
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.no-options {
|
||||
max-width: 640px;
|
||||
margin: 20px auto;
|
||||
padding: 24px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.no-options p {
|
||||
color: #4b5563;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-top: 12px;
|
||||
padding: 8px 18px;
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -20,6 +20,11 @@ const props = defineProps({
|
||||
listType: '',
|
||||
}),
|
||||
},
|
||||
// Steuert, ob je Gruppe der „E-Mail an Gruppe senden"-Button angezeigt wird.
|
||||
showGroupMail: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const today = format(new Date(), "yyyy-MM-dd");
|
||||
@@ -404,7 +409,8 @@ function mailToGroup(groupKey) {
|
||||
27 Jahre und älter: <strong>{{ getAgeCounts(participants)['27+'] ?? 0 }}</strong>
|
||||
</td>
|
||||
<td class="pl-summary-action">
|
||||
<input type="button" class="button" @click="mailToGroup(groupKey)" value="E-Mail an Gruppe senden" />
|
||||
<input v-if="showGroupMail" type="button" class="button" @click="mailToGroup(groupKey)"
|
||||
value="E-Mail an Gruppe senden"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -14,12 +14,13 @@ const props = defineProps({
|
||||
// neue Einträge bekommen serverseitig beim Speichern einen stabilen Slug.
|
||||
const questions = ref((props.event.participationOptions ?? []).map(q => ({
|
||||
key: q.key ?? '',
|
||||
title: q.title ?? '',
|
||||
label: q.label ?? '',
|
||||
options: (q.options ?? []).map(o => ({value: o.value ?? '', label: o.label ?? ''})),
|
||||
})))
|
||||
|
||||
function addQuestion() {
|
||||
questions.value.push({key: '', label: '', options: [{value: '', label: ''}, {value: '', label: ''}]})
|
||||
questions.value.push({key: '', title: '', label: '', options: [{value: '', label: ''}, {value: '', label: ''}]})
|
||||
}
|
||||
|
||||
function removeQuestion(index) {
|
||||
@@ -36,8 +37,8 @@ function removeOption(question, index) {
|
||||
|
||||
function validate() {
|
||||
for (const question of questions.value) {
|
||||
if (question.label.trim() === '') {
|
||||
toast.error('Bitte für jede Frage eine Beschriftung angeben.')
|
||||
if (question.title.trim() === '') {
|
||||
toast.error('Bitte für jede Frage einen Titel angeben.')
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -83,12 +84,23 @@ async function save() {
|
||||
|
||||
<div v-for="(question, qIndex) in questions" :key="qIndex" class="question-card">
|
||||
<div class="question-head">
|
||||
<input
|
||||
type="text"
|
||||
v-model="question.label"
|
||||
class="width-full"
|
||||
placeholder="Frage, z. B. „Welchen Kurs möchtest du belegen?“"
|
||||
/>
|
||||
<div class="question-fields">
|
||||
<label class="field-label">Titel (Überschrift / Feldbeschriftung)</label>
|
||||
<input
|
||||
type="text"
|
||||
v-model="question.title"
|
||||
class="width-full"
|
||||
placeholder="Titel, z. B. „Kurs“"
|
||||
/>
|
||||
|
||||
<label class="field-label">Frage / Erklärtext (optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
v-model="question.label"
|
||||
class="width-full"
|
||||
placeholder="z. B. „Welchen Kurs möchtest du belegen?“"
|
||||
/>
|
||||
</div>
|
||||
<label class="link remove-link" @click="removeQuestion(qIndex)">Frage entfernen</label>
|
||||
</div>
|
||||
|
||||
@@ -127,10 +139,25 @@ async function save() {
|
||||
|
||||
.question-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.question-fields {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
display: block;
|
||||
font-size: 0.78rem;
|
||||
color: #6b7280;
|
||||
margin: 6px 0 2px;
|
||||
}
|
||||
|
||||
.question-fields .field-label:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.options {
|
||||
margin-top: 12px;
|
||||
padding-left: 12px;
|
||||
|
||||
@@ -28,7 +28,10 @@ const next = () => emit('next', nextVisibleFrom(props.event, STEP_PARTICIPATION_
|
||||
|
||||
<table class="form-table">
|
||||
<tr v-for="question in questions" :key="question.key">
|
||||
<td>{{ question.label }}:</td>
|
||||
<td>
|
||||
{{ question.title || question.label }}:
|
||||
<span v-if="question.title && question.label" class="option-hint">{{ question.label }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<select v-model="formData.participationOptions[question.key]">
|
||||
<option value="">Bitte wählen</option>
|
||||
@@ -46,3 +49,14 @@ const next = () => emit('next', nextVisibleFrom(props.event, STEP_PARTICIPATION_
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.option-hint {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
font-size: 0.8rem;
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
color: #6b7280;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -40,7 +40,7 @@ const selectedParticipationOptions = computed(() =>
|
||||
(props.event.participationOptions ?? []).map(question => {
|
||||
const value = props.formData.participationOptions[question.key]
|
||||
const option = (question.options ?? []).find(o => o.value === value)
|
||||
return {label: question.label, value: option?.label ?? ''}
|
||||
return {label: question.title || question.label, value: option?.label ?? ''}
|
||||
}).filter(entry => entry.value !== '')
|
||||
)
|
||||
|
||||
|
||||
@@ -63,6 +63,84 @@ class EventParticipantRepository {
|
||||
return $participants;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gruppiert die (aktiven) Teilnehmenden nach ihren gewählten Teilnahmeoptionen. Je am Event definierter
|
||||
* Frage entsteht eine Sektion mit Gruppen „Frage: Option" (in Definition-Reihenfolge); Teilnehmende ohne
|
||||
* Antwort landen unter „Frage: (keine Angabe)". Ein Teilnehmer erscheint je Frage einmal, kann also über
|
||||
* mehrere Fragen hinweg in mehreren Gruppen auftauchen. `$filter` liefert nur die eine passende Gruppe
|
||||
* (für die E-Mail-Auflösung in ByGroupController).
|
||||
*/
|
||||
public function groupByParticipationOption(Event $event, Request $request, ?string $filter = null): array
|
||||
{
|
||||
$definitions = $event->participation_options ?? [];
|
||||
$allParticipants = $this->getForList($event, $request);
|
||||
$groups = [];
|
||||
|
||||
foreach ($definitions as $question) {
|
||||
$questionKey = $question['key'] ?? null;
|
||||
if ($questionKey === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Überschrift: bevorzugt der Titel, sonst die (längere) Frage bzw. der Key als Fallback.
|
||||
$questionLabel = trim((string)($question['title'] ?? '')) !== ''
|
||||
? $question['title']
|
||||
: ($question['label'] ?? $questionKey);
|
||||
$noAnswerKey = $questionLabel . ': (keine Angabe)';
|
||||
|
||||
// Vordefinierte Optionen in Reihenfolge: value => Gruppen-Key.
|
||||
$definedGroupKeys = [];
|
||||
foreach ($question['options'] ?? [] as $option) {
|
||||
$definedGroupKeys[$option['value']] = $questionLabel . ': ' . ($option['label'] ?? $option['value']);
|
||||
}
|
||||
|
||||
$collected = [];
|
||||
foreach ($allParticipants as $participant) {
|
||||
$answer = null;
|
||||
foreach ($participant['participationOptions'] ?? [] as $selectedOption) {
|
||||
if (($selectedOption['questionKey'] ?? null) === $questionKey) {
|
||||
$answer = $selectedOption;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($answer === null || ($answer['value'] ?? '') === '') {
|
||||
$collected[$noAnswerKey][] = $participant;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Bevorzugt die definierte Option; falls sie zwischenzeitlich entfernt wurde, den Snapshot nutzen.
|
||||
$groupKey = $definedGroupKeys[$answer['value']] ?? ($questionLabel . ': ' . ($answer['valueLabel'] ?? $answer['value']));
|
||||
$collected[$groupKey][] = $participant;
|
||||
}
|
||||
|
||||
// Ausgabe je Frage: definierte Optionen zuerst (in Reihenfolge), dann Snapshot-Reste, „(keine Angabe)" ans Ende.
|
||||
foreach ($definedGroupKeys as $groupKey) {
|
||||
if (!empty($collected[$groupKey])) {
|
||||
$groups[$groupKey] = $collected[$groupKey];
|
||||
unset($collected[$groupKey]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($collected as $groupKey => $participants) {
|
||||
if ($groupKey === $noAnswerKey) {
|
||||
continue;
|
||||
}
|
||||
$groups[$groupKey] = $participants;
|
||||
}
|
||||
|
||||
if (!empty($collected[$noAnswerKey])) {
|
||||
$groups[$noAnswerKey] = $collected[$noAnswerKey];
|
||||
}
|
||||
}
|
||||
|
||||
if ($filter !== null) {
|
||||
return array_key_exists($filter, $groups) ? [$filter => $groups[$filter]] : [];
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
public function getSignedOffParticipants(Event $event, Request $request, ?string $filter = null) : array {
|
||||
$allParticipants = $this->getForList($event, $request, true);
|
||||
$participants = [];
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import {nextTick, onMounted, onUnmounted, ref, watch} from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
show: Boolean,
|
||||
@@ -115,7 +115,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
max-height: 600px;
|
||||
max-height: 80vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref, shallowRef, onMounted } from "vue"
|
||||
import {onMounted, provide, ref, shallowRef} from "vue"
|
||||
|
||||
const props = defineProps({
|
||||
tabs: {
|
||||
@@ -55,6 +55,9 @@ async function selectTab(index) {
|
||||
}
|
||||
}
|
||||
|
||||
// Kind-Komponenten (z. B. Leer-Zustands-Hinweise) können damit gezielt zu einem anderen Tab springen.
|
||||
provide('selectTab', selectTab)
|
||||
|
||||
onMounted(() => {
|
||||
if (props.tabs.length > 0) {
|
||||
selectTab(props.subTabIndex)
|
||||
|
||||
Reference in New Issue
Block a user