feat: 实现20260730优化方案全部功能
- AI文件审查:.docx上传提取文本,支持多种文档类型 - 用工办理工作流:WorkProcess页面+后端API,支持入职/续签/终止等流程 - 企业自建文本库:Templates页面Tab切换,企业模板CRUD+渲染+下载Word - 考勤发布:Attendance发布/取消发布按钮,员工端MyAttendance页面 - 工资条发布:Money发布/定时发送按钮+弹窗,portal端publishStatus过滤 - 合同到期弹窗:Dashboard合同到期预警可点击打开弹窗,支持续签/终止操作 - Prisma schema新增WorkProcess/EnterpriseTemplate/AttendancePublish模型 - 前后端编译验证全部通过
This commit is contained in:
@@ -3,7 +3,7 @@ import { toast } from 'sonner'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, RadialBarChart, RadialBar, PolarAngleAxis } from 'recharts'
|
||||
import { Users, AlertTriangle, CheckSquare, DollarSign, ArrowRight, RefreshCw, FileText, Calendar, TrendingUp, Briefcase, Calculator, Wallet, Building2, Receipt, Check, X, Clock, LayoutDashboard, ListTodo, ShieldAlert, UserPlus, AlertCircle, Download, ChevronRight, TrendingDown, ShieldCheck, Lightbulb, BookOpen, Sparkles } from 'lucide-react'
|
||||
import { Users, AlertTriangle, CheckSquare, DollarSign, ArrowRight, RefreshCw, FileText, Calendar, TrendingUp, Briefcase, Calculator, Wallet, Building2, Receipt, Check, X, Clock, LayoutDashboard, ListTodo, ShieldAlert, UserPlus, AlertCircle, Download, ChevronRight, TrendingDown, ShieldCheck, Lightbulb, BookOpen, Sparkles, Repeat, XCircle, Loader2 } from 'lucide-react'
|
||||
import api from '../lib/api'
|
||||
import { useAuthStore } from '../store/authStore'
|
||||
import Card from '../components/ui/Card'
|
||||
@@ -41,6 +41,8 @@ export default function Dashboard() {
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'payroll' | 'risk' | 'task'>('overview')
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
|
||||
const [drillDownType, setDrillDownType] = useState<string | null>(null)
|
||||
const [showExpiringModal, setShowExpiringModal] = useState(false)
|
||||
const [dismissedExpiring, setDismissedExpiring] = useState(false)
|
||||
const { data, isLoading, refetch, isFetching } = useQuery<DashboardData>({
|
||||
queryKey: ['dashboard'],
|
||||
queryFn: async () => {
|
||||
@@ -441,30 +443,45 @@ export default function Dashboard() {
|
||||
</div>
|
||||
|
||||
{/* 合同到期预警 */}
|
||||
{expiringContracts && expiringContracts.length > 0 && (
|
||||
<Link to="/roster?contractStatus=expiring">
|
||||
<Card className="border-danger/30 bg-danger/5 hover:bg-danger/10 transition-colors cursor-pointer">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertCircle className="w-5 h-5 text-danger" />
|
||||
<div>
|
||||
<div className="text-sm font-medium text-danger">合同到期预警</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">
|
||||
{expiringContracts.slice(0, 3).map((c: any, i: number) => (
|
||||
<span key={c.employeeId}>
|
||||
{i > 0 && '、'}
|
||||
{c.employeeName}
|
||||
<span className="text-danger ml-1">({c.daysLeft}天)</span>
|
||||
</span>
|
||||
))}
|
||||
{expiringContracts.length > 3 && <span className="text-gray-500"> 等{expiringContracts.length}人</span>}
|
||||
</div>
|
||||
{expiringContracts && expiringContracts.length > 0 && !dismissedExpiring && (
|
||||
<Card className="border-danger/30 bg-danger/5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div
|
||||
className="flex items-center gap-2 cursor-pointer flex-1"
|
||||
onClick={() => setShowExpiringModal(true)}
|
||||
>
|
||||
<AlertCircle className="w-5 h-5 text-danger" />
|
||||
<div>
|
||||
<div className="text-sm font-medium text-danger">合同到期预警</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">
|
||||
{expiringContracts.slice(0, 3).map((c: any, i: number) => (
|
||||
<span key={c.employeeId}>
|
||||
{i > 0 && '、'}
|
||||
{c.employeeName}
|
||||
<span className="text-danger ml-1">({c.daysLeft}天)</span>
|
||||
</span>
|
||||
))}
|
||||
{expiringContracts.length > 3 && <span className="text-gray-500"> 等{expiringContracts.length}人</span>}
|
||||
</div>
|
||||
</div>
|
||||
<ArrowRight className="w-4 h-4 text-danger" />
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setShowExpiringModal(true)}
|
||||
className="text-xs text-primary hover:underline"
|
||||
>
|
||||
去处理
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setDismissedExpiring(true)}
|
||||
className="text-gray-400 hover:text-gray-600 p-1"
|
||||
title="稍后提醒"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 本月工作动态 + 风险分布 左右两列 */}
|
||||
@@ -1015,6 +1032,95 @@ export default function Dashboard() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 合同到期处理弹窗 */}
|
||||
{showExpiringModal && expiringContracts && (
|
||||
<div className="fixed inset-0 bg-black/30 flex items-center justify-center z-50 p-4" onClick={() => setShowExpiringModal(false)}>
|
||||
<Card className="max-w-2xl w-full max-h-[80vh] overflow-y-auto">
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertCircle className="w-5 h-5 text-danger" />
|
||||
<h3 className="text-sm font-medium">合同到期处理</h3>
|
||||
<span className="text-xs text-gray-500">({expiringContracts.length}人)</span>
|
||||
</div>
|
||||
<button onClick={() => setShowExpiringModal(false)} className="text-gray-400 hover:text-gray-600">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{expiringContracts.map((c: any) => (
|
||||
<div key={c.employeeId} className="flex items-center gap-3 p-3 rounded-md border border-gray-200">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-medium">{c.employeeName}</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{c.department} · 到期日:{c.endDate ? new Date(c.endDate).toLocaleDateString('zh-CN') : '未知'}
|
||||
<span className={`ml-2 ${c.daysLeft <= 7 ? 'text-danger' : c.daysLeft <= 30 ? 'text-warning' : 'text-gray-500'}`}>
|
||||
剩余 {c.daysLeft} 天
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => {
|
||||
api.post('/work-processes', {
|
||||
type: 'RENEW',
|
||||
title: `合同续签-${c.employeeName}`,
|
||||
employeeId: c.employeeId,
|
||||
formData: { employeeId: c.employeeId, oldContractId: c.contractId },
|
||||
status: 'DRAFT',
|
||||
}).then(() => {
|
||||
toast.success(`已创建 ${c.employeeName} 的续签流程`)
|
||||
setShowExpiringModal(false)
|
||||
}).catch((err) => {
|
||||
toast.error(err?.response?.data?.error?.message || '创建失败')
|
||||
})
|
||||
}}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs rounded text-primary hover:bg-primary/10 transition-colors"
|
||||
>
|
||||
<Repeat className="w-3 h-3" />续签
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
api.post('/work-processes', {
|
||||
type: 'TERMINATE',
|
||||
title: `合同终止-${c.employeeName}`,
|
||||
employeeId: c.employeeId,
|
||||
formData: { employeeId: c.employeeId, contractId: c.contractId },
|
||||
status: 'DRAFT',
|
||||
}).then(() => {
|
||||
toast.success(`已创建 ${c.employeeName} 的终止流程`)
|
||||
setShowExpiringModal(false)
|
||||
}).catch((err) => {
|
||||
toast.error(err?.response?.data?.error?.message || '创建失败')
|
||||
})
|
||||
}}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs rounded text-danger hover:bg-danger/10 transition-colors"
|
||||
>
|
||||
<XCircle className="w-3 h-3" />终止
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 pt-3 border-t flex items-center justify-between">
|
||||
<Link
|
||||
to="/roster?contractStatus=expiring"
|
||||
onClick={() => setShowExpiringModal(false)}
|
||||
className="text-xs text-primary hover:underline"
|
||||
>
|
||||
查看全部合同 →
|
||||
</Link>
|
||||
<Button size="sm" variant="secondary" onClick={() => setShowExpiringModal(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user