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:
freedakgmail
2026-07-30 10:21:22 +08:00
parent 38b8849332
commit 42e0c650a4
24 changed files with 3639 additions and 35 deletions
+128 -22
View File
@@ -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>
)
}