接入前端P0页面真实接口
This commit is contained in:
264
client/src/views/reservation/ReservationOrderDetailView.vue
Normal file
264
client/src/views/reservation/ReservationOrderDetailView.vue
Normal file
@@ -0,0 +1,264 @@
|
||||
<template>
|
||||
<div class="th-page order-detail-view">
|
||||
<div
|
||||
v-if="loading"
|
||||
class="th-section order-state"
|
||||
>
|
||||
Loading...
|
||||
</div>
|
||||
<div
|
||||
v-else-if="errorMessage"
|
||||
class="th-section order-state order-state--error"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
|
||||
<template v-else-if="detail">
|
||||
<section class="th-section order-summary">
|
||||
<header>
|
||||
<div>
|
||||
<p>{{ t('order.summary') }}</p>
|
||||
<h1>{{ detail.order.display_name }}</h1>
|
||||
</div>
|
||||
<ReservationStatusBadge :status="detail.order.order_status" />
|
||||
</header>
|
||||
|
||||
<dl>
|
||||
<div>
|
||||
<dt>{{ t('order.groupCode') }}</dt>
|
||||
<dd>{{ detail.order.group_code ?? '-' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ t('order.confirmationNumber') }}</dt>
|
||||
<dd>{{ detail.order.confirmation_number ?? '-' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ t('order.temporaryOrderNo') }}</dt>
|
||||
<dd>{{ detail.order.temporary_order_no ?? '-' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ t('order.blockCode') }}</dt>
|
||||
<dd>{{ detail.order.block_code ?? '-' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ t('order.createdAt') }}</dt>
|
||||
<dd>{{ formatReservationDateTime(detail.order.created_at) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ t('order.updatedAt') }}</dt>
|
||||
<dd>{{ formatReservationDateTime(detail.order.updated_at) }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<div class="order-grid">
|
||||
<aside class="order-side">
|
||||
<section class="th-section side-section">
|
||||
<div class="th-section-header">
|
||||
<h2 class="th-section-title">
|
||||
{{ t('order.taskQueue') }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="side-section__body">
|
||||
<ReservationTaskQueue
|
||||
:items="detail.tasks"
|
||||
:active-task-id="activeTaskId"
|
||||
@open-task="activeTaskId = $event"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="th-section source-gap">
|
||||
<h2>{{ t('task.evidence') }}</h2>
|
||||
<p>{{ t('order.sourceSummaryPending') }}</p>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
<ReservationTaskDetailPanel
|
||||
v-if="activeTaskId"
|
||||
:task-id="activeTaskId"
|
||||
/>
|
||||
<section
|
||||
v-else
|
||||
class="th-section order-state"
|
||||
>
|
||||
{{ t('common.noData') }}
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ReservationStatusBadge from '@/components/reservation/ReservationStatusBadge.vue'
|
||||
import ReservationTaskDetailPanel from '@/components/reservation/ReservationTaskDetailPanel.vue'
|
||||
import ReservationTaskQueue from '@/components/reservation/ReservationTaskQueue.vue'
|
||||
import { fetchReservationOrderDetail } from '@/services/reservationService'
|
||||
import type { ReservationOrderDetailResult } from '@/types/reservation'
|
||||
import { formatReservationDateTime } from '@/utils/reservationFormat'
|
||||
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
const loading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const detail = ref<ReservationOrderDetailResult | null>(null)
|
||||
const activeTaskId = ref('')
|
||||
|
||||
const orderId = computed(() => String(route.params.orderId ?? ''))
|
||||
|
||||
watch(
|
||||
orderId,
|
||||
(nextOrderId) => {
|
||||
void loadOrder(nextOrderId)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
async function loadOrder(nextOrderId: string): Promise<void> {
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
try {
|
||||
const nextOrder = await fetchReservationOrderDetail(nextOrderId)
|
||||
detail.value = nextOrder
|
||||
activeTaskId.value = nextOrder.tasks[0]?.task_id ?? ''
|
||||
} catch (error) {
|
||||
detail.value = null
|
||||
activeTaskId.value = ''
|
||||
errorMessage.value = error instanceof Error ? error.message : String(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.order-detail-view {
|
||||
max-width: 1320px;
|
||||
}
|
||||
|
||||
.order-state {
|
||||
min-height: 260px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--th-color-slate-500);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.order-state--error {
|
||||
color: var(--th-color-danger);
|
||||
}
|
||||
|
||||
.order-summary {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.order-summary header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.order-summary p,
|
||||
.order-summary h1,
|
||||
.order-summary dl {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.order-summary p {
|
||||
color: var(--th-color-blue-600);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.order-summary h1 {
|
||||
margin-top: 4px;
|
||||
color: var(--th-color-slate-900);
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.order-summary dl {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.order-summary dt {
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.order-summary dd {
|
||||
margin: 5px 0 0;
|
||||
color: var(--th-color-slate-900);
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.order-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(260px, 340px) minmax(0, 1fr);
|
||||
gap: 18px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.order-side,
|
||||
.side-section__body {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.side-section {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.side-section__body {
|
||||
padding: 14px 16px 0;
|
||||
}
|
||||
|
||||
.source-gap {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.source-gap h2,
|
||||
.source-gap p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.source-gap h2 {
|
||||
color: var(--th-color-slate-900);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.source-gap p {
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.order-summary dl {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.order-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.order-summary dl {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
206
client/src/views/reservation/ReservationOrderListView.vue
Normal file
206
client/src/views/reservation/ReservationOrderListView.vue
Normal file
@@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<div class="th-page list-view">
|
||||
<header class="page-heading">
|
||||
<div>
|
||||
<h1>{{ t('orderList.title') }}</h1>
|
||||
<p>{{ t('orderList.subtitle') }}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="th-section filter-section">
|
||||
<div class="th-section-header">
|
||||
<h2 class="th-section-title">
|
||||
{{ t('workbench.filters') }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="filter-grid">
|
||||
<label>
|
||||
<span>{{ t('orderList.keyword') }}</span>
|
||||
<input
|
||||
v-model="filters.keyword"
|
||||
type="search"
|
||||
placeholder="GRP-001 / CNF123456"
|
||||
>
|
||||
</label>
|
||||
<label>
|
||||
<span>{{ t('workbench.orderStatus') }}</span>
|
||||
<select v-model="filters.order_status">
|
||||
<option value="">All</option>
|
||||
<option value="TEMPORARY">{{ t('status.TEMPORARY') }}</option>
|
||||
<option value="ACTIVE">{{ t('status.ACTIVE') }}</option>
|
||||
<option value="ENDED">{{ t('status.ENDED') }}</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="th-section table-section">
|
||||
<div class="th-section-header">
|
||||
<div>
|
||||
<h2 class="th-section-title">
|
||||
{{ t('orderList.tableTitle') }}
|
||||
</h2>
|
||||
<p class="th-muted">
|
||||
{{ pendingMessage || t('common.noData') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="pendingMessage"
|
||||
class="empty-state"
|
||||
>
|
||||
<i
|
||||
class="pi pi-info-circle"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<strong>{{ t('common.endpointPending') }}</strong>
|
||||
<span>{{ pendingMessage }}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-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="!orders.length"
|
||||
class="empty-state"
|
||||
>
|
||||
{{ t('common.noData') }}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { EndpointPendingError, fetchReservationOrders } from '@/services/reservationService'
|
||||
import type { ReservationOrderListFilters, ReservationOrderListItem } from '@/types/reservation'
|
||||
|
||||
const { t } = useI18n()
|
||||
const loading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const pendingMessage = ref('')
|
||||
const orders = ref<ReservationOrderListItem[]>([])
|
||||
const filters = reactive<ReservationOrderListFilters>({
|
||||
keyword: '',
|
||||
order_status: '',
|
||||
page_num: 1,
|
||||
page_size: 20,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
void loadOrders()
|
||||
})
|
||||
|
||||
async function loadOrders(): Promise<void> {
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
pendingMessage.value = ''
|
||||
try {
|
||||
const result = await fetchReservationOrders({ ...filters })
|
||||
orders.value = result.items
|
||||
} catch (error) {
|
||||
orders.value = []
|
||||
if (error instanceof EndpointPendingError) {
|
||||
pendingMessage.value = t('orderList.endpointPending')
|
||||
return
|
||||
}
|
||||
errorMessage.value = error instanceof Error ? error.message : String(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.list-view {
|
||||
max-width: 1280px;
|
||||
}
|
||||
|
||||
.page-heading h1,
|
||||
.page-heading p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.page-heading h1 {
|
||||
color: var(--th-color-slate-900);
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.page-heading p {
|
||||
margin-top: 8px;
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.filter-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
padding: 18px 20px 0;
|
||||
}
|
||||
|
||||
.filter-grid label {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.filter-grid span {
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.filter-grid input,
|
||||
.filter-grid select {
|
||||
min-height: 38px;
|
||||
min-width: 0;
|
||||
border: 1px solid var(--th-color-slate-200);
|
||||
border-radius: var(--th-radius-sm);
|
||||
background: var(--th-color-white);
|
||||
color: var(--th-color-slate-900);
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
min-height: 220px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
gap: 8px;
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 13px;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-state strong {
|
||||
color: var(--th-color-slate-900);
|
||||
}
|
||||
|
||||
.empty-state--error {
|
||||
color: var(--th-color-danger);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.filter-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<div class="th-page conversation-view">
|
||||
<header class="page-heading">
|
||||
<div>
|
||||
<h1>{{ t('conversation.title') }}</h1>
|
||||
<p>{{ t('conversation.subtitle') }}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="th-section conversation-section">
|
||||
<div
|
||||
v-if="pendingMessage"
|
||||
class="empty-state"
|
||||
>
|
||||
<i
|
||||
class="pi pi-envelope"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<strong>{{ t('common.endpointPending') }}</strong>
|
||||
<span>{{ pendingMessage }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-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="!conversation"
|
||||
class="empty-state"
|
||||
>
|
||||
{{ t('common.noData') }}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { EndpointPendingError, fetchSourceMessageConversation } from '@/services/reservationService'
|
||||
import type { SourceMessageConversationResult } from '@/types/reservation'
|
||||
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
const loading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const pendingMessage = ref('')
|
||||
const conversation = ref<SourceMessageConversationResult | null>(null)
|
||||
const sourceMessageId = computed(() => String(route.params.sourceMessageId ?? ''))
|
||||
|
||||
watch(
|
||||
sourceMessageId,
|
||||
(nextSourceMessageId) => {
|
||||
void loadConversation(nextSourceMessageId)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
async function loadConversation(nextSourceMessageId: string): Promise<void> {
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
pendingMessage.value = ''
|
||||
conversation.value = null
|
||||
try {
|
||||
conversation.value = await fetchSourceMessageConversation(nextSourceMessageId)
|
||||
} catch (error) {
|
||||
if (error instanceof EndpointPendingError) {
|
||||
pendingMessage.value = t('conversation.endpointPending')
|
||||
return
|
||||
}
|
||||
errorMessage.value = error instanceof Error ? error.message : String(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.conversation-view {
|
||||
max-width: 1180px;
|
||||
}
|
||||
|
||||
.page-heading h1,
|
||||
.page-heading p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.page-heading h1 {
|
||||
color: var(--th-color-slate-900);
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.page-heading p {
|
||||
margin-top: 8px;
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.conversation-section {
|
||||
min-height: 360px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
min-height: 320px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
gap: 8px;
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 13px;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-state strong {
|
||||
color: var(--th-color-slate-900);
|
||||
}
|
||||
|
||||
.empty-state--error {
|
||||
color: var(--th-color-danger);
|
||||
}
|
||||
</style>
|
||||
21
client/src/views/reservation/ReservationTaskDetailView.vue
Normal file
21
client/src/views/reservation/ReservationTaskDetailView.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div class="th-page task-detail-view">
|
||||
<ReservationTaskDetailPanel :task-id="taskId" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import ReservationTaskDetailPanel from '@/components/reservation/ReservationTaskDetailPanel.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const taskId = computed(() => String(route.params.taskId ?? ''))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.task-detail-view {
|
||||
max-width: 1180px;
|
||||
}
|
||||
</style>
|
||||
363
client/src/views/reservation/ReservationTaskListView.vue
Normal file
363
client/src/views/reservation/ReservationTaskListView.vue
Normal file
@@ -0,0 +1,363 @@
|
||||
<template>
|
||||
<div class="th-page task-list-view">
|
||||
<header class="page-heading">
|
||||
<div>
|
||||
<h1>{{ t('taskList.title') }}</h1>
|
||||
<p>{{ t('taskList.subtitle') }}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="th-section filter-section">
|
||||
<div class="th-section-header">
|
||||
<h2 class="th-section-title">
|
||||
{{ t('workbench.filters') }}
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="text-button"
|
||||
@click="resetFilters"
|
||||
>
|
||||
{{ t('workbench.reset') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="filter-grid">
|
||||
<label>
|
||||
<span>{{ t('taskList.keyword') }}</span>
|
||||
<input
|
||||
v-model="filters.keyword"
|
||||
type="search"
|
||||
placeholder="GRP-001 / Booking"
|
||||
>
|
||||
</label>
|
||||
<label>
|
||||
<span>{{ t('taskList.taskType') }}</span>
|
||||
<select v-model="filters.task_type">
|
||||
<option value="">All</option>
|
||||
<option value="NEW_BOOKING">{{ t('cardType.NEW_BOOKING') }}</option>
|
||||
<option value="UPDATE_BOOKING">{{ t('cardType.UPDATE_BOOKING') }}</option>
|
||||
<option value="CANCEL_BOOKING">{{ t('cardType.CANCEL_BOOKING') }}</option>
|
||||
<option value="MANUAL_REVIEW">{{ t('cardType.MANUAL_REVIEW') }}</option>
|
||||
<option value="INFORMATIONAL_MESSAGE">{{ t('cardType.MESSAGE_NOTIFICATION') }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>{{ t('workbench.taskStatus') }}</span>
|
||||
<select v-model="filters.task_status">
|
||||
<option value="">All</option>
|
||||
<option value="PENDING_CONFIRM">{{ t('status.PENDING_CONFIRM') }}</option>
|
||||
<option value="READY">{{ t('status.READY') }}</option>
|
||||
<option value="BLOCKED">{{ t('status.BLOCKED') }}</option>
|
||||
<option value="COMPLETED">{{ t('status.COMPLETED') }}</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="th-section table-section">
|
||||
<div class="th-section-header">
|
||||
<div>
|
||||
<h2 class="th-section-title">
|
||||
{{ t('taskList.tableTitle') }}
|
||||
</h2>
|
||||
<p class="th-muted">
|
||||
{{ t('workbench.totalPrefix') }} {{ tasks.length }} {{ t('taskList.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="!tasks.length"
|
||||
class="empty-state"
|
||||
>
|
||||
{{ t('common.noData') }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="table-wrap"
|
||||
>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ t('taskList.columns.task') }}</th>
|
||||
<th>{{ t('taskList.columns.order') }}</th>
|
||||
<th>{{ t('taskList.columns.card') }}</th>
|
||||
<th>{{ t('taskList.columns.status') }}</th>
|
||||
<th>{{ t('taskList.columns.source') }}</th>
|
||||
<th>{{ t('taskList.columns.queue') }}</th>
|
||||
<th>{{ t('workbench.columns.operation') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="task in tasks"
|
||||
:key="task.task_id"
|
||||
>
|
||||
<td>
|
||||
<strong class="th-code">#{{ task.task_id }}</strong>
|
||||
<small>{{ task.task_type }} / {{ task.task_subtype ?? '-' }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<strong>{{ task.display_order_key ?? task.temporary_order_no ?? task.order_id }}</strong>
|
||||
<small class="th-code">{{ task.order_id }}</small>
|
||||
</td>
|
||||
<td>{{ task.card_name ?? task.task_type }}</td>
|
||||
<td><ReservationStatusBadge :status="task.task_status" /></td>
|
||||
<td>
|
||||
<span>{{ task.source_subject ?? t('taskList.noSourceSubject') }}</span>
|
||||
<small>{{ task.source_sender_summary ?? task.source_message_id ?? '-' }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ task.queue_sequence ?? '-' }}</span>
|
||||
<small>{{ task.can_process ? t('taskList.canProcess') : task.readonly_reason_code ?? '-' }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<div class="row-actions">
|
||||
<RouterLink
|
||||
class="row-action"
|
||||
:to="`/reservation/tasks/${task.task_id}`"
|
||||
>
|
||||
{{ t('taskList.viewTask') }}
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
class="row-action"
|
||||
:to="`/reservation/orders/${task.order_id}`"
|
||||
>
|
||||
{{ t('taskList.viewOrder') }}
|
||||
</RouterLink>
|
||||
<span
|
||||
v-if="!task.source_message_id || !task.external_conversation_id"
|
||||
class="pending-link"
|
||||
>
|
||||
{{ t('common.endpointMissing') }}
|
||||
</span>
|
||||
<RouterLink
|
||||
v-else
|
||||
class="row-action"
|
||||
:to="`/reservation/source-messages/${task.source_message_id}/conversation`"
|
||||
>
|
||||
{{ t('taskList.viewConversation') }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref, watch } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ReservationStatusBadge from '@/components/reservation/ReservationStatusBadge.vue'
|
||||
import { fetchReservationTaskList } from '@/services/reservationService'
|
||||
import type { ReservationTaskListFilters, ReservationTaskListItem } from '@/types/reservation'
|
||||
|
||||
const { t } = useI18n()
|
||||
const loading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const tasks = ref<ReservationTaskListItem[]>([])
|
||||
let taskListRequestSequence = 0
|
||||
const filters = reactive<ReservationTaskListFilters>({
|
||||
keyword: '',
|
||||
task_type: '',
|
||||
task_status: '',
|
||||
page_num: 1,
|
||||
page_size: 20,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
void loadTasks()
|
||||
})
|
||||
|
||||
watch(filters, () => {
|
||||
void loadTasks()
|
||||
})
|
||||
|
||||
async function loadTasks(): Promise<void> {
|
||||
const requestSequence = ++taskListRequestSequence
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
try {
|
||||
const result = await fetchReservationTaskList({ ...filters })
|
||||
if (requestSequence !== taskListRequestSequence) {
|
||||
return
|
||||
}
|
||||
tasks.value = result.items
|
||||
} catch (error) {
|
||||
if (requestSequence !== taskListRequestSequence) {
|
||||
return
|
||||
}
|
||||
tasks.value = []
|
||||
errorMessage.value = error instanceof Error ? error.message : String(error)
|
||||
} finally {
|
||||
if (requestSequence === taskListRequestSequence) {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resetFilters(): void {
|
||||
filters.keyword = ''
|
||||
filters.task_type = ''
|
||||
filters.task_status = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.task-list-view {
|
||||
max-width: 1320px;
|
||||
}
|
||||
|
||||
.page-heading h1,
|
||||
.page-heading p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.page-heading h1 {
|
||||
color: var(--th-color-slate-900);
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.page-heading p {
|
||||
margin-top: 8px;
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.filter-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
padding: 18px 20px 0;
|
||||
}
|
||||
|
||||
.filter-grid label {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.filter-grid span {
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.filter-grid input,
|
||||
.filter-grid select {
|
||||
min-height: 38px;
|
||||
min-width: 0;
|
||||
border: 1px solid var(--th-color-slate-200);
|
||||
border-radius: var(--th-radius-sm);
|
||||
background: var(--th-color-white);
|
||||
color: var(--th-color-slate-900);
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.text-button,
|
||||
.row-action {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--th-color-blue-600);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
min-height: 220px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 13px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.empty-state--error {
|
||||
color: var(--th-color-danger);
|
||||
}
|
||||
|
||||
.table-section {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
overflow-x: auto;
|
||||
padding: 14px 20px 20px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
min-width: 980px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
border-bottom: 1px solid var(--th-color-slate-200);
|
||||
padding: 14px 10px;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
th {
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
td {
|
||||
color: var(--th-color-slate-900);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
td strong,
|
||||
td small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
td small {
|
||||
margin-top: 4px;
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.row-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pending-link {
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.filter-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user