feat: 工作日历/考勤管理重构/AI人力报告/工作台员工分布/筛选优化/导入导出增强
- 新增工作日历页面(月历视图、事件管理、自定义事件) - 考勤管理重构为6 Tab模块(班次/排班/每日出勤/月度报表/休假记录) - AI顾问新增人力报告Tab,支持流式生成+Word导出 - 工作台总览新增员工分布统计(性别/年龄/学历/司龄饼图)+部门成本拆分 - 花名册/合同/解聘补偿新增部门和状态筛选 - 薪税管理新增工资表导入模板下载、银行代发CSV导出 - 社保公积金支持多公积金账户类型显示 - 数据导出新增花名册/解聘记录导出,中文文件名编码修复 - 数据导入新增模板下载(员工/增减员/工资表)+错误日志导出 - 移除工作台日历卡片(已迁移至独立工作日历页面) - 新增20260728/20260729更新测试指导文档
This commit is contained in:
@@ -19,14 +19,30 @@ function safeDecrypt(encrypted: string): number {
|
||||
|
||||
// ========== 花名册聚合 API ==========
|
||||
|
||||
// 获取部门列表(去重)
|
||||
router.get('/departments', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const employees = await prisma.employee.findMany({
|
||||
where: { orgId: req.user!.orgId, status: 'ACTIVE' },
|
||||
select: { department: true },
|
||||
distinct: 'department',
|
||||
})
|
||||
const departments = employees.map((e) => e.department).filter(Boolean).sort()
|
||||
res.json({ success: true, data: departments })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 花名册列表(含汇总信息,支持分页和过滤)
|
||||
router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page as string) || 1
|
||||
const pageSize = Math.min(parseInt(req.query.pageSize as string) || 20, 999)
|
||||
const pageSize = Math.min(parseInt(req.query.pageSize as string) || 20, 200)
|
||||
const search = req.query.search as string
|
||||
const status = req.query.status as string // ACTIVE | PRE_HIRE | RESIGNED
|
||||
const contractStatus = req.query.contractStatus as string // active | expiring | expired | unsigned | etc.
|
||||
const department = req.query.department as string
|
||||
const skip = (page - 1) * pageSize
|
||||
|
||||
// 使用本地日期午夜,避免时区问题导致当天入职被误判为预入职
|
||||
@@ -38,8 +54,12 @@ router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
todayEnd.setDate(todayEnd.getDate() + 1)
|
||||
|
||||
// 先查询满足 orgId 和搜索条件的员工
|
||||
const isIdCardSearch = search && /^\d{4}$/.test(search)
|
||||
const whereBase: any = { orgId: req.user!.orgId }
|
||||
if (search) {
|
||||
if (department) {
|
||||
whereBase.department = department
|
||||
}
|
||||
if (search && !isIdCardSearch) {
|
||||
whereBase.OR = [
|
||||
{ name: { contains: search } },
|
||||
{ department: { contains: search } },
|
||||
@@ -62,8 +82,8 @@ router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
whereBase.contracts = { none: {} }
|
||||
}
|
||||
|
||||
// 当有 contractStatus(非 unsigned)筛选时,需要先查全部再过滤后分页
|
||||
const needPostFilter = !!contractStatus && contractStatus !== 'unsigned'
|
||||
// 当有 contractStatus(非 unsigned)筛选或身份证号搜索时,需要先查全部再过滤后分页
|
||||
const needPostFilter = (!!contractStatus && contractStatus !== 'unsigned') || isIdCardSearch
|
||||
|
||||
const [dbTotal, employees] = await Promise.all([
|
||||
prisma.employee.count({ where: whereBase }),
|
||||
@@ -109,6 +129,18 @@ router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
const isResigned = e.terminations.some((t) => t.status === 'COMPLETED' && t.terminationDate <= today)
|
||||
const isPreHire = !isResigned && e.hireDate > todayEnd
|
||||
const dynamicStatus = isResigned ? 'RESIGNED' : (isPreHire ? 'PRE_HIRE' : 'ACTIVE')
|
||||
// 身份证号脱敏显示
|
||||
let idCardMasked: string | null = null
|
||||
if (e.idCardNumber) {
|
||||
try {
|
||||
const idCard = decrypt(e.idCardNumber)
|
||||
if (idCard.length >= 11) {
|
||||
idCardMasked = idCard.slice(0, 3) + '****' + idCard.slice(-4)
|
||||
} else {
|
||||
idCardMasked = '****'
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
return {
|
||||
id: e.id,
|
||||
name: e.name,
|
||||
@@ -123,6 +155,7 @@ router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
hireDate: e.hireDate,
|
||||
gender: e.gender,
|
||||
phone: e.phone,
|
||||
idCardMasked,
|
||||
monthlySalary: safeDecrypt(e.monthlySalary),
|
||||
isPregnant: e.isPregnant,
|
||||
isInMedicalPeriod: e.isInMedicalPeriod,
|
||||
@@ -140,9 +173,18 @@ router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
result = result.filter((e) => e.contractStatus === contractStatus)
|
||||
}
|
||||
|
||||
// 身份证号后4位搜索:在内存中过滤
|
||||
if (isIdCardSearch) {
|
||||
result = result.filter((e: any) => {
|
||||
if (!e.idCardMasked) return false
|
||||
return e.idCardMasked.endsWith(search!)
|
||||
})
|
||||
}
|
||||
|
||||
// 计算过滤后的总数和分页
|
||||
const filteredTotal = needPostFilter ? result.length : dbTotal
|
||||
if (needPostFilter) {
|
||||
const needMemoryPaging = needPostFilter || isIdCardSearch
|
||||
const filteredTotal = needMemoryPaging ? result.length : dbTotal
|
||||
if (needMemoryPaging) {
|
||||
result = result.slice(skip, skip + pageSize)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user