A2/A4/A6 实现员工头像后端、组织架构接口与合同模型
这个提交包含在:
@@ -0,0 +1,154 @@
|
||||
function buildAvatarUrl(employeeId, avatarPath) {
|
||||
return avatarPath ? `/api/avatars/${employeeId}` : null;
|
||||
}
|
||||
|
||||
function compareByEmployeeNo(left, right) {
|
||||
return left.employee_no.localeCompare(right.employee_no, "en");
|
||||
}
|
||||
|
||||
function mapDepartmentSummary(department) {
|
||||
if (!department) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: department.id,
|
||||
name: department.name,
|
||||
};
|
||||
}
|
||||
|
||||
function mapExternalManager(employee, departmentId, activeEmployeeById) {
|
||||
if (!employee.direct_manager_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const manager = activeEmployeeById.get(employee.direct_manager_id) ?? null;
|
||||
if (!manager) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (manager.department_id === departmentId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: manager.id,
|
||||
employeeNo: manager.employee_no,
|
||||
name: manager.name,
|
||||
department: mapDepartmentSummary(manager.department),
|
||||
};
|
||||
}
|
||||
|
||||
function mapOrganizationNode(employee, departmentId, activeEmployeeById, childrenByManagerId, stack) {
|
||||
const nextStack = new Set(stack);
|
||||
nextStack.add(employee.id);
|
||||
|
||||
const children = (childrenByManagerId.get(employee.id) ?? [])
|
||||
.slice()
|
||||
.sort(compareByEmployeeNo)
|
||||
.filter((item) => !nextStack.has(item.id))
|
||||
.map((item) => mapOrganizationNode(item, departmentId, activeEmployeeById, childrenByManagerId, nextStack));
|
||||
|
||||
return {
|
||||
id: employee.id,
|
||||
employeeNo: employee.employee_no,
|
||||
name: employee.name,
|
||||
title: employee.title,
|
||||
status: employee.status,
|
||||
avatarUrl: buildAvatarUrl(employee.id, employee.avatar_path),
|
||||
department: mapDepartmentSummary(employee.department),
|
||||
externalManager: mapExternalManager(employee, departmentId, activeEmployeeById),
|
||||
children,
|
||||
};
|
||||
}
|
||||
|
||||
function buildDepartmentSection(department, employees, activeEmployeeById) {
|
||||
const departmentId = department.id;
|
||||
const departmentEmployeeById = new Map(employees.map((item) => [item.id, item]));
|
||||
const childrenByManagerId = new Map();
|
||||
|
||||
for (const employee of employees) {
|
||||
childrenByManagerId.set(employee.id, []);
|
||||
}
|
||||
|
||||
for (const employee of employees) {
|
||||
if (!employee.direct_manager_id) {
|
||||
continue;
|
||||
}
|
||||
const manager = activeEmployeeById.get(employee.direct_manager_id);
|
||||
if (!manager || manager.department_id !== departmentId) {
|
||||
continue;
|
||||
}
|
||||
if (!departmentEmployeeById.has(manager.id)) {
|
||||
continue;
|
||||
}
|
||||
childrenByManagerId.get(manager.id).push(employee);
|
||||
}
|
||||
|
||||
const roots = employees
|
||||
.filter((employee) => {
|
||||
if (!employee.direct_manager_id) {
|
||||
return true;
|
||||
}
|
||||
const manager = activeEmployeeById.get(employee.direct_manager_id);
|
||||
return !manager || manager.department_id !== departmentId;
|
||||
})
|
||||
.slice()
|
||||
.sort(compareByEmployeeNo)
|
||||
.map((employee) => mapOrganizationNode(employee, departmentId, activeEmployeeById, childrenByManagerId, new Set()));
|
||||
|
||||
return {
|
||||
department: mapDepartmentSummary(department),
|
||||
roots,
|
||||
};
|
||||
}
|
||||
|
||||
async function listOrganizationDepartments(prisma) {
|
||||
const employees = await prisma.employee.findMany({
|
||||
where: { status: "active" },
|
||||
select: {
|
||||
id: true,
|
||||
employee_no: true,
|
||||
name: true,
|
||||
title: true,
|
||||
status: true,
|
||||
avatar_path: true,
|
||||
department_id: true,
|
||||
direct_manager_id: true,
|
||||
department: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const activeEmployeeById = new Map(employees.map((item) => [item.id, item]));
|
||||
const departmentGroups = new Map();
|
||||
|
||||
for (const employee of employees) {
|
||||
const departmentId = employee.department?.id ?? employee.department_id;
|
||||
if (!departmentId || !employee.department) {
|
||||
continue;
|
||||
}
|
||||
if (!departmentGroups.has(departmentId)) {
|
||||
departmentGroups.set(departmentId, {
|
||||
department: employee.department,
|
||||
employees: [],
|
||||
});
|
||||
}
|
||||
departmentGroups.get(departmentId).employees.push(employee);
|
||||
}
|
||||
|
||||
return [...departmentGroups.values()]
|
||||
.sort((left, right) => left.department.name.localeCompare(right.department.name, "zh-CN"))
|
||||
.map((group) => buildDepartmentSection(group.department, group.employees, activeEmployeeById));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
listOrganizationDepartments,
|
||||
__testables: {
|
||||
buildDepartmentSection,
|
||||
mapOrganizationNode,
|
||||
},
|
||||
};
|
||||
@@ -108,6 +108,7 @@ function mapPresenceEmployee(row) {
|
||||
employeeNo: row.employee_no,
|
||||
name: row.name,
|
||||
title: row.title,
|
||||
avatarUrl: row.avatar_path ? `/api/avatars/${row.id}` : null,
|
||||
employmentStatus: row.employment_status,
|
||||
department: row.department ? { id: row.department.id, name: row.department.name } : null,
|
||||
};
|
||||
@@ -309,6 +310,7 @@ async function listActivePresenceEmployees(prisma, filters = {}) {
|
||||
employee_no: true,
|
||||
name: true,
|
||||
title: true,
|
||||
avatar_path: true,
|
||||
employment_status: true,
|
||||
department: { select: { id: true, name: true } },
|
||||
},
|
||||
|
||||
在新工单中引用
屏蔽一个用户