fix: 薪资批次排除预入职员工(hireDate > 批次月末)
原查询只过滤status=ACTIVE,预入职员工status也是ACTIVE会被错误纳入。 增加hireDate <= monthEnd条件,确保只拉入已入职员工。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -341,14 +341,7 @@ router.post('/excel', authMiddleware, requireAdmin, upload.single('file'), async
|
||||
},
|
||||
})
|
||||
|
||||
// 仅在未 opt-out 时创建社保记录
|
||||
if (!socialInsOptOut) {
|
||||
await prisma.employeeSocialInsRecord.create({ data: { orgId, employeeId: emp.id, startMonth: dateToMonth(hireDate), endMonth: null, base: socialInsBase, changeType: 'ONBOARDING', createdBy: userId } })
|
||||
}
|
||||
// 仅在未 opt-out 时创建公积金记录
|
||||
if (!housingFundOptOut) {
|
||||
await prisma.employeeHousingFundRecord.create({ data: { orgId, employeeId: emp.id, startMonth: dateToMonth(hireDate), endMonth: null, base: housingFundBase, changeType: 'ONBOARDING', createdBy: userId } })
|
||||
}
|
||||
// 社保公积金记录不在导入时创建,由 HR 在社保模块办理增员后创建
|
||||
await prisma.salaryChangeRecord.create({ data: { orgId, employeeId: emp.id, oldSalary: 0, newSalary: num(salary), effectiveDate: hireDate, effectiveMonth: dateToMonth(hireDate), endMonth: null, changeType: 'ONBOARDING', createdBy: userId } })
|
||||
await prisma.employeeDepartmentRecord.create({ data: { orgId, employeeId: emp.id, oldDepartment: '', newDepartment: dept, effectiveMonth: dateToMonth(hireDate), endMonth: null, changeType: 'ONBOARDING', createdBy: userId } })
|
||||
|
||||
|
||||
@@ -222,7 +222,7 @@ router.get('/batches/:id', async (req: AuthRequest, res: Response, next: NextFun
|
||||
include: {
|
||||
entries: {
|
||||
include: {
|
||||
employee: { select: { id: true, name: true, department: true, status: true, bankAccount: true, bankName: true, idCardNumber: true } },
|
||||
employee: { select: { id: true, name: true, department: true, status: true, bankAccount: true, bankName: true, idCardNumber: true, contracts: { orderBy: { createdAt: 'desc' }, take: 1, select: { contractType: true } } } },
|
||||
},
|
||||
orderBy: { employee: { name: 'asc' } },
|
||||
},
|
||||
@@ -352,7 +352,9 @@ router.post('/batches', async (req: AuthRequest, res: Response, next: NextFuncti
|
||||
where: {
|
||||
orgId,
|
||||
OR: [
|
||||
{ status: 'ACTIVE' },
|
||||
// 在职且已入职(hireDate <= 批次月末,排除预入职)
|
||||
{ status: 'ACTIVE', hireDate: { lte: monthEnd } },
|
||||
// 本月离职的员工(离职当月仍需结算)
|
||||
{ status: 'RESIGNED', updatedAt: { gte: monthStart, lte: monthEnd } },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1285,12 +1285,24 @@ router.get('/monthly-changes', async (req: AuthRequest, res: Response, next: Nex
|
||||
const month = (req.query.month as string) || new Date().toISOString().slice(0, 7)
|
||||
const orgId = req.user!.orgId
|
||||
|
||||
// 增员:startMonth == month(排除劳务/实习协议员工)
|
||||
const additions = await prisma.employeeSocialInsRecord.findMany({
|
||||
where: { orgId, startMonth: month, employee: { contracts: { some: { contractType: { in: ['FIXED', 'UNFIXED'] } } } } },
|
||||
include: { employee: { select: { name: true, department: true, idCardNumber: true, contracts: { orderBy: { createdAt: 'desc' }, take: 1, select: { contractType: true } } } } },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
// 增员:入职月 == month + 有社保基数 + 无社保记录 + 非劳务/实习协议
|
||||
// 查询该月入职的在职员工
|
||||
const monthStart = new Date(`${month}-01T00:00:00.000Z`)
|
||||
const monthEnd = new Date(monthStart.getFullYear(), monthStart.getMonth() + 1, 1)
|
||||
const hiredEmployees = await prisma.employee.findMany({
|
||||
where: {
|
||||
orgId,
|
||||
status: 'ACTIVE',
|
||||
hireDate: { gte: monthStart, lt: monthEnd },
|
||||
contracts: { some: { contractType: { in: ['FIXED', 'UNFIXED'] } } },
|
||||
},
|
||||
include: {
|
||||
contracts: { orderBy: { createdAt: 'desc' }, take: 1, select: { contractType: true } },
|
||||
socialInsRecords: { where: { endMonth: null }, take: 1 },
|
||||
},
|
||||
})
|
||||
// 过滤掉已有社保记录的(已办理增员)
|
||||
const pendingAdditions = hiredEmployees.filter(e => e.socialInsRecords.length === 0)
|
||||
|
||||
// 减员:endMonth == month 且 changeType 为 TERMINATION 或 CITY_CHANGE
|
||||
const reductions = await prisma.employeeSocialInsRecord.findMany({
|
||||
@@ -1308,6 +1320,32 @@ router.get('/monthly-changes', async (req: AuthRequest, res: Response, next: Nex
|
||||
return configCache.get(city)
|
||||
}
|
||||
|
||||
// 增员:从员工信息构造
|
||||
const mapAddition = async (e: any) => {
|
||||
const city = e.city || '北京'
|
||||
const config = await getConfigForCity(city)
|
||||
const base = e.socialInsBase || 0
|
||||
const detail = config ? calcSocialDetail(base, config) : null
|
||||
return {
|
||||
recordId: null,
|
||||
employeeId: e.id,
|
||||
name: e.name,
|
||||
idCardNumber: decryptIdCard(e.idCardNumber),
|
||||
department: e.department,
|
||||
city,
|
||||
base,
|
||||
startMonth: month,
|
||||
endMonth: null,
|
||||
changeType: 'PENDING',
|
||||
detail: detail ? {
|
||||
items: detail.items,
|
||||
totalOrg: detail.totalOrg,
|
||||
totalEmp: detail.totalEmp,
|
||||
total: detail.totalOrg + detail.totalEmp,
|
||||
} : null,
|
||||
}
|
||||
}
|
||||
|
||||
const mapRecord = async (r: any) => {
|
||||
const config = await getConfigForCity(r.city)
|
||||
const detail = config ? calcSocialDetail(r.base, config) : null
|
||||
@@ -1332,20 +1370,26 @@ router.get('/monthly-changes', async (req: AuthRequest, res: Response, next: Nex
|
||||
}
|
||||
|
||||
// 按城市分组
|
||||
const allRecords = [...additions, ...reductions]
|
||||
const cities = [...new Set(allRecords.map((r) => r.city))]
|
||||
const allCities = [...new Set([
|
||||
...pendingAdditions.map((e) => e.city || '北京'),
|
||||
...reductions.map((r) => r.city),
|
||||
])]
|
||||
const configs: Record<string, any> = {}
|
||||
for (const c of cities) {
|
||||
for (const c of allCities) {
|
||||
const cfg = await getConfigForCity(c)
|
||||
if (cfg) configs[c] = { city: cfg.city, effectiveFrom: cfg.effectiveFrom, baseMin: cfg.baseMin, baseMax: cfg.baseMax }
|
||||
}
|
||||
|
||||
const additionsResult = await Promise.all(pendingAdditions.map(mapAddition))
|
||||
console.log(`[monthly-changes] orgId=${orgId} month=${month} hiredCount=${hiredEmployees.length} pendingCount=${pendingAdditions.length} additions=${additionsResult.length}`)
|
||||
|
||||
res.set('Cache-Control', 'no-cache, no-store, must-revalidate')
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
month,
|
||||
configs,
|
||||
additions: await Promise.all(additions.map(mapRecord)),
|
||||
additions: additionsResult,
|
||||
reductions: await Promise.all(reductions.map(mapRecord)),
|
||||
},
|
||||
})
|
||||
@@ -1354,17 +1398,66 @@ router.get('/monthly-changes', async (req: AuthRequest, res: Response, next: Nex
|
||||
}
|
||||
})
|
||||
|
||||
// 调试:查看增员列表原始数据
|
||||
router.get('/debug-pending', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const month = (req.query.month as string) || new Date().toISOString().slice(0, 7)
|
||||
const orgId = req.user!.orgId
|
||||
const monthStart = new Date(`${month}-01T00:00:00.000Z`)
|
||||
const monthEnd = new Date(monthStart.getFullYear(), monthStart.getMonth() + 1, 1)
|
||||
const hiredEmployees = await prisma.employee.findMany({
|
||||
where: {
|
||||
orgId,
|
||||
status: 'ACTIVE',
|
||||
hireDate: { gte: monthStart, lt: monthEnd },
|
||||
contracts: { some: { contractType: { in: ['FIXED', 'UNFIXED'] } } },
|
||||
},
|
||||
include: {
|
||||
contracts: { orderBy: { createdAt: 'desc' }, take: 1, select: { contractType: true } },
|
||||
socialInsRecords: { where: { endMonth: null }, take: 1 },
|
||||
},
|
||||
})
|
||||
const pendingAdditions = hiredEmployees.filter(e => e.socialInsRecords.length === 0)
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
orgId,
|
||||
month,
|
||||
monthStart: monthStart.toISOString(),
|
||||
monthEnd: monthEnd.toISOString(),
|
||||
hiredCount: hiredEmployees.length,
|
||||
pendingCount: pendingAdditions.length,
|
||||
pending: pendingAdditions.map(e => ({ name: e.name, hireDate: e.hireDate, city: e.city, socialInsBase: e.socialInsBase, contractType: e.contracts[0]?.contractType })),
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 公积金月度增减员
|
||||
router.get('/housing/monthly-changes', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const month = (req.query.month as string) || new Date().toISOString().slice(0, 7)
|
||||
const orgId = req.user!.orgId
|
||||
|
||||
const additions = await prisma.employeeHousingFundRecord.findMany({
|
||||
where: { orgId, startMonth: month, employee: { contracts: { some: { contractType: { in: ['FIXED', 'UNFIXED'] } } } } },
|
||||
include: { employee: { select: { name: true, department: true, idCardNumber: true, contracts: { orderBy: { createdAt: 'desc' }, take: 1, select: { contractType: true } } } } },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
// 增员:入职月 == month + 有公积金基数 + 无公积金记录 + 非劳务/实习协议
|
||||
const monthStart = new Date(`${month}-01T00:00:00.000Z`)
|
||||
const monthEnd = new Date(monthStart.getFullYear(), monthStart.getMonth() + 1, 1)
|
||||
const hiredEmployees = await prisma.employee.findMany({
|
||||
where: {
|
||||
orgId,
|
||||
status: 'ACTIVE',
|
||||
hireDate: { gte: monthStart, lt: monthEnd },
|
||||
contracts: { some: { contractType: { in: ['FIXED', 'UNFIXED'] } } },
|
||||
},
|
||||
include: {
|
||||
contracts: { orderBy: { createdAt: 'desc' }, take: 1, select: { contractType: true } },
|
||||
housingFundRecords: { where: { endMonth: null }, take: 1 },
|
||||
},
|
||||
})
|
||||
// 过滤掉已有公积金记录的(已办理增员)
|
||||
const pendingAdditions = hiredEmployees.filter(e => e.housingFundRecords.length === 0)
|
||||
|
||||
const reductions = await prisma.employeeHousingFundRecord.findMany({
|
||||
where: { orgId, endMonth: month, changeType: { in: ['TERMINATION', 'CITY_CHANGE'] }, employee: { contracts: { some: { contractType: { in: ['FIXED', 'UNFIXED'] } } } } },
|
||||
@@ -1380,6 +1473,27 @@ router.get('/housing/monthly-changes', async (req: AuthRequest, res: Response, n
|
||||
return configCache.get(city)
|
||||
}
|
||||
|
||||
// 增员:从员工信息构造
|
||||
const mapAddition = async (e: any) => {
|
||||
const city = e.city || '北京'
|
||||
const config = await getConfigForCity(city)
|
||||
const base = e.housingFundBase || 0
|
||||
const detail = config ? calcHousingDetail(base, config) : null
|
||||
return {
|
||||
recordId: null,
|
||||
employeeId: e.id,
|
||||
name: e.name,
|
||||
idCardNumber: decryptIdCard(e.idCardNumber),
|
||||
department: e.department,
|
||||
city,
|
||||
base,
|
||||
startMonth: month,
|
||||
endMonth: null,
|
||||
changeType: 'PENDING',
|
||||
detail: detail ? { orgAmount: detail.orgAmount, empAmount: detail.empAmount, total: detail.total } : null,
|
||||
}
|
||||
}
|
||||
|
||||
const mapRecord = async (r: any) => {
|
||||
const config = await getConfigForCity(r.city)
|
||||
const detail = config ? calcHousingDetail(r.base, config) : null
|
||||
@@ -1398,10 +1512,12 @@ router.get('/housing/monthly-changes', async (req: AuthRequest, res: Response, n
|
||||
}
|
||||
}
|
||||
|
||||
const allRecords = [...additions, ...reductions]
|
||||
const cities = [...new Set(allRecords.map((r) => r.city))]
|
||||
const allCities = [...new Set([
|
||||
...pendingAdditions.map((e) => e.city || '北京'),
|
||||
...reductions.map((r) => r.city),
|
||||
])]
|
||||
const configs: Record<string, any> = {}
|
||||
for (const c of cities) {
|
||||
for (const c of allCities) {
|
||||
const cfg = await getConfigForCity(c)
|
||||
if (cfg) configs[c] = { city: cfg.city, effectiveFrom: cfg.effectiveFrom, baseMin: cfg.baseMin, baseMax: cfg.baseMax, housingOrg: cfg.housingOrg, housingEmp: cfg.housingEmp }
|
||||
}
|
||||
@@ -1411,7 +1527,7 @@ router.get('/housing/monthly-changes', async (req: AuthRequest, res: Response, n
|
||||
data: {
|
||||
month,
|
||||
configs,
|
||||
additions: await Promise.all(additions.map(mapRecord)),
|
||||
additions: await Promise.all(pendingAdditions.map(mapAddition)),
|
||||
reductions: await Promise.all(reductions.map(mapRecord)),
|
||||
},
|
||||
})
|
||||
@@ -1486,6 +1602,110 @@ router.get('/active-declaration', async (req: AuthRequest, res: Response, next:
|
||||
}
|
||||
})
|
||||
|
||||
// ========== 办理增员(批量创建社保/公积金记录) ==========
|
||||
|
||||
// 办理社保增员
|
||||
router.post('/enroll-social', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const orgId = req.user!.orgId
|
||||
const userId = req.user!.id
|
||||
const { employeeIds, startMonth } = req.body as { employeeIds: string[]; startMonth: string }
|
||||
|
||||
if (!employeeIds || !employeeIds.length || !startMonth) {
|
||||
return res.status(400).json({ success: false, message: '缺少必要参数' })
|
||||
}
|
||||
|
||||
let enrolled = 0
|
||||
let skipped = 0
|
||||
for (const employeeId of employeeIds) {
|
||||
// 检查是否已有在保社保记录
|
||||
const existing = await prisma.employeeSocialInsRecord.findFirst({
|
||||
where: { employeeId, endMonth: null },
|
||||
})
|
||||
if (existing) { skipped++; continue }
|
||||
|
||||
const emp = await prisma.employee.findFirst({
|
||||
where: { id: employeeId, orgId },
|
||||
select: { socialInsBase: true, city: true },
|
||||
})
|
||||
if (!emp) { skipped++; continue }
|
||||
|
||||
await prisma.employeeSocialInsRecord.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId,
|
||||
startMonth,
|
||||
endMonth: null,
|
||||
base: emp.socialInsBase || 0,
|
||||
changeType: 'ONBOARDING',
|
||||
createdBy: userId,
|
||||
city: emp.city || '北京',
|
||||
},
|
||||
})
|
||||
// 同步员工便捷字段
|
||||
await prisma.employee.update({
|
||||
where: { id: employeeId },
|
||||
data: { socialInsStartMonth: startMonth, socialInsEndMonth: null },
|
||||
})
|
||||
enrolled++
|
||||
}
|
||||
|
||||
res.json({ success: true, data: { enrolled, skipped } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 办理公积金增员
|
||||
router.post('/enroll-housing', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const orgId = req.user!.orgId
|
||||
const userId = req.user!.id
|
||||
const { employeeIds, startMonth } = req.body as { employeeIds: string[]; startMonth: string }
|
||||
|
||||
if (!employeeIds || !employeeIds.length || !startMonth) {
|
||||
return res.status(400).json({ success: false, message: '缺少必要参数' })
|
||||
}
|
||||
|
||||
let enrolled = 0
|
||||
let skipped = 0
|
||||
for (const employeeId of employeeIds) {
|
||||
const existing = await prisma.employeeHousingFundRecord.findFirst({
|
||||
where: { employeeId, endMonth: null },
|
||||
})
|
||||
if (existing) { skipped++; continue }
|
||||
|
||||
const emp = await prisma.employee.findFirst({
|
||||
where: { id: employeeId, orgId },
|
||||
select: { housingFundBase: true, city: true },
|
||||
})
|
||||
if (!emp) { skipped++; continue }
|
||||
|
||||
await prisma.employeeHousingFundRecord.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId,
|
||||
startMonth,
|
||||
endMonth: null,
|
||||
base: emp.housingFundBase || 0,
|
||||
changeType: 'ONBOARDING',
|
||||
createdBy: userId,
|
||||
city: emp.city || '北京',
|
||||
},
|
||||
})
|
||||
await prisma.employee.update({
|
||||
where: { id: employeeId },
|
||||
data: { housingFundStartMonth: startMonth, housingFundEndMonth: null },
|
||||
})
|
||||
enrolled++
|
||||
}
|
||||
|
||||
res.json({ success: true, data: { enrolled, skipped } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 公积金在保人员
|
||||
router.get('/housing/active-declaration', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
|
||||
@@ -436,36 +436,8 @@ export async function createEmployee(orgId: string, userId: string, data: any) {
|
||||
},
|
||||
})
|
||||
|
||||
// 劳务/实习协议不创建社保公积金记录
|
||||
if (!isNoSocialContract) {
|
||||
await tx.employeeSocialInsRecord.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId: emp.id,
|
||||
startMonth: socialInsStartMonth,
|
||||
endMonth: null,
|
||||
base: socialInsBase,
|
||||
changeType: 'ONBOARDING',
|
||||
createdBy: userId,
|
||||
city: data.city || '北京',
|
||||
accountId: data.socialAccountId || null,
|
||||
},
|
||||
})
|
||||
|
||||
await tx.employeeHousingFundRecord.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId: emp.id,
|
||||
startMonth: housingFundStartMonth,
|
||||
endMonth: null,
|
||||
base: housingFundBase,
|
||||
changeType: 'ONBOARDING',
|
||||
createdBy: userId,
|
||||
city: data.city || '北京',
|
||||
accountId: data.housingAccountId || null,
|
||||
},
|
||||
})
|
||||
}
|
||||
// 社保公积金记录不在录入时创建,由 HR 在社保模块办理增员后创建
|
||||
// 录入时仅保存社保基数到 Employee 表,工资计算直接用 employee.socialInsBase
|
||||
|
||||
await tx.salaryChangeRecord.create({
|
||||
data: {
|
||||
@@ -620,36 +592,7 @@ export async function rehireEmployee(orgId: string, userId: string, id: string,
|
||||
},
|
||||
})
|
||||
|
||||
// 劳务/实习协议不创建社保公积金记录
|
||||
if (!isNoSocialContract) {
|
||||
// 创建新社保缴费记录
|
||||
await prisma.employeeSocialInsRecord.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId: id,
|
||||
startMonth: socialInsStartMonth,
|
||||
endMonth: null,
|
||||
base: socialInsBase,
|
||||
changeType: 'REHIRE',
|
||||
createdBy: userId,
|
||||
city: data.city || employee.city || '北京',
|
||||
},
|
||||
})
|
||||
|
||||
// 创建新公积金缴费记录
|
||||
await prisma.employeeHousingFundRecord.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId: id,
|
||||
startMonth: housingFundStartMonth,
|
||||
endMonth: null,
|
||||
base: housingFundBase,
|
||||
changeType: 'REHIRE',
|
||||
createdBy: userId,
|
||||
city: data.city || employee.city || '北京',
|
||||
},
|
||||
})
|
||||
}
|
||||
// 社保公积金记录不在重新入职时创建,由 HR 在社保模块办理增员后创建
|
||||
|
||||
// 创建新薪资记录
|
||||
await prisma.salaryChangeRecord.create({
|
||||
|
||||
@@ -677,6 +677,12 @@ export const socialInsuranceApi = {
|
||||
/** 员工参保信息列表 */
|
||||
employeeEnrollment: (keyword?: string) =>
|
||||
get('/social/employee-enrollment', { params: keyword ? { keyword } : {} }).then(unwrap<any[]>()),
|
||||
/** 办理社保增员(批量创建社保记录) */
|
||||
enrollSocial: (employeeIds: string[], startMonth: string) =>
|
||||
post('/social/enroll-social', { employeeIds, startMonth }).then(unwrap<any>()),
|
||||
/** 办理公积金增员(批量创建公积金记录) */
|
||||
enrollHousing: (employeeIds: string[], startMonth: string) =>
|
||||
post('/social/enroll-housing', { employeeIds, startMonth }).then(unwrap<any>()),
|
||||
}
|
||||
|
||||
// ========== 商业保险 ==========
|
||||
|
||||
@@ -6,7 +6,7 @@ import { toastError } from '../lib/errorToast'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { useConfirm } from '../hooks/useConfirm'
|
||||
import { Users, Plus, Check, UserX, UserPlus, DollarSign, Building2, RotateCcw, Upload, Download, Phone, MapPin, Search, Settings2, CheckCircle, FileText } from 'lucide-react'
|
||||
import { rosterApi, employeeApi, terminationApi, workProcessApi } from '../lib/api-services'
|
||||
import { rosterApi, employeeApi, terminationApi, workProcessApi, esignApi } from '../lib/api-services'
|
||||
import api from '../lib/api'
|
||||
import { copyToClipboard } from '../lib/clipboard'
|
||||
import { useAuthStore } from '../store/authStore'
|
||||
@@ -170,15 +170,38 @@ export default function Roster() {
|
||||
const addMutation = useMutation({
|
||||
mutationFn: async (data: any) => {
|
||||
const res = await employeeApi.create(data)
|
||||
return res
|
||||
return { res, data }
|
||||
},
|
||||
onSuccess: () => {
|
||||
onSuccess: async ({ res, data }) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['roster'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['dashboard'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['roster-profile'] })
|
||||
localStorage.removeItem('add-employee-draft')
|
||||
setShowAddModal(false)
|
||||
toast.success('员工已添加,请前往员工档案签订合同')
|
||||
// 电子签:自动创建电子签署记录
|
||||
if (data.contract?.signMethod === 'ELECTRONIC' && res?.id) {
|
||||
try {
|
||||
// 查询员工档案获取刚创建的合同
|
||||
const profile = await rosterApi.profile(res.id) as any
|
||||
const contract = profile?.contracts?.[0]
|
||||
if (contract?.id) {
|
||||
await esignApi.create({
|
||||
contractId: contract.id,
|
||||
employeeId: res.id,
|
||||
documentTitle: `${data.name || ''}的劳动合同`,
|
||||
remark: '新增员工时自动发起',
|
||||
scene: 'CONTRACT',
|
||||
})
|
||||
toast.success('员工已添加,电子签署记录已创建')
|
||||
} else {
|
||||
toast.success('员工已添加,请前往员工档案发起电子签署')
|
||||
}
|
||||
} catch {
|
||||
toast.success('员工已添加,电子签署记录创建失败(可稍后手动发起)')
|
||||
}
|
||||
} else {
|
||||
toast.success('员工已添加,请前往员工档案签订合同')
|
||||
}
|
||||
},
|
||||
onError: (err: any) => toastError(err, '创建失败'),
|
||||
})
|
||||
@@ -216,23 +239,38 @@ export default function Roster() {
|
||||
})
|
||||
|
||||
const rehireMutation = useMutation({
|
||||
mutationFn: (data: any) => employeeApi.rehire(rehireEmployee?.id, data),
|
||||
onSuccess: () => {
|
||||
mutationFn: async (data: any) => {
|
||||
const res = await employeeApi.rehire(rehireEmployee?.id, data)
|
||||
return { res, data }
|
||||
},
|
||||
onSuccess: async ({ res, data }) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['roster'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['dashboard'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['roster-profile'] })
|
||||
setShowRehireModal(false)
|
||||
// 弹出签署方式选择
|
||||
setSignChoice({
|
||||
open: true,
|
||||
employeeId: rehireEmployee?.id || '',
|
||||
employeeName: rehireEmployee?.name || '',
|
||||
employeeIdCardNumber: rehireEmployee?.idCardNumber,
|
||||
scene: 'CONTRACT',
|
||||
documentTitle: `${rehireEmployee?.name || ''}的劳动合同`,
|
||||
remark: '重新入职时发起',
|
||||
actionName: '重新入职',
|
||||
})
|
||||
// 电子签:自动创建电子签署记录
|
||||
if (data.contract?.signMethod === 'ELECTRONIC' && rehireEmployee?.id) {
|
||||
try {
|
||||
const profile = await rosterApi.profile(rehireEmployee.id) as any
|
||||
const contract = profile?.contracts?.[0]
|
||||
if (contract?.id) {
|
||||
await esignApi.create({
|
||||
contractId: contract.id,
|
||||
employeeId: rehireEmployee.id,
|
||||
documentTitle: `${rehireEmployee?.name || ''}的劳动合同`,
|
||||
remark: '重新入职时自动发起',
|
||||
scene: 'CONTRACT',
|
||||
})
|
||||
toast.success('重新入职成功,电子签署记录已创建')
|
||||
} else {
|
||||
toast.success('重新入职成功,请前往员工档案发起电子签署')
|
||||
}
|
||||
} catch {
|
||||
toast.success('重新入职成功,电子签署记录创建失败(可稍后手动发起)')
|
||||
}
|
||||
} else {
|
||||
toast.success('重新入职成功,请前往员工档案签订合同')
|
||||
}
|
||||
setRehireEmployee(null)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -117,6 +117,28 @@ export default function SocialInsurance() {
|
||||
}
|
||||
}
|
||||
|
||||
// 办理社保增员
|
||||
const enrollSocialMutation = useMutation({
|
||||
mutationFn: ({ employeeIds, startMonth }: { employeeIds: string[]; startMonth: string }) =>
|
||||
socialInsuranceApi.enrollSocial(employeeIds, startMonth),
|
||||
onSuccess: () => {
|
||||
toast.success('社保增员已办理')
|
||||
handleMonthlyProcess()
|
||||
},
|
||||
onError: () => toast.error('办理失败'),
|
||||
})
|
||||
|
||||
// 办理公积金增员
|
||||
const enrollHousingMutation = useMutation({
|
||||
mutationFn: ({ employeeIds, startMonth }: { employeeIds: string[]; startMonth: string }) =>
|
||||
socialInsuranceApi.enrollHousing(employeeIds, startMonth),
|
||||
onSuccess: () => {
|
||||
toast.success('公积金增员已办理')
|
||||
handleMonthlyProcess()
|
||||
},
|
||||
onError: () => toast.error('办理失败'),
|
||||
})
|
||||
|
||||
const completeProcessMutation = useMutation({
|
||||
mutationFn: async (type: 'SOCIAL' | 'HOUSING') => {
|
||||
const snapshot = type === 'SOCIAL' ? monthlyChanges.social : monthlyChanges.housing
|
||||
@@ -346,7 +368,7 @@ export default function SocialInsurance() {
|
||||
</div>
|
||||
)}
|
||||
<InlineAlert type="info" className="mb-3">
|
||||
展示当月社保/公积金新增(入职/重新入职)、减少(离职/解聘)及正常在保人员列表,用于经办机构申报。
|
||||
展示当月社保/公积金待办理增员(入职/重新入职但尚未参保)、减员(离职/解聘)及正常在保人员列表。点击「办理增员」确认参保后,员工状态从"待办理"变为"在保"。
|
||||
</InlineAlert>
|
||||
{(() => {
|
||||
if (!monthlyProcessed) {
|
||||
@@ -482,6 +504,19 @@ export default function SocialInsurance() {
|
||||
icon={<Shield className="w-4 h-4 text-blue-500" />}
|
||||
summary={`${sAddCity.length + sSubCity.length + sNormalCity.length} 人 | 企业 ¥${fmt([...sAddCity, ...sNormalCity].reduce((s: number, i: any) => s + (i.detail?.totalOrg || 0), 0))} + 个人 ¥${fmt([...sAddCity, ...sNormalCity].reduce((s: number, i: any) => s + (i.detail?.totalEmp || 0), 0))} = ¥${fmt(sTotal)}`}
|
||||
defaultOpen={true}
|
||||
action={sAddCity.filter((i: any) => i.changeType === 'PENDING').length > 0 ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
const pendingIds = sAddCity.filter((i: any) => i.changeType === 'PENDING').map((i: any) => i.employeeId)
|
||||
enrollSocialMutation.mutate({ employeeIds: pendingIds, startMonth: monthlyMonth })
|
||||
}}
|
||||
disabled={enrollSocialMutation.isPending}
|
||||
>
|
||||
{enrollSocialMutation.isPending ? '办理中...' : `办理增员(${sAddCity.filter((i: any) => i.changeType === 'PENDING').length}人)`}
|
||||
</Button>
|
||||
) : undefined}
|
||||
>
|
||||
{sTable}
|
||||
</CollapsibleSection>
|
||||
@@ -490,6 +525,19 @@ export default function SocialInsurance() {
|
||||
icon={<Home className="w-4 h-4 text-green-500" />}
|
||||
summary={`${hAddCity.length + hSubCity.length + hNormalCity.length} 人 | 企业 ¥${fmt([...hAddCity, ...hNormalCity].reduce((s: number, i: any) => s + (i.detail?.orgAmount || 0), 0))} + 个人 ¥${fmt([...hAddCity, ...hNormalCity].reduce((s: number, i: any) => s + (i.detail?.empAmount || 0), 0))} = ¥${fmt(hTotal)}`}
|
||||
defaultOpen={true}
|
||||
action={hAddCity.filter((i: any) => i.changeType === 'PENDING').length > 0 ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
const pendingIds = hAddCity.filter((i: any) => i.changeType === 'PENDING').map((i: any) => i.employeeId)
|
||||
enrollHousingMutation.mutate({ employeeIds: pendingIds, startMonth: monthlyMonth })
|
||||
}}
|
||||
disabled={enrollHousingMutation.isPending}
|
||||
>
|
||||
{enrollHousingMutation.isPending ? '办理中...' : `办理增员(${hAddCity.filter((i: any) => i.changeType === 'PENDING').length}人)`}
|
||||
</Button>
|
||||
) : undefined}
|
||||
>
|
||||
{hTable}
|
||||
</CollapsibleSection>
|
||||
@@ -518,21 +566,22 @@ export default function SocialInsurance() {
|
||||
)
|
||||
}
|
||||
|
||||
function CollapsibleSection({ title, icon, summary, defaultOpen = false, children }: { title: string; icon: React.ReactNode; summary?: string; defaultOpen?: boolean; children: React.ReactNode }) {
|
||||
function CollapsibleSection({ title, icon, summary, defaultOpen = false, action, children }: { title: string; icon: React.ReactNode; summary?: string; defaultOpen?: boolean; action?: React.ReactNode; children: React.ReactNode }) {
|
||||
const [open, setOpen] = useState(defaultOpen)
|
||||
return (
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
className="w-full flex items-center justify-between px-3 py-2 hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-full flex items-center justify-between px-3 py-2 hover:bg-gray-50 transition-colors">
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
className="flex items-center gap-2 flex-1"
|
||||
>
|
||||
{open ? <ChevronDown className="w-4 h-4 text-gray-400" /> : <ChevronRight className="w-4 h-4 text-gray-400" />}
|
||||
{icon}
|
||||
<span className="text-sm font-medium">{title}</span>
|
||||
{summary && <span className="text-xs text-gray-400 ml-2">{summary}</span>}
|
||||
</div>
|
||||
</button>
|
||||
</button>
|
||||
{action}
|
||||
</div>
|
||||
{open && <div className="px-3 pb-3 pt-1">{children}</div>}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1174,6 +1174,7 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
|
||||
<thead>
|
||||
<tr className="border-b text-left text-xs text-gray-500">
|
||||
<th className="py-2 px-2">员工</th>
|
||||
<th className="py-2 px-2">合同类型</th>
|
||||
<th className="py-2 px-2 text-right">基本工资</th>
|
||||
<th className="py-2 px-2 text-right">加班费</th>
|
||||
<th className="py-2 px-2 text-right">津贴</th>
|
||||
@@ -1203,6 +1204,29 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-2 px-2">
|
||||
{(() => {
|
||||
const ct = entry.employee.contracts?.[0]?.contractType
|
||||
const cfg: Record<string, { label: string; style: string }> = {
|
||||
FIXED: { label: '固定期限', style: 'bg-blue-50 text-blue-700' },
|
||||
UNFIXED: { label: '无固定期', style: 'bg-purple-50 text-purple-700' },
|
||||
LABOR: { label: '劳务协议', style: 'bg-amber-50 text-amber-700' },
|
||||
INTERNSHIP: { label: '实习协议', style: 'bg-teal-50 text-teal-700' },
|
||||
PARTTIME: { label: '兼职协议', style: 'bg-cyan-50 text-cyan-700' },
|
||||
OUTSOURCING: { label: '业务外包', style: 'bg-slate-50 text-slate-700' },
|
||||
DISPATCH: { label: '劳务派遣', style: 'bg-cyan-50 text-cyan-700' },
|
||||
UNSIGNED: { label: '未签合同', style: 'bg-red-50 text-danger' },
|
||||
}
|
||||
const c = cfg[ct || ''] || { label: '未签合同', style: 'bg-red-50 text-danger' }
|
||||
const noSocial = ct && ['LABOR', 'INTERNSHIP', 'PARTTIME', 'OUTSOURCING', 'UNSIGNED'].includes(ct)
|
||||
return (
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className={`px-1.5 py-0.5 rounded text-[11px] ${c.style}`}>{c.label}</span>
|
||||
{noSocial && <span className="text-[10px] text-gray-400">不缴社保</span>}
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</td>
|
||||
{renderCell(entry, 'baseSalary')}
|
||||
{renderCell(entry, 'overtimePay')}
|
||||
{renderCell(entry, 'allowance')}
|
||||
|
||||
@@ -89,6 +89,12 @@ export default function BasicInfo({ profile, employeeId, attachments }: { profil
|
||||
}
|
||||
buildDeptOptions(departments, null, 0)
|
||||
|
||||
// 拉取岗位字典列表,用于职务/岗位下拉选择
|
||||
const { data: positions = [] } = useQuery<any[]>({
|
||||
queryKey: ['positions'],
|
||||
queryFn: () => api.get('/positions').then(r => r.data),
|
||||
})
|
||||
|
||||
const [form, setForm] = useState({
|
||||
department: profile.department || '',
|
||||
position: profile.position || '',
|
||||
@@ -338,7 +344,7 @@ export default function BasicInfo({ profile, employeeId, attachments }: { profil
|
||||
)}
|
||||
<div><Label>手机号</Label><Input value={form.phone} onChange={(e) => setForm({ ...form, phone: e.target.value })} placeholder="选填" maxLength={11} /></div>
|
||||
<div><Label>学历</Label><Select value={form.education} onChange={(e) => setForm({ ...form, education: e.target.value })}><option value="">未选择</option><option value="博士">博士</option><option value="硕士">硕士</option><option value="本科">本科</option><option value="大专">大专</option><option value="高中">高中</option><option value="其他">其他</option></Select></div>
|
||||
<div><Label>职务/岗位</Label><Input value={form.position} onChange={(e) => setForm({ ...form, position: e.target.value })} placeholder="如:前端工程师" /></div>
|
||||
<div><Label>职务/岗位</Label><Select value={form.position} onChange={(e) => setForm({ ...form, position: e.target.value })}><option value="">未选择</option>{positions.map((p: any) => <option key={p.id} value={p.name}>{p.name}</option>)}</Select></div>
|
||||
<div><Label>入职日期</Label><Input type="date" value={form.hireDate} onChange={(e) => setForm({ ...form, hireDate: e.target.value })} /></div>
|
||||
<div>
|
||||
<Label>基本工资</Label>
|
||||
|
||||
@@ -472,6 +472,20 @@ export function RehireModal({ employee, onClose, onSubmit, loading, error }: {
|
||||
})
|
||||
}
|
||||
buildDeptOptions(departments, null, 0)
|
||||
// 拉取岗位字典列表,用于职务/岗位下拉选择
|
||||
const { data: positions = [] } = useQuery<any[]>({
|
||||
queryKey: ['positions'],
|
||||
queryFn: () => api.get('/positions').then(r => r.data),
|
||||
})
|
||||
// 根据选中部门过滤岗位:通用岗位(departmentId 为空)+ 该部门及父部门专属岗位
|
||||
const getDeptAncestorIds = (deptLabel: string): string[] => {
|
||||
const dept = departments.find((d: any) => d.name === deptLabel)
|
||||
if (!dept) return []
|
||||
const ids: string[] = []
|
||||
let cur: any = dept
|
||||
while (cur) { ids.push(cur.id); cur = departments.find((d: any) => d.id === cur.parentId) }
|
||||
return ids
|
||||
}
|
||||
const defaultEndDate = (() => {
|
||||
const d = new Date()
|
||||
d.setFullYear(d.getFullYear() + 3)
|
||||
@@ -481,7 +495,9 @@ export function RehireModal({ employee, onClose, onSubmit, loading, error }: {
|
||||
const [form, setForm] = useState({
|
||||
hireDate: todayStr,
|
||||
department: employee.department || '',
|
||||
position: employee.position || '',
|
||||
contractType: 'FIXED' as 'FIXED' | 'UNFIXED' | 'UNSIGNED',
|
||||
signMethod: 'PAPER' as 'PAPER' | 'ELECTRONIC',
|
||||
signDate: '',
|
||||
startDate: todayStr,
|
||||
endDate: defaultEndDate,
|
||||
@@ -494,6 +510,10 @@ export function RehireModal({ employee, onClose, onSubmit, loading, error }: {
|
||||
socialInsBase: '', socialInsStartMonth: '',
|
||||
housingFundBase: '', housingFundStartMonth: '',
|
||||
})
|
||||
// 根据选中部门过滤岗位
|
||||
const filteredPositions = form.department
|
||||
? positions.filter((p: any) => !p.departmentId || getDeptAncestorIds(form.department).includes(p.departmentId))
|
||||
: positions
|
||||
const hireMonth = form.hireDate ? form.hireDate.slice(0, 7) : ''
|
||||
|
||||
// 计算合同月数
|
||||
@@ -579,6 +599,7 @@ export function RehireModal({ employee, onClose, onSubmit, loading, error }: {
|
||||
const data: any = {
|
||||
hireDate: new Date(form.hireDate).toISOString(),
|
||||
department: form.department,
|
||||
position: form.position || undefined,
|
||||
baseSalary: base,
|
||||
performanceSalary: perf,
|
||||
monthlySalary: base + perf,
|
||||
@@ -593,6 +614,7 @@ export function RehireModal({ employee, onClose, onSubmit, loading, error }: {
|
||||
startDate: new Date(form.startDate).toISOString(),
|
||||
endDate: form.endDate ? new Date(form.endDate).toISOString() : null,
|
||||
contractType: form.contractType,
|
||||
signMethod: form.signMethod,
|
||||
contractYears: form.contractYears,
|
||||
probationMonths: form.probationMonths,
|
||||
probationSalary: form.probationSalary,
|
||||
@@ -620,7 +642,7 @@ export function RehireModal({ employee, onClose, onSubmit, loading, error }: {
|
||||
</div>
|
||||
<div>
|
||||
<Label>部门 *</Label>
|
||||
<Select value={form.department} onChange={(e) => setForm({ ...form, department: e.target.value })}>
|
||||
<Select value={form.department} onChange={(e) => setForm({ ...form, department: e.target.value, position: '' })}>
|
||||
<option value="">请选择部门</option>
|
||||
{deptOptions.map(d => (
|
||||
<option key={d.id} value={d.label}>{' '.repeat(d.level)}{d.label}</option>
|
||||
@@ -628,6 +650,15 @@ export function RehireModal({ employee, onClose, onSubmit, loading, error }: {
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>职务/岗位</Label>
|
||||
<Select value={form.position} onChange={(e) => setForm({ ...form, position: e.target.value })}>
|
||||
<option value="">未选择</option>
|
||||
{filteredPositions.map((p: any) => (
|
||||
<option key={p.id} value={p.name}>{p.name}</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>新入职日期 *</Label>
|
||||
<Input type="date" value={form.hireDate} onChange={(e) => handleHireDateChange(e.target.value)} />
|
||||
@@ -687,6 +718,18 @@ export function RehireModal({ employee, onClose, onSubmit, loading, error }: {
|
||||
{contractTypes.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
|
||||
</Select>
|
||||
</div>
|
||||
{form.contractType !== 'UNSIGNED' && (
|
||||
<div className="col-span-1">
|
||||
<Label>签订方式</Label>
|
||||
<Select value={form.signMethod} onChange={(e) => setForm({ ...form, signMethod: e.target.value as 'PAPER' | 'ELECTRONIC' })}>
|
||||
<option value="PAPER">纸质签署</option>
|
||||
<option value="ELECTRONIC">电子签署</option>
|
||||
</Select>
|
||||
{form.signMethod === 'ELECTRONIC' && (
|
||||
<div className="text-xs text-blue-600 mt-1">保存后将自动创建电子签署记录</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{form.contractType !== 'UNSIGNED' && (
|
||||
@@ -776,6 +819,18 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
|
||||
})
|
||||
}
|
||||
buildDeptOptions(departments, null, 0)
|
||||
// 拉取岗位字典列表,用于职务/岗位下拉选择
|
||||
const { data: positions = [] } = useQuery<any[]>({
|
||||
queryKey: ['positions'],
|
||||
queryFn: () => api.get('/positions').then(r => r.data),
|
||||
})
|
||||
// 根据选中部门过滤岗位:通用岗位(departmentId 为空)+ 该部门及父部门专属岗位
|
||||
const getDeptAncestorIds = (deptId: string): string[] => {
|
||||
const ids: string[] = []
|
||||
let cur: any = departments.find((d: any) => d.id === deptId)
|
||||
while (cur) { ids.push(cur.id); cur = departments.find((d: any) => d.id === cur.parentId) }
|
||||
return ids
|
||||
}
|
||||
const defaultEndDate = (() => {
|
||||
const d = new Date()
|
||||
d.setFullYear(d.getFullYear() + 3)
|
||||
@@ -793,12 +848,17 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
|
||||
idCardNumber: '', gender: '男' as '男' | '女', femaleWorkerType: '' as '' | 'CADRE' | 'WORKER', phone: '',
|
||||
city: '北京', education: '', status: 'ACTIVE',
|
||||
contractType: 'FIXED' as 'FIXED' | 'UNFIXED' | 'UNSIGNED',
|
||||
signMethod: 'PAPER' as 'PAPER' | 'ELECTRONIC',
|
||||
signDate: '', startDate: todayStr, endDate: defaultEndDate,
|
||||
contractYears: 3, probationMonths: 0, probationSalary: 0,
|
||||
socialInsBase: '', socialInsStartMonth: '',
|
||||
housingFundBase: '', housingFundStartMonth: '',
|
||||
}
|
||||
})
|
||||
// 根据选中部门过滤岗位:通用岗位(departmentId 为空)+ 该部门及父部门专属岗位
|
||||
const filteredPositions = form.departmentId
|
||||
? positions.filter((p: any) => !p.departmentId || getDeptAncestorIds(form.departmentId).includes(p.departmentId))
|
||||
: positions
|
||||
|
||||
// 持久化草稿到 localStorage,防止录入数据丢失
|
||||
useEffect(() => {
|
||||
@@ -1058,6 +1118,7 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
|
||||
endDate: form.endDate ? new Date(form.endDate).toISOString() : null,
|
||||
contractType: form.contractType, contractYears: form.contractYears,
|
||||
probationMonths: form.probationMonths, probationSalary: form.probationSalary,
|
||||
signMethod: form.signMethod,
|
||||
}
|
||||
}
|
||||
onSubmit(data)
|
||||
@@ -1093,7 +1154,7 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
|
||||
const opt = deptOptions.find(d => d.id === e.target.value)
|
||||
setForm({ ...form, departmentId: e.target.value, department: opt?.label || '' })
|
||||
}}><option value="">请选择部门</option>{deptOptions.map(d => <option key={d.id} value={d.id}>{' '.repeat(d.level)}{d.label}</option>)}</Select></div>
|
||||
<div><Label>职务/岗位</Label><Input value={form.position} onChange={(e) => setForm({ ...form, position: e.target.value })} placeholder="如:前端工程师" /></div>
|
||||
<div><Label>职务/岗位</Label><Select value={form.position} onChange={(e) => setForm({ ...form, position: e.target.value })}><option value="">未选择</option>{filteredPositions.map((p: any) => <option key={p.id} value={p.name}>{p.name}</option>)}</Select></div>
|
||||
<div><Label>证件号码 *</Label><Input value={form.idCardNumber} onChange={(e) => handleIdCardChange(e.target.value)} placeholder="18位" maxLength={18} /></div>
|
||||
{idCardDuplicate?.exists && (
|
||||
<div className="col-span-4 px-3 py-2 rounded-md bg-amber-50 text-amber-700 text-xs flex items-center gap-2">
|
||||
@@ -1266,6 +1327,18 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
|
||||
<div className="text-xs text-amber-600 mt-1">超龄人员不可签订劳动合同,仅可选劳务协议/实习协议/未签</div>
|
||||
)}
|
||||
</div>
|
||||
{form.contractType !== 'UNSIGNED' && (
|
||||
<div className="col-span-1">
|
||||
<Label>签订方式</Label>
|
||||
<Select value={form.signMethod} onChange={(e) => setForm({ ...form, signMethod: e.target.value as 'PAPER' | 'ELECTRONIC' })}>
|
||||
<option value="PAPER">纸质签署</option>
|
||||
<option value="ELECTRONIC">电子签署</option>
|
||||
</Select>
|
||||
{form.signMethod === 'ELECTRONIC' && (
|
||||
<div className="text-xs text-blue-600 mt-1">保存后将自动创建电子签署记录</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{form.contractType !== 'UNSIGNED' && (
|
||||
|
||||
@@ -11,8 +11,8 @@ export function MonthlyRow({ item: i, type, onCorrected }: { item: any; type: 'a
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [editing, setEditing] = useState(false)
|
||||
const [editBase, setEditBase] = useState(i.base?.toString() || '')
|
||||
const typeLabel = type === 'add' ? (i.changeType === 'CITY_CHANGE' ? '新增(城市变更)' : '新增') : type === 'sub' ? (i.changeType === 'CITY_CHANGE' ? '减员(城市变更)' : '减员') : '正常'
|
||||
const typeClass = type === 'add' ? 'bg-green-50 text-safe' : type === 'sub' ? 'bg-red-50 text-danger' : 'bg-gray-100 text-gray-500'
|
||||
const typeLabel = type === 'add' ? (i.changeType === 'PENDING' ? '待办理' : i.changeType === 'CITY_CHANGE' ? '新增(城市变更)' : '新增') : type === 'sub' ? (i.changeType === 'CITY_CHANGE' ? '减员(城市变更)' : '减员') : '正常'
|
||||
const typeClass = type === 'add' ? (i.changeType === 'PENDING' ? 'bg-amber-50 text-amber-600' : 'bg-green-50 text-safe') : type === 'sub' ? 'bg-red-50 text-danger' : 'bg-gray-100 text-gray-500'
|
||||
const d = i.detail
|
||||
|
||||
const correctMutation = useMutation({
|
||||
@@ -104,8 +104,8 @@ export function MonthlyRow({ item: i, type, onCorrected }: { item: any; type: 'a
|
||||
export function MonthlyHousingRow({ item: i, type, onCorrected }: { item: any; type: 'add' | 'sub' | 'normal'; onCorrected?: () => void }) {
|
||||
const [editing, setEditing] = useState(false)
|
||||
const [editBase, setEditBase] = useState(i.base?.toString() || '')
|
||||
const typeLabel = type === 'add' ? (i.changeType === 'CITY_CHANGE' ? '新增(城市变更)' : '新增') : type === 'sub' ? (i.changeType === 'CITY_CHANGE' ? '减员(城市变更)' : '减员') : '正常'
|
||||
const typeClass = type === 'add' ? 'bg-green-50 text-safe' : type === 'sub' ? 'bg-red-50 text-danger' : 'bg-gray-100 text-gray-500'
|
||||
const typeLabel = type === 'add' ? (i.changeType === 'PENDING' ? '待办理' : i.changeType === 'CITY_CHANGE' ? '新增(城市变更)' : '新增') : type === 'sub' ? (i.changeType === 'CITY_CHANGE' ? '减员(城市变更)' : '减员') : '正常'
|
||||
const typeClass = type === 'add' ? (i.changeType === 'PENDING' ? 'bg-amber-50 text-amber-600' : 'bg-green-50 text-safe') : type === 'sub' ? 'bg-red-50 text-danger' : 'bg-gray-100 text-gray-500'
|
||||
const d = i.detail
|
||||
|
||||
const correctMutation = useMutation({
|
||||
|
||||
Reference in New Issue
Block a user