feat: 分页组件、Dashboard待办图标、归档与工资条解耦、总览数据优化
- 新增公用 Pagination 组件,Roster/Money/Dashboard 列表加分页 - Dashboard 待办按类型显示不同图标(合同/薪资/解聘/月度) - 待办分为「风险提醒」「月度任务」两个顶层 tab - 归档与工资条生成解耦:归档只锁定批次,工资条单独生成 - 工资条管理新增「从批次汇总生成」按钮 - Dashboard 总览优先从已归档批次 BatchEntry 汇总数据 - 新增工资条生成待办提醒,生成后自动标记完成 - 修复高风险统计只含 CONTRACT/TERMINATION 类型 - 修复月度任务去重逻辑覆盖 SALARY 类型
This commit is contained in:
@@ -50,6 +50,27 @@ enum RiskLevel {
|
||||
LOW
|
||||
}
|
||||
|
||||
enum PayrollBatchType {
|
||||
REGULAR // 常规发薪
|
||||
TERMINATION // 离职结算
|
||||
BONUS // 年终奖/奖金
|
||||
}
|
||||
|
||||
enum PayrollBatchStatus {
|
||||
DRAFT // 草稿(可编辑)
|
||||
ARCHIVED // 归档(已发薪,锁定)
|
||||
}
|
||||
|
||||
enum PayslipItemType {
|
||||
INPUT // 手工输入项(计算依据)
|
||||
CALCULATED // 计算项(公式自动计算)
|
||||
}
|
||||
|
||||
enum PayslipStatus {
|
||||
PENDING // 待发布
|
||||
PUBLISHED // 已发布到员工端
|
||||
}
|
||||
|
||||
enum RiskStatus {
|
||||
PENDING
|
||||
RESOLVED
|
||||
@@ -90,6 +111,7 @@ model Organization {
|
||||
plan Plan @default(FREE)
|
||||
maxEmployees Int @default(20)
|
||||
city String?
|
||||
payrollFrequency Int @default(1) // 每月发薪次数(1=一次一批)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@ -101,6 +123,9 @@ model Organization {
|
||||
riskItems RiskItem[]
|
||||
auditLogs AuditLog[]
|
||||
payslips Payslip[]
|
||||
payrollBatches PayrollBatch[]
|
||||
payslipItems PayslipItem[]
|
||||
salaryChangeRecords SalaryChangeRecord[]
|
||||
onboardingLinks OnboardingLink[]
|
||||
confirmLinks ContractConfirmLink[]
|
||||
socialInsuranceConfig SocialInsuranceConfig?
|
||||
@@ -149,6 +174,10 @@ model Employee {
|
||||
isPregnant Boolean @default(false)
|
||||
isInMedicalPeriod Boolean @default(false)
|
||||
isWorkInjured Boolean @default(false)
|
||||
// 薪税扩展
|
||||
socialInsBase Float? // 社保缴费基数(按人核定)
|
||||
housingFundBase Float? // 公积金缴费基数(按人核定)
|
||||
specialDeduction Float @default(0) // 专项附加扣除(子女教育、赡养老人等,员工portal端填报)
|
||||
createdBy String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@ -158,6 +187,8 @@ model Employee {
|
||||
terminations TerminationRecord[]
|
||||
riskItems RiskItem[]
|
||||
payslips Payslip[]
|
||||
salaryChanges SalaryChangeRecord[]
|
||||
batchEntries BatchEntry[]
|
||||
attachments EmployeeAttachment[]
|
||||
disciplinaryRecords DisciplinaryRecord[]
|
||||
attendanceRecords AttendanceRecord[]
|
||||
@@ -436,6 +467,7 @@ model Payslip {
|
||||
employeeId String
|
||||
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
|
||||
month String // YYYY-MM
|
||||
// 薪酬构成
|
||||
baseSalary Float @default(0)
|
||||
overtimePay Float @default(0)
|
||||
weekdayOvertimePay Float @default(0)
|
||||
@@ -443,14 +475,122 @@ model Payslip {
|
||||
holidayOvertimePay Float @default(0)
|
||||
allowance Float @default(0)
|
||||
deduction Float @default(0)
|
||||
totalPay Float @default(0)
|
||||
bonus Float @default(0) // 奖金/年终奖
|
||||
totalPay Float @default(0) // 应发合计
|
||||
// 扣除项
|
||||
socialEmp Float @default(0) // 个人社保
|
||||
housingEmp Float @default(0) // 个人公积金
|
||||
tax Float @default(0) // 个人所得税
|
||||
netPay Float @default(0) // 实发工资 = totalPay - socialEmp - housingEmp - tax
|
||||
// 累计预扣法
|
||||
ytdIncome Float @default(0) // 当年累计收入
|
||||
ytdTaxDeducted Float @default(0) // 当年累计已扣税
|
||||
ytdSocialEmp Float @default(0) // 当年累计个人社保
|
||||
ytdHousingEmp Float @default(0) // 当年累计个人公积金
|
||||
// 状态
|
||||
status PayslipStatus @default(PENDING) // PENDING → PUBLISHED
|
||||
confirmedAt DateTime?
|
||||
confirmedIp String?
|
||||
publishedAt DateTime? // 工资条发布到员工端的时间
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([employeeId, month])
|
||||
@@index([orgId, month])
|
||||
@@index([orgId, status])
|
||||
}
|
||||
|
||||
model PayrollBatch {
|
||||
id String @id @default(cuid())
|
||||
orgId String
|
||||
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
||||
month String // YYYY-MM
|
||||
batchNo Int // 批次序号(1, 2, 3...)
|
||||
name String // 批次名称
|
||||
type PayrollBatchType @default(REGULAR)
|
||||
status PayrollBatchStatus @default(DRAFT)
|
||||
employeeCount Int @default(0)
|
||||
totalPay Float @default(0)
|
||||
totalNetPay Float @default(0)
|
||||
totalSocialOrg Float @default(0)
|
||||
totalSocialEmp Float @default(0)
|
||||
totalHousingOrg Float @default(0)
|
||||
totalHousingEmp Float @default(0)
|
||||
totalTax Float @default(0)
|
||||
remark String?
|
||||
createdBy String
|
||||
createdAt DateTime @default(now())
|
||||
archivedAt DateTime?
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
entries BatchEntry[]
|
||||
|
||||
@@unique([orgId, month, batchNo])
|
||||
@@index([orgId, month])
|
||||
@@index([orgId, status])
|
||||
}
|
||||
|
||||
model BatchEntry {
|
||||
id String @id @default(cuid())
|
||||
batchId String
|
||||
batch PayrollBatch @relation(fields: [batchId], references: [id], onDelete: Cascade)
|
||||
orgId String
|
||||
employeeId String
|
||||
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
|
||||
// 薪酬项(可编辑的输入项)
|
||||
baseSalary Float @default(0)
|
||||
overtimePay Float @default(0)
|
||||
allowance Float @default(0)
|
||||
deduction Float @default(0)
|
||||
bonus Float @default(0)
|
||||
// 自动计算项
|
||||
socialEmp Float @default(0)
|
||||
socialOrg Float @default(0)
|
||||
housingEmp Float @default(0)
|
||||
housingOrg Float @default(0)
|
||||
tax Float @default(0)
|
||||
totalPay Float @default(0) // 应发合计
|
||||
netPay Float @default(0) // 实发工资
|
||||
// 风险提示
|
||||
riskWarnings Json?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([batchId, employeeId])
|
||||
@@index([orgId, employeeId])
|
||||
}
|
||||
|
||||
model PayslipItem {
|
||||
id String @id @default(cuid())
|
||||
orgId String
|
||||
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
||||
name String // 显示名称
|
||||
code String // 字段代码
|
||||
type PayslipItemType @default(INPUT)
|
||||
formula String? // 计算公式(CALCULATED 类型),如 "baseSalary + overtimePay + allowance - deduction"
|
||||
order Int @default(0)
|
||||
isDefault Boolean @default(true) // 系统预置项不可删除
|
||||
isEditable Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([orgId, code])
|
||||
}
|
||||
|
||||
model SalaryChangeRecord {
|
||||
id String @id @default(cuid())
|
||||
orgId String
|
||||
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
||||
employeeId String
|
||||
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
|
||||
oldSalary Float
|
||||
newSalary Float
|
||||
effectiveDate DateTime // 生效日期
|
||||
reason String?
|
||||
createdBy String
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([orgId, employeeId])
|
||||
}
|
||||
|
||||
model OnboardingLink {
|
||||
|
||||
+251
-46
@@ -1,29 +1,92 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { randomBytes } from 'crypto'
|
||||
import { encrypt } from '../src/lib/crypto'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
async function main() {
|
||||
// 创建测试企业
|
||||
let org = await prisma.organization.findFirst({ where: { name: '测试科技有限公司' } })
|
||||
if (!org) {
|
||||
org = await prisma.organization.create({
|
||||
data: {
|
||||
name: '测试科技有限公司',
|
||||
plan: 'FREE',
|
||||
maxEmployees: 20,
|
||||
city: '上海',
|
||||
},
|
||||
})
|
||||
}
|
||||
// 社保计算(与 payroll.service.ts 一致)
|
||||
function calcSocial(base: number, config: any) {
|
||||
const actualBase = Math.min(Math.max(base, config.baseMin), config.baseMax)
|
||||
const socialEmp = actualBase * (config.pensionEmp + config.medicalEmp + config.unemploymentEmp) / 100
|
||||
const socialOrg = actualBase * (config.pensionOrg + config.medicalOrg + config.unemploymentOrg + config.injuryOrg + config.maternityOrg) / 100
|
||||
return { socialEmp: Math.round(socialEmp * 100) / 100, socialOrg: Math.round(socialOrg * 100) / 100 }
|
||||
}
|
||||
function calcHousing(base: number, config: any) {
|
||||
const actualBase = Math.min(Math.max(base, config.baseMin), config.baseMax)
|
||||
const housingEmp = actualBase * config.housingEmp / 100
|
||||
const housingOrg = actualBase * config.housingOrg / 100
|
||||
return { housingEmp: Math.round(housingEmp * 100) / 100, housingOrg: Math.round(housingOrg * 100) / 100 }
|
||||
}
|
||||
function calcTax(taxableIncome: number): number {
|
||||
if (taxableIncome <= 0) return 0
|
||||
let tax = 0
|
||||
if (taxableIncome <= 36000) tax = taxableIncome * 0.03
|
||||
else if (taxableIncome <= 144000) tax = taxableIncome * 0.10 - 2520
|
||||
else if (taxableIncome <= 300000) tax = taxableIncome * 0.20 - 16920
|
||||
else if (taxableIncome <= 420000) tax = taxableIncome * 0.25 - 31920
|
||||
else if (taxableIncome <= 660000) tax = taxableIncome * 0.30 - 52920
|
||||
else if (taxableIncome <= 960000) tax = taxableIncome * 0.35 - 85920
|
||||
else tax = taxableIncome * 0.45 - 181920
|
||||
return Math.max(0, Math.round(tax * 100) / 100)
|
||||
}
|
||||
|
||||
// 创建管理员用户
|
||||
// 9名员工完整数据
|
||||
const EMPLOYEES = [
|
||||
{ name: '张伟', gender: '男', dept: '技术部', phone: '13900000001', salary: 18000, hireDate: '2023-03-01', socialBase: 18000, housingBase: 18000, specialDeduction: 2000, contractType: 'FIXED', years: 3, probation: 3, probationSalary: 14400, bank: '工商银行', account: '6222021234567890001', emergency: '张父', emergencyPhone: '13800001001', address: '上海市浦东新区张江路100号' },
|
||||
{ name: '李娜', gender: '女', dept: '技术部', phone: '13900000002', salary: 15000, hireDate: '2023-06-15', socialBase: 15000, housingBase: 15000, specialDeduction: 1000, contractType: 'FIXED', years: 3, probation: 2, probationSalary: 12000, bank: '建设银行', account: '6227001234567890002', emergency: '李母', emergencyPhone: '13800001002', address: '上海市徐汇区漕河泾50号', pregnant: true },
|
||||
{ name: '王强', gender: '男', dept: '销售部', phone: '13900000003', salary: 12000, hireDate: '2024-01-10', socialBase: 12000, housingBase: 12000, specialDeduction: 3000, contractType: 'FIXED', years: 3, probation: 3, probationSalary: 9600, bank: '招商银行', account: '6225881234567890003', emergency: '王妻', emergencyPhone: '13800001003', address: '上海市闵行区莘庄路200号' },
|
||||
{ name: '赵敏', gender: '女', dept: '人事部', phone: '13900000004', salary: 10000, hireDate: '2022-09-01', socialBase: 10000, housingBase: 10000, specialDeduction: 1500, contractType: 'UNFIXED', years: 0, probation: 0, probationSalary: 0, bank: '农业银行', account: '6228481234567890004', emergency: '赵父', emergencyPhone: '13800001004', address: '上海市黄浦区南京东路300号' },
|
||||
{ name: '陈刚', gender: '男', dept: '销售部', phone: '13900000005', salary: 8000, hireDate: '2024-07-01', socialBase: 8000, housingBase: 8000, specialDeduction: 0, contractType: 'FIXED', years: 3, probation: 2, probationSalary: 6400, bank: '中国银行', account: '6217001234567890005', emergency: '陈母', emergencyPhone: '13800001005', address: '上海市杨浦区五角场400号' },
|
||||
{ name: '刘洋', gender: '男', dept: '技术部', phone: '13900000006', salary: 22000, hireDate: '2021-04-01', socialBase: 33891, housingBase: 33891, specialDeduction: 4000, contractType: 'UNFIXED', years: 0, probation: 0, probationSalary: 0, bank: '交通银行', account: '6222601234567890006', emergency: '刘妻', emergencyPhone: '13800001006', address: '上海市长宁区中山公园500号' },
|
||||
{ name: '周婷', gender: '女', dept: '财务部', phone: '13900000007', salary: 13000, hireDate: '2023-11-15', socialBase: 13000, housingBase: 13000, specialDeduction: 2500, contractType: 'FIXED', years: 3, probation: 2, probationSalary: 10400, bank: '浦发银行', account: '6225161234567890007', emergency: '周父', emergencyPhone: '13800001007', address: '上海市静安区南京西路600号' },
|
||||
{ name: '孙磊', gender: '男', dept: '技术部', phone: '13900000008', salary: 16000, hireDate: '2022-06-01', socialBase: 16000, housingBase: 16000, specialDeduction: 1000, contractType: 'FIXED', years: 3, probation: 3, probationSalary: 12800, bank: '民生银行', account: '6226161234567890008', emergency: '孙母', emergencyPhone: '13800001008', address: '上海市虹口区四川北路700号' },
|
||||
{ name: '吴芳', gender: '女', dept: '销售部', phone: '13900000009', salary: 9000, hireDate: '2025-02-15', socialBase: 9000, housingBase: 9000, specialDeduction: 500, contractType: 'FIXED', years: 3, probation: 2, probationSalary: 7200, bank: '光大银行', account: '6226621234567890009', emergency: '吴夫', emergencyPhone: '13800001009', address: '上海市宝山区牡丹江路800号' },
|
||||
]
|
||||
|
||||
async function main() {
|
||||
// 1. 清空所有数据(按依赖顺序删除)
|
||||
console.log('清空现有数据...')
|
||||
await prisma.notificationLog.deleteMany()
|
||||
await prisma.auditLog.deleteMany()
|
||||
await prisma.batchEntry.deleteMany()
|
||||
await prisma.payrollBatch.deleteMany()
|
||||
await prisma.payslipItem.deleteMany()
|
||||
await prisma.salaryChangeRecord.deleteMany()
|
||||
await prisma.payslip.deleteMany()
|
||||
await prisma.overtimeRecord.deleteMany()
|
||||
await prisma.terminationRecord.deleteMany()
|
||||
await prisma.riskItem.deleteMany()
|
||||
await prisma.employeeAttachment.deleteMany()
|
||||
await prisma.disciplinaryRecord.deleteMany()
|
||||
await prisma.attendanceRecord.deleteMany()
|
||||
await prisma.trainingRecord.deleteMany()
|
||||
await prisma.performanceRecord.deleteMany()
|
||||
await prisma.laborContract.deleteMany()
|
||||
await prisma.contractConfirmLink.deleteMany()
|
||||
await prisma.onboardingLink.deleteMany()
|
||||
await prisma.employee.deleteMany()
|
||||
await prisma.socialInsuranceConfig.deleteMany()
|
||||
await prisma.notificationSetting.deleteMany()
|
||||
await prisma.user.deleteMany()
|
||||
await prisma.organization.deleteMany()
|
||||
console.log('数据已清空')
|
||||
|
||||
// 2. 创建企业
|
||||
const org = await prisma.organization.create({
|
||||
data: {
|
||||
name: '智云科技有限公司',
|
||||
plan: 'PRO',
|
||||
maxEmployees: 50,
|
||||
city: '上海',
|
||||
payrollFrequency: 1,
|
||||
},
|
||||
})
|
||||
console.log('企业已创建:', org.name)
|
||||
|
||||
// 3. 创建管理员
|
||||
const passwordHash = await bcrypt.hash('12345678', 10)
|
||||
const admin = await prisma.user.upsert({
|
||||
where: { phone: '13800000001' },
|
||||
update: {},
|
||||
create: {
|
||||
const admin = await prisma.user.create({
|
||||
data: {
|
||||
orgId: org.id,
|
||||
phone: '13800000001',
|
||||
name: '管理员',
|
||||
@@ -31,40 +94,182 @@ async function main() {
|
||||
role: 'ADMIN',
|
||||
},
|
||||
})
|
||||
console.log('管理员已创建:', admin.phone)
|
||||
|
||||
// 创建测试员工
|
||||
const salaryHash = randomBytes(32).toString('hex')
|
||||
const employee = await prisma.employee.create({
|
||||
// 4. 创建社保配置(上海标准)
|
||||
await prisma.socialInsuranceConfig.create({
|
||||
data: {
|
||||
orgId: org.id,
|
||||
name: '张三',
|
||||
department: '技术部',
|
||||
hireDate: new Date('2026-01-15'),
|
||||
monthlySalary: 'encrypted:' + salaryHash,
|
||||
phone: '13900000001',
|
||||
gender: '男',
|
||||
createdBy: admin.id,
|
||||
city: '上海',
|
||||
pensionOrg: 16,
|
||||
pensionEmp: 8,
|
||||
medicalOrg: 9.8,
|
||||
medicalEmp: 2,
|
||||
unemploymentOrg: 0.5,
|
||||
unemploymentEmp: 0.5,
|
||||
injuryOrg: 0.2,
|
||||
maternityOrg: 0.8,
|
||||
housingOrg: 7,
|
||||
housingEmp: 7,
|
||||
baseMin: 7384,
|
||||
baseMax: 36921,
|
||||
},
|
||||
})
|
||||
console.log('社保配置已创建')
|
||||
|
||||
// 5. 创建通知设置
|
||||
await prisma.notificationSetting.create({
|
||||
data: {
|
||||
orgId: org.id,
|
||||
contractExpiry: true,
|
||||
expiryDays: 30,
|
||||
contractUnsigned: true,
|
||||
overtimeAlert: true,
|
||||
payslipReady: true,
|
||||
payrollDay: 10,
|
||||
socialInsDay: 15,
|
||||
housingFundDay: 15,
|
||||
taxDay: 15,
|
||||
},
|
||||
})
|
||||
|
||||
// 创建测试合同
|
||||
await prisma.laborContract.create({
|
||||
data: {
|
||||
orgId: org.id,
|
||||
employeeId: employee.id,
|
||||
signDate: new Date('2026-01-20'),
|
||||
startDate: new Date('2026-02-01'),
|
||||
endDate: new Date('2029-01-31'),
|
||||
contractType: 'FIXED',
|
||||
signMethod: 'PAPER',
|
||||
contractYears: 3,
|
||||
probationMonths: 2,
|
||||
probationSalary: 6400,
|
||||
createdBy: admin.id,
|
||||
},
|
||||
})
|
||||
// 6. 创建薪酬模版(预置项)
|
||||
const defaultItems: { name: string; code: string; type: 'INPUT' | 'CALCULATED'; formula: string | null; order: number; isDefault: boolean; isEditable: boolean }[] = [
|
||||
{ name: '基本工资', code: 'baseSalary', type: 'INPUT', formula: null, order: 1, isDefault: true, isEditable: true },
|
||||
{ name: '加班费', code: 'overtimePay', type: 'CALCULATED', formula: 'weekdayOvertimePay + weekendOvertimePay + holidayOvertimePay', order: 2, isDefault: true, isEditable: false },
|
||||
{ name: '津贴补贴', code: 'allowance', type: 'INPUT', formula: null, order: 3, isDefault: true, isEditable: true },
|
||||
{ name: '奖金', code: 'bonus', type: 'INPUT', formula: null, order: 4, isDefault: true, isEditable: true },
|
||||
{ name: '扣款', code: 'deduction', type: 'INPUT', formula: null, order: 5, isDefault: true, isEditable: true },
|
||||
{ name: '应发合计', code: 'totalPay', type: 'CALCULATED', formula: 'baseSalary + overtimePay + allowance + bonus - deduction', order: 6, isDefault: true, isEditable: false },
|
||||
{ name: '个人社保', code: 'socialEmp', type: 'CALCULATED', formula: 'SOCIAL_EMP', order: 7, isDefault: true, isEditable: false },
|
||||
{ name: '个人公积金', code: 'housingEmp', type: 'CALCULATED', formula: 'HOUSING_EMP', order: 8, isDefault: true, isEditable: false },
|
||||
{ name: '个人所得税', code: 'tax', type: 'CALCULATED', formula: 'TAX', order: 9, isDefault: true, isEditable: false },
|
||||
{ name: '实发工资', code: 'netPay', type: 'CALCULATED', formula: 'totalPay - socialEmp - housingEmp - tax', order: 10, isDefault: true, isEditable: false },
|
||||
]
|
||||
for (const item of defaultItems) {
|
||||
await prisma.payslipItem.create({
|
||||
data: { orgId: org.id, ...item },
|
||||
})
|
||||
}
|
||||
console.log('薪酬模版已创建')
|
||||
|
||||
console.log('Seed data created:', { org: org.id, admin: admin.id, employee: employee.id })
|
||||
// 7. 创建9名员工 + 合同
|
||||
for (let i = 0; i < EMPLOYEES.length; i++) {
|
||||
const e = EMPLOYEES[i]
|
||||
const emp = await prisma.employee.create({
|
||||
data: {
|
||||
orgId: org.id,
|
||||
name: e.name,
|
||||
department: e.dept,
|
||||
hireDate: new Date(e.hireDate),
|
||||
monthlySalary: encrypt(String(e.salary)),
|
||||
phone: e.phone,
|
||||
gender: e.gender,
|
||||
socialInsBase: e.socialBase,
|
||||
housingFundBase: e.housingBase,
|
||||
specialDeduction: e.specialDeduction,
|
||||
bankName: e.bank,
|
||||
bankAccount: encrypt(e.account),
|
||||
emergencyContact: e.emergency,
|
||||
emergencyPhone: e.emergencyPhone,
|
||||
address: e.address,
|
||||
isPregnant: e.pregnant || false,
|
||||
createdBy: admin.id,
|
||||
},
|
||||
})
|
||||
|
||||
// 创建合同
|
||||
const startDate = new Date(e.hireDate)
|
||||
const endDate = e.contractType === 'FIXED'
|
||||
? new Date(startDate.getFullYear() + e.years, startDate.getMonth(), startDate.getDate() - 1)
|
||||
: null
|
||||
|
||||
await prisma.laborContract.create({
|
||||
data: {
|
||||
orgId: org.id,
|
||||
employeeId: emp.id,
|
||||
signDate: new Date(e.hireDate),
|
||||
startDate,
|
||||
endDate,
|
||||
contractType: e.contractType as any,
|
||||
signMethod: 'PAPER',
|
||||
contractYears: e.years,
|
||||
probationMonths: e.probation,
|
||||
probationSalary: e.probationSalary,
|
||||
createdBy: admin.id,
|
||||
},
|
||||
})
|
||||
console.log(`员工 ${i + 1}/9 已创建: ${e.name} - ${e.dept} - ¥${e.salary}/月`)
|
||||
}
|
||||
|
||||
// 8. 生成 1-6 月历史工资条(已发布),使 7 月累计预扣个税有 YTD 数据
|
||||
console.log('\n生成 1-6 月历史工资条...')
|
||||
const socialConfig = await prisma.socialInsuranceConfig.findUnique({ where: { orgId: org.id } })
|
||||
const allEmployees = await prisma.employee.findMany({ where: { orgId: org.id } })
|
||||
const months = ['2026-01', '2026-02', '2026-03', '2026-04', '2026-05', '2026-06']
|
||||
|
||||
for (const emp of allEmployees) {
|
||||
// 跳过 2026 年之后入职的员工
|
||||
const hireYear = emp.hireDate.getFullYear()
|
||||
if (hireYear > 2026) continue
|
||||
const hireMonth = hireYear === 2026 ? emp.hireDate.getMonth() + 1 : 1
|
||||
|
||||
let ytdIncome = 0, ytdSocialEmp = 0, ytdHousingEmp = 0, ytdTaxDeducted = 0
|
||||
|
||||
for (let m = 1; m <= 6; m++) {
|
||||
if (m < hireMonth) continue
|
||||
const monthStr = `2026-${String(m).padStart(2, '0')}`
|
||||
const baseSalary = emp.socialInsBase || 0 // 用社保基数作为基本工资(简化)
|
||||
const social = calcSocial(emp.socialInsBase || baseSalary, socialConfig)
|
||||
const housing = calcHousing(emp.housingFundBase || baseSalary, socialConfig)
|
||||
const totalPay = baseSalary
|
||||
const specialDeduction = emp.specialDeduction * m
|
||||
|
||||
ytdIncome += totalPay
|
||||
ytdSocialEmp += social.socialEmp
|
||||
ytdHousingEmp += housing.housingEmp
|
||||
|
||||
const ytdTaxableIncome = Math.max(0, ytdIncome - 5000 * m - ytdSocialEmp - ytdHousingEmp - specialDeduction)
|
||||
const ytdTax = calcTax(ytdTaxableIncome)
|
||||
const monthTax = Math.max(0, Math.round((ytdTax - ytdTaxDeducted) * 100) / 100)
|
||||
ytdTaxDeducted += monthTax
|
||||
|
||||
const netPay = Math.round((totalPay - social.socialEmp - housing.housingEmp - monthTax) * 100) / 100
|
||||
|
||||
await prisma.payslip.create({
|
||||
data: {
|
||||
orgId: org.id,
|
||||
employeeId: emp.id,
|
||||
month: monthStr,
|
||||
baseSalary,
|
||||
overtimePay: 0,
|
||||
allowance: 0,
|
||||
deduction: 0,
|
||||
bonus: 0,
|
||||
totalPay,
|
||||
socialEmp: social.socialEmp,
|
||||
housingEmp: housing.housingEmp,
|
||||
tax: monthTax,
|
||||
netPay,
|
||||
ytdIncome,
|
||||
ytdTaxDeducted,
|
||||
ytdSocialEmp,
|
||||
ytdHousingEmp,
|
||||
status: 'PUBLISHED',
|
||||
publishedAt: new Date(`${monthStr}-10T10:00:00Z`),
|
||||
confirmedAt: new Date(`${monthStr}-12T10:00:00Z`),
|
||||
},
|
||||
})
|
||||
}
|
||||
console.log(` ${emp.name}: 1-6月工资条已生成`)
|
||||
}
|
||||
|
||||
console.log('\n===== 示例数据创建完成 =====')
|
||||
console.log(`企业: ${org.name}`)
|
||||
console.log(`管理员: 13800000001 / 密码: 12345678`)
|
||||
console.log(`员工: ${EMPLOYEES.length} 人`)
|
||||
console.log('社保配置: 上海标准')
|
||||
console.log('薪酬模版: 10项预置')
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user