42e0c650a4
- AI文件审查:.docx上传提取文本,支持多种文档类型 - 用工办理工作流:WorkProcess页面+后端API,支持入职/续签/终止等流程 - 企业自建文本库:Templates页面Tab切换,企业模板CRUD+渲染+下载Word - 考勤发布:Attendance发布/取消发布按钮,员工端MyAttendance页面 - 工资条发布:Money发布/定时发送按钮+弹窗,portal端publishStatus过滤 - 合同到期弹窗:Dashboard合同到期预警可点击打开弹窗,支持续签/终止操作 - Prisma schema新增WorkProcess/EnterpriseTemplate/AttendancePublish模型 - 前后端编译验证全部通过
315 lines
11 KiB
TypeScript
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
|