适配 M002 V3 前端任务流与同卡复核
This commit is contained in:
1 parent
fd027f124c
commit
155db63028
17 files changed
+1593
-85
No files matched your search
@@ -0,0 +1,352 @@
|
||||
<template>
|
||||
<section class="manual-resolution">
|
||||
<header>
|
||||
<div>
|
||||
<h2>{{ t('task.manualResolution.title') }}</h2>
|
||||
<p>{{ t('task.manualResolution.description') }}</p>
|
||||
</div>
|
||||
<span class="manual-resolution__status">
|
||||
{{ t('task.reviewStatus') }}: {{ reviewStatusLabel }}
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div
|
||||
v-if="isResolved"
|
||||
class="manual-resolution__resolved"
|
||||
>
|
||||
<strong>{{ t('task.manualResolution.resolvedTitle') }}</strong>
|
||||
<pre>{{ formatJson(reviewResolution) }}</pre>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div
|
||||
v-if="manualReview"
|
||||
class="manual-resolution__meta"
|
||||
>
|
||||
<dl>
|
||||
<div v-if="typeof manualReview.reason_code === 'string'">
|
||||
<dt>{{ t('task.manualResolution.reasonCode') }}</dt>
|
||||
<dd class="th-code">
|
||||
{{ manualReview.reason_code }}
|
||||
</dd>
|
||||
</div>
|
||||
<div v-if="typeof manualReview.review_instruction === 'string'">
|
||||
<dt>{{ t('task.manualResolution.reviewInstruction') }}</dt>
|
||||
<dd>{{ manualReview.review_instruction }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="manual-resolution__form">
|
||||
<label>
|
||||
<span>{{ t('task.manualResolution.confirmedOrderId') }}</span>
|
||||
<input
|
||||
v-model="confirmedOrderId"
|
||||
name="manual_resolution_confirmed_order_id"
|
||||
type="text"
|
||||
:disabled="busy"
|
||||
:placeholder="taskOrderId"
|
||||
>
|
||||
</label>
|
||||
|
||||
<div
|
||||
v-if="supportedMissingFields.length"
|
||||
class="manual-resolution__fields"
|
||||
>
|
||||
<label
|
||||
v-for="(item, index) in supportedMissingFields"
|
||||
:key="item.pointer"
|
||||
>
|
||||
<span>
|
||||
{{ item.label }}
|
||||
<small class="th-code">{{ item.pointer }}</small>
|
||||
</span>
|
||||
<input
|
||||
v-model="fieldOverrideInputs[item.pointer]"
|
||||
:name="`manual_resolution_field_override_${index}`"
|
||||
type="text"
|
||||
:disabled="busy"
|
||||
:placeholder="t('task.manualResolution.fieldValuePlaceholder')"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
<p
|
||||
v-else
|
||||
class="manual-resolution__empty"
|
||||
>
|
||||
{{ t('task.manualResolution.noSupportedMissingFields') }}
|
||||
</p>
|
||||
|
||||
<label>
|
||||
<span>{{ t('task.manualResolution.reason') }}</span>
|
||||
<textarea
|
||||
v-model="reason"
|
||||
name="manual_resolution_reason"
|
||||
rows="3"
|
||||
:disabled="busy"
|
||||
:placeholder="t('task.manualResolution.reasonPlaceholder')"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="formErrorMessage"
|
||||
class="manual-resolution__error"
|
||||
>
|
||||
{{ formErrorMessage }}
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<button
|
||||
type="button"
|
||||
class="primary-button"
|
||||
:disabled="busy || !canSubmit"
|
||||
@click="submitResolution"
|
||||
>
|
||||
<i
|
||||
class="pi pi-check-circle"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{{ t('task.manualResolution.submit') }}
|
||||
</button>
|
||||
</footer>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type {
|
||||
ReservationManualReviewResolutionRequest,
|
||||
ReservationRecord,
|
||||
ReservationTaskFieldResult,
|
||||
} from '@/types/reservation'
|
||||
import { normalizeStableCode } from '@/utils/reservationDisplay'
|
||||
|
||||
const supportedFieldPointers = [
|
||||
'/extracted_fields/pms_room_type_code',
|
||||
'/extracted_fields/room_items/0/pms_room_type_code',
|
||||
] as const
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
taskOrderId: string
|
||||
reviewStatus?: string | null
|
||||
reviewResolution?: ReservationRecord | null
|
||||
manualReview?: ReservationRecord | null
|
||||
fields?: ReservationTaskFieldResult[]
|
||||
fieldValues?: ReservationRecord
|
||||
busy?: boolean
|
||||
}>(),
|
||||
{
|
||||
reviewStatus: null,
|
||||
reviewResolution: null,
|
||||
manualReview: null,
|
||||
fields: () => [],
|
||||
fieldValues: () => ({}),
|
||||
busy: false,
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
submit: [request: ReservationManualReviewResolutionRequest]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const confirmedOrderId = ref(props.taskOrderId)
|
||||
const reason = ref('')
|
||||
const formErrorMessage = ref('')
|
||||
const fieldOverrideInputs = ref<Record<string, string>>({})
|
||||
const isResolved = computed(() => normalizeStableCode(props.reviewStatus) === 'RESOLVED')
|
||||
const reviewStatusLabel = computed(() => props.reviewStatus || t('task.manualResolution.pendingStatus'))
|
||||
const supportedMissingFields = computed(() =>
|
||||
extractMissingFields(props.manualReview)
|
||||
.filter((pointer) => supportedFieldPointers.includes(pointer as (typeof supportedFieldPointers)[number]))
|
||||
.map((pointer) => ({
|
||||
pointer,
|
||||
label: findFieldLabel(pointer),
|
||||
})),
|
||||
)
|
||||
const canSubmit = computed(() => {
|
||||
if (!supportedMissingFields.value.length || !confirmedOrderId.value.trim()) {
|
||||
return false
|
||||
}
|
||||
return supportedMissingFields.value.every((item) => String(fieldOverrideInputs.value[item.pointer] ?? '').trim())
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.taskOrderId,
|
||||
(nextOrderId) => {
|
||||
confirmedOrderId.value = nextOrderId
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
supportedMissingFields,
|
||||
(items) => {
|
||||
const nextInputs: Record<string, string> = {}
|
||||
items.forEach((item) => {
|
||||
nextInputs[item.pointer] = fieldOverrideInputs.value[item.pointer] || readCurrentFieldValue(item.pointer)
|
||||
})
|
||||
fieldOverrideInputs.value = nextInputs
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function submitResolution(): void {
|
||||
formErrorMessage.value = ''
|
||||
if (!canSubmit.value) {
|
||||
formErrorMessage.value = t('task.manualResolution.fieldRequired')
|
||||
return
|
||||
}
|
||||
|
||||
emit('submit', {
|
||||
confirmed_order_id: confirmedOrderId.value.trim(),
|
||||
reason: reason.value.trim() || undefined,
|
||||
field_overrides: supportedMissingFields.value.map((item) => ({
|
||||
field_pointer: item.pointer,
|
||||
value: String(fieldOverrideInputs.value[item.pointer] ?? '').trim(),
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
function extractMissingFields(manualReview: ReservationRecord | null | undefined): string[] {
|
||||
const missingFields = manualReview?.missing_fields
|
||||
if (!Array.isArray(missingFields)) {
|
||||
return []
|
||||
}
|
||||
return missingFields.filter((item): item is string => typeof item === 'string' && item.startsWith('/'))
|
||||
}
|
||||
|
||||
function findFieldLabel(pointer: string): string {
|
||||
const fieldPath = fieldPointerToFieldPath(pointer)
|
||||
return props.fields.find((field) => field.field_path === fieldPath)?.display_name ?? fieldPath
|
||||
}
|
||||
|
||||
function readCurrentFieldValue(pointer: string): string {
|
||||
const fieldPath = fieldPointerToFieldPath(pointer)
|
||||
const value = props.fieldValues[fieldPath] ?? props.fields.find((field) => field.field_path === fieldPath)?.value
|
||||
return value === undefined || value === null ? '' : String(value)
|
||||
}
|
||||
|
||||
function fieldPointerToFieldPath(pointer: string): string {
|
||||
if (pointer === '/extracted_fields/room_items/0/pms_room_type_code') {
|
||||
return 'extracted_fields.pms_room_type_code'
|
||||
}
|
||||
return pointer.replace(/^\//, '').replace(/\//g, '.')
|
||||
}
|
||||
|
||||
function formatJson(value: unknown): string {
|
||||
return JSON.stringify(value ?? {}, null, 2)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.manual-resolution {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
border: 1px solid var(--th-color-warning);
|
||||
border-radius: var(--th-radius-lg);
|
||||
background: color-mix(in srgb, var(--th-color-warning) 8%, var(--th-color-white));
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.manual-resolution header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.manual-resolution h2,
|
||||
.manual-resolution p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.manual-resolution p {
|
||||
margin-top: 4px;
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.manual-resolution__status {
|
||||
align-self: flex-start;
|
||||
border-radius: 999px;
|
||||
background: var(--th-color-white);
|
||||
color: var(--th-color-warning);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
padding: 5px 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.manual-resolution__meta dl,
|
||||
.manual-resolution__form,
|
||||
.manual-resolution__fields {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.manual-resolution__meta dl {
|
||||
margin: 0;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
}
|
||||
|
||||
.manual-resolution__meta dt {
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.manual-resolution__meta dd {
|
||||
margin: 3px 0 0;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.manual-resolution label {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
color: var(--th-color-slate-600);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.manual-resolution label small {
|
||||
display: block;
|
||||
margin-top: 3px;
|
||||
color: var(--th-color-slate-400);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.manual-resolution input,
|
||||
.manual-resolution textarea {
|
||||
border: 1px solid var(--th-color-slate-200);
|
||||
border-radius: var(--th-radius-md);
|
||||
background: var(--th-color-white);
|
||||
color: var(--th-color-slate-900);
|
||||
font: inherit;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.manual-resolution__error,
|
||||
.manual-resolution__empty {
|
||||
color: var(--th-color-danger);
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.manual-resolution footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.manual-resolution__resolved pre {
|
||||
margin: 8px 0 0;
|
||||
max-height: 180px;
|
||||
overflow: auto;
|
||||
border-radius: var(--th-radius-md);
|
||||
background: var(--th-color-slate-900);
|
||||
color: var(--th-color-white);
|
||||
padding: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -17,7 +17,7 @@
|
||||
<header class="task-detail-header">
|
||||
<div>
|
||||
<p class="task-eyebrow">
|
||||
{{ formatReservationTaskCard(t, detail.task_card_type) }}
|
||||
{{ formatReservationTaskRouteSummary(t, detail) }}
|
||||
</p>
|
||||
<h1>{{ t('task.title') }} {{ detail.task_id }}</h1>
|
||||
<span class="task-contract">
|
||||
@@ -25,7 +25,7 @@
|
||||
</span>
|
||||
<div class="task-links">
|
||||
<RouterLink
|
||||
v-if="!isSourceMessageOnlyDetail"
|
||||
v-if="!isReadonlyDiagnosticDetail"
|
||||
class="task-link"
|
||||
:to="`/reservation/orders/${detail.order_id}`"
|
||||
>
|
||||
@@ -75,6 +75,52 @@
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="task-route-info">
|
||||
<h2>{{ t('task.routeInfo') }}</h2>
|
||||
<dl>
|
||||
<div>
|
||||
<dt>{{ t('task.systemTaskType') }}</dt>
|
||||
<dd>
|
||||
{{ formatReservationTaskCard(t, detail.system_task_type) }}
|
||||
<small class="th-code">{{ detail.system_task_type }}</small>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ t('task.taskCardType') }}</dt>
|
||||
<dd>
|
||||
{{ formatReservationTaskCard(t, detail.task_card_type) }}
|
||||
<small class="th-code">{{ detail.task_card_type }}</small>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ t('task.resultType') }}</dt>
|
||||
<dd>{{ detail.result_type ? formatReservationResultType(t, detail.result_type) : '-' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ t('task.aiTaskType') }}</dt>
|
||||
<dd>{{ detail.ai_task_type || '-' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ t('task.taskSubtype') }}</dt>
|
||||
<dd>{{ detail.task_subtype ? formatReservationRouteCode(t, detail.task_subtype) : '-' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ t('task.routeCode') }}</dt>
|
||||
<dd class="th-code">
|
||||
{{ detail.route_code || '-' }}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ t('task.systemProcessCategory') }}</dt>
|
||||
<dd>{{ detail.system_process_category ? formatReservationSystemProcessCategory(t, detail.system_process_category) : '-' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ t('task.reviewStatus') }}</dt>
|
||||
<dd>{{ detail.review_status || '-' }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-if="isSourceMessageOnlyDetail"
|
||||
class="source-only-result"
|
||||
@@ -110,6 +156,24 @@
|
||||
<pre>{{ sourceMessageOnlyResult.raw_answer || '-' }}</pre>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="source-only-result__raw">
|
||||
<dt>{{ t('task.agentAssessment') }}</dt>
|
||||
<dd>
|
||||
<pre>{{ formatJson(sourceMessageOnlyResult.agent_assessment) }}</pre>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="source-only-result__raw">
|
||||
<dt>{{ t('task.sourceNotification') }}</dt>
|
||||
<dd>
|
||||
<pre>{{ formatJson(sourceMessageOnlyResult.notification) }}</pre>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="source-only-result__raw">
|
||||
<dt>{{ t('task.manualReviewData') }}</dt>
|
||||
<dd>
|
||||
<pre>{{ formatJson(sourceMessageOnlyResult.manual_review) }}</pre>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<p
|
||||
v-else
|
||||
@@ -119,6 +183,43 @@
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-if="manualReviewRecord"
|
||||
class="task-ai-diagnostics"
|
||||
>
|
||||
<h2>{{ t('task.manualReviewData') }}</h2>
|
||||
<pre>{{ formatJson(manualReviewRecord) }}</pre>
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-if="adapterContractErrors.length"
|
||||
class="task-ai-diagnostics task-ai-diagnostics--error"
|
||||
>
|
||||
<h2>{{ t('task.adapterContractErrors') }}</h2>
|
||||
<article
|
||||
v-for="item in adapterContractErrors"
|
||||
:key="item.transition_id || `${item.route_code}-${item.array_index}`"
|
||||
>
|
||||
<strong>{{ item.adapter_error_code || item.route_code || t('resultType.ADAPTER_CONTRACT_ERROR') }}</strong>
|
||||
<p>{{ item.adapter_error_message || '-' }}</p>
|
||||
<pre>{{ formatJson(item.payload_fragment) }}</pre>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-if="unhandledIntents.length"
|
||||
class="task-ai-diagnostics"
|
||||
>
|
||||
<h2>{{ t('task.unhandledIntents') }}</h2>
|
||||
<article
|
||||
v-for="item in unhandledIntents"
|
||||
:key="item.transition_id || `${item.route_code}-${item.array_index}`"
|
||||
>
|
||||
<strong>{{ item.route_code || item.task_subtype || t('resultType.UNHANDLED_CURRENT_INTENT') }}</strong>
|
||||
<pre>{{ formatJson(item.payload_fragment) }}</pre>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<div
|
||||
v-if="detail.availability.blocked"
|
||||
class="task-alert"
|
||||
@@ -149,8 +250,20 @@
|
||||
|
||||
<div class="task-detail-body">
|
||||
<div class="task-card-main">
|
||||
<ReservationManualReviewResolutionPanel
|
||||
v-if="isTypeKnownManualReviewDetail"
|
||||
:task-order-id="detail.order_id"
|
||||
:review-status="detail.review_status"
|
||||
:review-resolution="detail.review_resolution"
|
||||
:manual-review="detail.manual_review"
|
||||
:fields="detail.fields"
|
||||
:field-values="fieldValues"
|
||||
:busy="mutationBusy !== null"
|
||||
@submit="submitManualReviewResolution"
|
||||
/>
|
||||
|
||||
<ReservationManualReviewConversionPanel
|
||||
v-if="isManualReviewTask && !isSourceMessageOnlyDetail"
|
||||
v-if="isFallbackManualReviewDetail && !isTypeKnownManualReviewDetail && !isReadonlyDiagnosticDetail"
|
||||
:busy="mutationBusy !== null"
|
||||
@submit="submitManualReviewConversion"
|
||||
/>
|
||||
@@ -158,12 +271,12 @@
|
||||
<ReservationTaskFieldRenderer
|
||||
v-model="fieldValues"
|
||||
:fields="detail.fields"
|
||||
:read-only="detail.availability.read_only"
|
||||
:read-only="isFieldRendererReadOnly"
|
||||
:validation-errors="validationErrorMessages"
|
||||
/>
|
||||
|
||||
<footer
|
||||
v-if="canEditTask || canConfirmTask"
|
||||
v-if="canSave || canConfirm"
|
||||
class="task-actions"
|
||||
>
|
||||
<button
|
||||
@@ -197,10 +310,10 @@
|
||||
|
||||
<aside class="task-side-stack">
|
||||
<ReservationOperaPanel
|
||||
v-if="!isSourceMessageOnlyDetail"
|
||||
v-if="!isReadonlyDiagnosticDetail && !isPendingTypeKnownManualReviewDetail"
|
||||
:task-id="detail.task_id"
|
||||
:operations="detail.opera_operations"
|
||||
:executable="detail.availability.executable && canExecuteOpera"
|
||||
:executable="canExecuteOpera"
|
||||
:show-actions="canExecuteOpera"
|
||||
@operation-updated="replaceOperation"
|
||||
/>
|
||||
@@ -221,6 +334,7 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ReservationAuditTimeline from '@/components/reservation/ReservationAuditTimeline.vue'
|
||||
import ReservationManualReviewConversionPanel from '@/components/reservation/ReservationManualReviewConversionPanel.vue'
|
||||
import ReservationManualReviewResolutionPanel from '@/components/reservation/ReservationManualReviewResolutionPanel.vue'
|
||||
import ReservationOperaPanel from '@/components/reservation/ReservationOperaPanel.vue'
|
||||
import ReservationStatusBadge from '@/components/reservation/ReservationStatusBadge.vue'
|
||||
import ReservationTaskFieldRenderer from '@/components/reservation/ReservationTaskFieldRenderer.vue'
|
||||
@@ -229,10 +343,12 @@ import {
|
||||
confirmReservationTask,
|
||||
fetchReservationTaskAudits,
|
||||
fetchReservationTaskDetail,
|
||||
resolveReservationManualReviewTask,
|
||||
saveReservationTaskDraft,
|
||||
} from '@/services/reservationService'
|
||||
import type {
|
||||
ReservationManualReviewConversionRequest,
|
||||
ReservationManualReviewResolutionRequest,
|
||||
ReservationOperaOperationResult,
|
||||
ReservationRecord,
|
||||
ReservationTaskAuditLogResult,
|
||||
@@ -245,9 +361,17 @@ import {
|
||||
type ReservationFieldValidationErrors,
|
||||
} from '@/utils/reservationFieldRules'
|
||||
import {
|
||||
formatReservationResultType,
|
||||
formatReservationRouteCode,
|
||||
formatReservationMaybeStableReason,
|
||||
formatReservationSystemProcessCategory,
|
||||
formatReservationTaskCard,
|
||||
isReservationSourceMessageOnlyTask,
|
||||
formatReservationTaskRouteSummary,
|
||||
isReservationFallbackManualReviewTask,
|
||||
isReservationReadonlyDiagnosticTask,
|
||||
isReservationSourceMessageNotificationTask,
|
||||
isReservationTypeKnownManualReviewTask,
|
||||
normalizeStableCode,
|
||||
} from '@/utils/reservationDisplay'
|
||||
import { formatReservationDateTime } from '@/utils/reservationFormat'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
@@ -267,23 +391,56 @@ const audits = ref<ReservationTaskAuditLogResult[]>([])
|
||||
const fieldValues = ref<ReservationRecord>({})
|
||||
const validationErrors = ref<ReservationFieldValidationErrors>({})
|
||||
const validationSummaryActive = ref(false)
|
||||
const mutationBusy = ref<'save' | 'confirm' | 'manual-convert' | null>(null)
|
||||
const mutationBusy = ref<'save' | 'confirm' | 'manual-convert' | 'manual-resolve' | null>(null)
|
||||
|
||||
const isSourceMessageOnlyDetail = computed(() =>
|
||||
isReservationSourceMessageOnlyTask(detail.value?.system_task_type, detail.value?.task_card_type),
|
||||
isReservationSourceMessageNotificationTask(detail.value),
|
||||
)
|
||||
const isReadonlyDiagnosticDetail = computed(() => isReservationReadonlyDiagnosticTask(detail.value))
|
||||
const isTypeKnownManualReviewDetail = computed(() => isReservationTypeKnownManualReviewTask(detail.value))
|
||||
const isPendingTypeKnownManualReviewDetail = computed(
|
||||
() => isTypeKnownManualReviewDetail.value && normalizeStableCode(detail.value?.review_status) !== 'RESOLVED',
|
||||
)
|
||||
const isFallbackManualReviewDetail = computed(() => isReservationFallbackManualReviewTask(detail.value))
|
||||
const sourceMessageOnlyResult = computed(() => detail.value?.source_message_only_result ?? null)
|
||||
const canEditTask = computed(() => authStore.hasPermission('RESERVATION_TASK_EDIT') && !isSourceMessageOnlyDetail.value)
|
||||
const manualReviewRecord = computed(() => detail.value?.manual_review ?? sourceMessageOnlyResult.value?.manual_review ?? null)
|
||||
const adapterContractErrors = computed(() => detail.value?.adapter_contract_errors ?? [])
|
||||
const unhandledIntents = computed(() => detail.value?.unhandled_intents ?? [])
|
||||
const isFieldRendererReadOnly = computed(
|
||||
() =>
|
||||
Boolean(detail.value?.availability.read_only) ||
|
||||
Boolean(detail.value?.availability.blocked) ||
|
||||
isReadonlyDiagnosticDetail.value ||
|
||||
isPendingTypeKnownManualReviewDetail.value,
|
||||
)
|
||||
const canEditTask = computed(
|
||||
() =>
|
||||
authStore.hasPermission('RESERVATION_TASK_EDIT') &&
|
||||
!isReadonlyDiagnosticDetail.value &&
|
||||
!isPendingTypeKnownManualReviewDetail.value &&
|
||||
!detail.value?.availability.blocked &&
|
||||
!detail.value?.availability.read_only,
|
||||
)
|
||||
const canConfirmTask = computed(
|
||||
() => authStore.hasPermission('RESERVATION_TASK_CONFIRM') && !isSourceMessageOnlyDetail.value,
|
||||
() =>
|
||||
authStore.hasPermission('RESERVATION_TASK_CONFIRM') &&
|
||||
!isReadonlyDiagnosticDetail.value &&
|
||||
!isPendingTypeKnownManualReviewDetail.value &&
|
||||
!detail.value?.availability.blocked &&
|
||||
!detail.value?.availability.read_only,
|
||||
)
|
||||
const canExecuteOpera = computed(
|
||||
() => authStore.hasPermission('RESERVATION_OPERA_SIM_EXECUTE') && !isSourceMessageOnlyDetail.value,
|
||||
() =>
|
||||
authStore.hasPermission('RESERVATION_OPERA_SIM_EXECUTE') &&
|
||||
!isReadonlyDiagnosticDetail.value &&
|
||||
!isPendingTypeKnownManualReviewDetail.value &&
|
||||
!detail.value?.availability.blocked &&
|
||||
!detail.value?.availability.read_only &&
|
||||
Boolean(detail.value?.availability.executable),
|
||||
)
|
||||
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))
|
||||
|
||||
watch(
|
||||
@@ -368,6 +525,33 @@ async function submitManualReviewConversion(request: ReservationManualReviewConv
|
||||
}
|
||||
}
|
||||
|
||||
async function submitManualReviewResolution(request: ReservationManualReviewResolutionRequest): Promise<void> {
|
||||
if (!detail.value) {
|
||||
return
|
||||
}
|
||||
actionErrorMessage.value = ''
|
||||
notice.value = ''
|
||||
mutationBusy.value = 'manual-resolve'
|
||||
try {
|
||||
await resolveReservationManualReviewTask(detail.value.task_id, request)
|
||||
const [nextDetail, nextAudits] = await Promise.all([
|
||||
fetchReservationTaskDetail(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.manualResolution.succeeded')
|
||||
} catch (error) {
|
||||
actionErrorMessage.value = error instanceof Error ? error.message : String(error)
|
||||
} finally {
|
||||
mutationBusy.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function mutateTaskPayload(action: 'save' | 'confirm'): Promise<void> {
|
||||
if (!detail.value) {
|
||||
return
|
||||
@@ -415,22 +599,6 @@ function applyTaskData(nextDetail: ReservationTaskDetailResult, nextAudits: Rese
|
||||
validationSummaryActive.value = false
|
||||
}
|
||||
|
||||
function taskHasManualReviewCode(taskDetail: ReservationTaskDetailResult): boolean {
|
||||
const taskCodes = [
|
||||
taskDetail.system_task_type,
|
||||
taskDetail.task_card_type,
|
||||
...taskDetail.fields.flatMap((field) => [field.result_type, field.task_type, field.task_subtype]),
|
||||
]
|
||||
return taskCodes.some((code) => {
|
||||
const normalizedCode = normalizeStableCode(code)
|
||||
return ['MANUAL_REVIEW', 'FALLBACK', 'FALLBACK_REVIEW'].includes(normalizedCode)
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeStableCode(code: string | null | undefined): string {
|
||||
return code?.trim().replace(/[\s-]+/g, '_').toUpperCase() ?? ''
|
||||
}
|
||||
|
||||
function validateBeforeConfirm(taskDetail: ReservationTaskDetailResult): boolean {
|
||||
const nextErrors = validateReservationFieldValues(taskDetail.fields, fieldValues.value)
|
||||
validationErrors.value = nextErrors
|
||||
@@ -483,6 +651,10 @@ function replaceOperation(operation: ReservationOperaOperationResult): void {
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
function formatJson(value: unknown): string {
|
||||
return JSON.stringify(value ?? {}, null, 2)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -553,7 +725,9 @@ function replaceOperation(operation: ReservationOperaOperationResult): void {
|
||||
color: var(--th-color-slate-500);
|
||||
}
|
||||
|
||||
.task-evidence {
|
||||
.task-evidence,
|
||||
.task-route-info,
|
||||
.task-ai-diagnostics {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
border: 1px solid var(--th-color-slate-200);
|
||||
@@ -561,6 +735,18 @@ function replaceOperation(operation: ReservationOperaOperationResult): void {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.task-route-info {
|
||||
background: var(--th-color-slate-50);
|
||||
}
|
||||
|
||||
.task-ai-diagnostics {
|
||||
background: var(--th-color-white);
|
||||
}
|
||||
|
||||
.task-ai-diagnostics--error {
|
||||
border-color: var(--th-color-danger);
|
||||
}
|
||||
|
||||
.source-only-result {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
@@ -572,6 +758,10 @@ function replaceOperation(operation: ReservationOperaOperationResult): void {
|
||||
|
||||
.task-evidence h2,
|
||||
.task-evidence dl,
|
||||
.task-route-info h2,
|
||||
.task-route-info dl,
|
||||
.task-ai-diagnostics h2,
|
||||
.task-ai-diagnostics pre,
|
||||
.source-only-result h2,
|
||||
.source-only-result p,
|
||||
.source-only-result dl {
|
||||
@@ -579,6 +769,8 @@ function replaceOperation(operation: ReservationOperaOperationResult): void {
|
||||
}
|
||||
|
||||
.task-evidence h2,
|
||||
.task-route-info h2,
|
||||
.task-ai-diagnostics h2,
|
||||
.source-only-result h2 {
|
||||
color: var(--th-color-slate-900);
|
||||
font-size: 15px;
|
||||
@@ -596,7 +788,8 @@ function replaceOperation(operation: ReservationOperaOperationResult): void {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.task-evidence dl {
|
||||
.task-evidence dl,
|
||||
.task-route-info dl {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
@@ -608,13 +801,15 @@ function replaceOperation(operation: ReservationOperaOperationResult): void {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.task-evidence dt {
|
||||
.task-evidence dt,
|
||||
.task-route-info dt {
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.task-evidence dd,
|
||||
.task-route-info dd,
|
||||
.source-only-result dd {
|
||||
margin: 5px 0 0;
|
||||
color: var(--th-color-slate-900);
|
||||
@@ -626,7 +821,8 @@ function replaceOperation(operation: ReservationOperaOperationResult): void {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.source-only-result pre {
|
||||
.source-only-result pre,
|
||||
.task-ai-diagnostics pre {
|
||||
max-height: 180px;
|
||||
overflow: auto;
|
||||
border: 1px solid var(--th-color-slate-200);
|
||||
@@ -636,6 +832,24 @@ function replaceOperation(operation: ReservationOperaOperationResult): void {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.task-ai-diagnostics article {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
border-top: 1px solid var(--th-color-slate-200);
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.task-ai-diagnostics article:first-of-type {
|
||||
border-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.task-ai-diagnostics article p {
|
||||
margin: 0;
|
||||
color: var(--th-color-slate-600);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.task-alert,
|
||||
.task-notice {
|
||||
display: flex;
|
||||
@@ -715,13 +929,15 @@ function replaceOperation(operation: ReservationOperaOperationResult): void {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.task-evidence dl {
|
||||
.task-evidence dl,
|
||||
.task-route-info dl {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.task-evidence dl,
|
||||
.task-route-info dl,
|
||||
.source-only-result dl {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<span class="task-row__index">{{ task.queue_sequence ?? '-' }}</span>
|
||||
<span class="task-row__main">
|
||||
<strong>{{ formatReservationTaskCard(t, task.task_type, task.card_name) }}</strong>
|
||||
<small>{{ formatReservationTaskTypeSummary(t, task.task_type, task.task_subtype) }}</small>
|
||||
<small>{{ formatReservationTaskRouteSummary(t, task) }}</small>
|
||||
<em :class="{ 'task-row__reason--blocked': !task.can_process }">
|
||||
{{ task.can_process ? t('taskList.canProcess') : t('taskList.cannotProcess') }}
|
||||
<template v-if="!task.can_process && task.readonly_reason_code">
|
||||
@@ -52,7 +52,7 @@ import type { ReservationOrderTaskTimelineItem } from '@/types/reservation'
|
||||
import {
|
||||
formatReservationReadonlyReason,
|
||||
formatReservationTaskCard,
|
||||
formatReservationTaskTypeSummary,
|
||||
formatReservationTaskRouteSummary,
|
||||
} from '@/utils/reservationDisplay'
|
||||
|
||||
defineProps<{
|
||||
|
||||
Reference in new issue
Block a user