Files
admin-web/src/modules/appointment-calendar/WorkCommandForm.vue
T

106 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import InlineHelp from '@/shared/components/InlineHelp.vue'
import { computed, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { useAuthStore } from '@/core/auth/auth'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { listSuppliers, type Supplier } from '@/modules/suppliers'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import { submitWorkCommand, type WorkAction, type WorkDetail, type WorkCommand, type WorkCommandPayload, type WorkCommandResult } from './workApi'
import { actionAllowed, commandName } from './workPresentation'
import { rejectedCommand, useWorkDraft } from './useWorkDraft'
const props = defineProps<{ detail: WorkDetail; action: WorkAction; refreshing: boolean }>()
const emit = defineEmits<{ saved: [result: WorkCommandResult, command: WorkCommand['command']]; refresh: [] }>()
const auth = useAuthStore(), access = useAccessPreviewStore()
const command = computed(() => commandName(props.action))
const item = computed(() => props.detail.item)
const form = reactive({ flow: item.value.flow === 'DIRECT' ? 'DIRECT' : 'TRANSFER', ownerId: item.value.owner?.id || auth.user?.id || '', followupDate: item.value.followupDate || '', note: '', reason: '', date: item.value.target?.date || '', startTime: item.value.target?.startTime || '', endDate: item.value.target?.endDate || '', endTime: item.value.target?.endTime || '', bookingRequired: props.detail.requirement?.bookingRequired ?? true, receivingRequirements: props.detail.requirement?.receivingRequirements || '', recipientKind: 'EXTERNAL', recipientId: '', recipientName: '' })
const baseline = ref(JSON.stringify(form)), pending = ref<WorkCommand | null>(null), saving = ref(false), error = ref<ApiErrorDetails | null>(null)
const supplierKeyword = ref(''), suppliers = ref<Supplier[]>([]), supplierTotal = ref(0), supplierLoading = ref(false), supplierError = ref<ApiErrorDetails | null>(null)
let supplierRequest = 0
async function searchSuppliers() {
const request = ++supplierRequest; supplierError.value = null; suppliers.value = []; form.recipientId = ''
if (!access.can('platform:supplier:list')) { supplierError.value = { message: '当前账号没有供应商读取权限,请联系有权限的负责人选择车队。' }; return }
supplierLoading.value = true
try { const result = await listSuppliers({ page: 1, size: 20, category: 'TRUCKING', truckingScope: 'OVERSEAS', status: 'ACTIVE', keyword: supplierKeyword.value }); if (request === supplierRequest) { suppliers.value = result.records; supplierTotal.value = result.total } }
catch (cause) { if (request === supplierRequest) supplierError.value = getApiErrorDetails(cause, '还空车队读取失败') }
finally { if (request === supplierRequest) supplierLoading.value = false }
}
function changeRecipient() { form.recipientId = ''; form.recipientName = ''; if (form.recipientKind === 'SUPPLIER') void searchSuppliers() }
useWorkDraft(computed(() => baseline.value !== JSON.stringify(form)), computed(() => saving.value || !!pending.value))
const owners = computed(() => {
const people = [item.value.owner, auth.user ? { id: auth.user.id, name: auth.user.name } : null, ...props.detail.relatedItems.map(row => row.owner)]
return [...new Map(people.filter((person): person is { id: string; name: string } => !!person?.id).map(person => [person.id, person])).values()]
})
const permission = computed(() => command.value === 'SAVE_FOLLOWUP' || command.value === 'RESOLVE_REVIEW' ? 'platform:appointment:followup' : 'platform:appointment:manage-work')
const allowed = computed(() => !!command.value && actionAllowed(item.value, props.action) && access.can(permission.value))
const missingContext = computed(() => {
if (command.value === 'RESOLVE_REVIEW' && props.detail.review == null) return '当前没有可复核的影响版本,请刷新事项。'
if (command.value === 'ACCEPT_RETURN' && props.detail.returnHandoff == null) return '当前没有有效的还空交接,请刷新事项。'
if (command.value === 'SAVE_REQUIREMENT' && form.date && !item.value.timezone) return '作业地点时区未核实,暂不能保存带日期的要求。'
return ''
})
function payload(): WorkCommandPayload | null {
const note = form.note.trim(), reason = form.reason.trim()
switch (command.value) {
case 'CONFIRM_ROUTE': return { command: 'CONFIRM_ROUTE', payload: { flow: form.flow === 'DIRECT' ? 'DIRECT' : 'TRANSFER', reason: reason || null } }
case 'SAVE_FOLLOWUP':
if (!form.ownerId || !note) { ElMessage.warning('请选择跟进负责人并填写本次回复'); return null }
return { command: 'SAVE_FOLLOWUP', payload: { ownerId: form.ownerId, followupDate: form.followupDate || null, note } }
case 'RESOLVE_REVIEW':
if (!props.detail.review || !note) { ElMessage.warning('请核对当前影响并填写处理结果'); return null }
return { command: 'RESOLVE_REVIEW', payload: { reviewVersion: props.detail.review.version, note } }
case 'SAVE_REQUIREMENT':
if (!reason || (!form.bookingRequired && !form.receivingRequirements.trim())) { ElMessage.warning('请填写处理说明;无需预约时必须填写真实收货要求'); return null }
if ((form.startTime || form.endDate || form.endTime) && !form.date) { ElMessage.warning('填写时段前需明确目标日期'); return null }
if (!!form.endDate !== !!form.endTime) { ElMessage.warning('结束日期和结束时间须同时填写或同时留空'); return null }
if (form.endDate && (!form.startTime || form.endDate + 'T' + form.endTime <= form.date + 'T' + form.startTime)) { ElMessage.warning('完整的当地结束时间须晚于开始时间'); return null }
return { command: 'SAVE_REQUIREMENT', payload: { target: form.date && item.value.timezone ? { date: form.date, startTime: form.startTime || null, endDate: form.endDate || null, endTime: form.endTime || null, timezone: item.value.timezone } : null, bookingRequired: form.bookingRequired, receivingRequirements: form.receivingRequirements.trim(), reason } }
case 'HANDOFF_RETURN': {
const person = owners.value.find(owner => owner.id === form.recipientId)
const supplier = suppliers.value.find(candidate => candidate.id === form.recipientId)
const recipient = form.recipientKind === 'USER' && person ? { kind: 'USER' as const, id: person.id, name: person.name } : form.recipientKind === 'SUPPLIER' && supplier && !supplierLoading.value && !supplierError.value ? { kind: 'SUPPLIER' as const, id: supplier.id, name: supplier.name } : form.recipientKind === 'EXTERNAL' ? { kind: 'EXTERNAL' as const, id: null, name: form.recipientName.trim() } : null
if (!recipient?.name || !reason) { ElMessage.warning('请明确接手方并填写转交原因'); return null }
return { command: 'HANDOFF_RETURN', payload: { recipient, reason } }
}
case 'ACCEPT_RETURN':
if (!props.detail.returnHandoff) return null
if (props.detail.returnHandoff.to.kind !== 'USER' && !note) { ElMessage.warning('请记录外部接手方已确认的反馈依据'); return null }
return { command: 'ACCEPT_RETURN', payload: { handoffVersion: props.detail.returnHandoff.version, note: note || null } }
default: return null
}
}
async function save() {
if (!allowed.value || props.refreshing || saving.value || missingContext.value) return
if (!pending.value) { const request = payload(); if (!request) return; pending.value = { ...request, expectedVersion: item.value.version, idempotencyKey: crypto.randomUUID() } }
saving.value = true; error.value = null
try {
const request = pending.value, result = await submitWorkCommand(item.value.workItemKey, request)
pending.value = null; baseline.value = JSON.stringify(form); ElMessage.success('已保存,工作待办已更新'); emit('saved', result, request.command)
} catch (cause) {
error.value = getApiErrorDetails(cause, '本次保存结果待确认,请重试同一次保存')
if (rejectedCommand(cause)) { pending.value = null; emit('refresh') }
} finally { saving.value = false }
}
</script>
<template>
<form class="density-form work-form" @submit.prevent="save">
<h3>{{ action.label }}<InlineHelp v-if="command === 'SAVE_FOLLOWUP'" title="跟进日期说明"><p>跟进日期不代替作业日期;未完成事项仍保留在待办中。</p></InlineHelp><InlineHelp v-if="command === 'RESOLVE_REVIEW'" title="核对结果说明"><p>原安排需要改期时,请先处理原安排,再记录核对结果。</p></InlineHelp></h3>
<p v-if="!allowed">当前仅可查看。此操作需要有效的服务端资格和对应操作权限。</p>
<p v-if="missingContext" role="alert">{{ missingContext }}</p>
<fieldset :disabled="!allowed || refreshing || saving || !!pending">
<template v-if="command === 'CONFIRM_ROUTE'"><label>运输方式<select v-model="form.flow"><option value="TRANSFER">先入海外仓,再分批送仓</option><option value="DIRECT">整柜直接送目的仓</option></select></label><label>路线说明<textarea v-model="form.reason" maxlength="1000" /></label></template>
<template v-else-if="command === 'SAVE_FOLLOWUP'"><label>跟进负责人<select v-model="form.ownerId" required><option value="" disabled>请选择负责人</option><option v-for="person in owners" :key="person.id" :value="person.id">{{ person.name }}</option></select></label><label>本次回复<textarea v-model="form.note" required maxlength="1000" /></label><label>下次跟进日期(可留空)<el-date-picker v-model="form.followupDate" type="date" :disabled="!allowed || refreshing || saving || !!pending" format="YYYY-MM-DD" value-format="YYYY-MM-DD" clearable /></label></template>
<template v-else-if="command === 'RESOLVE_REVIEW'"><p v-for="reason in detail.review?.reasons" :key="reason">{{ reason }}</p><label>本次核对结果<textarea v-model="form.note" required maxlength="1000" /></label></template>
<template v-else-if="command === 'SAVE_REQUIREMENT'"><label>目标日期(可暂不填)<el-date-picker v-model="form.date" type="date" :disabled="!allowed || refreshing || saving || !!pending" format="YYYY-MM-DD" value-format="YYYY-MM-DD" clearable /></label><div class="time-fields"><label>开始时段(可留空)<el-time-picker v-model="form.startTime" :disabled="!allowed || refreshing || saving || !!pending" format="HH:mm" value-format="HH:mm" clearable /></label><label>结束日期(可留空)<el-date-picker v-model="form.endDate" type="date" :disabled="!allowed || refreshing || saving || !!pending" format="YYYY-MM-DD" value-format="YYYY-MM-DD" clearable /></label><label>结束时段(可留空)<el-time-picker v-model="form.endTime" :disabled="!allowed || refreshing || saving || !!pending" format="HH:mm" value-format="HH:mm" clearable /></label></div><p>{{ item.timezone || '地点时区待核实' }}</p><label v-if="item.type === 'DELIVERY'"><input v-model="form.bookingRequired" type="checkbox">需要收货方预约</label><label>收货要求<textarea v-model="form.receivingRequirements" :required="!form.bookingRequired" maxlength="1000" /></label><label>处理说明<textarea v-model="form.reason" required maxlength="1000" /></label></template>
<template v-else-if="command === 'HANDOFF_RETURN'"><p>转交保留原预约和责任记录;接手方确认前持续跟进。</p><label>接手方类型<select v-model="form.recipientKind" @change="changeRecipient"><option value="EXTERNAL">外部车队 / 联系人</option><option value="SUPPLIER">已登记海外车队</option><option value="USER">当前关联负责人</option></select></label><label v-if="form.recipientKind === 'EXTERNAL'">接手方名称<input v-model="form.recipientName" required maxlength="200"></label><template v-else-if="form.recipientKind === 'SUPPLIER'"><label>查找海外车队<input v-model="supplierKeyword" maxlength="80" @keydown.enter.prevent="searchSuppliers"></label><el-button :loading="supplierLoading" @click="searchSuppliers">查询车队</el-button><LoadErrorState v-if="supplierError" title="还空车队不可用" :message="supplierError.message" @retry="searchSuppliers" /><label>接手车队<select v-model="form.recipientId" required :disabled="supplierLoading || !!supplierError"><option value="" disabled>请选择已登记海外车队</option><option v-for="supplier in suppliers" :key="supplier.id" :value="supplier.id">{{ supplier.name }}</option></select></label><p v-if="supplierTotal > suppliers.length">请输入更完整名称缩小查询范围。</p></template><label v-else>接手负责人<select v-model="form.recipientId" required><option value="" disabled>请选择</option><option v-for="person in owners" :key="person.id" :value="person.id">{{ person.name }}</option></select></label><label>转交原因<textarea v-model="form.reason" required maxlength="1000" /></label></template>
<template v-else-if="command === 'ACCEPT_RETURN'"><p>原责任方:{{ detail.returnHandoff?.from.name || '—' }} → 接手方:{{ detail.returnHandoff?.to.name || '—' }}</p><label>接手反馈依据<textarea v-model="form.note" :required="detail.returnHandoff?.to.kind !== 'USER'" maxlength="1000" /></label><p>确认接手只记录责任,实际还空仍按原业务操作。</p></template>
</fieldset>
<LoadErrorState v-if="error" title="工作操作未保存" :message="error.message" :show-retry="false"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
<el-button type="primary" native-type="submit" :disabled="!allowed || refreshing || !!missingContext" :loading="saving">{{ pending ? '重试本次保存' : command === 'CONFIRM_ROUTE' ? '确认路线,继续本柜工作' : '保存并更新待办' }}</el-button>
</form>
</template>
<style scoped>.time-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--admin-space-3); }</style>