e8cd0f472b
- 管理端导航:电子签署移到首页分组(工作日历下) - 员工端新增电子签署页面(MyEsign.tsx): - 查看自己的签署记录列表,支持待签/已签/已取消状态 - 查看签署详情,含文件内容、关联合同标识 - 确认签署操作(框架阶段,对接易签宝后跳转签署页面) - 员工端导航(PortalNav)增加电子签署入口 - 员工首页(EmployeeHome)增加电子签署快捷入口 - 员工首页待办事项增加待签署文件提醒 - 后端portal路由新增3个接口:GET /esign、GET /esign/:id、POST /esign/:id/sign
239 lines
10 KiB
TypeScript
239 lines
10 KiB
TypeScript
/**
|
||
* 员工 Hub 首页 — 员工端统一入口
|
||
* 展示个人概览、待办事项、快捷入口、公司公告
|
||
*/
|
||
import { useQuery } from '@tanstack/react-query'
|
||
import { Link } from 'react-router-dom'
|
||
import {
|
||
DollarSign, FileText, CalendarCheck, ScrollText, CalendarClock,
|
||
AlertCircle, ChevronRight, UserX, PenTool,
|
||
} from 'lucide-react'
|
||
import { portalApi } from '../../lib/api-services'
|
||
import Card from '../../components/ui/Card'
|
||
import { InlineAlert } from '../../components/ui/InlineAlert'
|
||
|
||
/** 金额格式化 */
|
||
const fmt = (n: number) => (n || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||
|
||
/** 快捷入口配置 */
|
||
const QUICK_ACTIONS = [
|
||
{ path: '/portal/payslip', label: '工资条', icon: DollarSign, color: 'bg-emerald-50 text-emerald-600' },
|
||
{ path: '/portal/contract', label: '我的合同', icon: FileText, color: 'bg-blue-50 text-blue-600' },
|
||
{ path: '/portal/esign', label: '电子签署', icon: PenTool, color: 'bg-violet-50 text-violet-600' },
|
||
{ path: '/portal/attendance', label: '我的考勤', icon: CalendarCheck, color: 'bg-purple-50 text-purple-600' },
|
||
{ path: '/portal/leave', label: '休假申请', icon: CalendarClock, color: 'bg-cyan-50 text-cyan-600' },
|
||
{ path: '/portal/policies', label: '规章制度', icon: ScrollText, color: 'bg-amber-50 text-amber-600' },
|
||
]
|
||
|
||
export default function EmployeeHome() {
|
||
const employee = (() => {
|
||
try { return JSON.parse(localStorage.getItem('portalEmployee') || '{}') } catch { return {} }
|
||
})()
|
||
|
||
/** 获取员工首页概览数据 */
|
||
const { data: overview, isLoading } = useQuery<any>({
|
||
queryKey: ['portal-home-overview'],
|
||
queryFn: async () => {
|
||
return await portalApi.homeOverview()
|
||
},
|
||
})
|
||
|
||
if (isLoading) {
|
||
return <div className="space-y-4">
|
||
<div className="h-28 bg-gray-100 rounded-xl animate-pulse" />
|
||
<div className="h-32 bg-gray-100 rounded-xl animate-pulse" />
|
||
<div className="h-48 bg-gray-100 rounded-xl animate-pulse" />
|
||
</div>
|
||
}
|
||
|
||
const latestPayslip = overview?.latestPayslip
|
||
const contractInfo = overview?.contract
|
||
const pendingTasks = overview?.pendingTasks || []
|
||
const announcements = overview?.announcements || []
|
||
const attendanceSummary = overview?.attendance
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
{/* 欢迎卡片 */}
|
||
<Card className="bg-gradient-to-br from-indigo-600 to-indigo-700 text-white border-0">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h1 className="text-lg font-bold">你好,{employee.name || '同事'}</h1>
|
||
<p className="text-sm text-indigo-100 mt-1">{employee.department || ''} · {employee.position || ''}</p>
|
||
</div>
|
||
<div className="text-right">
|
||
<div className="text-xs text-indigo-100">本月实发</div>
|
||
<div className="text-xl font-bold">¥{fmt(latestPayslip?.netPay || 0)}</div>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
|
||
{/* 待办提醒 */}
|
||
{pendingTasks.length > 0 && (
|
||
<div className="space-y-2">
|
||
<h2 className="text-sm font-medium text-gray-700 flex items-center gap-1">
|
||
<AlertCircle className="w-4 h-4 text-amber-500" />
|
||
待办事项({pendingTasks.length})
|
||
</h2>
|
||
{pendingTasks.map((task: any, i: number) => (
|
||
<InlineAlert key={i} type={task.severity === 'high' ? 'error' : 'warning'} className="text-xs">
|
||
{task.message}
|
||
</InlineAlert>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* 快捷入口 */}
|
||
<div>
|
||
<h2 className="text-sm font-medium text-gray-700 mb-2">快捷入口</h2>
|
||
<div className="grid grid-cols-4 gap-3">
|
||
{QUICK_ACTIONS.map((action) => {
|
||
const Icon = action.icon
|
||
return (
|
||
<Link
|
||
key={action.path}
|
||
to={action.path}
|
||
className="flex flex-col items-center gap-1.5 p-3 rounded-xl bg-white border border-gray-100 hover:shadow-sm transition-shadow"
|
||
>
|
||
<div className={`w-10 h-10 rounded-xl flex items-center justify-center ${action.color}`}>
|
||
<Icon className="w-5 h-5" />
|
||
</div>
|
||
<span className="text-xs text-gray-600">{action.label}</span>
|
||
</Link>
|
||
)
|
||
})}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 最新工资条 */}
|
||
{latestPayslip && (
|
||
<Card>
|
||
<div className="flex items-center justify-between mb-3">
|
||
<h2 className="text-sm font-medium flex items-center gap-1">
|
||
<DollarSign className="w-4 h-4 text-emerald-500" />
|
||
最新工资条
|
||
</h2>
|
||
<Link to="/portal/payslip" className="text-xs text-primary flex items-center hover:underline">
|
||
查看全部 <ChevronRight className="w-3 h-3" />
|
||
</Link>
|
||
</div>
|
||
<div className="grid grid-cols-3 gap-3 text-sm">
|
||
<div>
|
||
<div className="text-xs text-gray-400">月份</div>
|
||
<div className="font-medium">{latestPayslip.month}</div>
|
||
</div>
|
||
<div>
|
||
<div className="text-xs text-gray-400">应发合计</div>
|
||
<div className="font-medium">¥{fmt(latestPayslip.grossPay)}</div>
|
||
</div>
|
||
<div>
|
||
<div className="text-xs text-gray-400">实发合计</div>
|
||
<div className="font-medium text-emerald-600">¥{fmt(latestPayslip.netPay)}</div>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
)}
|
||
|
||
{/* 合同状态 */}
|
||
{contractInfo && (
|
||
<Card>
|
||
<div className="flex items-center justify-between mb-3">
|
||
<h2 className="text-sm font-medium flex items-center gap-1">
|
||
<FileText className="w-4 h-4 text-blue-500" />
|
||
合同信息
|
||
</h2>
|
||
<Link to="/portal/contract" className="text-xs text-primary flex items-center hover:underline">
|
||
详情 <ChevronRight className="w-3 h-3" />
|
||
</Link>
|
||
</div>
|
||
<div className="space-y-2 text-sm">
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-400">合同类型</span>
|
||
<span className="font-medium">{contractInfo.typeLabel || '—'}</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-400">合同期间</span>
|
||
<span className="font-medium text-xs">{contractInfo.startDate} ~ {contractInfo.endDate || '无固定期'}</span>
|
||
</div>
|
||
{contractInfo.daysToExpire !== null && contractInfo.daysToExpire !== undefined && (
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-400">到期天数</span>
|
||
<span className={`font-medium ${contractInfo.daysToExpire < 30 ? 'text-amber-600' : 'text-gray-700'}`}>
|
||
{contractInfo.daysToExpire > 0 ? `${contractInfo.daysToExpire}天后到期` : '已到期'}
|
||
</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</Card>
|
||
)}
|
||
|
||
{/* 考勤概览 */}
|
||
{attendanceSummary && (
|
||
<Card>
|
||
<div className="flex items-center justify-between mb-3">
|
||
<h2 className="text-sm font-medium flex items-center gap-1">
|
||
<CalendarCheck className="w-4 h-4 text-purple-500" />
|
||
本月考勤
|
||
</h2>
|
||
<Link to="/portal/attendance" className="text-xs text-primary flex items-center hover:underline">
|
||
详情 <ChevronRight className="w-3 h-3" />
|
||
</Link>
|
||
</div>
|
||
<div className="grid grid-cols-4 gap-2 text-center">
|
||
<div className="p-2 rounded-lg bg-green-50">
|
||
<div className="text-lg font-bold text-emerald-600">{attendanceSummary.normalDays || 0}</div>
|
||
<div className="text-xs text-gray-400">正常</div>
|
||
</div>
|
||
<div className="p-2 rounded-lg bg-amber-50">
|
||
<div className="text-lg font-bold text-amber-600">{attendanceSummary.lateCount || 0}</div>
|
||
<div className="text-xs text-gray-400">迟到</div>
|
||
</div>
|
||
<div className="p-2 rounded-lg bg-blue-50">
|
||
<div className="text-lg font-bold text-blue-600">{attendanceSummary.leaveDays || 0}</div>
|
||
<div className="text-xs text-gray-400">请假</div>
|
||
</div>
|
||
<div className="p-2 rounded-lg bg-gray-50">
|
||
<div className="text-lg font-bold text-gray-600">{attendanceSummary.absentDays || 0}</div>
|
||
<div className="text-xs text-gray-400">缺勤</div>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
)}
|
||
|
||
{/* 公司公告 */}
|
||
{announcements.length > 0 && (
|
||
<Card>
|
||
<h2 className="text-sm font-medium mb-3 flex items-center gap-1">
|
||
<ScrollText className="w-4 h-4 text-gray-400" />
|
||
公司公告
|
||
</h2>
|
||
<div className="space-y-2">
|
||
{announcements.slice(0, 3).map((ann: any, i: number) => (
|
||
<div key={i} className="flex items-start gap-2 p-2 rounded-md hover:bg-gray-50">
|
||
<div className="flex-1 min-w-0">
|
||
<div className="text-sm font-medium text-gray-700 truncate">{ann.title}</div>
|
||
<div className="text-xs text-gray-400 mt-0.5">{ann.date} · {ann.author}</div>
|
||
</div>
|
||
<ChevronRight className="w-4 h-4 text-gray-300 shrink-0 mt-1" />
|
||
</div>
|
||
))}
|
||
</div>
|
||
</Card>
|
||
)}
|
||
|
||
{/* 辞职申请入口 */}
|
||
<Card>
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-2">
|
||
<UserX className="w-4 h-4 text-gray-400" />
|
||
<span className="text-sm font-medium">辞职申请</span>
|
||
</div>
|
||
<Link to="/portal/resignation" className="text-xs text-primary flex items-center hover:underline">
|
||
提交辞职申请 <ChevronRight className="w-3 h-3" />
|
||
</Link>
|
||
</div>
|
||
</Card>
|
||
</div>
|
||
)
|
||
}
|