fix: 合并采集协议单地区经理负责人筛选修复 (#2)
用户已确认 Web 与小程序测试通过并授权合入 V2.0。Refs #1;合并后执行集成回归再关闭工单。
This commit was merged in pull request #2.
This commit is contained in:
commit
05de8924b0
14 files changed
+458
-75
No files matched your search
@@ -19,6 +19,8 @@
|
||||
- 仅做任务必要的改动,不夹带无关重构或全仓库格式化;保留用户已有改动。
|
||||
- **不得自行停止、启动或重启 `web-admin` 开发服务与 `backend` 服务。** 需要重启时告知用户,由用户操作。
|
||||
- 提交使用 Conventional Commits,完整说明“改了什么、为什么”。
|
||||
- 需求开发与 Bug 修复:用户给出 Gitea 工单编号或链接时,先读取该工单描述、评论和验收条件再处理;未给编号时,先在当前工程 Gitea 仓库创建工单,再开发。继续当前已关联任务时复用原工单,不重复创建;已有授权下由 AI 创建和回写,不再要求用户代建。
|
||||
- 修复后回写工单,附提交、PR、验证结果及未完成项,标注“已修复”;待验收、未合并或未发布须分别说明,不能以“已修复”代替验收和关闭条件。
|
||||
- 不把真实凭据、业务导出、虚拟环境、构建产物和工具临时状态新增到版本管理。
|
||||
- 清理文件前核查代码、测试、脚本、部署及文档引用;不能只凭名称、日期或搜索无结果删除。
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from app.core.menu_guard import require_admin_or_super_admin
|
||||
from app.core.response import error_response, success_response
|
||||
from app.core.utils import parse_datetime
|
||||
from app.crud import collection_protocol as cp_svc
|
||||
from app.crud import collection_region as region_svc
|
||||
from app.models.models import (
|
||||
BizCollectionProtocol,
|
||||
BizProtocolPartyCustomer,
|
||||
@@ -146,6 +147,15 @@ async def list_collection_protocols(
|
||||
return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
|
||||
|
||||
|
||||
@router.get("/collection-protocols/region-managers")
|
||||
async def list_protocol_region_managers(
|
||||
user: dict = Depends(require_admin_or_super_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
tenant_no = _get_tenant_no(db, user)
|
||||
return success_response({"list": region_svc.list_active_region_leaders(db, tenant_no=tenant_no)})
|
||||
|
||||
|
||||
@router.get("/collection-protocols/{protocol_id}")
|
||||
async def get_collection_protocol_detail(
|
||||
protocol_id: str,
|
||||
|
||||
@@ -496,6 +496,35 @@ def list_regions_by_staff_user(
|
||||
return map_staff_region_rows_to_list([(r[0], r[1]) for r in q.all()])
|
||||
|
||||
|
||||
def list_active_region_leaders(db: Session, *, tenant_no: str) -> List[Dict[str, Any]]:
|
||||
"""协议表单候选:租户内有效地区负责人,按用户去重并保留负责地区。"""
|
||||
if not tenant_no:
|
||||
raise ValueError("租户信息缺失")
|
||||
rows = (
|
||||
db.query(SysUser.id, SysUser.nick_name, BizCollectionRegionStaff.region_id)
|
||||
.join(BizCollectionRegionStaff, BizCollectionRegionStaff.user_id == SysUser.id)
|
||||
.join(BizCollectionRegion, BizCollectionRegion.id == BizCollectionRegionStaff.region_id)
|
||||
.filter(
|
||||
BizCollectionRegion.tenant_no == tenant_no,
|
||||
BizCollectionRegion.del_flag == 0,
|
||||
BizCollectionRegionStaff.tenant_no == tenant_no,
|
||||
BizCollectionRegionStaff.del_flag == 0,
|
||||
BizCollectionRegionStaff.is_leader == 1,
|
||||
SysUser.tenant_no == tenant_no,
|
||||
SysUser.del_flag == 0,
|
||||
SysUser.status == "1",
|
||||
)
|
||||
.distinct()
|
||||
.order_by(SysUser.nick_name.asc(), SysUser.id.asc(), BizCollectionRegionStaff.region_id.asc())
|
||||
.all()
|
||||
)
|
||||
leaders: Dict[str, Dict[str, Any]] = {}
|
||||
for user_id, nick_name, region_id in rows:
|
||||
item = leaders.setdefault(user_id, {"id": user_id, "nickName": nick_name or "", "regionIds": []})
|
||||
item["regionIds"].append(region_id)
|
||||
return list(leaders.values())
|
||||
|
||||
|
||||
def list_staff(
|
||||
db: Session,
|
||||
*,
|
||||
|
||||
@@ -470,6 +470,24 @@ def get_form_options(
|
||||
'currentUserRegions': current_user_regions,
|
||||
}
|
||||
if normalized_module == 'protocol':
|
||||
# 地区经理独立取全部有效地区负责人,不随医院/地区人员联动缩小。
|
||||
manager_rows = (
|
||||
db.query(SysUser.id, SysUser.nick_name)
|
||||
.join(BizCollectionRegionStaff, BizCollectionRegionStaff.user_id == SysUser.id)
|
||||
.filter(
|
||||
BizCollectionRegionStaff.region_id.in_(active_region_ids),
|
||||
BizCollectionRegionStaff.tenant_no == ctx.tenant_no,
|
||||
BizCollectionRegionStaff.del_flag == 0,
|
||||
BizCollectionRegionStaff.is_leader == 1,
|
||||
SysUser.tenant_no == ctx.tenant_no,
|
||||
SysUser.del_flag == 0,
|
||||
SysUser.status == '1',
|
||||
)
|
||||
.distinct()
|
||||
.order_by(SysUser.nick_name.asc(), SysUser.id.asc())
|
||||
.all()
|
||||
)
|
||||
result['regionManagers'] = [_option(row.id, row.nick_name) for row in manager_rows]
|
||||
ethnicities = (
|
||||
db.query(SysEthnic)
|
||||
.filter(
|
||||
|
||||
@@ -131,6 +131,60 @@ def _seed_options(db):
|
||||
db.commit()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('region_id', [None, 'ra', 'rb'])
|
||||
def test_protocol_manager_options_only_include_active_tenant_region_leaders(region_id):
|
||||
db = _session()
|
||||
_seed_options(db)
|
||||
common = dict(tenant_no='tenant-a', create_dept_id='', del_flag=0)
|
||||
for suffix in ('c', 'd'):
|
||||
db.add(BizCollectionRegion(id=f'r{suffix}', region_name=f'{suffix}区', **common))
|
||||
db.add(SysUser(id=f'leader-{suffix}', user_no=f'l{suffix}', nick_name=f'{suffix}负责人', status='1', **common))
|
||||
db.add(BizCollectionRegionStaff(id=f'leader-binding-{suffix}', region_id=f'r{suffix}', user_id=f'leader-{suffix}', is_leader=1, **common))
|
||||
db.add(BizCollectionRegion(id='deleted-region', region_name='已删区', **{**common, 'del_flag': 1}))
|
||||
db.add(SysUser(id='deleted-user', user_no='deleted', nick_name='已删用户', status='1', **{**common, 'del_flag': 1}))
|
||||
for binding_id, region, user, tenant, deleted in [
|
||||
('duplicate-leader', 'rb', 'leader-a', 'tenant-a', 0),
|
||||
('disabled-leader', 'ra', 'disabled', 'tenant-a', 0),
|
||||
('deleted-user-leader', 'ra', 'deleted-user', 'tenant-a', 0),
|
||||
('deleted-region-leader', 'deleted-region', 'staff-a', 'tenant-a', 0),
|
||||
('deleted-binding', 'ra', 'staff-a', 'tenant-a', 1),
|
||||
('cross-tenant-user', 'ra', 'cross-user', 'tenant-a', 0),
|
||||
('cross-tenant-binding', 'ra', 'staff-a', 'tenant-b', 0),
|
||||
('cross-tenant-region', 'rx', 'staff-a', 'tenant-a', 0),
|
||||
]:
|
||||
db.add(BizCollectionRegionStaff(id=binding_id, region_id=region, user_id=user, is_leader=1,
|
||||
tenant_no=tenant, create_dept_id='', del_flag=deleted))
|
||||
db.commit()
|
||||
|
||||
out = market_support.get_form_options(db, module='protocol', ctx=_ctx(), user_info=_user(), region_id=region_id)
|
||||
assert out['regionManagers'] == [
|
||||
{'value': 'leader-a', 'label': 'A负责人'},
|
||||
{'value': 'user-b', 'label': 'B经理'},
|
||||
{'value': 'leader-c', 'label': 'c负责人'},
|
||||
{'value': 'leader-d', 'label': 'd负责人'},
|
||||
]
|
||||
from app.crud import collection_region
|
||||
leaders = collection_region.list_active_region_leaders(db, tenant_no='tenant-a')
|
||||
assert [{'value': row['id'], 'label': row['nickName']} for row in leaders] == out['regionManagers']
|
||||
assert leaders[0]['regionIds'] == ['ra', 'rb']
|
||||
assert all(set(row) == {'id', 'nickName', 'regionIds'} for row in leaders)
|
||||
if region_id == 'ra':
|
||||
assert 'staff-a' in {row['value'] for row in out['staff']}
|
||||
assert 'user-b' not in {row['value'] for row in out['staff']}
|
||||
db.close()
|
||||
|
||||
|
||||
def test_protocol_manager_options_are_empty_without_leaders():
|
||||
db = _session()
|
||||
_seed_options(db)
|
||||
db.query(BizCollectionRegionStaff).update({'is_leader': 0})
|
||||
db.commit()
|
||||
out = market_support.get_form_options(db, module='protocol', ctx=_ctx(), user_info=_user())
|
||||
assert out['regionManagers'] == []
|
||||
assert out['staff']
|
||||
db.close()
|
||||
|
||||
|
||||
def test_specialist_form_options_list_all_tenant_regions_like_web(monkeypatch):
|
||||
"""市场专员采集地区选项应与 Web 一致:本租户全部地区,不依赖人员绑定。"""
|
||||
from app.services.market_support import get_form_options
|
||||
@@ -2469,3 +2523,25 @@ def test_payment_transfer_response_carries_server_authorized_capabilities():
|
||||
'submit-finance',
|
||||
]
|
||||
assert pending['allowedActions'] == ['withdraw-transfer']
|
||||
|
||||
|
||||
def test_web_protocol_manager_options_derive_tenant_from_authenticated_user():
|
||||
from app.api import collection_protocol
|
||||
from app.core.menu_guard import require_admin_or_super_admin
|
||||
db = _session()
|
||||
_seed_options(db)
|
||||
response = asyncio.run(collection_protocol.list_protocol_region_managers(
|
||||
user={**_user(), 'tenant_no': 'tenant-b'}, db=db))
|
||||
assert response['data']['list'] == [
|
||||
{'id': 'leader-a', 'nickName': 'A负责人', 'regionIds': ['ra']},
|
||||
{'id': 'user-b', 'nickName': 'B经理', 'regionIds': ['rb']},
|
||||
]
|
||||
route = next(route for route in collection_protocol.router.routes
|
||||
if route.path == '/web/admin/collection-protocols/region-managers')
|
||||
assert any(dep.call == require_admin_or_super_admin for dep in route.dependant.dependencies)
|
||||
paths = [route.path for route in collection_protocol.router.routes]
|
||||
assert paths.index(route.path) < paths.index('/web/admin/collection-protocols/{protocol_id}')
|
||||
db.query(BizCollectionRegionStaff).update({'is_leader': 0})
|
||||
db.commit()
|
||||
assert asyncio.run(collection_protocol.list_protocol_region_managers(user=_user(), db=db))['data']['list'] == []
|
||||
db.close()
|
||||
@@ -8,6 +8,8 @@ const source = readFileSync(new URL('../views/market/MarketModuleForm.vue', impo
|
||||
const start = source.indexOf('async function applyCollectionRegion(')
|
||||
const end = source.indexOf('\nfunction sampleKey(', start)
|
||||
|
||||
const regionManagers = [{ value: 'ma', label: '经理 A' }, { value: 'mb', label: '经理 B' }]
|
||||
|
||||
function createHarness() {
|
||||
const formData = { value: { collection: {
|
||||
regionId: 'a', regionName: '地区 A', hospitalId: 'ha', hospitalName: '医院 A',
|
||||
@@ -17,7 +19,8 @@ function createHarness() {
|
||||
regions: [{ value: 'a', label: '地区 A' }, { value: 'b', label: '地区 B' }],
|
||||
hospitals: [{ value: 'ha', label: '医院 A' }],
|
||||
staff: [{ value: 'ma', label: '经理 A' }],
|
||||
managerStaff: [{ value: 'ma', label: '经理 A' }]
|
||||
managerStaff: regionManagers,
|
||||
regionManagers
|
||||
} }
|
||||
const requests = []
|
||||
const messages = []
|
||||
@@ -38,6 +41,7 @@ function createHarness() {
|
||||
}
|
||||
|
||||
const optionsB = {
|
||||
regionManagers,
|
||||
hospitals: [{ value: 'hb', label: '医院 B' }],
|
||||
staff: [{ value: 'mb', label: '经理 B', isLeader: true }]
|
||||
}
|
||||
@@ -53,7 +57,7 @@ test('changing protocol region clears the old manager and selects the new region
|
||||
assert.equal(during.regionManagerUserName, '')
|
||||
assert.deepEqual(pendingHospitals, [])
|
||||
assert.equal(h.formData.value.collection.regionManagerUserId, 'mb')
|
||||
assert.deepEqual(h.formOptions.value.managerStaff.map((item) => item.value), ['mb'])
|
||||
assert.deepEqual(h.formOptions.value.managerStaff.map((item) => item.value), ['ma', 'mb'])
|
||||
})
|
||||
|
||||
test('clearing protocol region invalidates a pending response and clears dependent selections', async () => {
|
||||
@@ -67,7 +71,7 @@ test('clearing protocol region invalidates a pending response and clears depende
|
||||
assert.equal(h.formData.value.collection.regionName, '')
|
||||
assert.equal(h.formData.value.collection.regionManagerUserId, '')
|
||||
assert.deepEqual(h.formOptions.value.hospitals, [])
|
||||
assert.deepEqual(h.formOptions.value.managerStaff, [])
|
||||
assert.deepEqual(h.formOptions.value.managerStaff, regionManagers)
|
||||
})
|
||||
|
||||
test('out-of-order protocol region responses cannot replace the latest selection', async () => {
|
||||
@@ -82,3 +86,13 @@ test('out-of-order protocol region responses cannot replace the latest selection
|
||||
assert.deepEqual(h.formOptions.value.hospitals.map((item) => item.value), ['hb'])
|
||||
assert.equal(h.formData.value.collection.regionManagerUserId, 'mb')
|
||||
})
|
||||
|
||||
|
||||
test('a region without a leader does not default its ordinary member as manager', async () => {
|
||||
const h = createHarness()
|
||||
const pending = h.change('b', { defaultManager: true })
|
||||
h.requests[0].resolve({ ...optionsB, staff: [{ value: 'member', label: '普通员工', isLeader: false }] })
|
||||
await pending
|
||||
assert.equal(h.formData.value.collection.regionManagerUserId, '')
|
||||
assert.deepEqual(h.formOptions.value.managerStaff, regionManagers)
|
||||
})
|
||||
@@ -115,6 +115,9 @@ export function hydrateProtocolReferenceOptions(options, detail) {
|
||||
const collection = detail?.collection || {}
|
||||
const samples = Array.isArray(detail?.samples) ? detail.samples : []
|
||||
const currentSampleCodeIds = samples.map((item) => item?.sampleCodeId).filter(Boolean)
|
||||
const regionManagers = personOptions(source.regionManagers, [])
|
||||
const managerId = normalizedText(collection.regionManagerUserId)
|
||||
const currentManagerIsEligible = regionManagers.some((item) => normalizedText(item.value) === managerId)
|
||||
const sampleTypeReference = (item) => {
|
||||
const extra = {}
|
||||
const storagePrice = item?.originalPrice ?? item?.price
|
||||
@@ -141,8 +144,11 @@ export function hydrateProtocolReferenceOptions(options, detail) {
|
||||
]
|
||||
),
|
||||
managerStaff: personOptions(
|
||||
source.staff,
|
||||
reference(collection.regionManagerUserId, collection.regionManagerUserName)
|
||||
regionManagers,
|
||||
// 历史负责人仅用于姓名回显,不重新加入可选人员。
|
||||
currentManagerIsEligible ? [] : reference(
|
||||
collection.regionManagerUserId, collection.regionManagerUserName, { displayOnly: true }
|
||||
)
|
||||
),
|
||||
regions: referenceOptions(source.regions, reference(collection.regionId, collection.regionName)),
|
||||
hospitals: referenceOptions(source.hospitals, reference(collection.hospitalId, collection.hospitalName)),
|
||||
|
||||
@@ -63,7 +63,8 @@ test('protocol options hydrate all selected reference values from detail labels'
|
||||
options.salesStaff.map((item) => item.label),
|
||||
['现有专员', '市场专员', '当前登录人']
|
||||
)
|
||||
assert.deepEqual(options.managerStaff.map((item) => item.label), ['现有专员', '地区经理'])
|
||||
assert.deepEqual(options.managerStaff.map((item) => item.label), ['地区经理'])
|
||||
assert.equal(options.managerStaff[0].displayOnly, true)
|
||||
assert.deepEqual(options.regions, [{ value: 'region-1', label: '合肥区域' }])
|
||||
assert.deepEqual(options.hospitals, [{ value: 'hospital-1', label: '省妇幼' }])
|
||||
assert.deepEqual(options.sampleTypes, [{ value: 'type-1', label: '脐带血' }])
|
||||
@@ -290,3 +291,27 @@ test('presign options retain current values without exposing raw ids', () => {
|
||||
assert.deepEqual(presign.orderStatuses, [{ value: 'raw-order-status', label: '未知' }])
|
||||
assert.deepEqual(presign.transferTypes, [{ value: 'raw-transfer', label: '未知' }])
|
||||
})
|
||||
|
||||
|
||||
test('protocol managers use only the dedicated leader list and keep old names for display', () => {
|
||||
const source = {
|
||||
staff: [{ value: 'member', label: '普通员工' }],
|
||||
regionManagers: [
|
||||
{ value: 'manager-a', label: 'A负责人' },
|
||||
{ value: 'manager-b', label: 'B负责人' }
|
||||
],
|
||||
currentUser: { value: 'member', label: '普通员工' }
|
||||
}
|
||||
const historical = hydrateProtocolReferenceOptions(source, {
|
||||
collection: { regionManagerUserId: 'old', regionManagerUserName: '历史负责人' }
|
||||
})
|
||||
assert.deepEqual(historical.managerStaff, [
|
||||
...source.regionManagers,
|
||||
{ value: 'old', label: '历史负责人', displayOnly: true }
|
||||
])
|
||||
const current = hydrateProtocolReferenceOptions(source, {
|
||||
collection: { regionManagerUserId: 'manager-a', regionManagerUserName: '旧姓名' }
|
||||
})
|
||||
assert.deepEqual(current.managerStaff, source.regionManagers)
|
||||
assert.deepEqual(hydrateProtocolReferenceOptions({ staff: source.staff }, {}).managerStaff, [])
|
||||
})
|
||||
@@ -128,7 +128,8 @@ const normalizedOptions = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
const pickerColumns = computed(() => normalizedOptions.value)
|
||||
// 历史引用可回显,但不作为新的选择候选。
|
||||
const pickerColumns = computed(() => normalizedOptions.value.filter((option) => !option.displayOnly))
|
||||
const columnsFieldNames = computed(() => ({
|
||||
text: 'text',
|
||||
value: 'value',
|
||||
@@ -170,8 +171,8 @@ const pickerValue = computed(() => {
|
||||
|
||||
const filteredOptions = computed(() => {
|
||||
const q = keyword.value.trim().toLowerCase()
|
||||
if (!q) return normalizedOptions.value
|
||||
return normalizedOptions.value.filter(
|
||||
if (!q) return pickerColumns.value
|
||||
return pickerColumns.value.filter(
|
||||
(o) =>
|
||||
String(o.text || '')
|
||||
.toLowerCase()
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
const source = readFileSync(new URL('./components/OaSelectField.vue', import.meta.url), 'utf8')
|
||||
const start = source.indexOf('const textKey = computed(')
|
||||
const end = source.indexOf('\nfunction emitValue(', start)
|
||||
|
||||
function selectState(props, keyword = '') {
|
||||
return new Function('props', 'computed', 'isMultiple', 'keyword',
|
||||
`${source.slice(start, end)}; return { pickerColumns, filteredOptions, displayText }`
|
||||
)(props, (read) => ({ get value() { return read() } }), { value: !!props.multiple }, { value: keyword })
|
||||
}
|
||||
|
||||
test('historical values retain readable labels but cannot appear in the picker or search', () => {
|
||||
const options = [
|
||||
{ value: 'active', label: '现任负责人' },
|
||||
{ value: 'old', label: '历史负责人', displayOnly: true }
|
||||
]
|
||||
const state = selectState({ options, modelValue: 'old' })
|
||||
assert.equal(state.displayText.value, '历史负责人')
|
||||
assert.deepEqual(state.pickerColumns.value.map((item) => item.value), ['active'])
|
||||
assert.deepEqual(state.filteredOptions.value.map((item) => item.value), ['active'])
|
||||
assert.deepEqual(selectState({ options, multiple: true }, '历史').filteredOptions.value, [])
|
||||
assert.equal(selectState({ options, modelValue: 'active' }).displayText.value, '现任负责人')
|
||||
})
|
||||
@@ -602,12 +602,11 @@ async function applyCollectionRegion(regionId, { resetHospital = true, defaultMa
|
||||
collection.regionManagerUserName = ''
|
||||
}
|
||||
formData.value = { ...formData.value, collection }
|
||||
formOptions.value = {
|
||||
formOptions.value = hydrateProtocolReferenceOptions({
|
||||
...formOptions.value,
|
||||
...(resetHospital ? { hospitals: [] } : {}),
|
||||
staff: [],
|
||||
managerStaff: []
|
||||
}
|
||||
staff: []
|
||||
}, formData.value)
|
||||
if (!value) return
|
||||
try {
|
||||
const options = await fetchMarketFormOptions(
|
||||
@@ -621,7 +620,11 @@ async function applyCollectionRegion(regionId, { resetHospital = true, defaultMa
|
||||
formData.value
|
||||
)
|
||||
if (defaultManager && !formData.value.collection?.regionManagerUserId) {
|
||||
const leader = (options?.staff || []).find((item) => item.isLeader) || options?.staff?.[0]
|
||||
const leader = (options?.staff || []).find((item) =>
|
||||
item.isLeader && (options?.regionManagers || []).some(
|
||||
(manager) => String(manager.value) === String(item.value)
|
||||
)
|
||||
)
|
||||
if (leader?.value) {
|
||||
formData.value = {
|
||||
...formData.value,
|
||||
|
||||
@@ -27,6 +27,14 @@ export function fetchCollectionProtocolList(params?: {
|
||||
})
|
||||
}
|
||||
|
||||
export type ProtocolRegionManager = { id: string; nickName: string; regionIds: string[] }
|
||||
|
||||
export function fetchProtocolRegionManagers() {
|
||||
return request.get<{ list: ProtocolRegionManager[] }>({
|
||||
url: '/web/admin/collection-protocols/region-managers'
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchCollectionProtocolDetail(id: string) {
|
||||
return request.get<{
|
||||
protocol: Record<string, unknown>
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
import ts from 'typescript'
|
||||
|
||||
const source = readFileSync(
|
||||
new URL('../views/cell-storage/protocol/ProtocolFormDialog.vue', import.meta.url),
|
||||
'utf8'
|
||||
)
|
||||
function method(name: string) {
|
||||
const asyncStart = source.indexOf(` async function ${name}(`)
|
||||
const start = asyncStart >= 0 ? asyncStart : source.indexOf(` function ${name}(`)
|
||||
const end = source.indexOf('\n }', start) + 4
|
||||
return source.slice(start, end)
|
||||
}
|
||||
function harness() {
|
||||
const requests: Array<{ resolve: (value: any) => void; reject: (error: Error) => void }> = []
|
||||
const hospitals: Array<{ resolve: () => void }> = []
|
||||
const refs = {
|
||||
managerLoading: { value: false },
|
||||
managerUserOptions: { value: [] as any[] },
|
||||
managerSnapshot: { value: null as any },
|
||||
regionManagerUserId: { value: null as string | null },
|
||||
formData: { regionId: 'a', regionManagerUserId: '' }
|
||||
}
|
||||
const deps = {
|
||||
...refs,
|
||||
linkedRegionMode: { value: false },
|
||||
regionOptions: { value: [] },
|
||||
hospitalOptions: { value: [] },
|
||||
fetchRegionsByStaffUser: async () => ({ list: [{ id: 'b', regionName: 'B区' }] }),
|
||||
loadHospitalsByRegion: () => new Promise<void>((resolve) => hospitals.push({ resolve })),
|
||||
fetchProtocolRegionManagers: () =>
|
||||
new Promise((resolve, reject) => requests.push({ resolve, reject })),
|
||||
fetchUserList: async () => ({ list: [{ id: 'member', nickName: '普通员工' }] }),
|
||||
fetchRegionStaffList: async () => ({
|
||||
list: [{ userId: 'member', nickName: '普通员工', isLeader: false }]
|
||||
}),
|
||||
makeManagerUserStub: (id: string, nickName: string) => ({ id, nickName }),
|
||||
ElMessage: { error: () => {} }
|
||||
}
|
||||
const code = ts.transpileModule(
|
||||
`let managerSearchVersion = 0; let managerDefaultVersion = 0;
|
||||
${method('remoteSearchManagerUsers')}
|
||||
${method('applyDefaultRegionManager')}
|
||||
${method('onManagerUserChange')}
|
||||
${method('applyRegionsForSalesUser')}
|
||||
return { search: remoteSearchManagerUsers, apply: applyDefaultRegionManager, select: onManagerUserChange, applySales: applyRegionsForSalesUser }`,
|
||||
{ compilerOptions: { target: ts.ScriptTarget.ES2022 } }
|
||||
).outputText
|
||||
return {
|
||||
...refs,
|
||||
requests,
|
||||
hospitals,
|
||||
...new Function(...Object.keys(deps), code)(...Object.values(deps))
|
||||
}
|
||||
}
|
||||
const leaders = [
|
||||
{ id: 'head-a', nickName: 'A负责人', regionIds: ['a'] },
|
||||
{ id: 'head-b', nickName: 'B负责人', regionIds: ['b'] }
|
||||
]
|
||||
|
||||
test('manager search uses all region leaders without appending historical ordinary users', async () => {
|
||||
const h = harness()
|
||||
h.regionManagerUserId.value = 'member'
|
||||
h.managerSnapshot.value = { id: 'member', nickName: '历史人员' }
|
||||
const pending = h.search('')
|
||||
h.requests[0]?.resolve({ list: leaders })
|
||||
await pending
|
||||
assert.deepEqual(
|
||||
h.managerUserOptions.value.map((u: any) => u.id),
|
||||
['head-a', 'head-b']
|
||||
)
|
||||
assert.equal(h.managerSnapshot.value.nickName, '历史人员')
|
||||
})
|
||||
|
||||
test('a region without a leader stays empty instead of defaulting to an ordinary member', async () => {
|
||||
const h = harness()
|
||||
const pending = h.apply('a')
|
||||
h.requests[0]?.resolve({ list: [leaders[1]] })
|
||||
await pending
|
||||
assert.equal(h.formData.regionManagerUserId, '')
|
||||
assert.equal(h.regionManagerUserId.value, null)
|
||||
})
|
||||
|
||||
test('default manager belongs to selected region while candidates include all leaders', async () => {
|
||||
const h = harness()
|
||||
const pending = h.apply('a')
|
||||
h.requests[0]?.resolve({ list: leaders })
|
||||
await pending
|
||||
assert.equal(h.formData.regionManagerUserId, 'head-a')
|
||||
assert.deepEqual(
|
||||
h.managerUserOptions.value.map((u: any) => u.id),
|
||||
['head-a', 'head-b']
|
||||
)
|
||||
})
|
||||
|
||||
test('clearing a region invalidates pending manager defaults', async () => {
|
||||
const h = harness()
|
||||
const pending = h.apply('a')
|
||||
h.formData.regionId = ''
|
||||
await h.apply('')
|
||||
h.requests[0]?.resolve({ list: leaders })
|
||||
await pending
|
||||
assert.equal(h.formData.regionManagerUserId, '')
|
||||
})
|
||||
|
||||
test('out-of-order manager searches cannot overwrite the latest result', async () => {
|
||||
const h = harness()
|
||||
const first = h.search('A')
|
||||
const second = h.search('B')
|
||||
h.requests[1]?.resolve({ list: leaders })
|
||||
await second
|
||||
h.requests[0]?.resolve({ list: leaders })
|
||||
await first
|
||||
assert.deepEqual(
|
||||
h.managerUserOptions.value.map((u: any) => u.id),
|
||||
['head-b']
|
||||
)
|
||||
})
|
||||
|
||||
test('a manual selection is not overwritten by a pending default response', async () => {
|
||||
const h = harness()
|
||||
const pending = h.apply('a')
|
||||
h.managerUserOptions.value = leaders
|
||||
h.select('head-b')
|
||||
h.requests[0].resolve({ list: leaders })
|
||||
await pending
|
||||
assert.equal(h.formData.regionManagerUserId, 'head-b')
|
||||
})
|
||||
|
||||
test('failed manager loading clears the old default and never falls back to ordinary users', async () => {
|
||||
const h = harness()
|
||||
h.formData.regionManagerUserId = 'old'
|
||||
h.regionManagerUserId.value = 'old'
|
||||
const pending = h.apply('a')
|
||||
h.requests[0].reject(new Error('network failure'))
|
||||
await pending
|
||||
assert.equal(h.formData.regionManagerUserId, '')
|
||||
assert.deepEqual(h.managerUserOptions.value, [])
|
||||
})
|
||||
|
||||
test('a delayed default call for an old region cannot clear the current manager', async () => {
|
||||
const h = harness()
|
||||
h.formData.regionId = 'b'
|
||||
h.formData.regionManagerUserId = 'head-b'
|
||||
h.regionManagerUserId.value = 'head-b'
|
||||
const pending = h.apply('a')
|
||||
h.requests[0]?.resolve({ list: leaders })
|
||||
await pending
|
||||
assert.equal(h.formData.regionManagerUserId, 'head-b')
|
||||
assert.equal(h.regionManagerUserId.value, 'head-b')
|
||||
})
|
||||
|
||||
test('sales-linked region loading does not overwrite a manager chosen while hospitals load', async () => {
|
||||
const h = harness()
|
||||
const pending = h.applySales('sales-b')
|
||||
await Promise.resolve()
|
||||
h.managerUserOptions.value = leaders
|
||||
h.select('head-a')
|
||||
h.hospitals[0].resolve()
|
||||
await Promise.resolve()
|
||||
h.requests[0].resolve({ list: leaders })
|
||||
await pending
|
||||
assert.equal(h.formData.regionManagerUserId, 'head-a')
|
||||
})
|
||||
@@ -154,10 +154,21 @@
|
||||
clearable
|
||||
reserve-keyword
|
||||
:remote-method="remoteSearchManagerUsers"
|
||||
@visible-change="(visible: boolean) => visible && remoteSearchManagerUsers('')"
|
||||
:loading="managerLoading"
|
||||
placeholder="默认取地区负责人,可修改"
|
||||
@change="onManagerUserChange"
|
||||
>
|
||||
<el-option
|
||||
v-if="
|
||||
managerSnapshot &&
|
||||
!managerUserOptions.some((u) => u.id === managerSnapshot?.id)
|
||||
"
|
||||
v-show="false"
|
||||
:label="managerSnapshot.nickName"
|
||||
:value="managerSnapshot.id"
|
||||
disabled
|
||||
/>
|
||||
<el-option
|
||||
v-for="u in managerUserOptions"
|
||||
:key="u.id"
|
||||
@@ -1162,6 +1173,7 @@
|
||||
} from '@element-plus/icons-vue'
|
||||
import {
|
||||
fetchCollectionProtocolDetail,
|
||||
fetchProtocolRegionManagers,
|
||||
fetchCreateCollectionProtocol,
|
||||
fetchUpdateCollectionProtocol,
|
||||
fetchSubmitReceiptFinance,
|
||||
@@ -1188,7 +1200,6 @@
|
||||
fetchItemUnitList,
|
||||
fetchCollectionRegionList,
|
||||
fetchHospitalList,
|
||||
fetchRegionStaffList,
|
||||
fetchRegionsByStaffUser,
|
||||
fetchSampleCodeList,
|
||||
fetchExpressCompanyList,
|
||||
@@ -1308,7 +1319,9 @@
|
||||
const salesSnapshot = ref<Api.User.UserItem | null>(null)
|
||||
|
||||
const regionManagerUserId = ref<string | null>(null)
|
||||
const managerUserOptions = ref<Api.User.UserItem[]>([])
|
||||
const managerUserOptions = ref<Array<Api.User.UserItem & { regionIds: string[] }>>([])
|
||||
let managerSearchVersion = 0
|
||||
let managerDefaultVersion = 0
|
||||
const managerLoading = ref(false)
|
||||
const managerSnapshot = ref<Api.User.UserItem | null>(null)
|
||||
const linkedRegionMode = ref(false)
|
||||
@@ -1607,6 +1620,9 @@
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
managerSearchVersion++
|
||||
managerDefaultVersion++
|
||||
managerLoading.value = false
|
||||
Object.assign(formData, {
|
||||
protocolNo: '',
|
||||
protocolNoId: '',
|
||||
@@ -2729,7 +2745,6 @@
|
||||
(user) => {
|
||||
managerSnapshot.value = { ...user }
|
||||
regionManagerUserId.value = user.id
|
||||
managerUserOptions.value = [{ ...user }]
|
||||
formData.regionManagerUserId = user.id
|
||||
},
|
||||
() => {
|
||||
@@ -2739,6 +2754,7 @@
|
||||
formData.regionManagerUserId = ''
|
||||
}
|
||||
)
|
||||
await remoteSearchManagerUsers('')
|
||||
}
|
||||
|
||||
function makeManagerUserStub(id: string, name: string): Api.User.UserItem {
|
||||
@@ -2974,28 +2990,29 @@
|
||||
}
|
||||
|
||||
async function remoteSearchManagerUsers(query: string) {
|
||||
const version = ++managerSearchVersion
|
||||
managerLoading.value = true
|
||||
try {
|
||||
const res = await fetchUserList({
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
keyword: (query || '').trim() || undefined,
|
||||
status: '1'
|
||||
})
|
||||
let list = [...(res.list || [])]
|
||||
const cur = regionManagerUserId.value
|
||||
if (cur && !list.some((x) => x.id === cur)) {
|
||||
const keep =
|
||||
managerSnapshot.value?.id === cur
|
||||
? managerSnapshot.value
|
||||
: managerUserOptions.value.find((x) => x.id === cur)
|
||||
if (keep) list = [keep, ...list]
|
||||
const res = await fetchProtocolRegionManagers()
|
||||
const keyword = (query || '').trim().toLowerCase()
|
||||
const list = (res.list || []).map((item) => ({
|
||||
...makeManagerUserStub(item.id, item.nickName),
|
||||
regionIds: item.regionIds
|
||||
}))
|
||||
if (version === managerSearchVersion) {
|
||||
managerUserOptions.value = list.filter((item) =>
|
||||
item.nickName.toLowerCase().includes(keyword)
|
||||
)
|
||||
}
|
||||
managerUserOptions.value = list
|
||||
return list
|
||||
} catch {
|
||||
managerUserOptions.value = []
|
||||
if (version === managerSearchVersion) {
|
||||
managerUserOptions.value = []
|
||||
ElMessage.error('地区负责人加载失败,请重试')
|
||||
}
|
||||
return []
|
||||
} finally {
|
||||
managerLoading.value = false
|
||||
if (version === managerSearchVersion) managerLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3016,53 +3033,34 @@
|
||||
}
|
||||
|
||||
function onManagerUserChange(val: string | null | undefined) {
|
||||
managerDefaultVersion++
|
||||
if (!val) {
|
||||
regionManagerUserId.value = null
|
||||
managerSnapshot.value = null
|
||||
formData.regionManagerUserId = ''
|
||||
return
|
||||
}
|
||||
const u = managerUserOptions.value.find((x) => x.id === val) || managerSnapshot.value
|
||||
if (!u) {
|
||||
regionManagerUserId.value = val
|
||||
formData.regionManagerUserId = val
|
||||
return
|
||||
}
|
||||
const u = managerUserOptions.value.find((x) => x.id === val)
|
||||
if (!u) return
|
||||
managerSnapshot.value = { ...u }
|
||||
regionManagerUserId.value = u.id
|
||||
formData.regionManagerUserId = u.id
|
||||
}
|
||||
|
||||
async function applyDefaultRegionManager(regionId: string) {
|
||||
if (!regionId) {
|
||||
regionManagerUserId.value = null
|
||||
formData.regionManagerUserId = ''
|
||||
managerUserOptions.value = []
|
||||
managerSnapshot.value = null
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res: any = await fetchRegionStaffList(regionId, { page: 1, pageSize: 100 })
|
||||
const list = (res?.list || []) as Array<Record<string, any>>
|
||||
const leader = list.find((x) => x.isLeader) || list[0]
|
||||
if (!leader?.userId) {
|
||||
regionManagerUserId.value = null
|
||||
formData.regionManagerUserId = ''
|
||||
return
|
||||
}
|
||||
const stub = makeManagerUserStub(
|
||||
String(leader.userId),
|
||||
String(leader.nickName || leader.userName || '')
|
||||
)
|
||||
regionManagerUserId.value = stub.id
|
||||
formData.regionManagerUserId = stub.id
|
||||
managerSnapshot.value = { ...stub }
|
||||
if (!managerUserOptions.value.some((x) => x.id === stub.id)) {
|
||||
managerUserOptions.value = [stub, ...managerUserOptions.value]
|
||||
}
|
||||
} catch {
|
||||
/* 地区无人员配置时留空,可手工选择 */
|
||||
}
|
||||
if (formData.regionId !== regionId) return
|
||||
const version = ++managerDefaultVersion
|
||||
regionManagerUserId.value = null
|
||||
formData.regionManagerUserId = ''
|
||||
managerSnapshot.value = null
|
||||
if (!regionId) return
|
||||
const list = await remoteSearchManagerUsers('')
|
||||
if (version !== managerDefaultVersion || formData.regionId !== regionId) return
|
||||
const leader = list.find((item) => item.regionIds.includes(regionId))
|
||||
if (!leader) return
|
||||
regionManagerUserId.value = leader.id
|
||||
formData.regionManagerUserId = leader.id
|
||||
managerSnapshot.value = { ...leader }
|
||||
}
|
||||
|
||||
async function applyRegionsForSalesUser(
|
||||
@@ -3112,10 +3110,10 @@
|
||||
if (regionChanged) {
|
||||
formData.hospitalId = ''
|
||||
hospitalOptions.value = []
|
||||
await loadHospitalsByRegion(nextId, '')
|
||||
if (!opts?.skipManagerDefault) {
|
||||
await applyDefaultRegionManager(nextId)
|
||||
}
|
||||
await Promise.all([
|
||||
loadHospitalsByRegion(nextId, ''),
|
||||
...(opts?.skipManagerDefault ? [] : [applyDefaultRegionManager(nextId)])
|
||||
])
|
||||
} else if (!opts?.skipManagerDefault && !formData.regionManagerUserId) {
|
||||
await applyDefaultRegionManager(nextId)
|
||||
}
|
||||
@@ -3246,6 +3244,7 @@
|
||||
}
|
||||
|
||||
async function onRegionChange(val: string | null | undefined) {
|
||||
managerDefaultVersion++
|
||||
if (!val) {
|
||||
formData.regionId = ''
|
||||
hospitalOptions.value = []
|
||||
@@ -3259,11 +3258,11 @@
|
||||
formData.regionId = val
|
||||
formData.hospitalId = ''
|
||||
hospitalOptions.value = []
|
||||
await loadHospitalsByRegion(val)
|
||||
await applyDefaultRegionManager(val)
|
||||
await Promise.all([loadHospitalsByRegion(val), applyDefaultRegionManager(val)])
|
||||
}
|
||||
|
||||
function onRegionClear() {
|
||||
managerDefaultVersion++
|
||||
formData.regionId = ''
|
||||
formData.hospitalId = ''
|
||||
hospitalOptions.value = []
|
||||
|
||||
Reference in new issue
Block a user