154b0b62ad
build / build (push) Successful in 1m20s
新增北京时间自然周校验与缓存隔离,默认展开到业务二级部门。 选中部门在统计页就地绑定;区域名称列表原位增删改,双版本锁、自动预览、独立读回及删除确认。 筛选和两张区域看板使用同一已保存目录,删除失效筛选回全部。 验证:完整 make verify、全 mirror 隔离 PostgreSQL race、自然周边界与缓存键负例通过;真实恢复库327部门逐行对拍及六项浏览器验收通过。 schema57无迁移,不改真实公司区域绑定或原始问答,不连接DobotAI生产源库;PNG导出本轮不冒充已验证。
123 lines
4.3 KiB
TypeScript
123 lines
4.3 KiB
TypeScript
import type { DepartmentRegion, RegionRule } from './qaOrg'
|
|
export type DepartmentNode = { id: string; parent_id: string; name: string }
|
|
export function defaultDepartmentCollapsed(nodes: DepartmentNode[]): Set<string> {
|
|
return new Set(
|
|
departmentTree(nodes, new Set())
|
|
.filter((r) => r.depth >= 2 && r.hasChildren)
|
|
.map((r) => r.department.id)
|
|
)
|
|
}
|
|
export function subtreeIDs(nodes: DepartmentNode[], ids: string[]): string[] {
|
|
const children = new Map<string, string[]>(),
|
|
known = new Set(nodes.map((n) => n.id))
|
|
for (const node of nodes) {
|
|
const list = children.get(node.parent_id) ?? []
|
|
list.push(node.id)
|
|
children.set(node.parent_id, list)
|
|
}
|
|
const seen = new Set<string>(),
|
|
queue = [...ids]
|
|
while (queue.length) {
|
|
const id = queue.pop()!
|
|
if (!known.has(id)) throw Error('部门不在当前通讯录')
|
|
if (seen.has(id)) continue
|
|
seen.add(id)
|
|
queue.push(...(children.get(id) ?? []))
|
|
}
|
|
return [...seen]
|
|
}
|
|
export function selectDepartmentBranch(
|
|
nodes: DepartmentNode[],
|
|
selected: string[],
|
|
id: string,
|
|
checked: boolean
|
|
): string[] {
|
|
const ids = subtreeIDs(nodes, [id]),
|
|
next = new Set(selected)
|
|
if (checked) ids.forEach((x) => next.add(x))
|
|
else {
|
|
ids.forEach((x) => next.delete(x))
|
|
const parents = new Map(nodes.map((n) => [n.id, n.parent_id]))
|
|
const seen = new Set<string>()
|
|
let parent = parents.get(id)
|
|
while (parent && !seen.has(parent)) {
|
|
seen.add(parent)
|
|
next.delete(parent)
|
|
parent = parents.get(parent)
|
|
}
|
|
}
|
|
return [...next]
|
|
}
|
|
export function departmentTree<T extends DepartmentNode>(
|
|
nodes: T[],
|
|
collapsed: ReadonlySet<string>,
|
|
search = '',
|
|
region = ''
|
|
): Array<{ department: T; depth: number; hasChildren: boolean }> {
|
|
const known = new Map(nodes.map((n) => [n.id, n])),
|
|
children = new Map<string, T[]>()
|
|
if (known.size !== nodes.length) throw Error('通讯录部门重复')
|
|
for (const node of nodes) {
|
|
if (node.parent_id && !known.has(node.parent_id)) throw Error('通讯录缺少父部门')
|
|
const list = children.get(node.parent_id) ?? []
|
|
list.push(node)
|
|
children.set(node.parent_id, list)
|
|
}
|
|
for (const list of children.values())
|
|
list.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN') || a.id.localeCompare(b.id))
|
|
const all: Array<{ department: T; depth: number; hasChildren: boolean; parents: string[]; path: string }> = [],
|
|
seen = new Set<string>()
|
|
const walk = (parent: string, parents: string[], names: string[]) => {
|
|
for (const node of children.get(parent) ?? []) {
|
|
if (seen.has(node.id) || parents.length > 100) throw Error('通讯录部门层级无效')
|
|
seen.add(node.id)
|
|
all.push({
|
|
department: node,
|
|
depth: parents.length,
|
|
hasChildren: !!children.get(node.id)?.length,
|
|
parents,
|
|
path: [...names, node.name].join('/'),
|
|
})
|
|
walk(node.id, [...parents, node.id], [...names, node.name])
|
|
}
|
|
}
|
|
walk('', [], [])
|
|
if (all.length !== nodes.length) throw Error('通讯录部门存在循环')
|
|
if (search || region) {
|
|
const matches = all.filter(
|
|
(r) =>
|
|
(!search || r.path.toLowerCase().includes(search.toLowerCase())) &&
|
|
(!region || (r.department as DepartmentNode & { region?: string }).region === region)
|
|
)
|
|
const visible = new Set(matches.flatMap((r) => [r.department.id, ...r.parents]))
|
|
return all.filter((r) => visible.has(r.department.id))
|
|
}
|
|
return all.filter((r) => !r.parents.some((id) => collapsed.has(id)))
|
|
}
|
|
export function applyBulkRegionRules(rules: RegionRule[], ids: string[], region: string): RegionRule[] {
|
|
const selected = new Set(ids)
|
|
if (!selected.size) return [...rules]
|
|
if (!region) throw new Error('请先选择区域')
|
|
return [
|
|
...rules.filter((r) => !selected.has(r.department_id)),
|
|
...Array.from(selected, (department_id) => ({ department_id, region })),
|
|
]
|
|
}
|
|
export function departmentLevels(
|
|
departments: DepartmentRegion[],
|
|
id: string,
|
|
nodes = new Map(departments.map((d) => [d.id, d]))
|
|
): string[] {
|
|
const seen = new Set<string>(),
|
|
names: string[] = []
|
|
let current = id
|
|
while (current) {
|
|
const node = nodes.get(current)
|
|
if (!node || seen.has(current) || names.length > 100) throw new Error('通讯录部门层级无效,请重新同步')
|
|
seen.add(current)
|
|
names.unshift(node.name)
|
|
current = node.parent_id
|
|
}
|
|
return names
|
|
}
|