diff --git a/client/src/App.vue b/client/src/App.vue index 6e9fc98..caeaf16 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -1,11 +1,16 @@ - + + diff --git a/client/src/components/reservation/ReservationOperaPanel.vue b/client/src/components/reservation/ReservationOperaPanel.vue index f411b4e..45f7bd1 100644 --- a/client/src/components/reservation/ReservationOperaPanel.vue +++ b/client/src/components/reservation/ReservationOperaPanel.vue @@ -9,7 +9,7 @@ - + () + showActions?: boolean +}>(), { + showActions: true, +}) const emit = defineEmits<{ operationUpdated: [operation: ReservationOperaOperationResult] diff --git a/client/src/components/reservation/ReservationTaskDetailPanel.vue b/client/src/components/reservation/ReservationTaskDetailPanel.vue index f04d73d..aabd3c4 100644 --- a/client/src/components/reservation/ReservationTaskDetailPanel.vue +++ b/client/src/components/reservation/ReservationTaskDetailPanel.vue @@ -117,8 +117,12 @@ :validation-errors="validationErrorMessages" /> - @@ -191,12 +200,14 @@ import { } from '@/utils/reservationFieldRules' import { formatReservationMaybeStableReason, formatReservationTaskCard } from '@/utils/reservationDisplay' import { formatReservationDateTime } from '@/utils/reservationFormat' +import { useAuthStore } from '@/stores/authStore' const props = defineProps<{ taskId: string }>() const { t } = useI18n() +const authStore = useAuthStore() const loading = ref(false) const errorMessage = ref('') const actionErrorMessage = ref('') @@ -208,8 +219,12 @@ const validationErrors = ref({}) const validationSummaryActive = ref(false) const mutationBusy = ref<'save' | 'confirm' | 'manual-convert' | null>(null) -const canSave = computed(() => Boolean(detail.value?.availability.editable)) -const canConfirm = computed(() => Boolean(detail.value?.availability.confirmable)) +const canEditTask = computed(() => authStore.hasPermission('RESERVATION_TASK_EDIT')) +const canConfirmTask = computed(() => authStore.hasPermission('RESERVATION_TASK_CONFIRM')) +const canExecuteOpera = computed(() => authStore.hasPermission('RESERVATION_OPERA_SIM_EXECUTE')) +const canViewAudit = computed(() => authStore.hasPermission('RESERVATION_AUDIT_READ')) +const canSave = computed(() => canEditTask.value && Boolean(detail.value?.availability.editable)) +const canConfirm = computed(() => canConfirmTask.value && Boolean(detail.value?.availability.confirmable)) const isManualReviewTask = computed(() => (detail.value ? taskHasManualReviewCode(detail.value) : false)) const validationErrorMessages = computed(() => toValidationErrorMessages(validationErrors.value)) @@ -237,7 +252,12 @@ async function loadTask(taskId: string): Promise { try { const [nextDetail, nextAudits] = await Promise.all([ fetchReservationTaskDetail(taskId), - fetchReservationTaskAudits(taskId), + canViewAudit.value + ? fetchReservationTaskAudits(taskId) + : Promise.resolve({ + task_id: taskId, + items: [], + }), ]) applyTaskData(nextDetail, nextAudits.items) } catch (error) { @@ -271,7 +291,12 @@ async function submitManualReviewConversion(request: ReservationManualReviewConv const result = await convertReservationManualReviewTask(detail.value.task_id, request) const [nextDetail, nextAudits] = await Promise.all([ fetchReservationTaskDetail(detail.value.task_id), - fetchReservationTaskAudits(detail.value.task_id), + canViewAudit.value + ? fetchReservationTaskAudits(detail.value.task_id) + : Promise.resolve({ + task_id: detail.value.task_id, + items: [], + }), ]) applyTaskData(nextDetail, nextAudits.items) notice.value = t('task.manualConversion.succeeded', { @@ -289,6 +314,9 @@ async function mutateTaskPayload(action: 'save' | 'confirm'): Promise { if (!detail.value) { return } + if ((action === 'save' && !canSave.value) || (action === 'confirm' && !canConfirm.value)) { + return + } const taskId = detail.value.task_id mutationBusy.value = action actionErrorMessage.value = '' @@ -310,7 +338,9 @@ async function mutateTaskPayload(action: 'save' | 'confirm'): Promise { confirmed_payload: result.confirmed_payload, opera_operations: result.opera_operations, } - await refreshAudits(taskId) + if (canViewAudit.value) { + await refreshAudits(taskId) + } notice.value = action === 'save' ? t('task.savedLocally') : t('task.confirmedLocally') } catch (error) { actionErrorMessage.value = error instanceof Error ? error.message : String(error) diff --git a/client/src/config/reservationConfig.ts b/client/src/config/reservationConfig.ts index f0a7878..4bf3fdd 100644 --- a/client/src/config/reservationConfig.ts +++ b/client/src/config/reservationConfig.ts @@ -1,3 +1,14 @@ const configuredReservationHotelId = import.meta.env.VITE_RESERVATION_HOTEL_ID?.trim() export const reservationHotelId = configuredReservationHotelId || 'HOTEL-TEST' + +let reservationHotelIdProvider: (() => string | null | undefined) | null = null + +export function setReservationHotelIdProvider(provider: (() => string | null | undefined) | null): void { + reservationHotelIdProvider = provider +} + +export function getReservationHotelId(): string { + const providedHotelId = reservationHotelIdProvider?.()?.trim() + return providedHotelId || reservationHotelId +} diff --git a/client/src/i18n/locales/en-US.ts b/client/src/i18n/locales/en-US.ts index 0abb5fa..93f2ee3 100644 --- a/client/src/i18n/locales/en-US.ts +++ b/client/src/i18n/locales/en-US.ts @@ -8,6 +8,21 @@ export default { healthDown: 'Backend offline', healthChecking: 'Checking', }, + auth: { + loginTitle: 'Sign in', + loginSubtitle: 'Use your system account to enter the TH Hotel reservation workspace.', + username: 'Username', + password: 'Password', + loginSubmit: 'Sign in', + loggingIn: 'Signing in', + loginFailed: 'The username or password is incorrect.', + logout: 'Sign out', + currentUser: 'Current user', + currentHotel: 'Current hotel', + unauthorizedTitle: 'No access', + unauthorizedMessage: 'Your account does not have permission to access this page or action.', + backHome: 'Back to available page', + }, nav: { orders: 'Order list', taskQueue: 'Task queue', diff --git a/client/src/i18n/locales/th-TH.ts b/client/src/i18n/locales/th-TH.ts index 13fc92a..47a2012 100644 --- a/client/src/i18n/locales/th-TH.ts +++ b/client/src/i18n/locales/th-TH.ts @@ -8,6 +8,21 @@ export default { healthDown: 'หลังบ้านออฟไลน์', healthChecking: 'กำลังตรวจสอบ', }, + auth: { + loginTitle: 'เข้าสู่ระบบ', + loginSubtitle: 'ใช้บัญชีระบบเพื่อเข้าสู่พื้นที่ทำงานการจองของ TH Hotel', + username: 'ชื่อผู้ใช้', + password: 'รหัสผ่าน', + loginSubmit: 'เข้าสู่ระบบ', + loggingIn: 'กำลังเข้าสู่ระบบ', + loginFailed: 'ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง', + logout: 'ออกจากระบบ', + currentUser: 'ผู้ใช้ปัจจุบัน', + currentHotel: 'โรงแรมปัจจุบัน', + unauthorizedTitle: 'ไม่มีสิทธิ์เข้าถึง', + unauthorizedMessage: 'บัญชีนี้ไม่มีสิทธิ์เข้าถึงหน้านี้หรือการดำเนินการนี้', + backHome: 'กลับไปหน้าที่เข้าถึงได้', + }, nav: { orders: 'รายการออเดอร์', taskQueue: 'คิวงาน', diff --git a/client/src/i18n/locales/zh-CN.ts b/client/src/i18n/locales/zh-CN.ts index c5e60f0..caea094 100644 --- a/client/src/i18n/locales/zh-CN.ts +++ b/client/src/i18n/locales/zh-CN.ts @@ -8,6 +8,21 @@ export default { healthDown: '后端未连接', healthChecking: '检查中', }, + auth: { + loginTitle: '登录系统', + loginSubtitle: '使用系统账号进入 TH Hotel 预订处理工作台。', + username: '用户名', + password: '密码', + loginSubmit: '登录', + loggingIn: '登录中', + loginFailed: '用户名或密码错误。', + logout: '退出登录', + currentUser: '当前用户', + currentHotel: '当前酒店', + unauthorizedTitle: '没有访问权限', + unauthorizedMessage: '当前账号没有访问这个页面或操作的权限,请联系管理员调整角色权限。', + backHome: '返回可访问页面', + }, nav: { orders: '订单列表', taskQueue: '任务队列', diff --git a/client/src/layouts/ReservationAppShell.vue b/client/src/layouts/ReservationAppShell.vue index 47fe110..88f7eb1 100644 --- a/client/src/layouts/ReservationAppShell.vue +++ b/client/src/layouts/ReservationAppShell.vue @@ -3,7 +3,7 @@ @@ -35,10 +35,57 @@ {{ pageTitle }} - {{ reservationHotelId }} + {{ currentHotelLabel }} + + + + {{ currentUserLabel }} + + + + + + + {{ hotel.hotel_name || hotel.hotel_id }} + + + + + + + {{ currentHotelLabel }} + + + + + + + @@ -88,16 +148,19 @@ + + diff --git a/client/src/views/auth/UnauthorizedView.vue b/client/src/views/auth/UnauthorizedView.vue new file mode 100644 index 0000000..bfb234c --- /dev/null +++ b/client/src/views/auth/UnauthorizedView.vue @@ -0,0 +1,74 @@ + + + + + {{ t('auth.unauthorizedTitle') }} + {{ t('auth.unauthorizedMessage') }} + + {{ t('auth.backHome') }} + + + + + + + +
{{ t('auth.unauthorizedMessage') }}