feat: 完成23项系统优化 - 花名册社保状态列/身份证复制/附件类型扩展, 用工办理姓名检索/直接提交/文书查看/批量证明, 风险中心跳转筛选+批量处理, 日历7/15/35天分组+逾期统计, 考勤单条编辑+按人导出, 工资条查看状态+工资流水导出, 辞职申请附件上传, 交接清单PDF下载, 操作完成下一步引导, 休假审批入口, 人效成本分部门

This commit is contained in:
freedakgmail
2026-08-04 22:55:03 +08:00
parent 338af5eee9
commit 5604d02de9
41 changed files with 2104 additions and 482 deletions
+17
View File
@@ -18,6 +18,7 @@ import {
getLeaveRecords,
createLeaveRecord,
deleteLeaveRecord,
manualCorrectAttendance,
} from '../services/attendance.service'
import { createEvidence } from '../services/evidence.service'
import prisma from '../lib/prisma'
@@ -206,6 +207,22 @@ router.delete('/shift-assignments/:id', authMiddleware, async (req: AuthRequest,
// ========== 每日出勤 ==========
router.post('/manual-correct', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const schema = z.object({
employeeId: z.string(),
date: z.string(),
checkInTime: z.string().optional(),
checkOutTime: z.string().optional(),
status: z.string().optional(),
remark: z.string().optional(),
})
const data = schema.parse(req.body)
const record = await manualCorrectAttendance(req.user!.orgId, { ...data, createdBy: req.user!.id })
res.json({ success: true, data: record })
} catch (err) { next(err) }
})
router.get('/daily', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const date = req.query.date as string
+1 -1
View File
@@ -79,7 +79,7 @@ router.get('/list', authMiddleware, async (req: AuthRequest, res, next) => {
status: { in: status },
...(department && { department }),
},
select: { id: true, name: true, department: true, position: true, phone: true, status: true },
select: { id: true, name: true, department: true, position: true, phone: true, gender: true, status: true },
orderBy: { name: 'asc' },
})
res.json({ success: true, data: employees })
+9 -4
View File
@@ -115,7 +115,11 @@ router.get('/payslip', portalAuth, async (req: any, res, next) => {
if (!payslip) {
return res.json({ success: true, data: null })
}
res.json({ success: true, data: payslip })
// 记录查看时间
if (!payslip.viewedAt) {
await prisma.payslip.update({ where: { id: payslip.id }, data: { viewedAt: new Date() } })
}
res.json({ success: true, data: { ...payslip, viewedAt: payslip.viewedAt || new Date() } })
} catch (err) {
next(err)
}
@@ -773,7 +777,7 @@ router.get('/onboarding/progress', portalAuth, async (req: any, res, next) => {
router.post('/resignation/submit', portalAuth, async (req: any, res, next) => {
try {
const { id: employeeId, orgId } = req.employee
const { reason, expectedDate, remark } = req.body
const { reason, expectedDate, remark, attachments } = req.body
if (!reason || !expectedDate) {
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '请填写离职原因和预计离职日期' } })
}
@@ -789,6 +793,7 @@ router.post('/resignation/submit', portalAuth, async (req: any, res, next) => {
if (existing) {
return res.status(400).json({ success: false, error: { code: 'DUPLICATE', message: '您已有一个待处理的离职申请' } })
}
const remarkText = `员工自主申请:${reason}${remark ? ';备注:' + remark : ''}${attachments && attachments.length > 0 ? `;附件:${attachments.length}张辞职信照片` : ''}`
const record = await (prisma as any).terminationRecord.create({
data: {
employeeId, orgId,
@@ -797,8 +802,8 @@ router.post('/resignation/submit', portalAuth, async (req: any, res, next) => {
resignationReason: reason,
terminationDate: new Date(expectedDate),
status: 'PENDING_APPROVAL',
checklist: [],
remark: `员工自主申请:${reason}${remark ? ';备注:' + remark : ''}`,
checklist: attachments && attachments.length > 0 ? attachments : [],
remark: remarkText,
createdBy: employeeId,
},
})
+9
View File
@@ -95,6 +95,7 @@ router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
include: {
contracts: { orderBy: { createdAt: 'desc' }, take: 1 },
terminations: { orderBy: { terminationDate: 'desc' }, take: 1 },
socialInsRecords: { orderBy: { startMonth: 'desc' }, take: 1 },
_count: {
select: {
disciplinaryRecords: true,
@@ -148,6 +149,7 @@ router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
id: e.id,
name: e.name,
department: e.department,
position: e.position,
city: e.city,
status: dynamicStatus,
hasTermination: e.terminations.length > 0,
@@ -168,6 +170,13 @@ router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
contractStatus: contractInfo.status,
contractStatusText: contractInfo.statusText,
riskLevel: contractInfo.riskLevel,
socialInsuranceStatus: (() => {
const sr = (e as any).socialInsRecords?.[0]
if (!sr) return null
// endMonth 为 null 表示在保,否则已停保
if (sr.endMonth) return 'SUSPENDED'
return 'ACTIVE'
})(),
probationInfo: (() => {
if (!latestContract || latestContract.probationMonths === 0) return null
const probEnd = new Date(e.hireDate)
@@ -261,6 +261,60 @@ export async function deleteShiftAssignment(orgId: string, id: string) {
// ========== 每日出勤 ==========
export async function manualCorrectAttendance(orgId: string, data: {
employeeId: string
date: string
checkInTime?: string
checkOutTime?: string
status?: string
remark?: string
createdBy?: string
}) {
const day = new Date(data.date)
day.setHours(0, 0, 0, 0)
const nextDay = new Date(day)
nextDay.setDate(nextDay.getDate() + 1)
const existing = await prisma.attendanceRecord.findFirst({
where: { orgId, employeeId: data.employeeId, date: { gte: day, lt: nextDay } },
})
const checkInTime = data.checkInTime ? new Date(`${data.date}T${data.checkInTime}`).toISOString() : null
const checkOutTime = data.checkOutTime ? new Date(`${data.date}T${data.checkOutTime}`).toISOString() : null
let workHours = 0
if (checkInTime && checkOutTime) {
workHours = Math.round((new Date(checkOutTime).getTime() - new Date(checkInTime).getTime()) / 3600000 * 100) / 100
}
if (existing) {
return prisma.attendanceRecord.update({
where: { id: existing.id },
data: {
checkInTime,
checkOutTime,
status: data.status || 'NORMAL',
workHours,
remark: data.remark || existing.remark,
},
})
} else {
return prisma.attendanceRecord.create({
data: {
orgId,
employeeId: data.employeeId,
date: day,
checkInTime,
checkOutTime,
status: data.status || 'NORMAL',
workHours,
remark: data.remark || null,
createdBy: data.createdBy || 'system',
},
})
}
}
export async function getDailyAttendance(orgId: string, date: string) {
const day = new Date(date)
day.setHours(0, 0, 0, 0)