feat: 完成23项系统优化 - 花名册社保状态列/身份证复制/附件类型扩展, 用工办理姓名检索/直接提交/文书查看/批量证明, 风险中心跳转筛选+批量处理, 日历7/15/35天分组+逾期统计, 考勤单条编辑+按人导出, 工资条查看状态+工资流水导出, 辞职申请附件上传, 交接清单PDF下载, 操作完成下一步引导, 休假审批入口, 人效成本分部门

This commit is contained in:
freedakgmail
2026-08-04 22:55:03 +08:00
parent 338af5eee9
commit 5604d02de9
41 changed files with 2104 additions and 482 deletions
+139 -14
View File
@@ -1,9 +1,11 @@
import { useState, useEffect, useRef } from 'react'
import { useNavigate } from 'react-router-dom'
import { HelpCircle, Search, ChevronDown, ChevronRight, Sparkles,
Home, Users, FileText, Calculator, Bot,
Settings, Lightbulb, AlertTriangle, CheckCircle, Phone } from 'lucide-react'
Home, Users, FileText, Calculator, Bot, Calendar,
Settings, Lightbulb, AlertTriangle, CheckCircle, Phone, RotateCcw, ShieldAlert, TrendingDown } from 'lucide-react'
import Modal from './ui/Modal'
import { aiApi } from '../lib/api-services'
import { resetOnboarding } from './OnboardingGuide'
import clsx from 'clsx'
interface HelpCategory {
@@ -23,6 +25,41 @@ interface HelpArticle {
}
const categories: HelpCategory[] = [
{
id: 'home',
title: '首页',
icon: Home,
articles: [
{
id: 'system-intro',
question: '本系统能帮企业做什么?',
answer: '「企业用工专家」是一站式人力资源管理平台,覆盖员工全生命周期管理,帮助企业高效管理人事业务的同时确保合规运营:\n• 员工管理:入职登记、合同签订、转正调岗、离职解聘\n• 薪税管理:工资计算、个税申报、社保公积金缴纳\n• 考勤管理:排班打卡、加班统计、休假记录、月度报表\n• 合同管理:电子合同、到期提醒、续签流程\n• 风险管控:自动扫描法律风险、合规预警、判赔预测\n• AI 助手:劳动法咨询、智能问答、文档生成',
},
{
id: 'compliance',
question: '系统如何保障用工合规?',
answer: '系统从以下维度帮助企业实现合规管理:\n• 合同合规:自动提醒合同到期续签,检测未签合同风险(入职1个月内未签合同需支付双倍工资)\n• 薪酬合规:自动计算个税、社保扣款,确保发薪准确无误\n• 考勤合规:记录加班时长,预警超时加班风险,留存考勤证据\n• 解聘合规:自动计算经济补偿金,生成规范解聘协议,降低劳动争议风险\n• 社保合规:跟踪社保缴纳情况,提醒漏缴断缴\n• 风险预警:统一风险中心实时扫描所有数据,按高/中/低分级预警',
tip: '建议每周查看风险中心,每月核对薪税和考勤数据,确保合规无遗漏。',
},
{
id: 'workflow',
question: '日常人事工作流程是怎样的?',
answer: '系统覆盖企业日常人事管理的完整流程:',
steps: [
'入职:添加员工信息 → 签订合同 → 设置社保 → 安排排班',
'日常:考勤打卡 → 加班审批 → 休假管理 → 补卡修正',
'月度:导入考勤 → 确认考勤 → 计算工资 → 发放工资条 → 缴纳社保公积金 → 个税申报',
'合同:到期提醒 → 续签合同 → 合同确认',
'离职:发起解聘 → 计算补偿金 → 生成协议 → 完成离职',
],
},
{
id: 'value',
question: '使用系统能带来什么价值?',
answer: '• 提效:自动化算薪、考勤统计、合同管理,减少 80% 人工操作\n• 降险:法律风险自动检测预警,避免因疏忽导致的劳动纠纷和罚款\n• 省心:到期提醒、月度任务提醒,不再遗漏关键时间节点\n• 透明:员工可通过手机端查看工资条、合同、考勤记录,信息透明\n• 合规:所有操作留存记录,满足劳动法合规要求,应对审计无忧',
},
],
},
{
id: 'start',
title: '快速入门',
@@ -146,21 +183,94 @@ const categories: HelpCategory[] = [
},
],
},
{
id: 'attendance',
title: '考勤管理',
icon: Calendar,
articles: [
{
id: 'attendance-overview',
question: '考勤管理有哪些功能?',
answer: '考勤管理包含 6 个子功能:\n• 考勤确认:导入考勤数据后批量确认并发布给员工\n• 班次管理:设置早班、晚班、弹性班等班次规则\n• 排班:按日期为员工分配班次,支持批量排班\n• 每日出勤:查看当日打卡情况,支持补卡修正\n• 月度报表:汇总月度出勤、迟到、加班数据\n• 休假记录:管理员工请假信息',
},
{
id: 'shift-setup',
question: '怎么设置班次?',
answer: '在考勤管理「班次管理」标签页中,点击「新增班次」按钮,设置班次名称、上下班时间、弹性时长和休息时长。每个班次可以设置不同颜色方便区分。',
tip: '常见班次:早班 08:00-17:00、晚班 14:00-23:00、弹性班 09:00-18:00(弹性30分钟)。',
},
{
id: 'schedule',
question: '怎么给员工排班?',
answer: '在考勤管理「排班」标签页中:',
steps: [
'选择日期',
'在员工列表中,未排班的员工行内有班次下拉框',
'选择班次后点击「排班」按钮即可',
'也可以点击「批量排班」按钮,勾选多个员工一次性分配班次',
],
tip: '支持按姓名或部门搜索,按部门筛选快速定位员工。',
},
{
id: 'attendance-import',
question: '怎么导入考勤数据?',
answer: '在考勤管理「考勤确认」标签页中,点击「导入考勤」按钮,下载模板填写后上传。系统会自动匹配员工并生成考勤记录。',
tip: '身份证号优先匹配,未填时用姓名匹配。',
},
{
id: 'attendance-correct',
question: '员工漏打卡了怎么办?',
answer: '在「每日出勤」标签页中,找到对应员工,点击「补卡」按钮,手动填写签到/签退时间和状态即可修正记录。',
},
],
},
{
id: 'risk',
title: '风险检测',
icon: AlertTriangle,
title: '风险中心',
icon: ShieldAlert,
articles: [
{
id: 'what-is-risk',
question: '风险检测是什么意思',
answer: '统会自动扫描您的员工合同数据,发现可能存在的法律风险。比如:合同到期未续签、试用期超长、未签合同等。风险分为高、中、低三个等级,建议优先处理高风险项。',
question: '风险中心是什么?',
answer: '统一风险中心会自动扫描您的员工合同、薪酬、社保等数据,汇总所有潜在风险。包括:合同到期未续签、未签合同、试用期超长、薪酬异常、社保漏缴、退休提醒等。风险分为高、中、低三个等级,建议优先处理高风险项。',
tip: '访问路径:左侧菜单「风险中心」或直接访问 /risk-center。',
},
{
id: 'how-to-fix',
question: '发现风险后怎么处理?',
answer: '在首页「总览」页面可以看到风险概览。点击风险项可以跳转到对应员工详情,然后根据系统建议进行处理。处理完成后风险会自动消除。',
tip: '建议每周查看一次风险提醒,及时处理避免法律纠纷。',
answer: '在风险中心页面,每个风险项右侧有快捷操作按钮(如「续签」「转正」「处理」),点击即可跳转到对应页面处理。处理完成后风险会自动消除。',
tip: '建议每周查看一次风险中心,及时处理避免法律纠纷。',
},
{
id: 'risk-types',
question: '有哪些类型的风险?',
answer: '系统目前检测以下风险类型:\n• 合同风险:到期未续签、未签合同\n• 薪酬风险:薪资异常波动\n• 解聘风险:可能存在劳动争议\n• 月度任务:发薪、社保、公积金、个税等截止日提醒\n• 入职手续:入职材料不完整\n• 退休提醒:员工即将达到退休年龄',
},
],
},
{
id: 'termination',
title: '解聘管理',
icon: TrendingDown,
articles: [
{
id: 'termination-process',
question: '员工离职怎么处理?',
answer: '在「解聘管理」页面处理离职流程:',
steps: [
'点击「发起解聘」选择员工',
'填写解聘原因、离职日期等信息',
'系统自动计算经济补偿金',
'生成解聘协议书等法律文件',
'确认后完成解聘流程',
],
warning: '不要直接删除员工记录,保留记录有助于日后查证和合规。',
},
{
id: 'compensation',
question: '经济补偿金怎么算?',
answer: '系统根据员工工龄和月均工资自动计算经济补偿金:\n• 每满一年支付一个月工资\n• 六个月以上不满一年按一年算\n• 不满六个月支付半个月工资\n• 月工资按离职前12个月平均工资计算',
tip: '工资高于当地社平工资3倍的,按3倍封顶,最长补偿12年。',
},
],
},
@@ -195,7 +305,12 @@ const categories: HelpCategory[] = [
{
id: 'notification',
question: '怎么设置提醒?',
answer: '在「通知管理」页面可以设置各类提醒:\n• 合同到期提前提醒天数\n• 未签合同提醒\n• 试用期到期提醒等\n点击通知铃铛图标可以查看所有未读提醒。',
answer: '在「设置」页面的「通知设置」标签中可以配置:\n• 合同到期提前提醒天数\n• 未签合同提醒\n• 加班超时提醒\n• 工资条发布通知\n• 月度事务提醒(发薪日、社保日、公积金日、个税日)\n• 企业微信 Webhook 推送\n• 邮件通知\n点击顶部通知铃铛图标可以查看所有未读提醒。',
},
{
id: 'salary-dashboard',
question: '薪酬分析看板有什么用?',
answer: '薪酬分析看板在「薪税管理」页面中,提供:\n• 薪酬概览(员工总数、月均薪酬、中位数、年度总薪酬)\n• 部门薪酬对比(含人均薪酬排名)\n• 月度薪酬趋势(同比环比变化)\n帮助您了解薪酬分布情况,辅助预算决策。',
},
{
id: 'change-password',
@@ -222,7 +337,7 @@ const categories: HelpCategory[] = [
{
id: 'data-export',
question: '可以导出数据吗?',
answer: '可以。在员工管理页面可以导出员工名单为 Excel 文件。工资批次可以导出为 Excel 方便财务对账。',
answer: '可以。在员工管理页面可以导出员工名单为 Excel 文件。工资批次可以导出为 Excel 方便财务对账。考勤管理支持导出每日出勤和月度报表为 CSV 文件。',
},
{
id: 'multi-user',
@@ -247,6 +362,7 @@ interface RAGResult {
}
export default function HelpModal({ open, onClose }: { open: boolean; onClose: () => void }) {
const navigate = useNavigate()
const [activeCategory, setActiveCategory] = useState(categories[0].id)
const [expandedArticle, setExpandedArticle] = useState<string | null>(null)
const [searchQuery, setSearchQuery] = useState('')
@@ -502,10 +618,19 @@ export default function HelpModal({ open, onClose }: { open: boolean; onClose: (
{/* 底部联系方式 */}
<div className="border-t border-gray-200 px-4 py-2.5 flex items-center justify-between text-xs text-gray-500">
<span className="flex items-center gap-1.5">
<HelpCircle className="w-3.5 h-3.5" />
AI
</span>
<div className="flex items-center gap-3">
<span className="flex items-center gap-1.5">
<HelpCircle className="w-3.5 h-3.5" />
AI
</span>
<button
onClick={() => { resetOnboarding(); onClose(); navigate('/'); setTimeout(() => window.location.reload(), 100) }}
className="flex items-center gap-1 text-primary hover:underline"
>
<RotateCcw className="w-3 h-3" />
</button>
</div>
<span>support@hr8ai.com</span>
</div>
</Modal>
+106 -42
View File
@@ -1,80 +1,144 @@
import { useState, useEffect } from 'react'
import { X, ArrowRight } from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import { X, ArrowRight, Home, Users, Calculator, CalendarCheck, ShieldAlert, Bot } from 'lucide-react'
const STORAGE_KEY = 'hr-onboarding-completed'
const STORAGE_KEY = 'hr-onboarding-dismissed'
const steps = [
const modules = [
{
icon: '🏠',
title: '这里看风险',
description: '首页展示企业用工风险总览,红色代表高风险项,点击「去处理」直接跳转操作。',
icon: Home,
color: 'text-blue-600',
bg: 'bg-blue-50',
title: '工作台',
desc: '风险总览、待办事项、日历事件',
path: '/',
},
{
icon: '',
title: '这里管花名册',
description: '花名册页面管理员工档案、劳动合同、附件,以及违纪、考勤、培训、绩效记录,可生成仲裁证据链。',
icon: Users,
color: 'text-indigo-600',
bg: 'bg-indigo-50',
title: '团队管理',
desc: '花名册、用工办理、离职管理、特殊状态',
path: '/roster',
},
{
icon: '💰',
title: '这里算薪税',
description: '薪税页面提供加班费、双倍工资、社保公积金计算器和工资条管理,输入参数实时计算。',
icon: Calculator,
color: 'text-amber-600',
bg: 'bg-amber-50',
title: '薪酬管理',
desc: '发薪批次、工资条、社保公积金、薪酬分析',
path: '/money',
},
{
icon: CalendarCheck,
color: 'text-green-600',
bg: 'bg-green-50',
title: '考勤时间',
desc: '考勤打卡、排班管理、休假审批',
path: '/attendance',
},
{
icon: ShieldAlert,
color: 'text-red-600',
bg: 'bg-red-50',
title: '合规风控',
desc: '风险中心、证据链、规章制度、用工体检',
path: '/risk-center',
},
{
icon: Bot,
color: 'text-purple-600',
bg: 'bg-purple-50',
title: 'AI 助手',
desc: '智能咨询、合同审查、判赔预测、人力分析',
path: '/ai-assistant',
},
]
export function isOnboardingDismissed() {
return localStorage.getItem(STORAGE_KEY) === '1'
}
export function dismissOnboarding() {
localStorage.setItem(STORAGE_KEY, '1')
}
export function resetOnboarding() {
localStorage.removeItem(STORAGE_KEY)
}
export default function OnboardingGuide() {
const [visible, setVisible] = useState(false)
const [step, setStep] = useState(0)
const [dontShow, setDontShow] = useState(false)
const navigate = useNavigate()
useEffect(() => {
const completed = localStorage.getItem(STORAGE_KEY)
if (!completed) {
if (!isOnboardingDismissed()) {
setVisible(true)
}
}, [])
const close = () => {
localStorage.setItem(STORAGE_KEY, '1')
if (dontShow) dismissOnboarding()
setVisible(false)
}
const goTo = (path: string) => {
if (dontShow) dismissOnboarding()
setVisible(false)
navigate(path)
}
if (!visible) return null
const current = steps[step]
const isLast = step === steps.length - 1
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-white rounded-xl shadow-xl max-w-sm w-full mx-4 overflow-hidden">
<div className="flex justify-end p-2">
<div className="bg-white rounded-xl shadow-xl max-w-lg w-full mx-4 overflow-hidden">
<div className="flex items-center justify-between px-5 py-3 border-b border-gray-100">
<h2 className="text-base font-semibold">使</h2>
<button onClick={close} className="text-gray-400 hover:text-gray-600">
<X className="w-5 h-5" />
</button>
</div>
<div className="px-6 pb-6">
<div className="text-5xl text-center mb-4">{current.icon}</div>
<h2 className="text-lg font-semibold text-center mb-2">{current.title}</h2>
<p className="text-sm text-gray-600 text-center mb-6">{current.description}</p>
{/* 进度指示器 */}
<div className="flex justify-center gap-1.5 mb-6">
{steps.map((_, i) => (
<div
key={i}
className={`h-1.5 rounded-full transition-all ${i === step ? 'w-6 bg-primary' : 'w-1.5 bg-gray-300'}`}
/>
))}
<div className="px-5 py-4">
<p className="text-sm text-gray-500 mb-4"> 6 </p>
<div className="grid grid-cols-2 gap-3">
{modules.map((m) => {
const Icon = m.icon
return (
<button
key={m.title}
onClick={() => goTo(m.path)}
className="flex items-start gap-3 p-3 rounded-lg border border-gray-100 hover:border-primary/30 hover:bg-primary/[0.02] transition-all text-left"
>
<div className={`flex items-center justify-center w-9 h-9 rounded-lg ${m.bg} ${m.color} shrink-0`}>
<Icon className="w-4.5 h-4.5" />
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-gray-800">{m.title}</div>
<div className="text-xs text-gray-500 mt-0.5 leading-relaxed">{m.desc}</div>
</div>
</button>
)
})}
</div>
<div className="flex justify-between">
{step > 0 ? (
<button onClick={() => setStep(step - 1)} className="text-sm text-gray-500"></button>
) : <span />}
<div className="mt-4 flex items-center justify-between">
<label className="flex items-center gap-2 text-sm text-gray-500 cursor-pointer">
<input
type="checkbox"
checked={dontShow}
onChange={(e) => setDontShow(e.target.checked)}
className="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary/10"
/>
</label>
<button
onClick={() => isLast ? close() : setStep(step + 1)}
className="flex items-center gap-1 text-sm font-medium text-primary"
onClick={close}
className="flex items-center gap-1 px-4 py-2 text-sm font-medium text-white bg-primary rounded-lg hover:bg-primary/90 transition-colors"
>
{isLast ? '开始使用' : '下一步'}
{!isLast && <ArrowRight className="w-4 h-4" />}
使
<ArrowRight className="w-4 h-4" />
</button>
</div>
</div>
+13 -6
View File
@@ -5,6 +5,7 @@
import { Link, useLocation } from 'react-router-dom'
import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import clsx from 'clsx'
import {
LayoutDashboard, Users, CalendarCheck, UserX,
@@ -16,6 +17,7 @@ import {
Building2, CalendarDays, ClipboardList, Heart, CalendarClock,
} from 'lucide-react'
import Logo from '../ui/Logo'
import { settingsApi } from '../../lib/api-services'
interface NavItem {
path: string
@@ -33,7 +35,7 @@ const navGroups: NavGroup[] = [
title: '首页',
items: [
{ path: '/', label: '工作台', icon: LayoutDashboard },
{ path: '/calendar', label: '日历', icon: CalendarDays },
{ path: '/calendar', label: '工作日历', icon: CalendarDays },
],
},
{
@@ -49,7 +51,7 @@ const navGroups: NavGroup[] = [
title: '薪酬',
items: [
{ path: '/money', label: '薪税管理', icon: Calculator },
{ path: '/social', label: '社保公积金', icon: Shield },
{ path: '/social', label: '社公商保', icon: Shield },
{ path: '/salary-dashboard', label: '薪酬分析', icon: BarChart3 },
],
},
@@ -64,7 +66,7 @@ const navGroups: NavGroup[] = [
title: '合规',
items: [
{ path: '/risk-center', label: '风险中心', icon: ShieldAlert },
{ path: '/evidence', label: '证据链', icon: FileSearch },
{ path: '/evidence', label: '证据链', icon: FileSearch },
{ path: '/policies', label: '规章制度', icon: FileText },
{ path: '/tools/health-check', label: '用工体检', icon: Stethoscope },
{ path: '/tools/medical-period', label: '医疗期', icon: HeartPulse },
@@ -79,7 +81,7 @@ const navGroups: NavGroup[] = [
{ path: '/notifications', label: '通知管理', icon: Bell },
{ path: '/audit', label: '操作日志', icon: ScrollText },
{ path: '/company-files', label: '公司文件', icon: Building2 },
{ path: '/settings', label: '设置', icon: Settings },
{ path: '/settings', label: '系统设置', icon: Settings },
],
},
]
@@ -89,13 +91,18 @@ const navGroups: NavGroup[] = [
*/
export default function SidebarNav({ mobileOpen, onClose }: { mobileOpen: boolean; onClose: () => void }) {
const location = useLocation()
const { data: orgData } = useQuery<any>({
queryKey: ['org-settings'],
queryFn: () => settingsApi.org(),
staleTime: 300000,
})
const isActive = (path: string) => {
if (path === '/') return location.pathname === '/'
return location.pathname.startsWith(path)
}
const activeGroup = navGroups.find(g => g.items.some(item => isActive(item.path)))
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(
new Set(activeGroup ? [activeGroup.title] : ['首页'])
new Set(navGroups.map(g => g.title))
)
const toggleGroup = (title: string) => {
@@ -131,7 +138,7 @@ export default function SidebarNav({ mobileOpen, onClose }: { mobileOpen: boolea
{/* Logo 区 */}
<div className="h-14 flex items-center gap-2 px-4 border-b border-gray-200 shrink-0">
<Logo className="w-5 h-5 text-primary" />
<span className="font-bold text-sm text-gray-900"></span>
<span className="font-bold text-sm text-gray-900 truncate">{orgData?.name || '企业用工专家'}</span>
</div>
{/* 导航菜单 */}
+1 -12
View File
@@ -3,7 +3,7 @@ import { ChevronDown, Settings as SettingsIcon, Bell, Menu, HelpCircle, Smartpho
import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { useAuthStore } from '../../store/authStore'
import { dashboardApi, settingsApi } from '../../lib/api-services'
import { dashboardApi } from '../../lib/api-services'
import Breadcrumb from './Breadcrumb'
import HelpModal from '../HelpModal'
import PortalQRModal from '../PortalQRModal'
@@ -22,11 +22,6 @@ export default function TopNav({ onMenuClick }: { onMenuClick?: () => void }) {
queryFn: () => dashboardApi.data(),
refetchInterval: 60000,
})
const { data: orgData } = useQuery<any>({
queryKey: ['org-settings'],
queryFn: () => settingsApi.org(),
staleTime: 300000,
})
const riskCount = dashboardData?.riskSummary?.pending || 0
return (
@@ -41,12 +36,6 @@ export default function TopNav({ onMenuClick }: { onMenuClick?: () => void }) {
>
<Menu className="w-5 h-5 text-gray-600" />
</button>
{orgData?.name && (
<span className="hidden sm:inline text-sm font-medium text-gray-700 shrink-0">
{orgData.name}
</span>
)}
{orgData?.name && <span className="hidden sm:inline text-gray-300 shrink-0">|</span>}
<Breadcrumb />
</div>