Files
dticket.tootaio.com/app/composables/useLocale.ts
xiaomai a56a6706b0 feat(bookings): add payment and document upload to confirmation page
Allow users to select payment method and upload receipts before confirming.
Add public API endpoints for payment updates and document management.
2026-05-09 13:15:45 +08:00

403 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
DEFAULT_LOCALE,
LOCALE_OPTIONS,
getOppositeLocale,
resolveLocale,
type AppLocale
} from '~~/shared/i18n'
const messages = {
en: {
'common.back': 'Back',
'common.cancel': 'Cancel',
'common.category': 'Category',
'common.date': 'Date',
'common.login': 'Login',
'common.logout': 'Logout',
'common.menu': 'Menu',
'common.close': 'Close',
'common.open': 'Open',
'common.phoneNumber': 'Phone Number',
'common.price': 'Price',
'common.public': 'Public',
'common.refresh': 'Refresh',
'common.seats': 'Seats',
'common.status': 'Status',
'common.time': 'Time',
'common.totalPrice': 'Total Price',
'common.venue': 'Venue',
'common.switchLanguage': 'Switch language',
'common.language': 'Language',
'layout.brand': 'Dinner Ticket System',
'layout.footer': '© 2026 DAP 60th Anniversary Committee. All rights reserved.',
'layout.signedOut': 'Signed out',
'layout.sessionCleared': 'Your session has been cleared.',
'layout.logoutFailed': 'Logout failed',
'layout.logoutFailedDescription': 'Unable to end the current session.',
'nav.bookings': 'Bookings',
'nav.security': 'Security',
'nav.users': 'Users',
'role.superAdmin': 'Super Admin',
'role.staff': 'Staff',
'booking.name': 'Name',
'booking.namePlaceholder': 'e.g. John Doe',
'booking.phonePlaceholder': 'e.g. +60123456789',
'booking.bookingMode': 'Booking Mode',
'booking.quantity': 'Quantity',
'booking.ticketCategory': 'Ticket Category',
'booking.personInCharge': 'Person In Charge',
'booking.bookNow': 'Book Now',
'booking.seatGeneration': 'This booking will generate {count} {seatLabel}.',
'booking.noPicTitle': 'No person in charge available',
'booking.noPicDescription': 'Add a user with a phone number in the management page first.',
'booking.whatsappOpened': 'WhatsApp booking draft opened',
'booking.whatsappOpenedDescription': 'Booking details and the confirmation link were sent to {name}.',
'booking.createFailed': 'Booking could not be created',
'booking.tryAgain': 'Please try again in a moment.',
'booking.nameRequired': 'Please enter the guest or organizer name.',
'booking.phoneRequired': 'Please enter a contact number.',
'booking.phoneInvalid': 'Use a valid phone number with country code, e.g. +60123456789.',
'booking.quantityMin': '{label} must be at least 1.',
'booking.modeRequired': 'Please select a booking mode.',
'booking.ticketRequired': 'Please select a ticket category.',
'confirm.badge': 'PIC Confirmation',
'confirm.title': 'Review Booking Details',
'confirm.description': 'Confirm the booking after verifying the details below.',
'confirm.status': 'Booking status',
'confirm.submitted': 'Submitted {date}',
'confirm.alreadyConfirmed': 'Booking already confirmed',
'confirm.confirmedOn': 'Confirmed on {date}.',
'confirm.guestOrganizer': 'Guest / Organizer',
'confirm.contactNumber': 'Contact Number',
'confirm.pic': 'Person In Charge',
'confirm.picPhone': 'PIC Phone',
'confirm.ticketCategory': 'Ticket Category',
'confirm.seatsCovered': 'Seats Covered',
'confirm.payment': 'Payment',
'confirm.paymentPendingDescription': 'Choose the payment method before confirming.',
'confirm.paymentConfirmedDescription': 'Payment method selected for this booking.',
'confirm.paymentCash': 'Cash',
'confirm.paymentBank': 'Bank',
'confirm.transactionDocument': 'Transaction Document',
'confirm.documentHelp': 'PDF, JPG, PNG, WEBP, HEIC, or HEIF - max 10MB.',
'confirm.upload': 'Upload',
'confirm.download': 'Download',
'confirm.delete': 'Delete',
'confirm.deleteDocumentPrompt': 'Delete transaction document?',
'confirm.documentUploaded': 'Document uploaded',
'confirm.documentUploadedDescription': 'The transaction document has been saved.',
'confirm.documentDeleted': 'Document deleted',
'confirm.documentDeletedDescription': 'The transaction document has been removed.',
'confirm.uploadFailed': 'Upload failed',
'confirm.deleteFailed': 'Delete failed',
'confirm.documentSizeInvalid': 'Transaction document must be 10MB or smaller.',
'confirm.submittedLabel': 'Submitted',
'confirm.confirmedAt': 'Confirmed At',
'confirm.backToForm': 'Back To Booking Form',
'confirm.openReceipt': 'Open Ticket Receipt',
'confirm.confirmBooking': 'Confirm This Booking',
'confirm.cancelConfirmation': 'Cancel Confirmation',
'confirm.alreadyConfirmedToast': 'Booking already confirmed',
'confirm.confirmedToast': 'Booking confirmed',
'confirm.alreadyConfirmedDescription': 'This booking had already been confirmed earlier.',
'confirm.receiptSent': 'Ticket receipt was sent to {phone}.',
'confirm.receiptNotSent': 'Booking confirmed, but the ticket receipt WhatsApp was not sent: {error}',
'confirm.failed': 'Confirmation failed',
'confirm.cancelPrompt': 'Cancel this confirmation? The booking will return to pending and the seats will be released.',
'confirm.alreadyPending': 'Booking already pending',
'confirm.cancelled': 'Confirmation cancelled',
'confirm.alreadyPendingDescription': 'This booking was already pending confirmation.',
'confirm.cancelledDescription': 'The booking has been returned to pending status.',
'confirm.cancelFailed': 'Cancellation failed',
'receipt.badge': 'Ticket Receipt',
'receipt.mainQr': 'Main QR',
'receipt.seatList': 'Seat List',
'receipt.shareSeats': 'Share Seats',
'receipt.batchShare': 'Batch Share',
'receipt.shared': 'shared',
'receipt.available': 'available',
'receipt.seatDetail': 'Seat Detail',
'receipt.openLink': 'Open Link',
'receipt.share': 'Share',
'receipt.sharedStatus': 'Shared',
'receipt.availableStatus': 'Available',
'receipt.unassigned': 'Unassigned',
'receipt.readyToShare': 'Ready to share',
'receipt.sharedAt': 'Shared {date}',
'receipt.openSeatLink': 'Open seat link',
'receipt.shareSeat': 'Share seat',
'receipt.unshareSeat': 'Unshare seat',
'receipt.unshare': 'Unshare',
'receipt.batchTitle': 'Batch Share Seats',
'receipt.seatsToShare': 'Seats To Share',
'receipt.nextSeats': 'Next Seats',
'receipt.noSeatsAvailable': 'No seats available',
'receipt.recipientName': 'Recipient Name',
'receipt.recipientPhone': 'Recipient Phone',
'receipt.optional': 'Optional',
'receipt.optionalPhonePlaceholder': 'Optional, e.g. +60123456789',
'receipt.recipient': 'Recipient',
'receipt.recipientPhoneLabel': 'Recipient Phone',
'receipt.guest': 'Guest',
'receipt.purchaser': 'Purchaser',
'receipt.seatsReady': 'Seats ready',
'receipt.seatsReadyDescription': '{count} {seatLabel} ready to send.',
'receipt.copied': 'Copied to clipboard.',
'receipt.copyPrompt': 'Copy this text',
'receipt.seatUpdateFailed': 'Seat update failed',
'receipt.seatUpdateFailedDescription': 'The share sheet opened, but the seat records could not be updated.',
'receipt.seatsShared': '{count} {seatLabel} shared',
'receipt.allSeatsSent': 'Next available seats were sent.',
'receipt.someSeatsFailed': 'Some seats were sent, but at least one update failed.',
'receipt.unableShareSeats': 'Unable to share seats',
'receipt.seatReady': 'Seat ready',
'receipt.seatReadyDescription': '{seat} is ready to send.',
'receipt.seatShared': '{seat} shared',
'receipt.unableShareSeat': 'Unable to share seat',
'receipt.unsharePrompt': 'Unshare {seat}? The previous link will stop working.',
'receipt.seatUnshared': '{seat} unshared',
'receipt.unableUnshareSeat': 'Unable to unshare seat',
'login.badge': 'Staff Access',
'login.title': 'Login to the management system',
'login.username': 'Username',
'login.usernamePlaceholder': 'Enter your username',
'login.password': 'Password',
'login.passwordPlaceholder': 'Enter your password',
'login.remember': 'Remember this device',
'login.signIn': 'Sign In',
'login.or': 'or',
'login.passkey': 'Sign In With Passkey',
'login.usernameRequired': 'Please enter your username.',
'login.passwordRequired': 'Please enter your password.',
'login.failed': 'Login failed',
'login.failedDescription': 'Unable to sign in with username and password.',
'login.passkeyFailed': 'Passkey login failed',
'login.passkeyFailedDescription': 'Unable to complete passkey login.',
'date.notAvailable': 'Not available'
},
zh: {
'common.back': '返回',
'common.cancel': '取消',
'common.category': '类别',
'common.date': '日期',
'common.login': '登录',
'common.logout': '退出登录',
'common.menu': '菜单',
'common.close': '关闭',
'common.open': '打开',
'common.phoneNumber': '联络号码',
'common.price': '价格',
'common.public': '公开页面',
'common.refresh': '刷新',
'common.seats': '座位',
'common.status': '状态',
'common.time': '时间',
'common.totalPrice': '总价',
'common.venue': '地点',
'common.switchLanguage': '切换语言',
'common.language': '语言',
'layout.brand': '晚宴票券系统',
'layout.footer': '© 2026 DAP 60周年委员会。版权所有。',
'layout.signedOut': '已退出登录',
'layout.sessionCleared': '您的登录会话已清除。',
'layout.logoutFailed': '退出登录失败',
'layout.logoutFailedDescription': '无法结束当前会话。',
'nav.bookings': '预订',
'nav.security': '安全设置',
'nav.users': '用户',
'role.superAdmin': '超级管理员',
'role.staff': '职员',
'booking.name': '姓名',
'booking.namePlaceholder': '例如:陈大文',
'booking.phonePlaceholder': '例如:+60123456789',
'booking.bookingMode': '预订方式',
'booking.quantity': '数量',
'booking.ticketCategory': '票券类别',
'booking.personInCharge': '负责人',
'booking.bookNow': '立即预订',
'booking.seatGeneration': '此预订将生成 {count} 个{seatLabel}。',
'booking.noPicTitle': '没有可用负责人',
'booking.noPicDescription': '请先在管理页面新增一个有电话号码的用户。',
'booking.whatsappOpened': 'WhatsApp 预订草稿已打开',
'booking.whatsappOpenedDescription': '预订详情和确认链接已发送给 {name}。',
'booking.createFailed': '无法创建预订',
'booking.tryAgain': '请稍后再试。',
'booking.nameRequired': '请输入宾客或订桌人姓名。',
'booking.phoneRequired': '请输入联络号码。',
'booking.phoneInvalid': '请输入包含国家区号的有效号码,例如 +60123456789。',
'booking.quantityMin': '{label} 至少为 1。',
'booking.modeRequired': '请选择预订方式。',
'booking.ticketRequired': '请选择票券类别。',
'confirm.badge': '负责人确认',
'confirm.title': '检查预订详情',
'confirm.description': '请核对以下资料后确认预订。',
'confirm.status': '预订状态',
'confirm.submitted': '提交于 {date}',
'confirm.alreadyConfirmed': '预订已确认',
'confirm.confirmedOn': '确认于 {date}。',
'confirm.guestOrganizer': '宾客 / 订桌人',
'confirm.contactNumber': '联络号码',
'confirm.pic': '负责人',
'confirm.picPhone': '负责人电话',
'confirm.ticketCategory': '票券类别',
'confirm.seatsCovered': '座位数量',
'confirm.payment': '付款方式',
'confirm.paymentPendingDescription': '请在确认前选择付款方式。',
'confirm.paymentConfirmedDescription': '此预订已选择的付款方式。',
'confirm.paymentCash': 'Cash',
'confirm.paymentBank': 'Bank',
'confirm.transactionDocument': 'Transaction Document',
'confirm.documentHelp': 'PDF、JPG、PNG、WEBP、HEIC 或 HEIF最大 10MB。',
'confirm.upload': '上传',
'confirm.download': '下载',
'confirm.delete': '删除',
'confirm.deleteDocumentPrompt': '确定删除 transaction document',
'confirm.documentUploaded': '文件已上传',
'confirm.documentUploadedDescription': 'Transaction document 已保存。',
'confirm.documentDeleted': '文件已删除',
'confirm.documentDeletedDescription': 'Transaction document 已移除。',
'confirm.uploadFailed': '上传失败',
'confirm.deleteFailed': '删除失败',
'confirm.documentSizeInvalid': 'Transaction document 必须是 10MB 或以下。',
'confirm.submittedLabel': '提交时间',
'confirm.confirmedAt': '确认时间',
'confirm.backToForm': '返回预订表格',
'confirm.openReceipt': '打开票券收据',
'confirm.confirmBooking': '确认此预订',
'confirm.cancelConfirmation': '取消确认',
'confirm.alreadyConfirmedToast': '预订已确认',
'confirm.confirmedToast': '预订已确认',
'confirm.alreadyConfirmedDescription': '此预订之前已经确认。',
'confirm.receiptSent': '票券收据已发送至 {phone}。',
'confirm.receiptNotSent': '预订已确认,但 WhatsApp 票券收据未发送:{error}',
'confirm.failed': '确认失败',
'confirm.cancelPrompt': '确定取消此确认?预订会回到待确认状态,座位也会释放。',
'confirm.alreadyPending': '预订已是待确认',
'confirm.cancelled': '已取消确认',
'confirm.alreadyPendingDescription': '此预订已经处于待确认状态。',
'confirm.cancelledDescription': '预订已回到待确认状态。',
'confirm.cancelFailed': '取消失败',
'receipt.badge': '票券收据',
'receipt.mainQr': '主二维码',
'receipt.seatList': '座位列表',
'receipt.shareSeats': '分享座位',
'receipt.batchShare': '批量分享',
'receipt.shared': '已分享',
'receipt.available': '可分享',
'receipt.seatDetail': '座位详情',
'receipt.openLink': '打开链接',
'receipt.share': '分享',
'receipt.sharedStatus': '已分享',
'receipt.availableStatus': '可分享',
'receipt.unassigned': '未指定',
'receipt.readyToShare': '可分享',
'receipt.sharedAt': '分享于 {date}',
'receipt.openSeatLink': '打开座位链接',
'receipt.shareSeat': '分享座位',
'receipt.unshareSeat': '取消分享座位',
'receipt.unshare': '取消分享',
'receipt.batchTitle': '批量分享座位',
'receipt.seatsToShare': '要分享的座位',
'receipt.nextSeats': '接下来的座位',
'receipt.noSeatsAvailable': '没有可分享的座位',
'receipt.recipientName': '接收人姓名',
'receipt.recipientPhone': '接收人电话',
'receipt.optional': '选填',
'receipt.optionalPhonePlaceholder': '选填,例如 +60123456789',
'receipt.recipient': '接收人',
'receipt.recipientPhoneLabel': '接收人电话',
'receipt.guest': '宾客',
'receipt.purchaser': '购票人',
'receipt.seatsReady': '座位已准备好',
'receipt.seatsReadyDescription': '{count} 个{seatLabel}已准备发送。',
'receipt.copied': '已复制到剪贴板。',
'receipt.copyPrompt': '复制这段文字',
'receipt.seatUpdateFailed': '座位更新失败',
'receipt.seatUpdateFailedDescription': '分享面板已打开,但座位记录无法更新。',
'receipt.seatsShared': '已分享 {count} 个{seatLabel}',
'receipt.allSeatsSent': '接下来的可用座位已发送。',
'receipt.someSeatsFailed': '部分座位已发送,但至少一个座位更新失败。',
'receipt.unableShareSeats': '无法分享座位',
'receipt.seatReady': '座位已准备好',
'receipt.seatReadyDescription': '{seat} 已准备发送。',
'receipt.seatShared': '{seat} 已分享',
'receipt.unableShareSeat': '无法分享座位',
'receipt.unsharePrompt': '确定取消分享 {seat}?旧链接将无法继续使用。',
'receipt.seatUnshared': '{seat} 已取消分享',
'receipt.unableUnshareSeat': '无法取消分享座位',
'login.badge': '职员入口',
'login.title': '登录管理系统',
'login.username': '用户名',
'login.usernamePlaceholder': '请输入用户名',
'login.password': '密码',
'login.passwordPlaceholder': '请输入密码',
'login.remember': '记住此设备',
'login.signIn': '登录',
'login.or': '或',
'login.passkey': '使用 Passkey 登录',
'login.usernameRequired': '请输入用户名。',
'login.passwordRequired': '请输入密码。',
'login.failed': '登录失败',
'login.failedDescription': '无法使用用户名和密码登录。',
'login.passkeyFailed': 'Passkey 登录失败',
'login.passkeyFailedDescription': '无法完成 Passkey 登录。',
'date.notAvailable': '无资料'
}
} as const
type MessageKey = keyof typeof messages.en
type MessageParams = Record<string, string | number | null | undefined>
function interpolate(message: string, params?: MessageParams) {
if (!params) {
return message
}
return message.replace(/\{(\w+)\}/g, (_, key: string) => String(params[key] ?? ''))
}
export function useLocale() {
const localeCookie = useCookie<AppLocale | null>('dts-locale', {
sameSite: 'lax'
})
const requestHeaders = import.meta.server ? useRequestHeaders(['accept-language']) : {}
const locale = useState<AppLocale>('dts-locale', () => resolveLocale(
localeCookie.value || requestHeaders['accept-language'],
DEFAULT_LOCALE
))
if (import.meta.client && !localeCookie.value) {
locale.value = resolveLocale(navigator.language, locale.value)
}
localeCookie.value = locale.value
function setLocale(nextLocale: string) {
locale.value = resolveLocale(nextLocale, locale.value)
localeCookie.value = locale.value
}
function toggleLocale() {
setLocale(getOppositeLocale(locale.value))
}
function t(key: MessageKey, params?: MessageParams) {
const message = messages[locale.value][key] || messages.en[key] || key
return interpolate(message, params)
}
const localeOptions = LOCALE_OPTIONS
const currentLocaleOption = computed(() => {
return localeOptions.find((option) => option.value === locale.value) || localeOptions[0]
})
return {
locale,
localeOptions,
currentLocaleOption,
setLocale,
toggleLocale,
t
}
}