diff --git a/app/pages/receipt/[token].vue b/app/pages/receipt/[token].vue index 6c63017..abcec7d 100644 --- a/app/pages/receipt/[token].vue +++ b/app/pages/receipt/[token].vue @@ -7,7 +7,6 @@ import { DINNER_EVENT_TITLE, DINNER_EVENT_VENUE, formatBookingCurrency, - getBookingModeLabel, getBookingStatusLabel, getSeatLabel, getTicketCatalogItem @@ -24,10 +23,9 @@ const apiClient = useApiClient() const token = String(route.params.token || '') const activeTab = ref('main') -const receiptActionLoading = ref(false) const shareSeatsLoading = ref(false) const seatActionId = ref(null) -const expandedSeatIds = ref([]) +const batchShareModalOpen = ref(false) const shareForm = reactive({ count: 1, recipientName: '', @@ -54,7 +52,6 @@ try { const receipt = ref(initialReceipt) const ticketLabel = computed(() => getTicketCatalogItem(receipt.value.booking.ticketType)?.label || receipt.value.booking.ticketType.toUpperCase()) -const totalFormatted = computed(() => formatBookingCurrency(receipt.value.booking.totalPrice)) const statusColor = computed(() => receipt.value.booking.status === 'confirmed' ? 'success' : 'warning') const sharedSeats = computed(() => receipt.value.seats.filter((seat) => Boolean(seat.sharedAt))) const availableSeats = computed(() => receipt.value.seats.filter((seat) => !seat.sharedAt)) @@ -65,27 +62,38 @@ const normalizedShareCount = computed(() => { const seatsToShare = computed(() => availableSeats.value.slice(0, normalizedShareCount.value)) const seatsToShareLabel = computed(() => seatsToShare.value.map((seat) => getSeatLabel(seat.seatNumber)).join(', ')) -const detailRows = computed(() => { - const rows = [ - { label: 'Guest', value: receipt.value.booking.customerName }, - { label: 'Phone', value: receipt.value.booking.customerPhone }, - { label: 'Booking', value: getBookingModeLabel(receipt.value.booking.bookingMode) }, - { label: 'Category', value: ticketLabel.value }, - { label: 'Quantity', value: String(receipt.value.booking.quantity) }, - { label: 'Seats', value: String(receipt.value.booking.seatCount) }, - { label: 'Submitted', value: formatDateTime(receipt.value.booking.createdAt) } +const statusRows = computed(() => { + return [ + { + label: 'Status', + value: getBookingStatusLabel(receipt.value.booking.status), + isBadge: true + }, + { + label: 'Guest', + value: receipt.value.booking.customerName + }, + { + label: 'Phone Number', + value: receipt.value.booking.customerPhone + }, + { + label: 'Category', + value: ticketLabel.value + }, + { + label: 'Seats', + value: `${receipt.value.booking.seatCount} seat${receipt.value.booking.seatCount === 1 ? '' : 's'}` + } ] - - if (receipt.value.booking.confirmedAt) { - rows.push({ - label: 'Confirmed', - value: formatDateTime(receipt.value.booking.confirmedAt) - }) - } - - return rows }) +const seatColumns = [ + { accessorKey: 'seatNumber', header: 'Seat Detail' }, + { id: 'open', header: 'Open Link' }, + { id: 'share', header: 'Share' } +] + watch( availableSeats, (nextSeats) => { @@ -101,25 +109,27 @@ function updateSeat(nextSeat: PublicBookingSeatWithUrl) { } } -function isSeatExpanded(seatId: string) { - return expandedSeatIds.value.includes(seatId) -} - -function toggleSeatExpanded(seatId: string) { - expandedSeatIds.value = isSeatExpanded(seatId) - ? expandedSeatIds.value.filter((id) => id !== seatId) - : [...expandedSeatIds.value, seatId] -} - -function buildSeatBundleText(seats: PublicBookingSeatWithUrl[]) { - const recipientLabel = shareForm.recipientName.trim() - ? `Recipient: ${shareForm.recipientName.trim()}` +function buildSeatBundleText( + seats: PublicBookingSeatWithUrl[], + options?: { + recipientName?: string + recipientPhone?: string + } +) { + const recipientName = options?.recipientName?.trim() || '' + const recipientPhone = options?.recipientPhone?.trim() || '' + const recipientLabel = recipientName + ? `Recipient: ${recipientName}` + : null + const recipientPhoneLabel = recipientPhone + ? `Recipient Phone: ${recipientPhone}` : null return [ DINNER_EVENT_TITLE, `Guest: ${receipt.value.booking.customerName}`, recipientLabel, + recipientPhoneLabel, `Seats: ${seats.map((seat) => getSeatLabel(seat.seatNumber)).join(', ')}`, `Category: ${ticketLabel.value}`, `Date: ${DINNER_EVENT_DATE_LABEL}`, @@ -198,60 +208,29 @@ async function patchSeatShare( return response.seat } -async function shareReceiptLink() { - if (receiptActionLoading.value) { - return - } - - receiptActionLoading.value = true - - try { - const shared = await shareLink({ - title: `${DINNER_EVENT_TITLE} receipt`, - text: `Ticket receipt for ${receipt.value.booking.customerName}.`, - url: receipt.value.receiptUrl, - successTitle: 'Receipt ready', - successDescription: 'Main receipt link prepared.' - }) - - if (shared && import.meta.client && navigator.share) { - toast.add({ - title: 'Receipt shared', - color: 'success', - icon: 'i-lucide-share-2' - }) - } - } catch (error) { - toast.add({ - title: 'Unable to share receipt', - description: getErrorMessage(error, 'Please try again in a moment.'), - color: 'error', - icon: 'i-lucide-circle-alert' - }) - } finally { - receiptActionLoading.value = false - } -} - async function shareSeats() { if (!availableSeats.value.length || shareSeatsLoading.value || seatActionId.value) { - return + return false } shareSeatsLoading.value = true try { const seats = seatsToShare.value + const shareText = buildSeatBundleText(seats, { + recipientName: shareForm.recipientName, + recipientPhone: shareForm.recipientPhone + }) const shared = await shareLink({ title: `${DINNER_EVENT_TITLE} seats`, - text: buildSeatBundleText(seats), - clipboardText: buildSeatBundleText(seats), + text: shareText, + clipboardText: shareText, successTitle: 'Seats ready', successDescription: `${seats.length} seat link${seats.length > 1 ? 's are' : ' is'} ready to send.` }) if (!shared) { - return + return false } let successCount = 0 @@ -276,7 +255,7 @@ async function shareSeats() { color: 'error', icon: 'i-lucide-circle-alert' }) - return + return false } toast.add({ @@ -287,6 +266,7 @@ async function shareSeats() { color: successCount === seats.length ? 'success' : 'warning', icon: successCount === seats.length ? 'i-lucide-check-check' : 'i-lucide-triangle-alert' }) + return true } catch (error) { toast.add({ title: 'Unable to share seats', @@ -294,11 +274,51 @@ async function shareSeats() { color: 'error', icon: 'i-lucide-circle-alert' }) + return false } finally { shareSeatsLoading.value = false } } +async function shareSeat(seat: PublicBookingSeatWithUrl) { + if (seatActionId.value || shareSeatsLoading.value || seat.sharedAt) { + return + } + + seatActionId.value = seat.id + + try { + const shared = await shareLink({ + title: `${DINNER_EVENT_TITLE} ${getSeatLabel(seat.seatNumber)}`, + text: buildSeatBundleText([seat]), + clipboardText: buildSeatBundleText([seat]), + successTitle: 'Seat ready', + successDescription: `${getSeatLabel(seat.seatNumber)} is ready to send.` + }) + + if (!shared) { + return + } + + await patchSeatShare(seat, { shared: true }) + + toast.add({ + title: `${getSeatLabel(seat.seatNumber)} shared`, + color: 'success', + icon: 'i-lucide-share-2' + }) + } catch (error) { + toast.add({ + title: 'Unable to share seat', + description: getErrorMessage(error, 'Please try again in a moment.'), + color: 'error', + icon: 'i-lucide-circle-alert' + }) + } finally { + seatActionId.value = null + } +} + async function unshareSeat(seat: PublicBookingSeatWithUrl) { if (seatActionId.value || shareSeatsLoading.value) { return @@ -329,6 +349,14 @@ async function unshareSeat(seat: PublicBookingSeatWithUrl) { seatActionId.value = null } } + +async function openBatchShare() { + const shared = await shareSeats() + + if (shared) { + batchShareModalOpen.value = false + } +}