feat(web-admin): gate load products by selected contract
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
bbab39e6b7
commit
9b8848ea8c
2 files changed
+159
-14
No files matched your search
@@ -284,6 +284,7 @@
|
||||
/>
|
||||
<ProductSelector
|
||||
:visible="productSelectorVisible"
|
||||
:allowed-product-ids="productSelectorAllowedIds"
|
||||
@update:visible="productSelectorVisible = $event"
|
||||
@confirm="handleProductConfirm"
|
||||
/>
|
||||
@@ -340,6 +341,7 @@
|
||||
usePlRemoteVehicleOptions
|
||||
} from '@/composables/usePlRemoteOptions'
|
||||
import { fetchCustomerOptions } from '@/api/crm/customer'
|
||||
import { getContract } from '@/api/crm/contract/index'
|
||||
import CustomerSelector from '@/components/CustomerSelector.vue'
|
||||
import ProductSelector from '@/components/business/product-selector/index.vue'
|
||||
import PlPlanLoadSection from '@/components/business/pl-plan-load-section/index.vue'
|
||||
@@ -396,6 +398,11 @@
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { formatDate } from '@/utils/format'
|
||||
import { pickBxData } from '@/utils/http/bx'
|
||||
import { isPlInternalCustomerName } from '@/utils/pl-internal-customer'
|
||||
import {
|
||||
collectContractProductIds,
|
||||
filterProductsByAllowedProductIds
|
||||
} from '@/utils/pl-plan-contract-product'
|
||||
import { getPersonnelAffiliationLabel } from '@/utils/pl-personnel-cert'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -504,6 +511,12 @@
|
||||
const customerOptions = ref<CustomerOptionRow[]>([])
|
||||
const customerOptionsLoading = ref(false)
|
||||
const productSelectorVisible = ref(false)
|
||||
/** 当前装货合同明细中的 CRM productId(非内部客户限制选品) */
|
||||
const loadContractProductIds = ref<string[]>([])
|
||||
const productSelectorAllowedIds = computed(() => {
|
||||
if (isPlInternalCustomerName(loadForm.value.customerName)) return undefined
|
||||
return loadContractProductIds.value
|
||||
})
|
||||
const vehicleViewVisible = ref(false)
|
||||
const vehicleLibraryVisible = ref(false)
|
||||
const carrierLibraryVisible = ref(false)
|
||||
@@ -994,6 +1007,40 @@
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => loadForm.value.contracts,
|
||||
async (contracts, prev) => {
|
||||
const id = String(contracts?.[0]?.contractId || '').trim()
|
||||
const prevId = String(prev?.[0]?.contractId || '').trim()
|
||||
if (!id) {
|
||||
loadContractProductIds.value = []
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await getContract(id)
|
||||
const detail = pickBxData<{ products?: Array<{ productId?: string | null }> }>(res)
|
||||
loadContractProductIds.value = collectContractProductIds(detail?.products || [])
|
||||
} catch {
|
||||
loadContractProductIds.value = []
|
||||
}
|
||||
// 合同 id 变化时过滤装货产品(详情首次灌入同 id / 加载中不刷)
|
||||
if (
|
||||
prev &&
|
||||
id !== prevId &&
|
||||
!detailLoading.value &&
|
||||
!suppressStopProductSync.value
|
||||
) {
|
||||
const nextProducts = filterProductsByAllowedProductIds(
|
||||
loadForm.value.products || [],
|
||||
loadContractProductIds.value
|
||||
)
|
||||
loadForm.value = { ...loadForm.value, products: nextProducts }
|
||||
syncFromLoadProducts()
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
function openCustomerSelector(side: CustomerSide, deliveryIndex = 0) {
|
||||
customerSelectorSide.value = side
|
||||
customerSelectorDeliveryIndex.value = deliveryIndex
|
||||
@@ -1665,6 +1712,64 @@
|
||||
return true
|
||||
}
|
||||
|
||||
function stopHasSelectedContract(contracts?: { contractId?: string }[] | null) {
|
||||
return (contracts || []).some((item) => !!String(item.contractId || '').trim())
|
||||
}
|
||||
|
||||
/** 非内部客户:装/送货段合同至多 1 份;已选 1 份时校验装货产品归属合同 */
|
||||
function validateSingleContractPerStop(): boolean {
|
||||
if (
|
||||
!isPlInternalCustomerName(loadForm.value.customerName) &&
|
||||
(loadForm.value.contracts || []).length > 1
|
||||
) {
|
||||
ElMessage.warning('装货合同只能保留 1 份,请移除多余合同后再保存')
|
||||
return false
|
||||
}
|
||||
for (let i = 0; i < deliveryForms.value.length; i += 1) {
|
||||
const row = deliveryForms.value[i]
|
||||
if (isPlInternalCustomerName(row.customerName)) continue
|
||||
if ((row.contracts || []).length > 1) {
|
||||
ElMessage.warning(`送货段#${i + 1}合同只能保留 1 份,请移除多余合同后再保存`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (
|
||||
!isPlInternalCustomerName(loadForm.value.customerName) &&
|
||||
(loadForm.value.contracts || []).length === 1 &&
|
||||
loadContractProductIds.value.length
|
||||
) {
|
||||
const allowed = new Set(loadContractProductIds.value.map((id) => String(id).trim()))
|
||||
const orphan = (loadForm.value.products || []).some(
|
||||
(row) => !allowed.has(String(row.productId || '').trim())
|
||||
)
|
||||
if (orphan) {
|
||||
ElMessage.warning('装货产品须属于所选合同')
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/** 安排运输前:非内部客户(山峡/云栋)的装/送货段必须至少选一份合同 */
|
||||
function validateContractsForArrange() {
|
||||
if (
|
||||
!isPlInternalCustomerName(loadForm.value.customerName) &&
|
||||
!stopHasSelectedContract(loadForm.value.contracts)
|
||||
) {
|
||||
ElMessage.warning('安排运输前请为装货客户选择合同')
|
||||
return false
|
||||
}
|
||||
for (let i = 0; i < deliveryForms.value.length; i += 1) {
|
||||
const row = deliveryForms.value[i]
|
||||
if (isPlInternalCustomerName(row.customerName)) continue
|
||||
if (!stopHasSelectedContract(row.contracts)) {
|
||||
ElMessage.warning(`安排运输前请为送货段#${i + 1}选择合同`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/** 安排运输前必填:创建日期、计划日期、归属公司、单据类型、跟进人 */
|
||||
function getArrangePrerequisiteGaps(): string[] {
|
||||
const missing: string[] = []
|
||||
@@ -1688,6 +1793,7 @@
|
||||
try {
|
||||
const valid = await basicFormRef.value?.validate().catch(() => false)
|
||||
if (!valid || !validateBusinessRules()) return
|
||||
if (!validateSingleContractPerStop()) return
|
||||
|
||||
if (mode === 'saveAndArrange' || mode === 'rearrange') {
|
||||
const missing = getArrangePrerequisiteGaps()
|
||||
@@ -1699,6 +1805,7 @@
|
||||
ElMessage.warning('安排运输前请选择车辆')
|
||||
return
|
||||
}
|
||||
if (!validateContractsForArrange()) return
|
||||
}
|
||||
|
||||
mirrorPlanProductsFromLoad()
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
<PlPlanContractMultiSelect
|
||||
v-model="contractsModel"
|
||||
:customer-id="modelValue.customerId"
|
||||
:disabled="!modelValue.customerId || contractDisabled"
|
||||
:required="false"
|
||||
:disabled="!modelValue.customerId || isInternal"
|
||||
:required="!!modelValue.customerId && !isInternal"
|
||||
compact-summary
|
||||
>
|
||||
<template #toolbar="{ open, disabled, hint, hintWarn }">
|
||||
@@ -47,7 +47,11 @@
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="装货合同" class="pl-plan-party-select-row__contract">
|
||||
<el-form-item
|
||||
v-if="!isInternal"
|
||||
label="装货合同"
|
||||
class="pl-plan-party-select-row__contract"
|
||||
>
|
||||
<div class="pl-plan-contract-trigger">
|
||||
<el-button class="btn-ghost" :disabled="disabled" @click="open">
|
||||
<el-icon><Search /></el-icon>选择合同
|
||||
@@ -104,27 +108,35 @@
|
||||
<div class="pl-plan-dibang-row">
|
||||
<el-checkbox
|
||||
:model-value="modelValue.generateDibang === 1"
|
||||
:disabled="isInternal"
|
||||
disabled
|
||||
@update:model-value="handleDibangToggle"
|
||||
>
|
||||
生成地磅单({{ dibangDirectionLabel }})
|
||||
</el-checkbox>
|
||||
<span v-if="isInternal" class="pl-plan-dibang-row__hint">本段客户为内部客户时默认勾选</span>
|
||||
<span v-else-if="modelValue.customerId" class="pl-plan-dibang-row__hint">
|
||||
仅安庆山峡/安徽云栋可生成地磅单
|
||||
</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="pl-plan-stop-products-head">
|
||||
<span class="pl-plan-stop-products-head__title">装货产品</span>
|
||||
<el-button
|
||||
<span
|
||||
v-if="!readonly"
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="emit('add-products')"
|
||||
class="pl-plan-stop-products-head__add-wrap"
|
||||
@click="handleAddProductsClick"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>添加产品
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="addProductsDisabled"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>添加产品
|
||||
</el-button>
|
||||
</span>
|
||||
</div>
|
||||
<p v-if="!modelValue.products.length" class="pl-plan-load-section__empty-hint">
|
||||
请添加至少一条装货产品
|
||||
@@ -180,7 +192,20 @@
|
||||
const customerOptionsLoading = ref(false)
|
||||
|
||||
const isInternal = computed(() => isPlInternalCustomerName(props.modelValue.customerName))
|
||||
const contractDisabled = computed(() => isInternal.value)
|
||||
|
||||
/** 非内部客户须先选装货合同才能添加产品;内部客户不限制 */
|
||||
const addProductsDisabled = computed(() => {
|
||||
if (isInternal.value) return false
|
||||
return !(props.modelValue.contracts || []).some((c) => String(c.contractId || '').trim())
|
||||
})
|
||||
|
||||
function handleAddProductsClick() {
|
||||
if (addProductsDisabled.value) {
|
||||
ElMessage.warning('请先选择装货合同后再添加产品')
|
||||
return
|
||||
}
|
||||
emit('add-products')
|
||||
}
|
||||
|
||||
const dibangDirectionLabel = computed(() => {
|
||||
if (props.modelValue.dibangDirection === 'outbound') return '出库'
|
||||
@@ -363,10 +388,14 @@
|
||||
}
|
||||
|
||||
function handleDibangToggle(checked: boolean) {
|
||||
const direction = isInternal.value ? 'outbound' : props.modelValue.dibangDirection || 'none'
|
||||
// 仅山峡/云栋可生成地磅单,且由客户类型自动决定,不允许手动勾选
|
||||
if (!isInternal.value) {
|
||||
patch({ generateDibang: 0, dibangDirection: 'none' })
|
||||
return
|
||||
}
|
||||
patch({
|
||||
generateDibang: checked ? 1 : 0,
|
||||
dibangDirection: checked ? direction : 'none'
|
||||
dibangDirection: checked ? 'outbound' : 'none'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -400,6 +429,15 @@
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.pl-plan-stop-products-head__add-wrap {
|
||||
display: inline-flex;
|
||||
cursor: pointer;
|
||||
|
||||
:deep(.el-button.is-disabled) {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.pl-plan-load-section__empty-hint {
|
||||
margin: 8px 0 0;
|
||||
font-size: 12px;
|
||||
|
||||
Reference in new issue
Block a user