feat: 工作台合规评分优化、面包屑补全、花名册斑马纹
- 合规评分维度拆分:薪酬社保拆为薪酬工资+社保公积金两个独立维度 - 合同管理评分纳入待签署合同(signDate为null)扣分 - 维度评分hover显示得分依据findings和建议recommendations - 概览统计卡片支持点击跳转(在管员工/高风险/待办/工资条) - 面包屑补全10个缺失路由,统一分组名与侧边栏一致 - 移除子Tab中重复的PageGuide组件 - 花名册列表添加斑马纹隔行变色 - 薪酬结构模版表格列宽优化,计算公式列截断+title显示 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1441,6 +1441,7 @@ export async function getComplianceScore(orgId: string) {
|
|||||||
const [
|
const [
|
||||||
totalEmployees,
|
totalEmployees,
|
||||||
unsignedContracts,
|
unsignedContracts,
|
||||||
|
pendingSignContracts,
|
||||||
expiringContracts,
|
expiringContracts,
|
||||||
pendingRisks,
|
pendingRisks,
|
||||||
highRisks,
|
highRisks,
|
||||||
@@ -1456,6 +1457,14 @@ export async function getComplianceScore(orgId: string) {
|
|||||||
prisma.employee.count({ where: { orgId, status: 'ACTIVE' } }),
|
prisma.employee.count({ where: { orgId, status: 'ACTIVE' } }),
|
||||||
// 未签合同:完全无合同记录的在职员工
|
// 未签合同:完全无合同记录的在职员工
|
||||||
prisma.employee.count({ where: { orgId, status: 'ACTIVE', contracts: { none: {} } } }),
|
prisma.employee.count({ where: { orgId, status: 'ACTIVE', contracts: { none: {} } } }),
|
||||||
|
// 待签署:有合同记录但 signDate 为 null(合同已创建但未完成签署)
|
||||||
|
prisma.employee.count({
|
||||||
|
where: {
|
||||||
|
orgId,
|
||||||
|
status: 'ACTIVE',
|
||||||
|
contracts: { some: { signDate: null } },
|
||||||
|
},
|
||||||
|
}),
|
||||||
prisma.laborContract.count({
|
prisma.laborContract.count({
|
||||||
where: {
|
where: {
|
||||||
orgId,
|
orgId,
|
||||||
@@ -1483,8 +1492,8 @@ export async function getComplianceScore(orgId: string) {
|
|||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
// 5 维度评分计算
|
// 5 维度评分计算(未签合同 + 待签署都扣分)
|
||||||
const contractScore = Math.max(0, 100 - unsignedContracts * 15 - expiringContracts * 5)
|
const contractScore = Math.max(0, 100 - unsignedContracts * 15 - pendingSignContracts * 10 - expiringContracts * 5)
|
||||||
const policyScore = Math.max(0, 100 - policiesWithoutPublish * 12)
|
const policyScore = Math.max(0, 100 - policiesWithoutPublish * 12)
|
||||||
const attendanceScore = Math.max(0, 100 - attendanceUnconfirmed * 8)
|
const attendanceScore = Math.max(0, 100 - attendanceUnconfirmed * 8)
|
||||||
const salaryScore = totalPayslips > 0
|
const salaryScore = totalPayslips > 0
|
||||||
@@ -1493,7 +1502,7 @@ export async function getComplianceScore(orgId: string) {
|
|||||||
const socialScore = (socialConfig ? 50 : 0) + (housingConfig ? 50 : 0)
|
const socialScore = (socialConfig ? 50 : 0) + (housingConfig ? 50 : 0)
|
||||||
|
|
||||||
const dimensions = [
|
const dimensions = [
|
||||||
{ key: 'contract', name: '合同管理', score: contractScore, todoCount: unsignedContracts + expiringContracts },
|
{ key: 'contract', name: '合同管理', score: contractScore, todoCount: unsignedContracts + pendingSignContracts + expiringContracts },
|
||||||
{ key: 'policy', name: '规章制度', score: policyScore, todoCount: policiesWithoutPublish },
|
{ key: 'policy', name: '规章制度', score: policyScore, todoCount: policiesWithoutPublish },
|
||||||
{ key: 'attendance', name: '考勤工时', score: attendanceScore, todoCount: attendanceUnconfirmed },
|
{ key: 'attendance', name: '考勤工时', score: attendanceScore, todoCount: attendanceUnconfirmed },
|
||||||
{ key: 'salary', name: '薪酬工资', score: salaryScore, todoCount: unconfirmedPayslips },
|
{ key: 'salary', name: '薪酬工资', score: salaryScore, todoCount: unconfirmedPayslips },
|
||||||
@@ -1613,6 +1622,7 @@ export async function getHealthCheck(orgId: string) {
|
|||||||
const [
|
const [
|
||||||
totalEmployees,
|
totalEmployees,
|
||||||
unsignedContracts,
|
unsignedContracts,
|
||||||
|
pendingSignContracts,
|
||||||
expiringContracts,
|
expiringContracts,
|
||||||
policiesWithoutPublish,
|
policiesWithoutPublish,
|
||||||
totalPolicies,
|
totalPolicies,
|
||||||
@@ -1632,6 +1642,14 @@ export async function getHealthCheck(orgId: string) {
|
|||||||
prisma.employee.count({ where: { orgId, status: 'ACTIVE' } }),
|
prisma.employee.count({ where: { orgId, status: 'ACTIVE' } }),
|
||||||
// 未签合同:完全无合同记录的在职员工
|
// 未签合同:完全无合同记录的在职员工
|
||||||
prisma.employee.count({ where: { orgId, status: 'ACTIVE', contracts: { none: {} } } }),
|
prisma.employee.count({ where: { orgId, status: 'ACTIVE', contracts: { none: {} } } }),
|
||||||
|
// 待签署:有合同记录但 signDate 为 null(合同已创建但未完成签署)
|
||||||
|
prisma.employee.count({
|
||||||
|
where: {
|
||||||
|
orgId,
|
||||||
|
status: 'ACTIVE',
|
||||||
|
contracts: { some: { signDate: null } },
|
||||||
|
},
|
||||||
|
}),
|
||||||
prisma.laborContract.count({
|
prisma.laborContract.count({
|
||||||
where: {
|
where: {
|
||||||
orgId,
|
orgId,
|
||||||
@@ -1665,37 +1683,49 @@ export async function getHealthCheck(orgId: string) {
|
|||||||
{
|
{
|
||||||
key: 'contract',
|
key: 'contract',
|
||||||
name: '合同管理',
|
name: '合同管理',
|
||||||
score: Math.max(0, 100 - unsignedContracts * 15 - expiringContracts * 5),
|
score: Math.max(0, 100 - unsignedContracts * 15 - pendingSignContracts * 10 - expiringContracts * 5),
|
||||||
findings: [
|
findings: [
|
||||||
unsignedContracts > 0 ? `${unsignedContracts} 名员工未签书面合同,存在双倍工资风险` : '全员已签书面合同',
|
unsignedContracts > 0 ? `${unsignedContracts} 名员工未签书面合同,存在双倍工资风险` : '全员已有合同记录',
|
||||||
|
pendingSignContracts > 0 ? `${pendingSignContracts} 份合同待签署(已创建未完成签署)` : '',
|
||||||
expiringContracts > 0 ? `${expiringContracts} 份合同 30 天内到期,需及时续签` : '无即将到期合同',
|
expiringContracts > 0 ? `${expiringContracts} 份合同 30 天内到期,需及时续签` : '无即将到期合同',
|
||||||
].filter(Boolean),
|
].filter(Boolean),
|
||||||
recommendations: [
|
recommendations: [
|
||||||
unsignedContracts > 0 ? '立即补签书面合同,超过1个月未签需支付双倍工资' : '',
|
unsignedContracts > 0 ? '立即补签书面合同,超过1个月未签需支付双倍工资' : '',
|
||||||
|
pendingSignContracts > 0 ? '尽快完成待签署合同的签署流程,未签署合同在仲裁中可能无效' : '',
|
||||||
expiringContracts > 0 ? '提前30天发送续签意向书,保留协商证据' : '',
|
expiringContracts > 0 ? '提前30天发送续签意向书,保留协商证据' : '',
|
||||||
].filter(Boolean),
|
].filter(Boolean),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'salary_social',
|
key: 'salary',
|
||||||
name: '薪酬社保',
|
name: '薪酬工资',
|
||||||
|
score: totalPayslips > 0
|
||||||
|
? Math.max(0, 100 - Math.round((unconfirmedPayslips / totalPayslips) * 100))
|
||||||
|
: 100,
|
||||||
|
findings: [
|
||||||
|
totalPayslips > 0
|
||||||
|
? `${unconfirmedPayslips}/${totalPayslips} 张工资条未确认`
|
||||||
|
: '暂无工资条记录',
|
||||||
|
].filter(Boolean),
|
||||||
|
recommendations: [
|
||||||
|
unconfirmedPayslips > 0 ? '提醒员工及时确认工资条,保留薪酬合规证据' : '',
|
||||||
|
].filter(Boolean),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'social',
|
||||||
|
name: '社保公积金',
|
||||||
score: Math.round(
|
score: Math.round(
|
||||||
(socialConfig ? 40 : 0) +
|
(socialConfig ? 50 : 0) +
|
||||||
(housingConfig ? 20 : 0) +
|
(housingConfig ? 30 : 0) +
|
||||||
(totalPayslips > 0 ? Math.max(0, 20 - Math.round((unconfirmedPayslips / totalPayslips) * 20)) : 20) +
|
|
||||||
(employeesNoSocial === 0 ? 20 : Math.max(0, 20 - employeesNoSocial * 2))
|
(employeesNoSocial === 0 ? 20 : Math.max(0, 20 - employeesNoSocial * 2))
|
||||||
),
|
),
|
||||||
findings: [
|
findings: [
|
||||||
socialConfig ? '社保配置已设置' : '未配置社保比例,建议尽快设置',
|
socialConfig ? '社保配置已设置' : '未配置社保比例,建议尽快设置',
|
||||||
housingConfig ? '公积金配置已设置' : '未配置公积金比例',
|
housingConfig ? '公积金配置已设置' : '未配置公积金比例',
|
||||||
totalPayslips > 0
|
|
||||||
? `${unconfirmedPayslips}/${totalPayslips} 张工资条未确认`
|
|
||||||
: '暂无工资条记录',
|
|
||||||
employeesNoSocial > 0 ? `${employeesNoSocial} 名员工社保记录异常` : '社保缴纳记录正常',
|
employeesNoSocial > 0 ? `${employeesNoSocial} 名员工社保记录异常` : '社保缴纳记录正常',
|
||||||
].filter(Boolean),
|
].filter(Boolean),
|
||||||
recommendations: [
|
recommendations: [
|
||||||
!socialConfig ? '前往社保管理页面设置企业/个人缴纳比例' : '',
|
!socialConfig ? '前往社保管理页面设置企业/个人缴纳比例' : '',
|
||||||
!housingConfig ? '根据当地政策配置公积金比例' : '',
|
!housingConfig ? '根据当地政策配置公积金比例' : '',
|
||||||
unconfirmedPayslips > 0 ? '提醒员工及时确认工资条,保留薪酬合规证据' : '',
|
|
||||||
].filter(Boolean),
|
].filter(Boolean),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,29 +12,46 @@ interface BreadcrumbItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ROUTE_MAP: Record<string, BreadcrumbItem> = {
|
const ROUTE_MAP: Record<string, BreadcrumbItem> = {
|
||||||
'/': { group: '工作台', label: '总览' },
|
// 首页
|
||||||
'/calendar': { group: '工作台', label: '工作日历' },
|
'/': { group: '首页', label: '工作台' },
|
||||||
'/roster': { group: '员工管理', label: '花名册' },
|
'/calendar': { group: '首页', label: '工作日历' },
|
||||||
'/work-process': { group: '员工管理', label: '批量流程' },
|
// 团队
|
||||||
'/attendance': { group: '员工管理', label: '考勤确认' },
|
'/roster': { group: '团队', label: '花名册' },
|
||||||
'/leave-approval': { group: '员工管理', label: '休假审批' },
|
'/org-chart': { group: '团队', label: '组织架构' },
|
||||||
'/termination': { group: '员工管理', label: '解聘补偿' },
|
'/esign': { group: '团队', label: '待签合同' },
|
||||||
'/special-status': { group: '员工管理', label: '特殊状态' },
|
'/termination': { group: '团队', label: '离职管理' },
|
||||||
'/training-records': { group: '员工管理', label: '培训记录' },
|
'/training-records': { group: '团队', label: '培训记录' },
|
||||||
'/performance-records': { group: '员工管理', label: '绩效考核' },
|
'/performance-records': { group: '团队', label: '绩效考核' },
|
||||||
'/disciplinary-records': { group: '员工管理', label: '违纪记录' },
|
'/disciplinary-records': { group: '团队', label: '违纪记录' },
|
||||||
'/money': { group: '薪税社保', label: '薪税管理' },
|
'/special-status': { group: '团队', label: '特殊员工' },
|
||||||
'/social': { group: '薪税社保', label: '社保公积金' },
|
// 时间
|
||||||
'/evidence': { group: '合规风控', label: '证据链' },
|
'/attendance': { group: '时间', label: '考勤排班' },
|
||||||
'/policies': { group: '合规风控', label: '规章制度' },
|
'/leave-approval': { group: '时间', label: '休假审批' },
|
||||||
'/tools/health-check': { group: '合规风控', label: '用工体检' },
|
// 薪酬
|
||||||
'/tools/medical-period': { group: '合规风控', label: '医疗期计算' },
|
'/money': { group: '薪酬', label: '薪税管理' },
|
||||||
'/tools/annual-value': { group: '合规风控', label: '年度价值' },
|
'/social': { group: '薪酬', label: '社保公积金' },
|
||||||
'/ai-assistant': { group: 'AI 辅助', label: 'AI 顾问' },
|
'/overtime': { group: '薪酬', label: '加班费计算' },
|
||||||
'/templates': { group: 'AI 辅助', label: '文本模板' },
|
'/commission-bonus': { group: '薪酬', label: '提成奖金' },
|
||||||
'/notifications': { group: '系统', label: '通知管理' },
|
'/salary-dashboard': { group: '薪酬', label: '薪酬分析' },
|
||||||
'/audit': { group: '系统', label: '操作日志' },
|
// 福利保障
|
||||||
'/settings': { group: '系统', label: '设置' },
|
'/commercial-insurance': { group: '福利保障', label: '商业保险' },
|
||||||
|
'/benefits': { group: '福利保障', label: '员工福利' },
|
||||||
|
// 合规
|
||||||
|
'/risk-center': { group: '合规', label: '风险中心' },
|
||||||
|
'/evidence': { group: '合规', label: '证据链条' },
|
||||||
|
'/policies': { group: '合规', label: '规章制度' },
|
||||||
|
// 工具
|
||||||
|
'/tools/health-check': { group: '工具', label: '用工体检' },
|
||||||
|
'/tools/medical-period': { group: '工具', label: '医疗期' },
|
||||||
|
'/tools/annual-value': { group: '工具', label: '年度价值' },
|
||||||
|
// 更多
|
||||||
|
'/ai-assistant': { group: '更多', label: 'AI顾问' },
|
||||||
|
'/templates': { group: '更多', label: '文本模板' },
|
||||||
|
'/notifications': { group: '更多', label: '通知管理' },
|
||||||
|
'/audit': { group: '更多', label: '操作日志' },
|
||||||
|
'/company-files': { group: '更多', label: '公司文件' },
|
||||||
|
'/settings': { group: '更多', label: '系统设置' },
|
||||||
|
'/support': { group: '更多', label: '客服支持' },
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Breadcrumb() {
|
export default function Breadcrumb() {
|
||||||
@@ -50,7 +67,7 @@ export default function Breadcrumb() {
|
|||||||
<Home className="w-3 h-3" />
|
<Home className="w-3 h-3" />
|
||||||
<span>首页</span>
|
<span>首页</span>
|
||||||
</Link>
|
</Link>
|
||||||
{item.group !== '工作台' && (
|
{item.group !== '首页' && (
|
||||||
<>
|
<>
|
||||||
<ChevronRight className="w-3 h-3 text-gray-300" />
|
<ChevronRight className="w-3 h-3 text-gray-300" />
|
||||||
<span className="text-gray-400">{item.group}</span>
|
<span className="text-gray-400">{item.group}</span>
|
||||||
|
|||||||
@@ -692,9 +692,6 @@ function ShiftsTab() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<PageGuide>
|
|
||||||
班次管理用于定义考勤班次(如早班、晚班、全天班)。设置班次名称、上下班时间、弹性时间等。排班时引用此处定义的班次。
|
|
||||||
</PageGuide>
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button onClick={() => { setEditShift(null); setForm({ name: '', startTime: '09:00', endTime: '18:00', flexibleMinutes: 0, restMinutes: 60, color: '#3b82f6' }); setShowAdd(true) }}>
|
<Button onClick={() => { setEditShift(null); setForm({ name: '', startTime: '09:00', endTime: '18:00', flexibleMinutes: 0, restMinutes: 60, color: '#3b82f6' }); setShowAdd(true) }}>
|
||||||
<Plus className="w-4 h-4 mr-1" />新增班次
|
<Plus className="w-4 h-4 mr-1" />新增班次
|
||||||
@@ -858,9 +855,6 @@ function ScheduleTab() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<PageGuide>
|
|
||||||
排班用于按日期为员工分配班次。选择日期后可查看当日排班情况,点击「排班」按钮为员工分配班次。支持批量排班和复制排班。
|
|
||||||
</PageGuide>
|
|
||||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
@@ -1051,9 +1045,6 @@ function DailyTab() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<PageGuide>
|
|
||||||
每日出勤记录展示当日所有员工的打卡情况,包括上班/下班时间、迟到/早退/缺卡状态。可手动补卡或修正异常记录。
|
|
||||||
</PageGuide>
|
|
||||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
@@ -1279,9 +1270,6 @@ function MonthlyTab() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<PageGuide>
|
|
||||||
月度报表汇总当月各员工的出勤天数、迟到次数、加班时长等统计数据。可导出月度考勤表用于发薪参考。
|
|
||||||
</PageGuide>
|
|
||||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
@@ -1431,9 +1419,6 @@ function LeavesTab() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<PageGuide>
|
|
||||||
休假记录管理员工的请假信息,包括事假、病假、年假等。可手动录入休假记录,系统自动计算天数并关联考勤数据。
|
|
||||||
</PageGuide>
|
|
||||||
<div className="flex justify-end items-center gap-3">
|
<div className="flex justify-end items-center gap-3">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
@@ -214,10 +214,10 @@ export default function Dashboard() {
|
|||||||
const yearCost = data.yearCostSummary
|
const yearCost = data.yearCostSummary
|
||||||
|
|
||||||
const stats = [
|
const stats = [
|
||||||
{ label: '在管员工', value: data.stats.employeeCount, icon: Users, color: 'text-primary' },
|
{ label: '在管员工', value: data.stats.employeeCount, icon: Users, color: 'text-primary', to: '/roster' },
|
||||||
{ label: '高风险', value: data.stats.highRiskCount, icon: AlertTriangle, color: 'text-danger' },
|
{ label: '高风险', value: data.stats.highRiskCount, icon: AlertTriangle, color: 'text-danger', to: '/risk-center' },
|
||||||
{ label: '待办事项', value: data.stats.todoCount, icon: CheckSquare, color: 'text-warning' },
|
{ label: '待办事项', value: data.stats.todoCount, icon: CheckSquare, color: 'text-warning', to: '', tab: 'risk' as const },
|
||||||
{ label: '工资条', value: payroll?.payslipCount ?? 0, icon: Receipt, color: 'text-safe' },
|
{ label: '工资条', value: payroll?.payslipCount ?? 0, icon: Receipt, color: 'text-safe', to: '/money' },
|
||||||
]
|
]
|
||||||
|
|
||||||
// 当月人力成本
|
// 当月人力成本
|
||||||
@@ -385,16 +385,13 @@ export default function Dashboard() {
|
|||||||
{/* 概览 Tab */}
|
{/* 概览 Tab */}
|
||||||
{activeTab === 'overview' && (
|
{activeTab === 'overview' && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<PageGuide>
|
|
||||||
概览页展示企业人力全局数据:在管员工数、待办事项、当月人力成本及年度累计成本。可点击各统计卡片快速跳转到对应详情页。右上角可自定义显示/隐藏模块。
|
|
||||||
</PageGuide>
|
|
||||||
{/* 统计卡片 */}
|
{/* 统计卡片 */}
|
||||||
{isSectionVisible('overview_stats') && (
|
{isSectionVisible('overview_stats') && (
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||||
{stats.map((stat) => {
|
{stats.map((stat) => {
|
||||||
const Icon = stat.icon
|
const Icon = stat.icon
|
||||||
return (
|
const inner = (
|
||||||
<Card key={stat.label} className="flex items-center gap-2.5">
|
<Card key={stat.label} className="flex items-center gap-2.5 hover:shadow-md transition-shadow cursor-pointer">
|
||||||
<Icon className={`w-6 h-6 ${stat.color}`} />
|
<Icon className={`w-6 h-6 ${stat.color}`} />
|
||||||
<div>
|
<div>
|
||||||
<div className="text-base font-bold">{stat.value}</div>
|
<div className="text-base font-bold">{stat.value}</div>
|
||||||
@@ -402,6 +399,10 @@ export default function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
if (stat.tab) {
|
||||||
|
return <div key={stat.label} onClick={() => setActiveTab(stat.tab)}>{inner}</div>
|
||||||
|
}
|
||||||
|
return <Link key={stat.label} to={stat.to}>{inner}</Link>
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -428,18 +429,46 @@ export default function Dashboard() {
|
|||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
<div className="absolute inset-0 flex flex-col items-center justify-center px-2 overflow-hidden">
|
<div className="absolute inset-0 flex flex-col items-center justify-center px-2 overflow-hidden">
|
||||||
<span className={`text-2xl font-bold leading-none ${complianceScore?.level === 'safe' ? 'text-safe' : complianceScore?.level === 'warning' ? 'text-warning' : 'text-danger'}`}>{complianceScore?.overallScore ?? complianceScore?.totalScore ?? 0}</span>
|
<span className={`text-2xl font-bold leading-none ${complianceScore?.level === 'safe' ? 'text-safe' : complianceScore?.level === 'warning' ? 'text-warning' : 'text-danger'}`}>{complianceScore?.overallScore ?? complianceScore?.totalScore ?? 0}</span>
|
||||||
<span className="text-xs text-gray-500 mt-1 truncate max-w-full">{complianceScore?.levelLabel ?? ''}</span>
|
<span className="text-xs text-gray-500 mt-1 truncate max-w-full">{complianceScore?.levelLabel ?? complianceScore?.summary ?? ''}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* 维度评分 */}
|
{/* 维度评分 */}
|
||||||
<div className="flex-1 grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-2">
|
<div className="flex-1 grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-7 gap-2">
|
||||||
{complianceScore?.dimensions?.map((dim: any) => (
|
{complianceScore?.dimensions?.map((dim: any) => {
|
||||||
<Link key={dim.key} to={dim.key === 'contract' ? '/roster' : dim.key === 'policy' ? '/policies' : dim.key === 'attendance' ? '/attendance' : dim.key === 'salary' ? '/money' : '/social'} className="flex flex-col items-center p-1.5 rounded-lg hover:bg-gray-50 transition-colors">
|
const dimRouteMap: Record<string, string> = {
|
||||||
<span className={`text-lg font-bold ${dim.score >= 85 ? 'text-safe' : dim.score >= 60 ? 'text-warning' : 'text-danger'}`}>{dim.score}</span>
|
contract: '/roster',
|
||||||
<span className="text-xs text-gray-600">{dim.name}</span>
|
salary: '/money',
|
||||||
{dim.todoCount > 0 && <span className="text-[10px] text-gray-400">{dim.todoCount}待办</span>}
|
social: '/social',
|
||||||
</Link>
|
attendance: '/attendance',
|
||||||
))}
|
policy: '/policies',
|
||||||
|
termination: '/termination',
|
||||||
|
evidence: '/evidence',
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Link key={dim.key} to={dimRouteMap[dim.key] || '/roster'} className="flex flex-col items-center p-1.5 rounded-lg hover:bg-gray-50 transition-colors group relative">
|
||||||
|
<span className={`text-lg font-bold ${dim.score >= 85 ? 'text-safe' : dim.score >= 60 ? 'text-warning' : 'text-danger'}`}>{dim.score}</span>
|
||||||
|
<span className="text-xs text-gray-600">{dim.name}</span>
|
||||||
|
{dim.todoCount > 0 && <span className="text-[10px] text-gray-400">{dim.todoCount}待办</span>}
|
||||||
|
{/* hover 显示得分依据 */}
|
||||||
|
{dim.findings && dim.findings.length > 0 && (
|
||||||
|
<div className="hidden group-hover:block absolute top-full left-1/2 -translate-x-1/2 z-20 mt-1 w-56 bg-gray-900 text-white text-xs rounded-lg p-2.5 shadow-lg whitespace-normal">
|
||||||
|
<div className="font-medium mb-1">得分依据</div>
|
||||||
|
{dim.findings.map((f: string, i: number) => (
|
||||||
|
<div key={i} className="text-gray-300 leading-relaxed mb-0.5">• {f}</div>
|
||||||
|
))}
|
||||||
|
{dim.recommendations && dim.recommendations.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="font-medium mt-1.5 mb-0.5 text-amber-300">建议</div>
|
||||||
|
{dim.recommendations.map((r: string, i: number) => (
|
||||||
|
<div key={i} className="text-gray-300 leading-relaxed mb-0.5">• {r}</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* 合同到期预警内联 */}
|
{/* 合同到期预警内联 */}
|
||||||
@@ -575,9 +604,6 @@ export default function Dashboard() {
|
|||||||
{/* 人力成本 Tab */}
|
{/* 人力成本 Tab */}
|
||||||
{activeTab === 'cost' && (
|
{activeTab === 'cost' && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<PageGuide>
|
|
||||||
人力成本页展示当月及年度累计的工资、社保、公积金、加班费、补偿金等各项成本明细。可导出薪税汇总表,点击月份卡片可查看历史趋势。
|
|
||||||
</PageGuide>
|
|
||||||
{/* 成本专属指标卡片 */}
|
{/* 成本专属指标卡片 */}
|
||||||
{isSectionVisible('cost_metrics') && (
|
{isSectionVisible('cost_metrics') && (
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||||
@@ -842,9 +868,6 @@ export default function Dashboard() {
|
|||||||
{/* 人员分析 Tab */}
|
{/* 人员分析 Tab */}
|
||||||
{activeTab === 'workforce' && (
|
{activeTab === 'workforce' && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<PageGuide>
|
|
||||||
人员分析页展示本月入离职动态、员工分布及绩效统计。可按部门筛选查看人员构成,洞察团队变化趋势。
|
|
||||||
</PageGuide>
|
|
||||||
{/* 本月工作动态 */}
|
{/* 本月工作动态 */}
|
||||||
{isSectionVisible('workforce_activities') && (
|
{isSectionVisible('workforce_activities') && (
|
||||||
<Card>
|
<Card>
|
||||||
@@ -977,11 +1000,6 @@ export default function Dashboard() {
|
|||||||
{/* 风险提醒 Tab */}
|
{/* 风险提醒 Tab */}
|
||||||
{(activeTab === 'risk' || activeTab === 'task') && (
|
{(activeTab === 'risk' || activeTab === 'task') && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<PageGuide>
|
|
||||||
{activeTab === 'risk'
|
|
||||||
? '风险提醒页汇总合同到期、未签合同、离职风险、退休预警等高风险待办事项。可批量处理或逐项解决,处理完成后自动归档至已办事项。'
|
|
||||||
: '月度任务页汇总社保办理、发薪批次等周期性待办事项。可批量处理或逐项解决,处理完成后自动归档至已办事项。'}
|
|
||||||
</PageGuide>
|
|
||||||
{/* 风险分布饼图(仅风险提醒Tab) */}
|
{/* 风险分布饼图(仅风险提醒Tab) */}
|
||||||
{activeTab === 'risk' && isSectionVisible('risk_distribution') && (
|
{activeTab === 'risk' && isSectionVisible('risk_distribution') && (
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
@@ -783,10 +783,10 @@ export default function Roster() {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{employees.map((e: any) => (
|
{employees.map((e: any, idx: number) => (
|
||||||
<tr
|
<tr
|
||||||
key={e.id}
|
key={e.id}
|
||||||
className="border-b border-gray-50 last:border-0 cursor-pointer transition-colors hover:bg-primary/[0.03] group"
|
className={`border-b border-gray-100 last:border-0 cursor-pointer transition-colors hover:bg-primary/[0.05] group ${idx % 2 === 1 ? 'bg-gray-100/60' : ''}`}
|
||||||
onClick={() => setSelectedId(e.id)}
|
onClick={() => setSelectedId(e.id)}
|
||||||
>
|
>
|
||||||
<td className="px-4 py-2.5" onClick={(ev) => ev.stopPropagation()}>
|
<td className="px-4 py-2.5" onClick={(ev) => ev.stopPropagation()}>
|
||||||
|
|||||||
@@ -255,9 +255,6 @@ export default function SocialInsurance() {
|
|||||||
{/* ========== 月度办理 Tab ========== */}
|
{/* ========== 月度办理 Tab ========== */}
|
||||||
{tab === 'monthly' && (
|
{tab === 'monthly' && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<PageGuide>
|
|
||||||
月度办理用于按月获取社保增减员名单并完成办理。流程:①选择月份点击「获取」→ ②系统自动汇总新增、减员、正常缴费人员 → ③确认无误后点击「完成办理」保存记录。办理完成后自动更新员工社保状态。关联:社保基数影响「薪税管理」的工资计算;离职员工的社保截止请在「离职管理」中确认。
|
|
||||||
</PageGuide>
|
|
||||||
<Card>
|
<Card>
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<h2 className="text-sm font-medium">月度办理</h2>
|
<h2 className="text-sm font-medium">月度办理</h2>
|
||||||
|
|||||||
@@ -205,9 +205,6 @@ export function BatchManager() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<PageGuide>
|
|
||||||
发薪批次用于按月生成工资数据。流程:①选择月份和发薪类型(常规/解聘/奖金/补偿)→ ②选择员工范围或复制上月 → ③系统自动计算社保、公积金、个税 → ④确认并发布工资条。支持批量编辑、导出Excel。
|
|
||||||
</PageGuide>
|
|
||||||
{/* 重复发薪提醒 */}
|
{/* 重复发薪提醒 */}
|
||||||
{checkResult?.hasArchivedBatch && (
|
{checkResult?.hasArchivedBatch && (
|
||||||
<div className="flex items-center gap-2 p-2 rounded-md bg-amber-50 text-warning text-xs">
|
<div className="flex items-center gap-2 p-2 rounded-md bg-amber-50 text-warning text-xs">
|
||||||
|
|||||||
@@ -69,9 +69,6 @@ export function PayslipManager({ filterEmployeeId }: { filterEmployeeId?: string
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<PageGuide>
|
|
||||||
工资条管理用于查看、编辑和发布已生成的工资明细。支持按月份筛选、批量确认发送、导出个人工资条PDF。员工可在手机端查看已确认的工资条。关联:发薪批次请在「批次管理」中创建;社保公积金基数请在「社保管理」中设置,影响工资计算。
|
|
||||||
</PageGuide>
|
|
||||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Input type="month" value={month} onChange={(e) => setMonth(e.target.value)} className="w-48" />
|
<Input type="month" value={month} onChange={(e) => setMonth(e.target.value)} className="w-48" />
|
||||||
|
|||||||
@@ -107,9 +107,6 @@ export function TemplateManager() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<PageGuide>
|
|
||||||
薪酬模版用于定义发薪项目结构(基本工资、绩效奖金、补贴、扣款等)。创建模版后可在发薪批次中快速套用,避免逐项配置。支持设置计算公式和适用范围。
|
|
||||||
</PageGuide>
|
|
||||||
<Card>
|
<Card>
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div>
|
<div>
|
||||||
@@ -127,27 +124,27 @@ export function TemplateManager() {
|
|||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b text-left text-xs text-gray-500">
|
<tr className="border-b text-left text-xs text-gray-500">
|
||||||
<th className="py-2 px-2">序号</th>
|
<th className="py-2 px-2 w-10">序号</th>
|
||||||
<th className="py-2 px-2">名称</th>
|
<th className="py-2 px-2 w-24">名称</th>
|
||||||
<th className="py-2 px-2">字段代码</th>
|
<th className="py-2 px-2 w-32">字段代码</th>
|
||||||
<th className="py-2 px-2">类型</th>
|
<th className="py-2 px-2 w-20">类型</th>
|
||||||
<th className="py-2 px-2">计算公式</th>
|
<th className="py-2 px-2 w-48">计算公式</th>
|
||||||
<th className="py-2 px-2">可编辑</th>
|
<th className="py-2 px-2 w-16">可编辑</th>
|
||||||
<th className="py-2 px-2 text-right">操作</th>
|
<th className="py-2 px-2 text-right w-24">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{items?.map((item: any) => (
|
{items?.map((item: any) => (
|
||||||
<tr key={item.id} className="border-b last:border-0 hover:bg-gray-50">
|
<tr key={item.id} className="border-b last:border-0 hover:bg-gray-50">
|
||||||
<td className="py-2 px-2 text-gray-500">{item.order}</td>
|
<td className="py-2 px-2 text-gray-500">{item.order}</td>
|
||||||
<td className="py-2 px-2 text-sm font-medium">{item.name}</td>
|
<td className="py-2 px-2 text-sm font-medium whitespace-nowrap">{item.name}</td>
|
||||||
<td className="py-2 px-2 text-gray-500 font-mono">{item.code}</td>
|
<td className="py-2 px-2 text-gray-500 font-mono whitespace-nowrap">{item.code}</td>
|
||||||
<td className="py-2 px-2">
|
<td className="py-2 px-2">
|
||||||
<span className={`px-2 py-0.5 rounded text-xs ${item.type === 'INPUT' ? 'bg-blue-50 text-blue-600' : 'bg-purple-50 text-purple-600'}`}>
|
<span className={`px-2 py-0.5 rounded text-xs whitespace-nowrap ${item.type === 'INPUT' ? 'bg-blue-50 text-blue-600' : 'bg-purple-50 text-purple-600'}`}>
|
||||||
{item.type === 'INPUT' ? '输入项' : '计算项'}
|
{item.type === 'INPUT' ? '输入项' : '计算项'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2 px-2 text-gray-500 font-mono">{item.formula || '—'}</td>
|
<td className="py-2 px-2 text-gray-500 font-mono text-xs max-w-48 truncate" title={item.formula || ''}>{item.formula || '—'}</td>
|
||||||
<td className="py-2 px-2">
|
<td className="py-2 px-2">
|
||||||
<span className={`text-xs ${item.isEditable ? 'text-safe' : 'text-gray-500'}`}>
|
<span className={`text-xs ${item.isEditable ? 'text-safe' : 'text-gray-500'}`}>
|
||||||
{item.isEditable ? '可编辑' : '不可编辑'}
|
{item.isEditable ? '可编辑' : '不可编辑'}
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ function DisciplinaryForm({ employees, record, onSubmit, onClose }: {
|
|||||||
)}
|
)}
|
||||||
<div className="flex justify-end gap-2 pt-2">
|
<div className="flex justify-end gap-2 pt-2">
|
||||||
<Button variant="secondary" size="sm" onClick={onClose}>取消</Button>
|
<Button variant="secondary" size="sm" onClick={onClose}>取消</Button>
|
||||||
<Button size="sm" onClick={() => onSubmit(form)} disabled={!form.employeeId || !form.description}>保存</Button>
|
<Button size="sm" onClick={() => onSubmit({ ...form, recordId: record?.id })} disabled={!form.employeeId || !form.description}>保存</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user