fix(admin): save dirty ocean booking before release
This commit is contained in:
@@ -97,11 +97,13 @@ function blankData(): OceanBookingDraftData {
|
||||
}
|
||||
}
|
||||
const form = reactive<OceanBookingSaveRequest>({ expectedVersion: 0, data: blankData() })
|
||||
const savedDraftSignature = ref('')
|
||||
const rules: FormRules = {}
|
||||
const editable = computed(() => isNew.value || detail.value?.status === 'DRAFT')
|
||||
const canSave = computed(() => editable.value && access.can(isNew.value ? 'platform:firstleg:ocean-booking:create' : 'platform:firstleg:ocean-booking:edit'))
|
||||
const canDelete = computed(() => detail.value?.status === 'DRAFT' && access.can('platform:firstleg:ocean-booking:delete'))
|
||||
const canRelease = computed(() => detail.value?.status === 'DRAFT' && access.can('platform:firstleg:ocean-booking:release'))
|
||||
const workflowBusy = computed(() => saving.value || actionSaving.value)
|
||||
const releaseDialogOpen = ref(false)
|
||||
|
||||
function statusLabel(status: OceanBookingStatus | string | null | undefined) {
|
||||
@@ -136,16 +138,25 @@ function selectCustomer(value: unknown) {
|
||||
const customer = customerId == null ? null : customers.value.find((item) => String(item.id) === customerId)
|
||||
form.data.customerName = customer?.tenantName || null
|
||||
}
|
||||
function normalizeDraftData() {
|
||||
form.data.customerId = form.data.customerId || null
|
||||
form.data.customerName = form.data.customerId ? form.data.customerName : null
|
||||
form.data.serviceTerm = form.data.serviceTerm?.trim() || null
|
||||
form.data.freightTerm = form.data.freightTerm?.trim() || null
|
||||
form.data.billOfLadingNo = form.data.billOfLadingNo.trim()
|
||||
form.data.vesselName = form.data.vesselName.trim()
|
||||
form.data.voyageNo = form.data.voyageNo.trim()
|
||||
form.data.containerRequirements.forEach((requirement) => { requirement.quantity = Number(requirement.quantity) })
|
||||
function draftPayload(): OceanBookingDraftData {
|
||||
const customerId = form.data.customerId || null
|
||||
return {
|
||||
...form.data,
|
||||
customerId,
|
||||
customerName: customerId ? form.data.customerName : null,
|
||||
channelCode: form.data.channelCode?.trim() || null,
|
||||
warehouseId: form.data.warehouseId?.trim() || null,
|
||||
serviceTerm: form.data.serviceTerm?.trim() || null,
|
||||
freightTerm: form.data.freightTerm?.trim() || null,
|
||||
billOfLadingNo: form.data.billOfLadingNo.trim(),
|
||||
vesselName: form.data.vesselName.trim(),
|
||||
voyageNo: form.data.voyageNo.trim(),
|
||||
parties: form.data.parties.filter((party) => party.partyName.trim()).map((party) => ({ ...party, partyName: party.partyName.trim() })),
|
||||
containerRequirements: form.data.containerRequirements.map(({ containerType, containerKind, quantity, capacityCbm, maxWeightKg }) => ({ containerType, containerKind, quantity: Number(quantity), capacityCbm, maxWeightKg })),
|
||||
}
|
||||
}
|
||||
function draftSignature() { return JSON.stringify(draftPayload()) }
|
||||
const hasUnsavedDraftChanges = computed(() => Boolean(detail.value?.status === 'DRAFT' && savedDraftSignature.value && draftSignature() !== savedDraftSignature.value))
|
||||
function selectWarehouse(value: unknown) {
|
||||
const warehouseId = value == null || value === '' ? null : String(value)
|
||||
form.data.warehouseId = warehouseId
|
||||
@@ -229,6 +240,7 @@ function applyDetail(value: OceanBookingDetail) {
|
||||
containerRequirements: value.containerRequirements.length ? value.containerRequirements.map(({ requirementId, containerType, containerKind, quantity, remainingQuantity, capacityCbm, maxWeightKg }) => ({ requirementId, containerType, containerKind, quantity, remainingQuantity, capacityCbm, maxWeightKg })) : [createBlankContainerRequirement()],
|
||||
}
|
||||
selectedChannelName.value = value.channelName || ''
|
||||
savedDraftSignature.value = draftSignature()
|
||||
channels.value = []
|
||||
if (form.data.warehouseId) void loadChannels(form.data.warehouseId)
|
||||
if (form.data.carrierCode) void loadBookingAgents(form.data.carrierCode)
|
||||
@@ -241,47 +253,45 @@ async function load() {
|
||||
catch (cause) { error.value = getApiErrorDetails(cause) }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
async function save() {
|
||||
if (!canSave.value) return
|
||||
actionError.value = null
|
||||
normalizeDraftData()
|
||||
async function validateDraftForm() {
|
||||
const valid = await formRef.value?.validate().catch(() => false)
|
||||
if (valid === false) { actionError.value = { message: '请检查表单中的必填项和格式' }; return false }
|
||||
const requirementMessage = containerRequirementError(form.data.containerRequirements)
|
||||
if (requirementMessage) { actionError.value = { message: requirementMessage }; return }
|
||||
saving.value = true
|
||||
if (requirementMessage) { actionError.value = { message: requirementMessage }; return false }
|
||||
return true
|
||||
}
|
||||
async function persistDraft(showSuccess: boolean) {
|
||||
if (!canSave.value || !(await validateDraftForm())) return null
|
||||
actionError.value = null
|
||||
try {
|
||||
const payload = {
|
||||
...form.data,
|
||||
channelCode: form.data.channelCode?.trim() || null,
|
||||
warehouseId: form.data.warehouseId?.trim() || null,
|
||||
parties: form.data.parties.filter((party) => party.partyName.trim()).map((party) => ({ ...party, partyName: party.partyName.trim() })),
|
||||
containerRequirements: form.data.containerRequirements.map(({ containerType, containerKind, quantity, capacityCbm, maxWeightKg }) => ({ containerType, containerKind, quantity, capacityCbm, maxWeightKg })),
|
||||
}
|
||||
const payload = draftPayload()
|
||||
const saved = isNew.value
|
||||
? await createOceanBooking({ expectedVersion: 0, data: payload })
|
||||
: await updateOceanBookingDraft(id.value, { expectedVersion: detail.value?.version || 0, data: payload })
|
||||
ElMessage.success(isNew.value ? `订舱创建成功:${saved.bookingNo || saved.id}` : '订舱草稿已保存')
|
||||
if (showSuccess) ElMessage.success(isNew.value ? `订舱创建成功:${saved.bookingNo || saved.id}` : '订舱草稿已保存')
|
||||
if (isNew.value) {
|
||||
workspace.removeByIds(['ocean-booking-create'])
|
||||
await router.replace('/ocean-bookings')
|
||||
return
|
||||
return saved
|
||||
}
|
||||
applyDetail(saved)
|
||||
} catch (cause) { actionError.value = getApiErrorDetails(cause) }
|
||||
return saved
|
||||
} catch (cause) { actionError.value = getApiErrorDetails(cause); return null }
|
||||
}
|
||||
async function save() {
|
||||
if (!canSave.value || workflowBusy.value) return
|
||||
saving.value = true
|
||||
try { await persistDraft(true) }
|
||||
finally { saving.value = false }
|
||||
}
|
||||
function openReleaseDialog() {
|
||||
normalizeDraftData()
|
||||
async function openReleaseDialog() {
|
||||
if (!canRelease.value || workflowBusy.value || !(await validateDraftForm())) return
|
||||
const validationError = releaseValidationError()
|
||||
if (validationError) { actionError.value = { message: validationError }; return }
|
||||
releaseDialogOpen.value = true
|
||||
}
|
||||
function releaseValidationError() {
|
||||
if (!form.data.carrierCode) return '放舱前请重新选择船公司,所选船公司必须配置标准编码'
|
||||
if (detail.value && (detail.value.carrierCode !== form.data.carrierCode
|
||||
|| String(detail.value.shippingCompanyId || '') !== String(form.data.shippingCompanyId || ''))) {
|
||||
return '船公司标准编码尚未保存,请先保存草稿后再放舱'
|
||||
}
|
||||
const requiredFields: Array<[unknown, string]> = [
|
||||
[form.data.customerId, '客户'], [form.data.warehouseId, '装柜仓库'], [form.data.channelCode, '物流渠道'],
|
||||
[form.data.billOfLadingNo, '提单号'], [form.data.vesselName, '船名'], [form.data.voyageNo, '航次'],
|
||||
@@ -296,13 +306,20 @@ function releaseValidationError() {
|
||||
return containerRequirementError(form.data.containerRequirements)
|
||||
}
|
||||
async function submitRelease() {
|
||||
if (!detail.value) return
|
||||
const validationError = releaseValidationError()
|
||||
if (validationError) { actionError.value = { message: validationError }; releaseDialogOpen.value = false; return }
|
||||
if (!detail.value || workflowBusy.value) return
|
||||
actionSaving.value = true
|
||||
actionError.value = null
|
||||
try {
|
||||
applyDetail(await releaseOceanBooking(id.value, { expectedVersion: detail.value.version }))
|
||||
if (!(await validateDraftForm())) { releaseDialogOpen.value = false; return }
|
||||
const validationError = releaseValidationError()
|
||||
if (validationError) { actionError.value = { message: validationError }; releaseDialogOpen.value = false; return }
|
||||
let releaseVersion = detail.value.version
|
||||
if (hasUnsavedDraftChanges.value) {
|
||||
const saved = await persistDraft(false)
|
||||
if (!saved) return
|
||||
releaseVersion = saved.version
|
||||
}
|
||||
applyDetail(await releaseOceanBooking(id.value, { expectedVersion: releaseVersion }))
|
||||
releaseDialogOpen.value = false
|
||||
ElMessage.success('确认放舱成功')
|
||||
} catch (cause) { actionError.value = getApiErrorDetails(cause) }
|
||||
@@ -349,9 +366,9 @@ onMounted(() => { void load(); void loadOptions() })
|
||||
<div class="section-title">
|
||||
<div><h2>订舱信息</h2><p v-if="detail?.status === 'DRAFT'">草稿可反复保存;放舱后资料同步至关联货柜并锁定。</p><p v-else-if="detail">已放舱资料只读。</p></div>
|
||||
<div class="actions">
|
||||
<el-button v-if="canDelete" type="danger" plain :loading="actionSaving" @click="removeDraft"><Trash2 :size="16" />删除草稿</el-button>
|
||||
<el-button v-if="canRelease" type="primary" plain :loading="actionSaving" @click="openReleaseDialog">放舱</el-button>
|
||||
<el-button v-if="canSave" type="primary" :loading="saving" @click="save"><Save :size="16" />保存草稿</el-button>
|
||||
<el-button v-if="canDelete" type="danger" plain :loading="actionSaving" :disabled="workflowBusy" @click="removeDraft"><Trash2 :size="16" />删除草稿</el-button>
|
||||
<el-button v-if="canRelease" type="primary" plain :loading="actionSaving" :disabled="workflowBusy" @click="openReleaseDialog">放舱</el-button>
|
||||
<el-button v-if="canSave" type="primary" :loading="saving" :disabled="workflowBusy" @click="save"><Save :size="16" />保存草稿</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-alert v-if="optionsError" type="warning" :closable="false" show-icon :title="optionsError"><el-button link type="warning" :loading="optionsLoading" @click="loadOptions">重新加载选项</el-button></el-alert>
|
||||
@@ -437,15 +454,15 @@ onMounted(() => { void load(); void loadOptions() })
|
||||
</template>
|
||||
</DetailPageLayout>
|
||||
|
||||
<AppDialog v-model="releaseDialogOpen" title="确认放舱" width="min(560px, 94vw)" :confirm-loading="actionSaving" confirm-text="确认放舱" cancel-text="取消" @confirm="submitRelease">
|
||||
<AppDialog v-model="releaseDialogOpen" title="确认放舱" width="min(560px, 94vw)" :confirm-loading="actionSaving" :confirm-disabled="workflowBusy" confirm-text="确认放舱" cancel-text="取消" @confirm="submitRelease">
|
||||
<el-form label-position="top">
|
||||
<el-alert title="放舱后资料将同步到关联货柜,不能修改、不能删除;填写错误只能由管理员处理数据库,请确认资料无误。" type="warning" :closable="false" show-icon />
|
||||
<el-descriptions class="release-summary" :column="2" border>
|
||||
<el-descriptions-item label="提单号">{{ detail?.billOfLadingNo || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="船名">{{ detail?.vesselName || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="航次">{{ detail?.voyageNo || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="预计开船">{{ detail?.estimatedSailingAt || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="预计到港">{{ detail?.estimatedArrivalAt || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="提单号">{{ form.data.billOfLadingNo || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="船名">{{ form.data.vesselName || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="航次">{{ form.data.voyageNo || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="预计开船">{{ form.data.estimatedSailingAt || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="预计到港">{{ form.data.estimatedArrivalAt || '—' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-form>
|
||||
</AppDialog>
|
||||
|
||||
Reference in New Issue
Block a user