fb36b10402
- 新增工作日历页面(月历视图、事件管理、自定义事件) - 考勤管理重构为6 Tab模块(班次/排班/每日出勤/月度报表/休假记录) - AI顾问新增人力报告Tab,支持流式生成+Word导出 - 工作台总览新增员工分布统计(性别/年龄/学历/司龄饼图)+部门成本拆分 - 花名册/合同/解聘补偿新增部门和状态筛选 - 薪税管理新增工资表导入模板下载、银行代发CSV导出 - 社保公积金支持多公积金账户类型显示 - 数据导出新增花名册/解聘记录导出,中文文件名编码修复 - 数据导入新增模板下载(员工/增减员/工资表)+错误日志导出 - 移除工作台日历卡片(已迁移至独立工作日历页面) - 新增20260728/20260729更新测试指导文档
409 lines
12 KiB
TypeScript
409 lines
12 KiB
TypeScript
import prisma from '../lib/prisma'
|
|
|
|
/**
|
|
* 考勤确认服务
|
|
*/
|
|
|
|
/**
|
|
* 创建月度考勤确认记录
|
|
*/
|
|
export async function createAttendanceConfirmation(orgId: string, userId: string, data: {
|
|
employeeId: string
|
|
month: string
|
|
workDays: number
|
|
weekdayHours: number
|
|
weekendHours: number
|
|
holidayHours: number
|
|
overtimePay: number
|
|
}) {
|
|
const existing = await prisma.attendanceConfirmation.findUnique({
|
|
where: { orgId_employeeId_month: { orgId, employeeId: data.employeeId, month: data.month } },
|
|
})
|
|
if (existing) {
|
|
throw { code: 'CONFLICT', message: '该月考勤确认记录已存在' }
|
|
}
|
|
|
|
return prisma.attendanceConfirmation.create({
|
|
data: {
|
|
orgId,
|
|
employeeId: data.employeeId,
|
|
month: data.month,
|
|
workDays: data.workDays,
|
|
weekdayHours: data.weekdayHours,
|
|
weekendHours: data.weekendHours,
|
|
holidayHours: data.holidayHours,
|
|
overtimePay: data.overtimePay,
|
|
createdBy: userId,
|
|
},
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 批量创建月度考勤确认记录
|
|
*/
|
|
export async function batchCreateAttendanceConfirmations(orgId: string, userId: string, month: string, items: Array<{
|
|
employeeId: string
|
|
workDays: number
|
|
weekdayHours: number
|
|
weekendHours: number
|
|
holidayHours: number
|
|
overtimePay: number
|
|
}>) {
|
|
const results: Array<{ employeeId: string; success: boolean; error?: string }> = []
|
|
|
|
for (const item of items) {
|
|
try {
|
|
const existing = await prisma.attendanceConfirmation.findUnique({
|
|
where: { orgId_employeeId_month: { orgId, employeeId: item.employeeId, month } },
|
|
})
|
|
if (existing) {
|
|
// 更新已有记录
|
|
await prisma.attendanceConfirmation.update({
|
|
where: { id: existing.id },
|
|
data: {
|
|
workDays: item.workDays,
|
|
weekdayHours: item.weekdayHours,
|
|
weekendHours: item.weekendHours,
|
|
holidayHours: item.holidayHours,
|
|
overtimePay: item.overtimePay,
|
|
status: 'PENDING',
|
|
},
|
|
})
|
|
} else {
|
|
await prisma.attendanceConfirmation.create({
|
|
data: {
|
|
orgId,
|
|
employeeId: item.employeeId,
|
|
month,
|
|
workDays: item.workDays,
|
|
weekdayHours: item.weekdayHours,
|
|
weekendHours: item.weekendHours,
|
|
holidayHours: item.holidayHours,
|
|
overtimePay: item.overtimePay,
|
|
createdBy: userId,
|
|
},
|
|
})
|
|
}
|
|
results.push({ employeeId: item.employeeId, success: true })
|
|
} catch (err: any) {
|
|
results.push({ employeeId: item.employeeId, success: false, error: err.message })
|
|
}
|
|
}
|
|
|
|
return { total: items.length, success: results.filter(r => r.success).length, results }
|
|
}
|
|
|
|
/**
|
|
* 获取月度考勤确认列表
|
|
*/
|
|
export async function getAttendanceConfirmations(orgId: string, month: string, status?: string, department?: string) {
|
|
const where: any = { orgId, month }
|
|
if (status) where.status = status
|
|
if (department) where.employee = { department }
|
|
|
|
return prisma.attendanceConfirmation.findMany({
|
|
where,
|
|
include: { employee: { select: { id: true, name: true, department: true } } },
|
|
orderBy: { employee: { name: 'asc' } },
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 员工确认考勤(员工端)
|
|
*/
|
|
export async function confirmAttendance(orgId: string, employeeId: string, month: string, ip: string, disputeNote?: string) {
|
|
const record = await prisma.attendanceConfirmation.findUnique({
|
|
where: { orgId_employeeId_month: { orgId, employeeId, month } },
|
|
})
|
|
if (!record) {
|
|
throw { code: 'NOT_FOUND', message: '考勤确认记录不存在' }
|
|
}
|
|
|
|
if (disputeNote) {
|
|
// 有异议
|
|
return prisma.attendanceConfirmation.update({
|
|
where: { id: record.id },
|
|
data: { status: 'DISPUTED', disputeNote, confirmIp: ip },
|
|
})
|
|
}
|
|
|
|
// 确认无误
|
|
return prisma.attendanceConfirmation.update({
|
|
where: { id: record.id },
|
|
data: { status: 'CONFIRMED', confirmedAt: new Date(), confirmIp: ip },
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 获取考勤确认统计
|
|
*/
|
|
export async function getAttendanceStats(orgId: string, month: string) {
|
|
const records = await prisma.attendanceConfirmation.findMany({
|
|
where: { orgId, month },
|
|
})
|
|
|
|
return {
|
|
total: records.length,
|
|
pending: records.filter(r => r.status === 'PENDING').length,
|
|
confirmed: records.filter(r => r.status === 'CONFIRMED').length,
|
|
disputed: records.filter(r => r.status === 'DISPUTED').length,
|
|
}
|
|
}
|
|
|
|
// ========== 班次管理 ==========
|
|
|
|
export async function getShifts(orgId: string) {
|
|
return prisma.shift.findMany({
|
|
where: { orgId },
|
|
orderBy: { startTime: 'asc' },
|
|
})
|
|
}
|
|
|
|
export async function createShift(orgId: string, userId: string, data: {
|
|
name: string
|
|
startTime: string
|
|
endTime: string
|
|
flexibleMinutes?: number
|
|
restMinutes?: number
|
|
color?: string
|
|
}) {
|
|
return prisma.shift.create({
|
|
data: {
|
|
orgId,
|
|
name: data.name,
|
|
startTime: data.startTime,
|
|
endTime: data.endTime,
|
|
flexibleMinutes: data.flexibleMinutes || 0,
|
|
restMinutes: data.restMinutes || 0,
|
|
color: data.color || '#3b82f6',
|
|
createdBy: userId,
|
|
},
|
|
})
|
|
}
|
|
|
|
export async function updateShift(orgId: string, id: string, data: {
|
|
name?: string
|
|
startTime?: string
|
|
endTime?: string
|
|
flexibleMinutes?: number
|
|
restMinutes?: number
|
|
color?: string
|
|
}) {
|
|
return prisma.shift.update({ where: { id }, data })
|
|
}
|
|
|
|
export async function deleteShift(orgId: string, id: string) {
|
|
return prisma.shift.delete({ where: { id } })
|
|
}
|
|
|
|
// ========== 排班管理 ==========
|
|
|
|
export async function getShiftAssignments(orgId: string, date: string) {
|
|
const day = new Date(date)
|
|
day.setHours(0, 0, 0, 0)
|
|
const nextDay = new Date(day)
|
|
nextDay.setDate(nextDay.getDate() + 1)
|
|
|
|
return prisma.shiftAssignment.findMany({
|
|
where: { orgId, date: { gte: day, lt: nextDay } },
|
|
include: {
|
|
employee: { select: { id: true, name: true, department: true } },
|
|
shift: true,
|
|
},
|
|
orderBy: { employee: { name: 'asc' } },
|
|
})
|
|
}
|
|
|
|
export async function batchAssignShifts(orgId: string, userId: string, items: Array<{
|
|
employeeId: string
|
|
shiftId: string
|
|
date: string
|
|
}>) {
|
|
const results: Array<{ employeeId: string; date: string; success: boolean; error?: string }> = []
|
|
|
|
for (const item of items) {
|
|
try {
|
|
const date = new Date(item.date)
|
|
date.setHours(0, 0, 0, 0)
|
|
|
|
const existing = await prisma.shiftAssignment.findUnique({
|
|
where: { employeeId_date: { employeeId: item.employeeId, date } },
|
|
})
|
|
|
|
if (existing) {
|
|
await prisma.shiftAssignment.update({
|
|
where: { id: existing.id },
|
|
data: { shiftId: item.shiftId },
|
|
})
|
|
} else {
|
|
await prisma.shiftAssignment.create({
|
|
data: {
|
|
orgId,
|
|
employeeId: item.employeeId,
|
|
shiftId: item.shiftId,
|
|
date,
|
|
createdBy: userId,
|
|
},
|
|
})
|
|
}
|
|
results.push({ employeeId: item.employeeId, date: item.date, success: true })
|
|
} catch (err: any) {
|
|
results.push({ employeeId: item.employeeId, date: item.date, success: false, error: err.message })
|
|
}
|
|
}
|
|
|
|
return { total: items.length, success: results.filter(r => r.success).length, results }
|
|
}
|
|
|
|
export async function deleteShiftAssignment(orgId: string, id: string) {
|
|
return prisma.shiftAssignment.delete({ where: { id } })
|
|
}
|
|
|
|
// ========== 每日出勤 ==========
|
|
|
|
export async function getDailyAttendance(orgId: string, date: string) {
|
|
const day = new Date(date)
|
|
day.setHours(0, 0, 0, 0)
|
|
const nextDay = new Date(day)
|
|
nextDay.setDate(nextDay.getDate() + 1)
|
|
|
|
const [records, assignments, employees] = await Promise.all([
|
|
prisma.attendanceRecord.findMany({
|
|
where: { orgId, date: { gte: day, lt: nextDay } },
|
|
}),
|
|
prisma.shiftAssignment.findMany({
|
|
where: { orgId, date: { gte: day, lt: nextDay } },
|
|
include: { shift: true },
|
|
}),
|
|
prisma.employee.findMany({
|
|
where: { orgId, status: 'ACTIVE' },
|
|
select: { id: true, name: true, department: true },
|
|
orderBy: { name: 'asc' },
|
|
}),
|
|
])
|
|
|
|
const recordMap = new Map(records.map(r => [r.employeeId, r]))
|
|
const shiftMap = new Map(assignments.map(a => [a.employeeId, a.shift]))
|
|
|
|
return employees.map(emp => {
|
|
const record = recordMap.get(emp.id)
|
|
const shift = shiftMap.get(emp.id)
|
|
return {
|
|
employeeId: emp.id,
|
|
name: emp.name,
|
|
department: emp.department,
|
|
shift: shift ? { name: shift.name, startTime: shift.startTime, endTime: shift.endTime, color: shift.color } : null,
|
|
checkInTime: record?.checkInTime || null,
|
|
checkOutTime: record?.checkOutTime || null,
|
|
status: record?.status || 'UNREGISTERED',
|
|
lateMinutes: record?.lateMinutes || 0,
|
|
earlyMinutes: record?.earlyMinutes || 0,
|
|
workHours: record?.workHours || 0,
|
|
overtimeHours: record?.overtimeHours || 0,
|
|
remark: record?.remark || null,
|
|
}
|
|
})
|
|
}
|
|
|
|
// ========== 月度出勤报表 ==========
|
|
|
|
export async function getMonthlyReport(orgId: string, month: string) {
|
|
const monthStart = new Date(month + '-01')
|
|
const monthEnd = new Date(monthStart)
|
|
monthEnd.setMonth(monthEnd.getMonth() + 1)
|
|
|
|
const [records, confirmations, overtimes, leaves] = await Promise.all([
|
|
prisma.attendanceRecord.findMany({
|
|
where: { orgId, date: { gte: monthStart, lt: monthEnd } },
|
|
}),
|
|
prisma.attendanceConfirmation.findMany({
|
|
where: { orgId, month },
|
|
include: { employee: { select: { id: true, name: true, department: true } } },
|
|
}),
|
|
prisma.overtimeRecord.findMany({
|
|
where: { orgId, month },
|
|
}),
|
|
prisma.leaveRecord.findMany({
|
|
where: { orgId, startDate: { lt: monthEnd }, endDate: { gte: monthStart } },
|
|
}),
|
|
])
|
|
|
|
const employees = await prisma.employee.findMany({
|
|
where: { orgId, status: 'ACTIVE' },
|
|
select: { id: true, name: true, department: true },
|
|
orderBy: { name: 'asc' },
|
|
})
|
|
|
|
const otMap = new Map<string, number>()
|
|
for (const ot of overtimes) {
|
|
const totalHours = (ot.weekdayHours || 0) + (ot.weekendHours || 0) + (ot.holidayHours || 0)
|
|
otMap.set(ot.employeeId, (otMap.get(ot.employeeId) || 0) + totalHours)
|
|
}
|
|
|
|
const leaveMap = new Map<string, number>()
|
|
for (const lv of leaves) {
|
|
leaveMap.set(lv.employeeId, (leaveMap.get(lv.employeeId) || 0) + lv.days)
|
|
}
|
|
|
|
return employees.map(emp => {
|
|
const empRecords = records.filter(r => r.employeeId === emp.id)
|
|
const confirmation = confirmations.find(c => c.employeeId === emp.id)
|
|
|
|
return {
|
|
employeeId: emp.id,
|
|
name: emp.name,
|
|
department: emp.department,
|
|
workDays: confirmation?.workDays || empRecords.filter(r => r.status === 'NORMAL').length,
|
|
lateCount: empRecords.filter(r => r.status === 'LATE').length,
|
|
earlyLeaveCount: empRecords.filter(r => r.status === 'EARLY_LEAVE').length,
|
|
absentDays: empRecords.filter(r => r.status === 'ABSENT').length,
|
|
leaveDays: leaveMap.get(emp.id) || 0,
|
|
overtimeHours: confirmation ? (confirmation.weekdayHours + confirmation.weekendHours + confirmation.holidayHours) : (otMap.get(emp.id) || 0),
|
|
overtimePay: confirmation?.overtimePay || 0,
|
|
confirmationStatus: confirmation?.status || null,
|
|
}
|
|
})
|
|
}
|
|
|
|
// ========== 休假记录 ==========
|
|
|
|
export async function getLeaveRecords(orgId: string, employeeId?: string) {
|
|
const where: any = { orgId }
|
|
if (employeeId) where.employeeId = employeeId
|
|
|
|
return prisma.leaveRecord.findMany({
|
|
where,
|
|
include: { employee: { select: { id: true, name: true, department: true } } },
|
|
orderBy: { startDate: 'desc' },
|
|
})
|
|
}
|
|
|
|
export async function createLeaveRecord(orgId: string, userId: string, data: {
|
|
employeeId: string
|
|
leaveType: string
|
|
startDate: string
|
|
endDate: string
|
|
days: number
|
|
reason?: string
|
|
remark?: string
|
|
}) {
|
|
return prisma.leaveRecord.create({
|
|
data: {
|
|
orgId,
|
|
employeeId: data.employeeId,
|
|
leaveType: data.leaveType,
|
|
startDate: new Date(data.startDate),
|
|
endDate: new Date(data.endDate),
|
|
days: data.days,
|
|
reason: data.reason || null,
|
|
remark: data.remark || null,
|
|
createdBy: userId,
|
|
},
|
|
include: { employee: { select: { id: true, name: true, department: true } } },
|
|
})
|
|
}
|
|
|
|
export async function deleteLeaveRecord(orgId: string, id: string) {
|
|
return prisma.leaveRecord.delete({ where: { id } })
|
|
}
|