再次提交一下代码
This commit is contained in:
@@ -49,21 +49,27 @@
|
||||
<ReservationStatusBadge :status="detail.task_status" />
|
||||
</header>
|
||||
|
||||
<section class="task-evidence">
|
||||
<h2>{{ t('task.evidence') }}</h2>
|
||||
<dl>
|
||||
<section class="task-source-email">
|
||||
<header>
|
||||
<div>
|
||||
<dt>{{ t('task.sourceMessageId') }}</dt>
|
||||
<dd class="th-code">
|
||||
{{ detail.source_message_id || '-' }}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ t('task.sourceSubject') }}</dt>
|
||||
<dd>
|
||||
{{ detail.source_subject ?? t('task.sourceSummaryPending') }}
|
||||
</dd>
|
||||
<p>{{ t('task.sourceEmailPreview') }}</p>
|
||||
<h2>{{ detail.source_subject ?? t('task.sourceSummaryPending') }}</h2>
|
||||
</div>
|
||||
<span
|
||||
v-if="!detail.source_message_id"
|
||||
class="task-link task-link--disabled"
|
||||
>
|
||||
{{ t('task.sourceEmailFullConversation') }} · {{ t('common.endpointMissing') }}
|
||||
</span>
|
||||
<RouterLink
|
||||
v-else
|
||||
class="task-link"
|
||||
:to="`/reservation/source-messages/${detail.source_message_id}/conversation`"
|
||||
>
|
||||
{{ t('task.sourceEmailFullConversation') }}
|
||||
</RouterLink>
|
||||
</header>
|
||||
<dl class="task-source-email__meta">
|
||||
<div>
|
||||
<dt>{{ t('task.sourceSender') }}</dt>
|
||||
<dd>{{ detail.source_sender_summary ?? '-' }}</dd>
|
||||
@@ -72,53 +78,41 @@
|
||||
<dt>{{ t('task.sourceReceivedAt') }}</dt>
|
||||
<dd>{{ formatReservationDateTime(detail.source_received_at) }}</dd>
|
||||
</div>
|
||||
</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>
|
||||
{{ formatReservationTaskCardLabel(t, detail) }}
|
||||
<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>
|
||||
<dt>{{ t('task.sourceMessageId') }}</dt>
|
||||
<dd class="th-code">
|
||||
{{ detail.route_code || '-' }}
|
||||
{{ detail.source_message_id || '-' }}
|
||||
</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>
|
||||
<dt>{{ t('conversation.messageCount') }}</dt>
|
||||
<dd>{{ detail.conversation_message_count ?? '-' }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div class="task-source-email__content">
|
||||
<section>
|
||||
<h3>{{ t('task.sourceEmailExcerpt') }}</h3>
|
||||
<p>{{ sourceEmailExcerpt ?? t('task.sourceEmailNoExcerpt') }}</p>
|
||||
</section>
|
||||
<section v-if="sourceEmailAttachments.length">
|
||||
<h3>{{ t('task.sourceEmailAttachments') }}</h3>
|
||||
<ul>
|
||||
<li
|
||||
v-for="item in sourceEmailAttachments"
|
||||
:key="item.key"
|
||||
>
|
||||
<i
|
||||
class="pi pi-paperclip"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>
|
||||
{{ item.name }}
|
||||
<small v-if="item.meta">{{ item.meta }}</small>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
@@ -396,6 +390,52 @@
|
||||
/>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<details class="task-route-info">
|
||||
<summary>{{ t('task.routeInfo') }}</summary>
|
||||
<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>
|
||||
{{ formatReservationTaskCardLabel(t, detail) }}
|
||||
<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>
|
||||
</details>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
@@ -430,6 +470,7 @@ import type {
|
||||
import {
|
||||
buildEditableFieldValues,
|
||||
buildInitialFieldValues,
|
||||
readReservationFieldValue,
|
||||
validateReservationFieldValues,
|
||||
type ReservationFieldValidationErrors,
|
||||
} from '@/utils/reservationFieldRules'
|
||||
@@ -481,6 +522,8 @@ const manualReviewRecord = computed(() => detail.value?.manual_review ?? sourceM
|
||||
const manualReviewEvidence = computed(() => buildManualReviewEvidence(detail.value, manualReviewRecord.value))
|
||||
const adapterContractErrors = computed(() => detail.value?.adapter_contract_errors ?? [])
|
||||
const unhandledIntents = computed(() => detail.value?.unhandled_intents ?? [])
|
||||
const sourceEmailExcerpt = computed(() => readSourceEmailExcerpt(detail.value, fieldValues.value))
|
||||
const sourceEmailAttachments = computed(() => readSourceEmailAttachments(detail.value, fieldValues.value))
|
||||
const isFieldRendererReadOnly = computed(
|
||||
() =>
|
||||
Boolean(detail.value?.availability.read_only) ||
|
||||
@@ -731,6 +774,175 @@ function replaceOperation(operation: ReservationOperaOperationResult): void {
|
||||
}
|
||||
}
|
||||
|
||||
interface SourceEmailAttachmentItem {
|
||||
key: string
|
||||
name: string
|
||||
meta: string
|
||||
}
|
||||
|
||||
const sourceEmailAttachmentPathTokens = ['attachment', 'attachments', 'file_reference', 'file_references']
|
||||
|
||||
function readSourceEmailExcerpt(
|
||||
taskDetail: ReservationTaskDetailResult | null,
|
||||
values: ReservationRecord,
|
||||
): string | null {
|
||||
if (!taskDetail) {
|
||||
return null
|
||||
}
|
||||
const fieldExcerpt = stringifySourceEmailText(readFirstEvidenceFieldValue(taskDetail, values, ['relevant_message_excerpt']))
|
||||
if (fieldExcerpt) {
|
||||
return fieldExcerpt
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function readSourceEmailAttachments(
|
||||
taskDetail: ReservationTaskDetailResult | null,
|
||||
values: ReservationRecord,
|
||||
): SourceEmailAttachmentItem[] {
|
||||
if (!taskDetail) {
|
||||
return []
|
||||
}
|
||||
const attachmentValues = taskDetail.fields
|
||||
.filter((field) => isSourceEmailAttachmentField(field))
|
||||
.map((field) => readReservationFieldValue(field, values))
|
||||
const seen = new Set<string>()
|
||||
return attachmentValues
|
||||
.flatMap((value) => normalizeAttachmentValue(value))
|
||||
.map((value, index) => toSourceEmailAttachmentItem(value, index))
|
||||
.filter((item) => {
|
||||
const key = `${item.name}-${item.meta}`
|
||||
if (seen.has(key)) {
|
||||
return false
|
||||
}
|
||||
seen.add(key)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
function readFirstEvidenceFieldValue(
|
||||
taskDetail: ReservationTaskDetailResult,
|
||||
values: ReservationRecord,
|
||||
pathTokens: string[],
|
||||
): unknown {
|
||||
const field = taskDetail.fields.find((item) => {
|
||||
const fieldPath = item.field_path.toLowerCase()
|
||||
const pointer = item.field_pointer?.toLowerCase() ?? ''
|
||||
return pathTokens.some((token) => fieldPath.includes(token) || pointer.includes(token.split('.').join('/')))
|
||||
})
|
||||
return field ? readReservationFieldValue(field, values) : null
|
||||
}
|
||||
|
||||
function isSourceEmailAttachmentField(field: ReservationTaskDetailResult['fields'][number]): boolean {
|
||||
const fieldPath = field.field_path.toLowerCase()
|
||||
const pointer = field.field_pointer?.toLowerCase() ?? ''
|
||||
return sourceEmailAttachmentPathTokens.some((token) => fieldPath.includes(token) || pointer.includes(token))
|
||||
}
|
||||
|
||||
function stringifySourceEmailText(value: unknown): string | null {
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
return value.trim()
|
||||
}
|
||||
if (typeof value === 'number' || typeof value === 'boolean') {
|
||||
return String(value)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function normalizeAttachmentValue(value: unknown): unknown[] {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return []
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.flatMap((item) => normalizeAttachmentValue(item))
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const trimmedValue = value.trim()
|
||||
if (!trimmedValue) {
|
||||
return []
|
||||
}
|
||||
const parsedValue = parseMaybeJson(trimmedValue)
|
||||
return parsedValue === trimmedValue ? [trimmedValue] : normalizeAttachmentValue(parsedValue)
|
||||
}
|
||||
return [value]
|
||||
}
|
||||
|
||||
function toSourceEmailAttachmentItem(value: unknown, index: number): SourceEmailAttachmentItem {
|
||||
const record = readRecord(value)
|
||||
const name = record
|
||||
? readFirstString(record, ['name', 'file_name', 'filename', 'original_file_name', 'display_name', 'title'])
|
||||
: stringifySourceEmailText(value)
|
||||
const rawType = record ? readFirstString(record, ['content_type', 'mime_type', 'media_type', 'file_type', 'type']) : null
|
||||
const size = record ? readFirstNumber(record, ['size_bytes', 'file_size', 'size']) : null
|
||||
const typeLabel = formatAttachmentTypeLabel(name, rawType)
|
||||
const sizeLabel = formatAttachmentSize(size)
|
||||
const id = record ? readFirstString(record, ['id', 'media_id', 'file_id', 'attachment_id']) : null
|
||||
const displayName = name || t('conversation.unnamedAttachment')
|
||||
return {
|
||||
key: id ? `${id}-${index}` : `${displayName}-${index}`,
|
||||
name: displayName,
|
||||
meta: [typeLabel, sizeLabel].filter(Boolean).join(' · '),
|
||||
}
|
||||
}
|
||||
|
||||
function parseMaybeJson(value: string): unknown {
|
||||
if (!value.startsWith('{') && !value.startsWith('[')) {
|
||||
return value
|
||||
}
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
function readFirstNumber(record: ReservationRecord, keys: string[]): number | null {
|
||||
for (const key of keys) {
|
||||
const value = record[key]
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
return value
|
||||
}
|
||||
if (typeof value === 'string' && Number.isFinite(Number(value))) {
|
||||
return Number(value)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function formatAttachmentTypeLabel(fileName: string | null, rawType: string | null): string {
|
||||
const source = `${rawType ?? ''} ${fileName ?? ''}`.toLowerCase()
|
||||
if (source.includes('pdf') || source.endsWith('.pdf')) {
|
||||
return 'PDF'
|
||||
}
|
||||
if (source.includes('spreadsheet') || source.includes('excel') || /\.(xlsx|xls|csv)$/.test(source)) {
|
||||
return 'Excel'
|
||||
}
|
||||
if (source.includes('word') || /\.(docx|doc)$/.test(source)) {
|
||||
return 'Word'
|
||||
}
|
||||
if (source.includes('image') || /\.(png|jpe?g|gif|webp)$/.test(source)) {
|
||||
return 'Image'
|
||||
}
|
||||
if (source.includes('message/rfc822') || source.endsWith('.eml')) {
|
||||
return 'EML'
|
||||
}
|
||||
const extension = fileName?.match(/\.([a-z0-9]{2,8})$/i)?.[1]
|
||||
return extension ? extension.toUpperCase() : ''
|
||||
}
|
||||
|
||||
function formatAttachmentSize(size: number | null): string {
|
||||
if (!size || size <= 0) {
|
||||
return ''
|
||||
}
|
||||
if (size < 1024) {
|
||||
return `${size} B`
|
||||
}
|
||||
if (size < 1024 * 1024) {
|
||||
return `${(size / 1024).toFixed(1)} KB`
|
||||
}
|
||||
return `${(size / 1024 / 1024).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
function formatJson(value: unknown): string {
|
||||
return JSON.stringify(value ?? {}, null, 2)
|
||||
}
|
||||
@@ -921,7 +1133,7 @@ function formatStringList(items: string[]): string {
|
||||
color: var(--th-color-slate-500);
|
||||
}
|
||||
|
||||
.task-evidence,
|
||||
.task-source-email,
|
||||
.task-route-info,
|
||||
.task-ai-diagnostics {
|
||||
display: grid;
|
||||
@@ -931,10 +1143,54 @@ function formatStringList(items: string[]): string {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.task-source-email {
|
||||
gap: 16px;
|
||||
background: var(--th-color-white);
|
||||
}
|
||||
|
||||
.task-source-email header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.task-source-email header p,
|
||||
.task-source-email header h2,
|
||||
.task-source-email__content h3,
|
||||
.task-source-email__content p,
|
||||
.task-source-email__content ul {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.task-source-email header p {
|
||||
color: var(--th-color-blue-600);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.task-source-email header h2 {
|
||||
margin-top: 6px;
|
||||
color: var(--th-color-slate-900);
|
||||
font-size: 18px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.task-route-info {
|
||||
background: var(--th-color-slate-50);
|
||||
}
|
||||
|
||||
.task-route-info summary {
|
||||
color: var(--th-color-slate-700);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.task-route-info[open] summary {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.task-ai-diagnostics {
|
||||
background: var(--th-color-white);
|
||||
}
|
||||
@@ -961,9 +1217,6 @@ function formatStringList(items: string[]): string {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.task-evidence h2,
|
||||
.task-evidence dl,
|
||||
.task-route-info h2,
|
||||
.task-route-info dl,
|
||||
.task-ai-diagnostics h2,
|
||||
.task-ai-diagnostics pre,
|
||||
@@ -977,8 +1230,6 @@ function formatStringList(items: string[]): string {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.task-evidence h2,
|
||||
.task-route-info h2,
|
||||
.task-ai-diagnostics h2,
|
||||
.manual-review-evidence h2,
|
||||
.source-only-result h2 {
|
||||
@@ -1010,7 +1261,7 @@ function formatStringList(items: string[]): string {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.task-evidence dl,
|
||||
.task-source-email__meta,
|
||||
.task-route-info dl,
|
||||
.manual-review-evidence > dl {
|
||||
display: grid;
|
||||
@@ -1024,7 +1275,7 @@ function formatStringList(items: string[]): string {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.task-evidence dt,
|
||||
.task-source-email__meta dt,
|
||||
.task-route-info dt,
|
||||
.manual-review-evidence > dl dt {
|
||||
color: var(--th-color-slate-500);
|
||||
@@ -1032,7 +1283,7 @@ function formatStringList(items: string[]): string {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.task-evidence dd,
|
||||
.task-source-email__meta dd,
|
||||
.task-route-info dd,
|
||||
.manual-review-evidence > dl dd,
|
||||
.source-only-result dd {
|
||||
@@ -1042,6 +1293,71 @@ function formatStringList(items: string[]): string {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.task-source-email__content {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.35fr) minmax(260px, 0.65fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.task-source-email__content section {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
border: 1px solid var(--th-color-slate-200);
|
||||
border-radius: var(--th-radius-sm);
|
||||
background: var(--th-color-slate-50);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.task-source-email__content section:only-child {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.task-source-email__content h3 {
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.task-source-email__content p {
|
||||
color: var(--th-color-slate-800);
|
||||
font-size: 13px;
|
||||
line-height: 1.65;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.task-source-email__content ul {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.task-source-email__content li {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
color: var(--th-color-slate-900);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.task-source-email__content li i {
|
||||
flex: 0 0 auto;
|
||||
margin-top: 2px;
|
||||
color: var(--th-color-blue-600);
|
||||
}
|
||||
|
||||
.task-source-email__content li span {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.task-source-email__content li small {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.manual-review-evidence__candidates {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
@@ -1213,7 +1529,7 @@ function formatStringList(items: string[]): string {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.task-evidence dl,
|
||||
.task-source-email__meta,
|
||||
.task-route-info dl,
|
||||
.manual-review-evidence > dl,
|
||||
.candidate-grid {
|
||||
@@ -1222,7 +1538,16 @@ function formatStringList(items: string[]): string {
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.task-evidence dl,
|
||||
.task-source-email header,
|
||||
.task-source-email__content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.task-source-email header {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.task-source-email__meta,
|
||||
.task-route-info dl,
|
||||
.manual-review-evidence > dl,
|
||||
.candidate-grid,
|
||||
|
||||
@@ -418,6 +418,12 @@ export default {
|
||||
sourceSender: 'Sender',
|
||||
sourceReceivedAt: 'Received at',
|
||||
sourceSummaryPending: 'Source email summary fields are pending backend support',
|
||||
sourceEmailPreview: 'Source email preview',
|
||||
sourceEmailExcerpt: 'Email excerpt',
|
||||
sourceEmailAttachments: 'Attachments',
|
||||
sourceEmailNoExcerpt: 'No email excerpt',
|
||||
sourceEmailNoAttachments: 'No attachments',
|
||||
sourceEmailFullConversation: 'View full email conversation',
|
||||
sourceOnlyResult: 'Source message entry result',
|
||||
sourceOnlyReadonly: 'This task only shows the source message entry result. It does not enter order handling, field editing, or OPERA operations.',
|
||||
sourceOnlyNoOpera: 'This task type does not involve OPERA operations.',
|
||||
|
||||
@@ -418,6 +418,12 @@ export default {
|
||||
sourceSender: 'ผู้ส่ง',
|
||||
sourceReceivedAt: 'เวลารับ',
|
||||
sourceSummaryPending: 'ข้อมูลสรุปอีเมลต้นทางยังรอหลังบ้านเพิ่มเติม',
|
||||
sourceEmailPreview: 'ตัวอย่างอีเมลต้นทาง',
|
||||
sourceEmailExcerpt: 'ข้อความบางส่วนของอีเมล',
|
||||
sourceEmailAttachments: 'ไฟล์แนบ',
|
||||
sourceEmailNoExcerpt: 'ไม่มีข้อความบางส่วน',
|
||||
sourceEmailNoAttachments: 'ไม่มีไฟล์แนบ',
|
||||
sourceEmailFullConversation: 'ดูเธรดอีเมลทั้งหมด',
|
||||
sourceOnlyResult: 'ผลลัพธ์ทางเข้าของข้อความต้นทาง',
|
||||
sourceOnlyReadonly: 'งานนี้ใช้แสดงผลลัพธ์ทางเข้าของข้อความต้นทางเท่านั้น ไม่เข้าสู่การจัดการออเดอร์ การแก้ไขข้อมูล หรือ OPERA',
|
||||
sourceOnlyNoOpera: 'งานประเภทนี้ไม่เกี่ยวข้องกับ OPERA',
|
||||
|
||||
@@ -418,6 +418,12 @@ export default {
|
||||
sourceSender: '发件人',
|
||||
sourceReceivedAt: '接收时间',
|
||||
sourceSummaryPending: '来源邮件摘要字段待后端补充',
|
||||
sourceEmailPreview: '来源邮件预览',
|
||||
sourceEmailExcerpt: '邮件片段',
|
||||
sourceEmailAttachments: '相关附件',
|
||||
sourceEmailNoExcerpt: '暂无邮件片段',
|
||||
sourceEmailNoAttachments: '无附件',
|
||||
sourceEmailFullConversation: '查看完整邮件会话',
|
||||
sourceOnlyResult: '来源消息入口结果',
|
||||
sourceOnlyReadonly: '该任务仅用于展示来源消息入口结果,不进入订单处理、字段编辑或 OPERA 操作。',
|
||||
sourceOnlyNoOpera: '该类任务不涉及 OPERA 操作。',
|
||||
|
||||
@@ -651,6 +651,90 @@ describe('ReservationTaskDetailPanel', () => {
|
||||
expect(wrapper.text()).not.toContain('接口待补')
|
||||
})
|
||||
|
||||
it('renders source email preview with excerpt, attachments and full conversation entry', async () => {
|
||||
vi.mocked(service.fetchReservationTaskDetail).mockResolvedValue(
|
||||
createTaskDetail({
|
||||
fields: [
|
||||
createRequiredField({
|
||||
field_path: 'relevant_message_excerpt',
|
||||
display_name: '邮件片段',
|
||||
value: 'LLT260509FA213 11-13/05/2026 2N 14 TWN +1 DBL',
|
||||
editable: 'N',
|
||||
input_editable: 'N',
|
||||
required_rule: null,
|
||||
control_type: 'evidence_text',
|
||||
edit_scope: 'never',
|
||||
write_target: 'none',
|
||||
raw_readonly: true,
|
||||
} as Partial<ReservationTaskFieldResult>),
|
||||
createRequiredField({
|
||||
field_path: 'source_message.attachments',
|
||||
display_name: '来源附件',
|
||||
value: [
|
||||
{
|
||||
id: 'att-1',
|
||||
name: 'WYNDHAM LIANTAI 2026 UPDATE BOOKING 12-05-2026 NO.1.xlsx',
|
||||
content_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
size_bytes: 2048,
|
||||
},
|
||||
],
|
||||
editable: 'N',
|
||||
input_editable: 'N',
|
||||
file_display: 'Y',
|
||||
required_rule: null,
|
||||
control_type: 'file',
|
||||
edit_scope: 'never',
|
||||
write_target: 'none',
|
||||
raw_readonly: true,
|
||||
} as Partial<ReservationTaskFieldResult>),
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
const wrapper = await mountPanel()
|
||||
|
||||
expect(wrapper.text()).toContain('来源邮件预览')
|
||||
expect(wrapper.text()).toContain('Booking Request')
|
||||
expect(wrapper.text()).toContain('guest@example.test')
|
||||
expect(wrapper.text()).toContain('2026-07-08 10:00:00')
|
||||
expect(wrapper.text()).toContain('LLT260509FA213 11-13/05/2026 2N 14 TWN +1 DBL')
|
||||
expect(wrapper.text()).toContain('WYNDHAM LIANTAI 2026 UPDATE BOOKING 12-05-2026 NO.1.xlsx')
|
||||
expect(wrapper.text()).toContain('查看完整邮件会话')
|
||||
expect(wrapper.text()).not.toContain('{"id":"att-1"')
|
||||
})
|
||||
|
||||
it('does not surface source email excerpt or attachments from unexposed backend context', async () => {
|
||||
vi.mocked(service.fetchReservationTaskDetail).mockResolvedValue(
|
||||
createTaskDetail({
|
||||
fields: [],
|
||||
context_used: {
|
||||
relevant_message_excerpt: 'Hidden context excerpt',
|
||||
attachments: [
|
||||
{
|
||||
name: 'hidden-context-attachment.pdf',
|
||||
},
|
||||
],
|
||||
},
|
||||
manual_review: {
|
||||
attachments: [
|
||||
{
|
||||
name: 'hidden-review-attachment.pdf',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
const wrapper = await mountPanel()
|
||||
const sourceEmailPreview = wrapper.find('.task-source-email')
|
||||
|
||||
expect(sourceEmailPreview.text()).toContain('暂无邮件片段')
|
||||
expect(sourceEmailPreview.text()).not.toContain('Hidden context excerpt')
|
||||
expect(sourceEmailPreview.text()).not.toContain('hidden-context-attachment.pdf')
|
||||
expect(sourceEmailPreview.text()).not.toContain('hidden-review-attachment.pdf')
|
||||
expect(sourceEmailPreview.text()).not.toContain('相关附件')
|
||||
})
|
||||
|
||||
it('renders source-message-only entry result without editable or OPERA actions', async () => {
|
||||
vi.mocked(service.fetchReservationTaskDetail).mockResolvedValue(createSourceMessageOnlyTaskDetail())
|
||||
|
||||
|
||||
@@ -397,8 +397,11 @@ describe('reservation P0 views', () => {
|
||||
const wrapper = await mountWithPlugins(ReservationOrderListView)
|
||||
await vi.dynamicImportSettled()
|
||||
|
||||
const rowText = wrapper.find('tbody').text()
|
||||
expect(wrapper.text()).toContain('GRP-001')
|
||||
expect(wrapper.text()).toContain('有效')
|
||||
expect(rowText).not.toContain('20001')
|
||||
expect(rowText).not.toContain('有效')
|
||||
expect(wrapper.text()).not.toContain('未关闭任务')
|
||||
expect(wrapper.text()).toContain('继续处理')
|
||||
expect(wrapper.text()).not.toContain('接口待接入')
|
||||
})
|
||||
|
||||
@@ -108,8 +108,6 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ t('orderList.columns.order') }}</th>
|
||||
<th>{{ t('orderList.columns.status') }}</th>
|
||||
<th>{{ t('orderList.columns.openTasks') }}</th>
|
||||
<th>{{ t('orderList.columns.createdAt') }}</th>
|
||||
<th>{{ t('workbench.columns.updatedAt') }}</th>
|
||||
<th>{{ t('workbench.columns.operation') }}</th>
|
||||
@@ -122,10 +120,7 @@
|
||||
>
|
||||
<td>
|
||||
<strong>{{ formatOrderKey(order) }}</strong>
|
||||
<small class="th-code">{{ order.order_id }}</small>
|
||||
</td>
|
||||
<td><ReservationStatusBadge :status="order.order_status" /></td>
|
||||
<td>{{ order.open_task_count ?? 0 }}</td>
|
||||
<td>{{ formatReservationDateTime(order.created_at) }}</td>
|
||||
<td>{{ formatReservationDateTime(order.updated_at) }}</td>
|
||||
<td>
|
||||
@@ -197,7 +192,6 @@ import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ReservationStatusBadge from '@/components/reservation/ReservationStatusBadge.vue'
|
||||
import { fetchReservationOrders } from '@/services/reservationService'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import type { ReservationOrderListFilters, ReservationOrderListItem, ReservationPageResult } from '@/types/reservation'
|
||||
|
||||
Reference in New Issue
Block a user