feat: send ticket receipts via WhatsApp and normalize phone numbers
Add WhatsApp API integration for automated receipt delivery Enforce country codes for all phone number inputs (defaults to +60)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PublicBooking } from '~~/shared/booking'
|
||||
import type { ConfirmBookingResponse, PublicBooking } from '~~/shared/booking'
|
||||
|
||||
import {
|
||||
formatBookingCurrency,
|
||||
@@ -85,7 +85,7 @@ async function confirmBooking() {
|
||||
confirming.value = true
|
||||
|
||||
try {
|
||||
const response = await apiClient<{ booking: PublicBooking, alreadyConfirmed: boolean }>(
|
||||
const response = await apiClient<ConfirmBookingResponse>(
|
||||
`/api/public/bookings/${token}/confirm`,
|
||||
{
|
||||
method: 'POST'
|
||||
@@ -98,8 +98,10 @@ async function confirmBooking() {
|
||||
title: response.alreadyConfirmed ? 'Booking already confirmed' : 'Booking confirmed',
|
||||
description: response.alreadyConfirmed
|
||||
? 'This booking had already been confirmed earlier.'
|
||||
: 'The booking details have been confirmed successfully.',
|
||||
color: 'success',
|
||||
: response.ticketReceiptWhatsApp.sent
|
||||
? `Ticket receipt was sent to ${response.ticketReceiptWhatsApp.recipientPhone}.`
|
||||
: `Booking confirmed, but the ticket receipt WhatsApp was not sent: ${response.ticketReceiptWhatsApp.error}`,
|
||||
color: response.alreadyConfirmed || response.ticketReceiptWhatsApp.sent ? 'success' : 'warning',
|
||||
icon: 'i-lucide-check-circle-2'
|
||||
})
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<script lang="ts" setup>
|
||||
import type { FormError, FormSubmitEvent } from '@nuxt/ui'
|
||||
|
||||
import { isValidPhoneNumber, type PublicContact } from '~~/shared/auth'
|
||||
import {
|
||||
DEFAULT_PHONE_COUNTRY_CODE,
|
||||
isValidPhoneNumber,
|
||||
normalizePhoneNumber,
|
||||
type PublicContact
|
||||
} from '~~/shared/auth'
|
||||
import type { CreateBookingResponse } from '~~/shared/booking'
|
||||
import {
|
||||
BOOKING_MODE_OPTIONS,
|
||||
@@ -51,7 +56,7 @@ const personInCharge = computed(() => {
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
phone: '',
|
||||
phone: DEFAULT_PHONE_COUNTRY_CODE,
|
||||
bookingMode: 'table' as BookingMode,
|
||||
quantity: 1,
|
||||
ticketType: 'vip' as TicketType
|
||||
@@ -85,7 +90,7 @@ function validateBooking(state: typeof form): FormError[] {
|
||||
if (!state.phone.trim()) {
|
||||
errors.push({ name: 'phone', message: 'Please enter a contact number.' })
|
||||
} else if (!isValidPhoneNumber(state.phone.trim())) {
|
||||
errors.push({ name: 'phone', message: 'Use a valid phone number with 8 to 15 digits.' })
|
||||
errors.push({ name: 'phone', message: 'Use a valid phone number with country code, e.g. +60123456789.' })
|
||||
}
|
||||
|
||||
if (state.quantity < 1) {
|
||||
@@ -117,7 +122,7 @@ async function bookTicket(event: FormSubmitEvent<typeof form>) {
|
||||
method: 'POST',
|
||||
body: {
|
||||
customerName: form.name.trim(),
|
||||
customerPhone: form.phone.trim(),
|
||||
customerPhone: normalizePhoneNumber(form.phone),
|
||||
bookingMode: form.bookingMode,
|
||||
quantity: form.quantity,
|
||||
ticketType: form.ticketType,
|
||||
@@ -181,7 +186,7 @@ async function bookTicket(event: FormSubmitEvent<typeof form>) {
|
||||
</UFormField>
|
||||
|
||||
<UFormField name="phone" label="Phone Number" required>
|
||||
<UInput v-model="form.phone" size="xl" type="tel" class="w-full" placeholder="e.g. 0123456789" />
|
||||
<UInput v-model="form.phone" size="xl" type="tel" class="w-full" placeholder="e.g. +60123456789" />
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@
|
||||
size="lg"
|
||||
type="tel"
|
||||
class="w-full"
|
||||
placeholder="e.g. 0123456789"
|
||||
placeholder="e.g. +60123456789"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
@@ -208,6 +208,7 @@
|
||||
import type { FormError, FormSubmitEvent } from '@nuxt/ui'
|
||||
|
||||
import {
|
||||
DEFAULT_PHONE_COUNTRY_CODE,
|
||||
hasValidFullName,
|
||||
isValidPhoneNumber,
|
||||
isValidUsername,
|
||||
@@ -242,7 +243,7 @@ const editingUserId = ref<string | null>(null)
|
||||
const userForm = reactive({
|
||||
fullName: '',
|
||||
username: '',
|
||||
phoneNumber: '',
|
||||
phoneNumber: DEFAULT_PHONE_COUNTRY_CODE,
|
||||
role: 'staff' as UserRole
|
||||
})
|
||||
|
||||
@@ -287,7 +288,7 @@ await refreshUsers()
|
||||
function resetUserForm() {
|
||||
userForm.fullName = ''
|
||||
userForm.username = ''
|
||||
userForm.phoneNumber = ''
|
||||
userForm.phoneNumber = DEFAULT_PHONE_COUNTRY_CODE
|
||||
userForm.role = 'staff'
|
||||
editingUserId.value = null
|
||||
}
|
||||
@@ -303,7 +304,7 @@ function openEditModal(user: ManagedUser) {
|
||||
editingUserId.value = user.id
|
||||
userForm.fullName = user.fullName
|
||||
userForm.username = user.username
|
||||
userForm.phoneNumber = user.phoneNumber || ''
|
||||
userForm.phoneNumber = user.phoneNumber ? normalizePhoneNumber(user.phoneNumber) : DEFAULT_PHONE_COUNTRY_CODE
|
||||
userForm.role = user.role
|
||||
editorOpen.value = true
|
||||
}
|
||||
@@ -329,7 +330,7 @@ function validateUserForm(state: typeof userForm): FormError[] {
|
||||
}
|
||||
|
||||
if (!isValidPhoneNumber(state.phoneNumber)) {
|
||||
errors.push({ name: 'phoneNumber', message: 'Use a valid phone number with 8 to 15 digits.' })
|
||||
errors.push({ name: 'phoneNumber', message: 'Use a valid phone number with country code, e.g. +60123456789.' })
|
||||
}
|
||||
|
||||
return errors
|
||||
|
||||
@@ -607,7 +607,7 @@ async function openBatchShare() {
|
||||
<UInput
|
||||
v-model="shareForm.recipientName"
|
||||
class="w-full"
|
||||
placeholder="Optional"
|
||||
placeholder="Optional, e.g. +60123456789"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user