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