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 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}:00Z`).toISOString() : null const checkOutTime = data.checkOutTime ? new Date(`${data.date}T${data.checkOutTime}:00Z`).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) 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() for (const ot of overtimes) { const totalHours = (ot.weekdayHours || 0) + (ot.weekendHours || 0) + (ot.holidayHours || 0) const prev = otMap.get(ot.employeeId) || { hours: 0, pay: 0 } otMap.set(ot.employeeId, { hours: prev.hours + totalHours, pay: prev.pay + (ot.totalPay || 0) }) } const leaveMap = new Map() 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)?.hours || 0), overtimePay: confirmation?.overtimePay || otMap.get(emp.id)?.pay || 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 } }) }