refactor(app): event header status menu and volunteer share dialog

Replace separate status chips/buttons with one status dropdown next to
edit, move dates under the title, add share dialog for registration URL,
and remove RegistrationLinkCard.

Made-with: Cursor
This commit is contained in:
2026-04-14 22:19:09 +02:00
parent 7bc0f1a0c7
commit 5bd028f408
3 changed files with 174 additions and 137 deletions

View File

@@ -77,7 +77,6 @@ declare module 'vue' {
RegistrationFieldCard: typeof import('./src/components/event/RegistrationFieldCard.vue')['default']
RegistrationFieldFormDialog: typeof import('./src/components/event/RegistrationFieldFormDialog.vue')['default']
RegistrationFieldTemplatesTab: typeof import('./src/components/organisation/RegistrationFieldTemplatesTab.vue')['default']
RegistrationLinkCard: typeof import('./src/components/events/RegistrationLinkCard.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
ScrollToTop: typeof import('./src/@core/components/ScrollToTop.vue')['default']

View File

@@ -7,7 +7,6 @@ import { useAuthStore } from '@/stores/useAuthStore'
import { useNotificationStore } from '@/stores/useNotificationStore'
import { useOrganisationStore } from '@/stores/useOrganisationStore'
import EditEventDialog from '@/components/events/EditEventDialog.vue'
import RegistrationLinkCard from '@/components/events/RegistrationLinkCard.vue'
import type { EventStatus } from '@/types/event'
const route = useRoute()
@@ -39,10 +38,6 @@ function transitionButtonColor(current: EventStatus, target: EventStatus): strin
return 'primary'
}
function transitionButtonVariant(current: EventStatus, target: EventStatus): 'flat' | 'outlined' {
return transitionVisualKind(current, target) === 'backward' ? 'outlined' : 'flat'
}
function openTransitionConfirm(target: EventStatus) {
pendingTransitionStatus.value = target
confirmTransitionOpen.value = true
@@ -96,9 +91,23 @@ watch(eventId, (id) => {
}, { immediate: true })
const isEditDialogOpen = ref(false)
const statusMenuOpen = ref(false)
const shareDialogOpen = ref(false)
const linkCopied = ref(false)
const statusColor: Record<EventStatus, string> = {
draft: 'default',
const portalBaseUrl = import.meta.env.VITE_PORTAL_URL || 'https://portal.crewli.app'
const registrationUrl = computed(() =>
event.value ? `${portalBaseUrl}/register/${event.value.slug}` : '',
)
const isRegistrationOpen = computed(() => event.value?.status === 'registration_open')
const showVolunteerShare = computed(() => Boolean(event.value && !event.value.parent_event_id))
/** Kleuren voor statusknop / icoon (VBtn; concept = secondary i.p.v. chip-default). */
const statusActionColor: Record<EventStatus, string> = {
draft: 'secondary',
published: 'info',
registration_open: 'cyan',
buildup: 'warning',
@@ -122,6 +131,21 @@ function formatDate(iso: string) {
return dateFormatter.format(new Date(iso))
}
function onSelectTransition(target: EventStatus) {
statusMenuOpen.value = false
openTransitionConfirm(target)
}
async function copyRegistrationLink() {
if (!registrationUrl.value)
return
await navigator.clipboard.writeText(registrationUrl.value)
linkCopied.value = true
setTimeout(() => {
linkCopied.value = false
}, 2000)
}
const baseTabs = [
{ label: 'Overzicht', icon: 'tabler-layout-dashboard', route: 'events-id' },
{ label: 'Personen', icon: 'tabler-users', route: 'events-id-persons' },
@@ -204,65 +228,106 @@ const backRoute = computed(() => {
<template v-else-if="event">
<!-- Header -->
<div class="d-flex flex-wrap justify-space-between align-center gap-y-4 mb-6">
<div class="d-flex flex-wrap align-center gap-x-3 gap-y-2">
<div class="d-flex flex-wrap justify-space-between align-start gap-y-4 gap-x-4 mb-6">
<div class="d-flex align-start gap-x-3 min-w-0">
<VBtn
icon="tabler-arrow-left"
variant="text"
class="mt-1 flex-shrink-0"
:to="backRoute"
/>
<h4 class="text-h4">
<template v-if="event.is_sub_event && event.parent && event.parent_event_id">
<RouterLink
:to="{ name: 'events-id', params: { id: event.parent_event_id } }"
class="text-medium-emphasis text-decoration-none"
<div class="min-w-0">
<div class="d-flex flex-wrap align-center gap-x-2 gap-y-1">
<h4 class="text-h4 mb-0">
<template v-if="event.is_sub_event && event.parent && event.parent_event_id">
<RouterLink
:to="{ name: 'events-id', params: { id: event.parent_event_id } }"
class="text-medium-emphasis text-decoration-none"
>
{{ event.parent.name }}
</RouterLink>
<span class="text-medium-emphasis mx-1">&raquo;</span>
</template>
{{ event.name }}
</h4>
<VChip
v-if="event.event_type === 'festival' || event.event_type === 'series'"
:color="eventTypeColor[event.event_type]"
size="small"
variant="tonal"
class="flex-shrink-0"
>
{{ event.parent.name }}
</RouterLink>
<span class="text-medium-emphasis mx-1">&raquo;</span>
</template>
{{ event.name }}
</h4>
<VChip
v-if="event.event_type === 'festival' || event.event_type === 'series'"
:color="eventTypeColor[event.event_type]"
size="small"
variant="tonal"
>
{{ event.event_type_label ?? (event.event_type === 'festival' ? 'Festival' : 'Serie') }}
</VChip>
<VChip
:color="statusColor[event.status]"
size="small"
>
{{ eventStatusLabelNl(event.status) }}
</VChip>
<div
v-if="allowedTransitions.length"
class="d-flex flex-wrap align-center gap-2"
>
<VBtn
v-for="target in allowedTransitions"
:key="target"
size="small"
:color="transitionButtonColor(event.status, target)"
:variant="transitionButtonVariant(event.status, target)"
:disabled="isTransitionPending"
@click="openTransitionConfirm(target)"
>
{{ eventStatusLabelNl(target) }}
</VBtn>
{{ event.event_type_label ?? (event.event_type === 'festival' ? 'Festival' : 'Serie') }}
</VChip>
</div>
<div class="text-body-1 text-medium-emphasis mt-2">
{{ formatDate(event.start_date) }} {{ formatDate(event.end_date) }}
</div>
</div>
<span class="text-body-1 text-disabled">
{{ formatDate(event.start_date) }} {{ formatDate(event.end_date) }}
</span>
</div>
<VBtn
prepend-icon="tabler-edit"
@click="isEditDialogOpen = true"
>
Bewerken
</VBtn>
<div class="d-flex align-center gap-2 flex-shrink-0 ms-auto">
<VTooltip
v-if="showVolunteerShare"
:disabled="isRegistrationOpen"
location="top"
max-width="320"
>
<template #activator="{ props: tooltipProps }">
<span v-bind="tooltipProps">
<VBtn
prepend-icon="tabler-share-3"
variant="outlined"
:disabled="!isRegistrationOpen"
@click="shareDialogOpen = true"
>
Delen
</VBtn>
</span>
</template>
Zet de status op 'Registratie open' om het formulier te activeren.
</VTooltip>
<VMenu
v-model="statusMenuOpen"
location="bottom end"
>
<template #activator="{ props: menuProps }">
<VBtn
v-bind="menuProps"
:color="statusActionColor[event.status]"
:variant="allowedTransitions.length ? 'flat' : 'tonal'"
:disabled="!allowedTransitions.length || isTransitionPending"
append-icon="tabler-chevron-down"
>
{{ eventStatusLabelNl(event.status) }}
</VBtn>
</template>
<VList
density="compact"
class="py-1"
>
<VListItem
v-for="target in allowedTransitions"
:key="target"
:title="eventStatusLabelNl(target)"
@click="onSelectTransition(target)"
>
<template #prepend>
<VIcon
:icon="transitionVisualKind(event.status, target) === 'dangerous' ? 'tabler-alert-triangle' : 'tabler-arrow-right'"
:color="transitionButtonColor(event.status, target)"
size="20"
/>
</template>
</VListItem>
</VList>
</VMenu>
<VBtn
prepend-icon="tabler-edit"
@click="isEditDialogOpen = true"
>
Bewerken
</VBtn>
</div>
</div>
<VDialog
@@ -297,11 +362,57 @@ const backRoute = computed(() => {
</VCard>
</VDialog>
<!-- Registration link (top-level events only) -->
<RegistrationLinkCard
v-if="!event.parent_event_id"
:event="event"
/>
<VDialog
v-if="showVolunteerShare"
v-model="shareDialogOpen"
max-width="520"
>
<VCard>
<VCardTitle class="text-h6">
Vrijwilligersformulier delen
</VCardTitle>
<VCardText>
<p class="text-body-2 text-medium-emphasis mb-4">
Deel deze link met vrijwilligers om zich aan te melden.
</p>
<VTextField
:model-value="registrationUrl"
readonly
variant="outlined"
density="comfortable"
hide-details
class="mb-4"
/>
<div class="d-flex flex-wrap gap-2">
<VBtn
variant="outlined"
prepend-icon="tabler-copy"
@click="copyRegistrationLink"
>
{{ linkCopied ? 'Gekopieerd!' : 'Kopieer link' }}
</VBtn>
<VBtn
color="primary"
prepend-icon="tabler-external-link"
:href="registrationUrl"
target="_blank"
rel="noopener noreferrer"
>
Open formulier
</VBtn>
</div>
</VCardText>
<VCardActions>
<VSpacer />
<VBtn
variant="text"
@click="shareDialogOpen = false"
>
Sluiten
</VBtn>
</VCardActions>
</VCard>
</VDialog>
<!-- Horizontal tabs -->
<VTabs

View File

@@ -1,73 +0,0 @@
<script setup lang="ts">
import type { EventItem } from '@/types/event'
const props = defineProps<{
event: EventItem
}>()
const portalBaseUrl = import.meta.env.VITE_PORTAL_URL || 'https://portal.crewli.app'
const registrationUrl = computed(() =>
`${portalBaseUrl}/register/${props.event.slug}`,
)
const isRegistrationOpen = computed(() =>
props.event.status === 'registration_open',
)
const copied = ref(false)
async function copyLink() {
await navigator.clipboard.writeText(registrationUrl.value)
copied.value = true
setTimeout(() => { copied.value = false }, 2000)
}
</script>
<template>
<VCard
v-if="event"
variant="outlined"
class="mb-4"
>
<VCardText>
<div class="d-flex align-center justify-space-between flex-wrap ga-2">
<div>
<div class="text-subtitle-2 text-medium-emphasis mb-1">
Vrijwilligersregistratie
</div>
<template v-if="isRegistrationOpen">
<code class="text-body-2">{{ registrationUrl }}</code>
</template>
<template v-else>
<span class="text-body-2 text-medium-emphasis">
Zet de status op 'Registratie open' om het formulier te activeren.
</span>
</template>
</div>
<div
v-if="isRegistrationOpen"
class="d-flex ga-2"
>
<VBtn
size="small"
variant="outlined"
prepend-icon="tabler-copy"
@click="copyLink"
>
{{ copied ? 'Gekopieerd!' : 'Kopieer link' }}
</VBtn>
<VBtn
size="small"
variant="outlined"
prepend-icon="tabler-external-link"
:href="registrationUrl"
target="_blank"
>
Open formulier
</VBtn>
</div>
</div>
</VCardText>
</VCard>
</template>