feat: 系统优化Phase2 - 面包屑导航/侧边栏间距/制度公示阅读签收/模板变量中文化/通知类型补全

- 面包屑导航组件,集成至TopNav header
- 侧边栏菜单分组间距增大,分组间分隔线
- 制度公示员工阅读签收:PolicyReadRecord模型、portal路由、管理端阅读统计
- 修复Policies.tsx民主程序推进bug(字段名/API路径/参数)
- 用工文本模板变量名英文转中文显示
- 通知类型TYPE_LABELS补全(RISK_ALERT/SOCIAL_INS/OVERTIME_ALERT/PAYSLIP_READY)
- 通知示例数据补充
- h2标题统一为text-sm font-medium
- 新增run.md
This commit is contained in:
selfrelease
2026-07-26 20:32:38 +08:00
parent 9cb0d1f63b
commit d79e3baa34
71 changed files with 18561 additions and 3230 deletions
+150
View File
@@ -0,0 +1,150 @@
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) {
const where: any = { orgId, month }
if (status) where.status = status
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,
}
}