实现V4目录管理后台前端
This commit is contained in:
@@ -70,6 +70,12 @@ const allTabs: Array<{
|
||||
icon: 'pi pi-building',
|
||||
permission: 'HOTEL_MANAGE',
|
||||
},
|
||||
{
|
||||
path: '/system/reservation-catalogs',
|
||||
labelKey: 'nav.systemReservationCatalogs',
|
||||
icon: 'pi pi-list-check',
|
||||
permission: 'RESERVATION_CATALOG_MANAGE',
|
||||
},
|
||||
{
|
||||
path: '/system/audits',
|
||||
labelKey: 'nav.systemAudits',
|
||||
|
||||
808
client/src/views/system/SystemReservationCatalogsView.vue
Normal file
808
client/src/views/system/SystemReservationCatalogsView.vue
Normal file
@@ -0,0 +1,808 @@
|
||||
<template>
|
||||
<section class="system-admin-panel reservation-catalog-admin">
|
||||
<section class="th-section catalog-intro">
|
||||
<div class="th-section-header">
|
||||
<div>
|
||||
<h2 class="th-section-title">
|
||||
{{ t('systemAdmin.catalogs.title') }}
|
||||
</h2>
|
||||
<p class="th-muted">
|
||||
{{ t('systemAdmin.catalogs.subtitle') }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="secondary-button"
|
||||
:disabled="loading"
|
||||
@click="loadCatalog"
|
||||
>
|
||||
{{ t('common.refresh') }}
|
||||
</button>
|
||||
</div>
|
||||
<p class="form-message">
|
||||
{{ t('systemAdmin.catalogs.disabledLookupNotice') }}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<nav
|
||||
class="catalog-type-tabs"
|
||||
:aria-label="t('systemAdmin.catalogs.typeNavigation')"
|
||||
>
|
||||
<button
|
||||
v-for="item in catalogTypes"
|
||||
:key="item.kind"
|
||||
type="button"
|
||||
class="catalog-type-tabs__item"
|
||||
:class="{ 'catalog-type-tabs__item--active': activeCatalog === item.kind }"
|
||||
:data-testid="`catalog-tab-${item.kind}`"
|
||||
@click="selectCatalog(item.kind)"
|
||||
>
|
||||
<i
|
||||
:class="item.icon"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{{ t(item.labelKey) }}</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="th-section filter-section">
|
||||
<div class="th-section-header">
|
||||
<h2 class="th-section-title">
|
||||
{{ t('systemAdmin.catalogs.filters') }}
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="text-button"
|
||||
@click="resetFilters"
|
||||
>
|
||||
{{ t('workbench.reset') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="filter-grid">
|
||||
<label>
|
||||
<span>{{ t('systemAdmin.catalogs.hotel') }}</span>
|
||||
<select
|
||||
v-model="filters.hotel_id"
|
||||
@change="resetToFirstPage"
|
||||
>
|
||||
<option value="">
|
||||
{{ t('systemAdmin.catalogs.defaultHotel') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="hotel in authStore.hotels"
|
||||
:key="hotel.hotel_id"
|
||||
:value="hotel.hotel_id"
|
||||
>
|
||||
{{ hotel.hotel_name }} ({{ hotel.hotel_id }})
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>{{ t('systemAdmin.common.keyword') }}</span>
|
||||
<input
|
||||
v-model="filters.keyword"
|
||||
type="search"
|
||||
:placeholder="t('systemAdmin.catalogs.keywordPlaceholder')"
|
||||
@input="resetToFirstPage"
|
||||
>
|
||||
</label>
|
||||
<label>
|
||||
<span>{{ t('systemAdmin.catalogs.catalogStatus') }}</span>
|
||||
<select
|
||||
v-model="filters.status"
|
||||
@change="resetToFirstPage"
|
||||
>
|
||||
<option value="">
|
||||
{{ t('common.all') }}
|
||||
</option>
|
||||
<option value="ACTIVE">
|
||||
{{ t('systemAdmin.status.ACTIVE') }}
|
||||
</option>
|
||||
<option value="DISABLED">
|
||||
{{ t('systemAdmin.status.DISABLED') }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="th-section action-section">
|
||||
<div class="th-section-header">
|
||||
<div>
|
||||
<h2 class="th-section-title">
|
||||
{{ t('systemAdmin.catalogs.createTitle', { type: activeCatalogLabel }) }}
|
||||
</h2>
|
||||
<p class="th-muted">
|
||||
{{ t('systemAdmin.catalogs.createHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<form
|
||||
data-testid="catalog-create-form"
|
||||
class="form-grid catalog-create-form"
|
||||
@submit.prevent="createCatalog"
|
||||
>
|
||||
<template v-if="activeCatalog === 'accounts'">
|
||||
<label>
|
||||
<span>{{ t('systemAdmin.catalogs.accountCode') }}</span>
|
||||
<input
|
||||
v-model="accountForm.account_code"
|
||||
data-testid="catalog-account-code"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
required
|
||||
>
|
||||
</label>
|
||||
<label>
|
||||
<span>{{ t('systemAdmin.catalogs.accountName') }}</span>
|
||||
<input
|
||||
v-model="accountForm.account_name"
|
||||
data-testid="catalog-account-name"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
required
|
||||
>
|
||||
</label>
|
||||
<label>
|
||||
<span>{{ t('systemAdmin.catalogs.marketCode') }}</span>
|
||||
<input
|
||||
:value="fixedMarketCode"
|
||||
type="text"
|
||||
readonly
|
||||
>
|
||||
</label>
|
||||
<label>
|
||||
<span>{{ t('systemAdmin.catalogs.sourceCode') }}</span>
|
||||
<input
|
||||
:value="fixedSourceCode"
|
||||
type="text"
|
||||
readonly
|
||||
>
|
||||
</label>
|
||||
<label>
|
||||
<span>{{ t('systemAdmin.catalogs.externalAccountId') }}</span>
|
||||
<input
|
||||
v-model="accountForm.external_account_id"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
>
|
||||
</label>
|
||||
</template>
|
||||
<template v-else>
|
||||
<label>
|
||||
<span>{{ t('systemAdmin.catalogs.code') }}</span>
|
||||
<input
|
||||
v-model="codeForm.code"
|
||||
data-testid="catalog-code"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
required
|
||||
>
|
||||
</label>
|
||||
<label>
|
||||
<span>{{ t('systemAdmin.catalogs.displayName') }}</span>
|
||||
<input
|
||||
v-model="codeForm.display_name"
|
||||
data-testid="catalog-display-name"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
required
|
||||
>
|
||||
</label>
|
||||
<label>
|
||||
<span>{{ t('systemAdmin.catalogs.externalId') }}</span>
|
||||
<input
|
||||
v-model="codeForm.external_id"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
>
|
||||
</label>
|
||||
<label>
|
||||
<span>{{ t('systemAdmin.catalogs.sortOrder') }}</span>
|
||||
<input
|
||||
v-model.number="codeForm.sort_order"
|
||||
type="number"
|
||||
>
|
||||
</label>
|
||||
</template>
|
||||
<label>
|
||||
<span>{{ t('systemAdmin.catalogs.catalogVersion') }}</span>
|
||||
<input
|
||||
v-if="activeCatalog === 'accounts'"
|
||||
v-model="accountForm.catalog_version"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
>
|
||||
<input
|
||||
v-else
|
||||
v-model="codeForm.catalog_version"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
>
|
||||
</label>
|
||||
<label class="catalog-metadata-field">
|
||||
<span>{{ t('systemAdmin.catalogs.metadataJson') }}</span>
|
||||
<input
|
||||
v-if="activeCatalog === 'accounts'"
|
||||
v-model="accountForm.metadata_json"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
>
|
||||
<input
|
||||
v-else
|
||||
v-model="codeForm.metadata_json"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
>
|
||||
</label>
|
||||
<button
|
||||
type="submit"
|
||||
class="primary-button"
|
||||
:disabled="saving"
|
||||
>
|
||||
{{ saving ? t('common.saving') : t('systemAdmin.catalogs.createAction') }}
|
||||
</button>
|
||||
</form>
|
||||
<p class="form-message">
|
||||
{{ t('systemAdmin.catalogs.defaultActiveHint') }}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="th-section table-section">
|
||||
<div class="th-section-header">
|
||||
<div>
|
||||
<h2 class="th-section-title">
|
||||
{{ t('systemAdmin.catalogs.tableTitle', { type: activeCatalogLabel }) }}
|
||||
</h2>
|
||||
<p class="th-muted">
|
||||
{{ t('workbench.totalPrefix') }} {{ total }} {{ t('systemAdmin.catalogs.totalSuffix') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="loading"
|
||||
class="empty-state"
|
||||
>
|
||||
{{ t('common.loading') }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="errorMessage"
|
||||
class="empty-state empty-state--error"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="!currentItems.length"
|
||||
class="empty-state"
|
||||
>
|
||||
{{ t('common.noData') }}
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="table-wrap"
|
||||
>
|
||||
<table v-if="activeCatalog === 'accounts'">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ t('systemAdmin.catalogs.columns.catalog') }}</th>
|
||||
<th>{{ t('systemAdmin.catalogs.columns.status') }}</th>
|
||||
<th>{{ t('systemAdmin.catalogs.columns.marketSource') }}</th>
|
||||
<th>{{ t('systemAdmin.catalogs.columns.source') }}</th>
|
||||
<th>{{ t('workbench.columns.updatedAt') }}</th>
|
||||
<th>{{ t('workbench.columns.operation') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="account in accountItems"
|
||||
:key="account.id"
|
||||
>
|
||||
<td>
|
||||
<strong>{{ account.account_code }}</strong>
|
||||
<small class="th-code">{{ account.account_name }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
class="status-pill"
|
||||
:class="{ 'status-pill--active': account.status === 'ACTIVE' }"
|
||||
>
|
||||
{{ statusLabel(account.status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<strong>{{ account.market_code }}</strong>
|
||||
<small class="th-code">{{ account.source_code }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<strong>{{ account.catalog_source }}</strong>
|
||||
<small class="th-code">{{ account.catalog_version ?? '-' }}</small>
|
||||
</td>
|
||||
<td>{{ formatReservationDateTime(account.updated_at) }}</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
class="row-action"
|
||||
:data-testid="`catalog-status-${account.id}`"
|
||||
:disabled="saving"
|
||||
@click="toggleCatalogStatus(account)"
|
||||
>
|
||||
{{ statusActionLabel(account.status) }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table v-else>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ t('systemAdmin.catalogs.columns.catalog') }}</th>
|
||||
<th>{{ t('systemAdmin.catalogs.columns.status') }}</th>
|
||||
<th>{{ t('systemAdmin.catalogs.columns.source') }}</th>
|
||||
<th>{{ t('systemAdmin.catalogs.columns.sort') }}</th>
|
||||
<th>{{ t('workbench.columns.updatedAt') }}</th>
|
||||
<th>{{ t('workbench.columns.operation') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="code in codeItems"
|
||||
:key="code.id"
|
||||
>
|
||||
<td>
|
||||
<strong>{{ code.code }}</strong>
|
||||
<small class="th-code">{{ code.display_name }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
class="status-pill"
|
||||
:class="{ 'status-pill--active': code.status === 'ACTIVE' }"
|
||||
>
|
||||
{{ statusLabel(code.status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<strong>{{ code.catalog_source }}</strong>
|
||||
<small class="th-code">{{ code.catalog_version ?? '-' }}</small>
|
||||
</td>
|
||||
<td>{{ code.sort_order ?? '-' }}</td>
|
||||
<td>{{ formatReservationDateTime(code.updated_at) }}</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
class="row-action"
|
||||
:data-testid="`catalog-status-${code.id}`"
|
||||
:disabled="saving"
|
||||
@click="toggleCatalogStatus(code)"
|
||||
>
|
||||
{{ statusActionLabel(code.status) }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<footer class="pagination-bar">
|
||||
<label class="page-size-control">
|
||||
<span>{{ t('common.pageSize') }}</span>
|
||||
<select
|
||||
v-model.number="filters.page_size"
|
||||
@change="resetToFirstPage"
|
||||
>
|
||||
<option :value="10">
|
||||
10
|
||||
</option>
|
||||
<option :value="20">
|
||||
20
|
||||
</option>
|
||||
<option :value="50">
|
||||
50
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="pagination-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="secondary-button"
|
||||
:disabled="currentPage <= 1 || loading"
|
||||
@click="goToPage(currentPage - 1)"
|
||||
>
|
||||
{{ t('common.previousPage') }}
|
||||
</button>
|
||||
<span>{{ t('common.pageStatus', { current: currentPage, total: totalPages }) }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="secondary-button"
|
||||
:disabled="currentPage >= totalPages || loading"
|
||||
@click="goToPage(currentPage + 1)"
|
||||
>
|
||||
{{ t('common.nextPage') }}
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
</section>
|
||||
|
||||
<p
|
||||
v-if="successMessage"
|
||||
class="action-message action-message--success"
|
||||
>
|
||||
{{ successMessage }}
|
||||
</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { ApiError } from '@/services/httpClient'
|
||||
import {
|
||||
createAdminReservationCatalogAccount,
|
||||
createAdminReservationCatalogRateCode,
|
||||
createAdminReservationCatalogRoomType,
|
||||
fetchAdminReservationCatalogAccounts,
|
||||
fetchAdminReservationCatalogRateCodes,
|
||||
fetchAdminReservationCatalogRoomTypes,
|
||||
updateAdminReservationCatalogAccountStatus,
|
||||
updateAdminReservationCatalogRateCodeStatus,
|
||||
updateAdminReservationCatalogRoomTypeStatus,
|
||||
} from '@/services/systemAdminService'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import type {
|
||||
AdminReservationCatalogAccountCreateRequest,
|
||||
AdminReservationCatalogAccountResult,
|
||||
AdminReservationCatalogCodeCreateRequest,
|
||||
AdminReservationCatalogCodeResult,
|
||||
AdminReservationCatalogListFilters,
|
||||
PlatformPaginationResult,
|
||||
} from '@/types/systemAdmin'
|
||||
import { formatReservationDateTime } from '@/utils/reservationFormat'
|
||||
|
||||
type CatalogKind = 'accounts' | 'room-types' | 'rate-codes'
|
||||
type CatalogRecord = AdminReservationCatalogAccountResult | AdminReservationCatalogCodeResult
|
||||
|
||||
const fixedMarketCode = 'LEISURE'
|
||||
const fixedSourceCode = 'TRAVEL_AGENT'
|
||||
const catalogTypes: Array<{ kind: CatalogKind; labelKey: string; icon: string }> = [
|
||||
{
|
||||
kind: 'accounts',
|
||||
labelKey: 'systemAdmin.catalogs.types.accounts',
|
||||
icon: 'pi pi-briefcase',
|
||||
},
|
||||
{
|
||||
kind: 'room-types',
|
||||
labelKey: 'systemAdmin.catalogs.types.roomTypes',
|
||||
icon: 'pi pi-home',
|
||||
},
|
||||
{
|
||||
kind: 'rate-codes',
|
||||
labelKey: 'systemAdmin.catalogs.types.rateCodes',
|
||||
icon: 'pi pi-tags',
|
||||
},
|
||||
]
|
||||
|
||||
const { t } = useI18n()
|
||||
const authStore = useAuthStore()
|
||||
const activeCatalog = ref<CatalogKind>('accounts')
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const successMessage = ref('')
|
||||
const accountItems = ref<AdminReservationCatalogAccountResult[]>([])
|
||||
const codeItems = ref<AdminReservationCatalogCodeResult[]>([])
|
||||
const page = ref(createPage(1, 20, 0))
|
||||
const total = computed(() => page.value.total)
|
||||
const currentPage = computed(() => page.value.page_num)
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil(page.value.total / Math.max(page.value.page_size, 1))))
|
||||
const currentItems = computed<CatalogRecord[]>(() =>
|
||||
activeCatalog.value === 'accounts' ? accountItems.value : codeItems.value,
|
||||
)
|
||||
const activeCatalogLabel = computed(() => {
|
||||
const activeType = catalogTypes.find((item) => item.kind === activeCatalog.value) ?? catalogTypes[0]!
|
||||
return t(activeType.labelKey)
|
||||
})
|
||||
const filters = reactive<AdminReservationCatalogListFilters>({
|
||||
hotel_id: authStore.selectedHotelId ?? '',
|
||||
keyword: '',
|
||||
status: '',
|
||||
page_num: 1,
|
||||
page_size: 20,
|
||||
})
|
||||
const accountForm = reactive({
|
||||
account_code: '',
|
||||
account_name: '',
|
||||
external_account_id: '',
|
||||
catalog_version: '',
|
||||
metadata_json: '',
|
||||
})
|
||||
const codeForm = reactive({
|
||||
code: '',
|
||||
display_name: '',
|
||||
external_id: '',
|
||||
sort_order: 100,
|
||||
catalog_version: '',
|
||||
metadata_json: '',
|
||||
})
|
||||
let requestSequence = 0
|
||||
|
||||
onMounted(() => {
|
||||
void loadCatalog()
|
||||
})
|
||||
|
||||
watch(filters, () => {
|
||||
void loadCatalog()
|
||||
})
|
||||
|
||||
watch(activeCatalog, () => {
|
||||
resetForms()
|
||||
successMessage.value = ''
|
||||
if ((filters.page_num ?? 1) === 1) {
|
||||
void loadCatalog()
|
||||
return
|
||||
}
|
||||
filters.page_num = 1
|
||||
})
|
||||
|
||||
watch(
|
||||
() => authStore.selectedHotelId,
|
||||
(hotelId) => {
|
||||
if (!filters.hotel_id && hotelId) {
|
||||
filters.hotel_id = hotelId
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
async function loadCatalog(): Promise<void> {
|
||||
const currentRequest = ++requestSequence
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
try {
|
||||
const query = { ...filters }
|
||||
const result = activeCatalog.value === 'accounts'
|
||||
? await fetchAdminReservationCatalogAccounts(query)
|
||||
: activeCatalog.value === 'room-types'
|
||||
? await fetchAdminReservationCatalogRoomTypes(query)
|
||||
: await fetchAdminReservationCatalogRateCodes(query)
|
||||
if (currentRequest !== requestSequence) {
|
||||
return
|
||||
}
|
||||
if (activeCatalog.value === 'accounts') {
|
||||
accountItems.value = result.items as AdminReservationCatalogAccountResult[]
|
||||
codeItems.value = []
|
||||
} else {
|
||||
accountItems.value = []
|
||||
codeItems.value = result.items as AdminReservationCatalogCodeResult[]
|
||||
}
|
||||
page.value = result.page
|
||||
} catch (error) {
|
||||
if (currentRequest !== requestSequence) {
|
||||
return
|
||||
}
|
||||
accountItems.value = []
|
||||
codeItems.value = []
|
||||
page.value = createPage(filters.page_num ?? 1, filters.page_size ?? 20, 0)
|
||||
errorMessage.value = formatOperationError(error)
|
||||
} finally {
|
||||
if (currentRequest === requestSequence) {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function createCatalog(): Promise<void> {
|
||||
const hotelId = activeHotelId()
|
||||
if (!hotelId) {
|
||||
errorMessage.value = t('systemAdmin.catalogs.hotelRequired')
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
errorMessage.value = ''
|
||||
successMessage.value = ''
|
||||
try {
|
||||
if (activeCatalog.value === 'accounts') {
|
||||
await createAdminReservationCatalogAccount(buildAccountCreateRequest(hotelId))
|
||||
} else if (activeCatalog.value === 'room-types') {
|
||||
await createAdminReservationCatalogRoomType(buildCodeCreateRequest(hotelId))
|
||||
} else {
|
||||
await createAdminReservationCatalogRateCode(buildCodeCreateRequest(hotelId))
|
||||
}
|
||||
successMessage.value = t('systemAdmin.catalogs.createSucceeded')
|
||||
resetForms()
|
||||
await loadCatalog()
|
||||
} catch (error) {
|
||||
errorMessage.value = formatOperationError(error)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleCatalogStatus(record: CatalogRecord): Promise<void> {
|
||||
const nextStatus = record.status === 'ACTIVE' ? 'DISABLED' : 'ACTIVE'
|
||||
saving.value = true
|
||||
errorMessage.value = ''
|
||||
successMessage.value = ''
|
||||
try {
|
||||
if (activeCatalog.value === 'accounts') {
|
||||
await updateAdminReservationCatalogAccountStatus(record.id, { status: nextStatus })
|
||||
} else if (activeCatalog.value === 'room-types') {
|
||||
await updateAdminReservationCatalogRoomTypeStatus(record.id, { status: nextStatus })
|
||||
} else {
|
||||
await updateAdminReservationCatalogRateCodeStatus(record.id, { status: nextStatus })
|
||||
}
|
||||
successMessage.value = t('systemAdmin.catalogs.statusUpdated')
|
||||
await loadCatalog()
|
||||
} catch (error) {
|
||||
errorMessage.value = formatOperationError(error)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function selectCatalog(kind: CatalogKind): void {
|
||||
if (activeCatalog.value !== kind) {
|
||||
activeCatalog.value = kind
|
||||
}
|
||||
}
|
||||
|
||||
function resetFilters(): void {
|
||||
filters.hotel_id = authStore.selectedHotelId ?? ''
|
||||
filters.keyword = ''
|
||||
filters.status = ''
|
||||
filters.page_num = 1
|
||||
filters.page_size = 20
|
||||
}
|
||||
|
||||
function resetToFirstPage(): void {
|
||||
filters.page_num = 1
|
||||
}
|
||||
|
||||
function goToPage(pageNum: number): void {
|
||||
filters.page_num = Math.min(Math.max(pageNum, 1), totalPages.value)
|
||||
}
|
||||
|
||||
function resetForms(): void {
|
||||
accountForm.account_code = ''
|
||||
accountForm.account_name = ''
|
||||
accountForm.external_account_id = ''
|
||||
accountForm.catalog_version = ''
|
||||
accountForm.metadata_json = ''
|
||||
codeForm.code = ''
|
||||
codeForm.display_name = ''
|
||||
codeForm.external_id = ''
|
||||
codeForm.sort_order = 100
|
||||
codeForm.catalog_version = ''
|
||||
codeForm.metadata_json = ''
|
||||
}
|
||||
|
||||
function activeHotelId(): string {
|
||||
return filters.hotel_id || authStore.selectedHotelId || ''
|
||||
}
|
||||
|
||||
function buildAccountCreateRequest(hotelId: string): AdminReservationCatalogAccountCreateRequest {
|
||||
return {
|
||||
hotel_id: hotelId,
|
||||
account_code: accountForm.account_code.trim(),
|
||||
account_name: accountForm.account_name.trim(),
|
||||
market_code: fixedMarketCode,
|
||||
source_code: fixedSourceCode,
|
||||
external_account_id: optionalText(accountForm.external_account_id),
|
||||
catalog_version: optionalText(accountForm.catalog_version),
|
||||
metadata_json: optionalText(accountForm.metadata_json),
|
||||
}
|
||||
}
|
||||
|
||||
function buildCodeCreateRequest(hotelId: string): AdminReservationCatalogCodeCreateRequest {
|
||||
return {
|
||||
hotel_id: hotelId,
|
||||
code: codeForm.code.trim(),
|
||||
display_name: codeForm.display_name.trim(),
|
||||
external_id: optionalText(codeForm.external_id),
|
||||
sort_order: Number.isFinite(codeForm.sort_order) ? codeForm.sort_order : undefined,
|
||||
catalog_version: optionalText(codeForm.catalog_version),
|
||||
metadata_json: optionalText(codeForm.metadata_json),
|
||||
}
|
||||
}
|
||||
|
||||
function optionalText(value: string): string | undefined {
|
||||
const trimmed = value.trim()
|
||||
return trimmed || undefined
|
||||
}
|
||||
|
||||
function statusLabel(status: string): string {
|
||||
return t(`systemAdmin.status.${status}`)
|
||||
}
|
||||
|
||||
function statusActionLabel(status: string): string {
|
||||
return status === 'ACTIVE' ? t('systemAdmin.catalogs.disableAction') : t('systemAdmin.catalogs.enableAction')
|
||||
}
|
||||
|
||||
function createPage(pageNum: number, pageSize: number, total: number): PlatformPaginationResult {
|
||||
return {
|
||||
page_num: pageNum,
|
||||
page_size: pageSize,
|
||||
total,
|
||||
}
|
||||
}
|
||||
|
||||
function formatOperationError(error: unknown): string {
|
||||
if (error instanceof ApiError) {
|
||||
const details = error.details
|
||||
if (details && typeof details === 'object' && !Array.isArray(details)) {
|
||||
const record = details as { message?: unknown; error_code?: unknown }
|
||||
if (typeof record.message === 'string' && record.message) {
|
||||
return record.message
|
||||
}
|
||||
if (typeof record.error_code === 'string' && record.error_code) {
|
||||
return record.error_code
|
||||
}
|
||||
}
|
||||
}
|
||||
return error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import './system-admin.css';
|
||||
|
||||
.reservation-catalog-admin {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.catalog-intro {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.catalog-type-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.catalog-type-tabs__item {
|
||||
min-height: 38px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border: 1px solid var(--th-color-slate-200);
|
||||
border-radius: var(--th-radius-sm);
|
||||
background: var(--th-color-white);
|
||||
color: var(--th-color-slate-700);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.catalog-type-tabs__item--active {
|
||||
border-color: var(--th-color-blue-600);
|
||||
background: var(--th-color-blue-50);
|
||||
color: var(--th-color-blue-600);
|
||||
}
|
||||
|
||||
.catalog-create-form {
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.catalog-metadata-field {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.catalog-create-form {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.catalog-create-form {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.catalog-metadata-field {
|
||||
grid-column: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user