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:
selfrelease
2026-08-17 17:33:48 +08:00
parent 5196d7c80a
commit aea88fd385
10 changed files with 146 additions and 108 deletions
+44 -14
View File
@@ -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),
}, },
{ {
+41 -24
View File
@@ -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>
-15
View File
@@ -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"
+43 -25
View File
@@ -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> = {
contract: '/roster',
salary: '/money',
social: '/social',
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-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> <span className="text-xs text-gray-600">{dim.name}</span>
{dim.todoCount > 0 && <span className="text-[10px] text-gray-400">{dim.todoCount}</span>} {dim.todoCount > 0 && <span className="text-[10px] text-gray-400">{dim.todoCount}</span>}
</Link> {/* 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>
+2 -2
View File
@@ -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()}>
-3
View File
@@ -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>
-3
View File
@@ -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">
-3
View File
@@ -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" />
+11 -14
View File
@@ -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>