Files
TurboHR/backend/src/routes/attendance.routes.ts
T
freedakgmail 42e0c650a4 feat: 实现20260730优化方案全部功能
- AI文件审查:.docx上传提取文本,支持多种文档类型
- 用工办理工作流:WorkProcess页面+后端API,支持入职/续签/终止等流程
- 企业自建文本库:Templates页面Tab切换,企业模板CRUD+渲染+下载Word
- 考勤发布:Attendance发布/取消发布按钮,员工端MyAttendance页面
- 工资条发布:Money发布/定时发送按钮+弹窗,portal端publishStatus过滤
- 合同到期弹窗:Dashboard合同到期预警可点击打开弹窗,支持续签/终止操作
- Prisma schema新增WorkProcess/EnterpriseTemplate/AttendancePublish模型
- 前后端编译验证全部通过
2026-07-30 10:21:22 +08:00

315 lines
11 KiB
TypeScript

import { Router, Response, NextFunction } from 'express'
import { authMiddleware, AuthRequest } from '../middleware/auth'
import { z } from 'zod'
import {
createAttendanceConfirmation,
batchCreateAttendanceConfirmations,
getAttendanceConfirmations,
confirmAttendance,
getAttendanceStats,
getShifts,
createShift,
updateShift,
deleteShift,
getShiftAssignments,
batchAssignShifts,
deleteShiftAssignment,
getDailyAttendance,
getMonthlyReport,
getLeaveRecords,
createLeaveRecord,
deleteLeaveRecord,
} from '../services/attendance.service'
import { createEvidence } from '../services/evidence.service'
import prisma from '../lib/prisma'
const router = Router()
/** 获取月度考勤确认列表 */
router.get('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const month = req.query.month as string
if (!month) {
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '缺少 month 参数' } })
}
const status = req.query.status as string | undefined
const department = req.query.department as string | undefined
const data = await getAttendanceConfirmations(req.user!.orgId, month, status, department)
res.json({ success: true, data })
} catch (err) {
next(err)
}
})
/** 获取考勤确认统计 */
router.get('/stats', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const month = req.query.month as string
if (!month) {
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '缺少 month 参数' } })
}
const data = await getAttendanceStats(req.user!.orgId, month)
res.json({ success: true, data })
} catch (err) {
next(err)
}
})
/** 批量创建考勤确认记录 */
router.post('/batch', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const schema = z.object({
month: z.string().regex(/^\d{4}-\d{2}$/),
items: z.array(z.object({
employeeId: z.string(),
workDays: z.number().int().min(0),
weekdayHours: z.number().min(0),
weekendHours: z.number().min(0),
holidayHours: z.number().min(0),
overtimePay: z.number().min(0),
})),
})
const { month, items } = schema.parse(req.body)
const result = await batchCreateAttendanceConfirmations(req.user!.orgId, req.user!.id, month, items)
res.json({ success: true, data: result })
} catch (err) {
next(err)
}
})
/** 员工确认考勤(员工端) */
router.post('/confirm', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const schema = z.object({
employeeId: z.string(),
month: z.string().regex(/^\d{4}-\d{2}$/),
disputeNote: z.string().optional(),
})
const { employeeId, month, disputeNote } = schema.parse(req.body)
const ip = req.ip || req.socket.remoteAddress || ''
const result = await confirmAttendance(req.user!.orgId, employeeId, month, ip, disputeNote)
await createEvidence({
orgId: req.user!.orgId,
category: 'ATTENDANCE',
refId: result.id,
employeeId,
events: [{ action: '考勤确认', timestamp: new Date().toISOString(), ip, userAgent: req.headers['user-agent'], ...(disputeNote ? { location: disputeNote } : {}) }],
createdBy: req.user!.id,
}).catch(() => {})
res.json({ success: true, data: result })
} catch (err: any) {
if (err?.code === 'NOT_FOUND') {
return res.status(404).json({ success: false, error: { code: err.code, message: err.message } })
}
next(err)
}
})
// ========== 班次管理 ==========
router.get('/shifts', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const data = await getShifts(req.user!.orgId)
res.json({ success: true, data })
} catch (err) { next(err) }
})
router.post('/shifts', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const schema = z.object({
name: z.string().min(1),
startTime: z.string().regex(/^\d{2}:\d{2}$/),
endTime: z.string().regex(/^\d{2}:\d{2}$/),
flexibleMinutes: z.number().int().min(0).optional(),
restMinutes: z.number().int().min(0).optional(),
color: z.string().optional(),
})
const data = await createShift(req.user!.orgId, req.user!.id, schema.parse(req.body))
res.json({ success: true, data })
} catch (err) { next(err) }
})
router.put('/shifts/:id', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const schema = z.object({
name: z.string().min(1).optional(),
startTime: z.string().regex(/^\d{2}:\d{2}$/).optional(),
endTime: z.string().regex(/^\d{2}:\d{2}$/).optional(),
flexibleMinutes: z.number().int().min(0).optional(),
restMinutes: z.number().int().min(0).optional(),
color: z.string().optional(),
})
const data = await updateShift(req.user!.orgId, req.params.id, schema.parse(req.body))
res.json({ success: true, data })
} catch (err) { next(err) }
})
router.delete('/shifts/:id', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
await deleteShift(req.user!.orgId, req.params.id)
res.json({ success: true })
} catch (err) { next(err) }
})
// ========== 排班管理 ==========
router.get('/shift-assignments', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const date = req.query.date as string
if (!date) return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '缺少 date 参数' } })
const data = await getShiftAssignments(req.user!.orgId, date)
res.json({ success: true, data })
} catch (err) { next(err) }
})
router.post('/shift-assignments/batch', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const schema = z.object({
items: z.array(z.object({
employeeId: z.string(),
shiftId: z.string(),
date: z.string(),
})),
})
const { items } = schema.parse(req.body)
const result = await batchAssignShifts(req.user!.orgId, req.user!.id, items)
res.json({ success: true, data: result })
} catch (err) { next(err) }
})
router.delete('/shift-assignments/:id', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
await deleteShiftAssignment(req.user!.orgId, req.params.id)
res.json({ success: true })
} catch (err) { next(err) }
})
// ========== 每日出勤 ==========
router.get('/daily', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const date = req.query.date as string
if (!date) return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '缺少 date 参数' } })
const data = await getDailyAttendance(req.user!.orgId, date)
res.json({ success: true, data })
} catch (err) { next(err) }
})
// ========== 月度出勤报表 ==========
router.get('/monthly-report', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const month = req.query.month as string
if (!month) return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '缺少 month 参数' } })
const data = await getMonthlyReport(req.user!.orgId, month)
res.json({ success: true, data })
} catch (err) { next(err) }
})
// ========== 休假记录 ==========
router.get('/leaves', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const employeeId = req.query.employeeId as string | undefined
const data = await getLeaveRecords(req.user!.orgId, employeeId)
res.json({ success: true, data })
} catch (err) { next(err) }
})
router.post('/leaves', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const schema = z.object({
employeeId: z.string(),
leaveType: z.enum(['SICK', 'PERSONAL', 'ANNUAL', 'MATERNITY', 'OTHER']),
startDate: z.string(),
endDate: z.string(),
days: z.number().min(0),
reason: z.string().optional(),
remark: z.string().optional(),
})
const data = await createLeaveRecord(req.user!.orgId, req.user!.id, schema.parse(req.body))
res.json({ success: true, data })
} catch (err) { next(err) }
})
router.delete('/leaves/:id', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
await deleteLeaveRecord(req.user!.orgId, req.params.id)
res.json({ success: true })
} catch (err) { next(err) }
})
// ========== 考勤发布 ==========
// 发布考勤表
router.post('/publish', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { month, title } = req.body
if (!month) {
return res.status(400).json({ success: false, error: { code: 'MISSING_MONTH', message: '请选择月份' } })
}
// 检查是否已发布且未取消
const existing = await (prisma as any).attendancePublish.findFirst({
where: { orgId: req.user!.orgId, month, status: 'PUBLISHED' },
})
if (existing) {
return res.status(400).json({ success: false, error: { code: 'ALREADY_PUBLISHED', message: `${month}月考勤表已发布` } })
}
// 如果有已取消的记录,删除后重新创建
const cancelled = await (prisma as any).attendancePublish.findFirst({
where: { orgId: req.user!.orgId, month, status: 'CANCELLED' },
})
if (cancelled) {
await (prisma as any).attendancePublish.delete({ where: { id: cancelled.id } })
}
const record = await (prisma as any).attendancePublish.create({
data: {
orgId: req.user!.orgId,
month,
title: title || `${month}月考勤表`,
createdBy: req.user!.id,
},
})
res.json({ success: true, data: record })
} catch (err) {
next(err)
}
})
// 发布记录列表
router.get('/publish-records', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const records = await (prisma as any).attendancePublish.findMany({
where: { orgId: req.user!.orgId },
orderBy: { createdAt: 'desc' },
})
res.json({ success: true, data: records })
} catch (err) {
next(err)
}
})
// 取消发布
router.post('/publish/:id/cancel', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const record = await (prisma as any).attendancePublish.findFirst({
where: { id: req.params.id, orgId: req.user!.orgId },
})
if (!record) {
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '发布记录不存在' } })
}
if (record.status !== 'PUBLISHED') {
return res.status(400).json({ success: false, error: { code: 'NOT_PUBLISHED', message: '仅已发布状态可取消' } })
}
const updated = await (prisma as any).attendancePublish.update({
where: { id: record.id },
data: { status: 'CANCELLED' },
})
res.json({ success: true, data: updated })
} catch (err) {
next(err)
}
})
export default router