refactor(ui): streamline booking form layout and table actions
Consolidate booking table row actions into a dropdown menu Update booking page layout to use a sidebar card for event details
This commit is contained in:
@@ -285,62 +285,43 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #actions-cell="{ row }">
|
<template #actions-cell="{ row }">
|
||||||
<div class="flex flex-wrap justify-end gap-1.5 py-0.5">
|
<div class="flex flex-nowrap items-center justify-end gap-1 py-0.5 whitespace-nowrap">
|
||||||
<UButton
|
<UTooltip text="Open booking">
|
||||||
:to="confirmationPath(row.original)"
|
<UButton
|
||||||
label="Open"
|
:to="confirmationPath(row.original)"
|
||||||
color="neutral"
|
color="neutral"
|
||||||
variant="outline"
|
variant="ghost"
|
||||||
icon="i-lucide-external-link"
|
icon="i-lucide-external-link"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
class="min-w-9 justify-center"
|
||||||
<UButton
|
aria-label="Open booking"
|
||||||
:to="receiptPath(row.original)"
|
/>
|
||||||
label="Receipt"
|
</UTooltip>
|
||||||
color="neutral"
|
|
||||||
variant="outline"
|
<UTooltip text="Open receipt">
|
||||||
icon="i-lucide-receipt"
|
<UButton
|
||||||
size="sm"
|
:to="receiptPath(row.original)"
|
||||||
/>
|
color="neutral"
|
||||||
<UButton
|
variant="ghost"
|
||||||
label="Edit"
|
icon="i-lucide-receipt"
|
||||||
color="neutral"
|
size="sm"
|
||||||
variant="outline"
|
class="min-w-9 justify-center"
|
||||||
icon="i-lucide-pencil-line"
|
aria-label="Open receipt"
|
||||||
size="sm"
|
/>
|
||||||
:disabled="!bookingConfig"
|
</UTooltip>
|
||||||
@click="openBookingEditor(row.original)"
|
|
||||||
/>
|
<UDropdownMenu :items="bookingActionMenuItems(row.original)">
|
||||||
<UButton
|
<UButton
|
||||||
label="Transfer"
|
color="neutral"
|
||||||
color="neutral"
|
variant="ghost"
|
||||||
variant="outline"
|
icon="i-lucide-ellipsis-vertical"
|
||||||
icon="i-lucide-send"
|
size="sm"
|
||||||
size="sm"
|
class="min-w-9 justify-center"
|
||||||
:disabled="!hasTransferTargets(row.original)"
|
:loading="isBookingRowActionBusy(row.original)"
|
||||||
@click="openTransferEditor(row.original)"
|
:disabled="isBookingRowActionBusy(row.original)"
|
||||||
/>
|
aria-label="More actions"
|
||||||
<UButton
|
/>
|
||||||
v-if="row.original.status === 'confirmed'"
|
</UDropdownMenu>
|
||||||
label="Unconfirm"
|
|
||||||
color="error"
|
|
||||||
variant="outline"
|
|
||||||
icon="i-lucide-x-circle"
|
|
||||||
size="sm"
|
|
||||||
:loading="cancellingBookingId === row.original.id"
|
|
||||||
:disabled="cancellingBookingId !== null && cancellingBookingId !== row.original.id"
|
|
||||||
@click="cancelBookingConfirmation(row.original)"
|
|
||||||
/>
|
|
||||||
<UButton
|
|
||||||
label="Delete"
|
|
||||||
color="error"
|
|
||||||
variant="outline"
|
|
||||||
icon="i-lucide-trash-2"
|
|
||||||
size="sm"
|
|
||||||
:loading="deletingBookingId === row.original.id"
|
|
||||||
:disabled="Boolean(deletingBookingId) && deletingBookingId !== row.original.id"
|
|
||||||
@click="deleteBooking(row.original)"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UTable>
|
</UTable>
|
||||||
@@ -798,6 +779,59 @@ function receiptPath(booking: PublicBooking) {
|
|||||||
return `/receipt/${booking.receiptToken}`
|
return `/receipt/${booking.receiptToken}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isBookingRowActionBusy(booking: PublicBooking) {
|
||||||
|
return Boolean(
|
||||||
|
(deletingBookingId.value && deletingBookingId.value !== booking.id)
|
||||||
|
|| (cancellingBookingId.value && cancellingBookingId.value !== booking.id)
|
||||||
|
|| deletingBookingId.value === booking.id
|
||||||
|
|| cancellingBookingId.value === booking.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function bookingActionMenuItems(booking: PublicBooking) {
|
||||||
|
const busy = isBookingRowActionBusy(booking)
|
||||||
|
const items = [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: 'Edit booking',
|
||||||
|
icon: 'i-lucide-pencil-line',
|
||||||
|
disabled: busy || !bookingConfig.value,
|
||||||
|
onSelect: () => openBookingEditor(booking)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Transfer booking',
|
||||||
|
icon: 'i-lucide-send',
|
||||||
|
disabled: busy || !hasTransferTargets(booking),
|
||||||
|
onSelect: () => openTransferEditor(booking)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
if (booking.status === 'confirmed') {
|
||||||
|
items.push([
|
||||||
|
{
|
||||||
|
label: 'Unconfirm booking',
|
||||||
|
icon: 'i-lucide-x-circle',
|
||||||
|
color: 'error',
|
||||||
|
disabled: busy,
|
||||||
|
onSelect: () => cancelBookingConfirmation(booking)
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push([
|
||||||
|
{
|
||||||
|
label: 'Delete booking',
|
||||||
|
icon: 'i-lucide-trash-2',
|
||||||
|
color: 'error',
|
||||||
|
disabled: busy,
|
||||||
|
onSelect: () => deleteBooking(booking)
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
function openBookingEditor(booking: PublicBooking) {
|
function openBookingEditor(booking: PublicBooking) {
|
||||||
detailsBooking.value = booking
|
detailsBooking.value = booking
|
||||||
detailsForm.customerName = booking.customerName
|
detailsForm.customerName = booking.customerName
|
||||||
|
|||||||
@@ -235,23 +235,28 @@ async function bookTicket(event: FormSubmitEvent<typeof form>) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UContainer class="page-shell-narrow">
|
<UContainer class="page-shell-narrow">
|
||||||
<div class="grid gap-5 xl:grid-cols-[minmax(0,1fr)_34rem] xl:items-start xl:gap-8">
|
<div class="grid gap-5 xl:grid-cols-[22rem_minmax(0,1fr)] xl:items-start xl:gap-8">
|
||||||
<section class="space-y-4 xl:sticky xl:top-6 xl:space-y-6">
|
<UCard
|
||||||
<div class="page-header">
|
class="surface-card overflow-hidden rounded-lg xl:sticky xl:top-6"
|
||||||
<UBadge :label="t('layout.brand')" color="primary" variant="soft" class="page-eyebrow" />
|
:ui="{ header: 'px-4 py-4 sm:px-5 sm:py-5', body: 'space-y-4 px-4 pb-4 pt-0 sm:px-5 sm:pb-5' }"
|
||||||
<h1 class="page-title">
|
>
|
||||||
{{ bookingConfig.event.title }}
|
<template #header>
|
||||||
</h1>
|
<div class="space-y-1">
|
||||||
<p class="page-description">
|
<UBadge :label="t('layout.brand')" color="primary" variant="soft" class="page-eyebrow" />
|
||||||
{{ t('booking.seatGeneration', { count: seatCount, seatLabel: locale === 'zh' ? '座位' : `seat${seatCount === 1 ? '' : 's'}` }) }}
|
<h1 class="text-2xl font-bold leading-tight text-highlighted sm:text-3xl xl:text-2xl">
|
||||||
</p>
|
{{ bookingConfig.event.title }}
|
||||||
</div>
|
</h1>
|
||||||
|
<p class="page-description">
|
||||||
|
{{ t('booking.seatGeneration', { count: seatCount, seatLabel: locale === 'zh' ? '座位' : `seat${seatCount === 1 ? '' : 's'}` }) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div class="grid gap-2 sm:grid-cols-3 xl:grid-cols-1 xl:gap-3">
|
<div class="grid gap-3">
|
||||||
<div
|
<div
|
||||||
v-for="detail in eventDetails"
|
v-for="detail in eventDetails"
|
||||||
:key="detail.label"
|
:key="detail.label"
|
||||||
class="surface-card rounded-lg p-3 sm:p-4"
|
class="surface-panel rounded-lg p-4"
|
||||||
>
|
>
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<div class="flex size-10 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
<div class="flex size-10 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
||||||
@@ -268,7 +273,7 @@ async function bookTicket(event: FormSubmitEvent<typeof form>) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</UCard>
|
||||||
|
|
||||||
<UCard
|
<UCard
|
||||||
id="booking-form"
|
id="booking-form"
|
||||||
@@ -285,15 +290,13 @@ async function bookTicket(event: FormSubmitEvent<typeof form>) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UForm :state="form" :validate="validateBooking" class="space-y-5 sm:space-y-6" @submit="bookTicket">
|
<UForm :state="form" :validate="validateBooking" class="space-y-5 sm:space-y-6" @submit="bookTicket">
|
||||||
<div class="grid gap-4 sm:grid-cols-2 sm:gap-5">
|
<UFormField name="name" :label="t('booking.name')" required>
|
||||||
<UFormField name="name" :label="t('booking.name')" required>
|
<UInput v-model="form.name" size="xl" class="w-full" :placeholder="t('booking.namePlaceholder')" />
|
||||||
<UInput v-model="form.name" size="xl" class="w-full" :placeholder="t('booking.namePlaceholder')" />
|
</UFormField>
|
||||||
</UFormField>
|
|
||||||
|
|
||||||
<UFormField name="phone" :label="t('common.phoneNumber')" required>
|
<UFormField name="phone" :label="t('common.phoneNumber')" required>
|
||||||
<UInput v-model="form.phone" size="xl" type="tel" class="w-full" :placeholder="t('booking.phonePlaceholder')" />
|
<UInput v-model="form.phone" size="xl" type="tel" class="w-full" :placeholder="t('booking.phonePlaceholder')" />
|
||||||
</UFormField>
|
</UFormField>
|
||||||
</div>
|
|
||||||
|
|
||||||
<UFormField :label="t('booking.bookingMode')" name="bookingMode">
|
<UFormField :label="t('booking.bookingMode')" name="bookingMode">
|
||||||
<URadioGroup
|
<URadioGroup
|
||||||
@@ -303,30 +306,28 @@ async function bookTicket(event: FormSubmitEvent<typeof form>) {
|
|||||||
indicator="hidden"
|
indicator="hidden"
|
||||||
:items="bookingModeOptions"
|
:items="bookingModeOptions"
|
||||||
:ui="{
|
:ui="{
|
||||||
fieldset: 'grid grid-cols-1 gap-3 sm:grid-cols-2',
|
fieldset: 'grid grid-cols-2 gap-3',
|
||||||
item: 'min-h-14 rounded-lg border border-default bg-default p-3 transition-colors data-[state=checked]:border-primary data-[state=checked]:bg-primary/5'
|
item: 'min-h-14 rounded-lg border border-default bg-default p-3 transition-colors data-[state=checked]:border-primary data-[state=checked]:bg-primary/5'
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
</UFormField>
|
</UFormField>
|
||||||
|
|
||||||
<div class="grid gap-4 sm:grid-cols-[minmax(0,1fr)_minmax(0,1.1fr)] sm:gap-5">
|
<UFormField :label="quantityLabel" name="quantity">
|
||||||
<UFormField :label="quantityLabel" name="quantity">
|
<UInputNumber v-model="form.quantity" size="xl" class="w-full" :min="1" :step="1" />
|
||||||
<UInputNumber v-model="form.quantity" size="xl" class="w-full" :min="1" :step="1" />
|
<template #help>
|
||||||
<template #help>
|
{{ t('booking.seatGeneration', { count: seatCount, seatLabel: locale === 'zh' ? '座位' : `seat${seatCount === 1 ? '' : 's'}` }) }}
|
||||||
{{ t('booking.seatGeneration', { count: seatCount, seatLabel: locale === 'zh' ? '座位' : `seat${seatCount === 1 ? '' : 's'}` }) }}
|
</template>
|
||||||
</template>
|
</UFormField>
|
||||||
</UFormField>
|
|
||||||
|
|
||||||
<UFormField :label="t('booking.personInCharge')">
|
<UFormField :label="t('booking.personInCharge')">
|
||||||
<USelect
|
<USelect
|
||||||
v-model="selectedPersonInCharge"
|
v-model="selectedPersonInCharge"
|
||||||
size="xl"
|
size="xl"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
:items="personInCharge"
|
:items="personInCharge"
|
||||||
:disabled="!personInCharge.length"
|
:disabled="!personInCharge.length"
|
||||||
/>
|
/>
|
||||||
</UFormField>
|
</UFormField>
|
||||||
</div>
|
|
||||||
|
|
||||||
<UFormField :label="t('booking.ticketCategory')" name="ticketType">
|
<UFormField :label="t('booking.ticketCategory')" name="ticketType">
|
||||||
<URadioGroup
|
<URadioGroup
|
||||||
@@ -336,7 +337,7 @@ async function bookTicket(event: FormSubmitEvent<typeof form>) {
|
|||||||
indicator="hidden"
|
indicator="hidden"
|
||||||
:items="ticketCatalogOptions"
|
:items="ticketCatalogOptions"
|
||||||
:ui="{
|
:ui="{
|
||||||
fieldset: 'grid grid-cols-1 gap-3 sm:grid-cols-2',
|
fieldset: 'grid grid-cols-2 gap-3',
|
||||||
item: 'min-h-14 rounded-lg border border-default bg-default p-3 transition-colors data-[state=checked]:border-primary data-[state=checked]:bg-primary/5'
|
item: 'min-h-14 rounded-lg border border-default bg-default p-3 transition-colors data-[state=checked]:border-primary data-[state=checked]:bg-primary/5'
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user