feat: 完成23项系统优化 - 花名册社保状态列/身份证复制/附件类型扩展, 用工办理姓名检索/直接提交/文书查看/批量证明, 风险中心跳转筛选+批量处理, 日历7/15/35天分组+逾期统计, 考勤单条编辑+按人导出, 工资条查看状态+工资流水导出, 辞职申请附件上传, 交接清单PDF下载, 操作完成下一步引导, 休假审批入口, 人效成本分部门
This commit is contained in:
@@ -664,6 +664,7 @@ model Payslip {
|
||||
publishedAt DateTime? // 工资条发布到员工端的时间
|
||||
publishStatus String? // UNPUBLISHED/PUBLISHED/SCHEDULED
|
||||
scheduledAt DateTime? // 定时发送时间
|
||||
viewedAt DateTime? // 员工查看工资条的时间
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user