feat: 系统优化Phase2 - 面包屑导航/侧边栏间距/制度公示阅读签收/模板变量中文化/通知类型补全
- 面包屑导航组件,集成至TopNav header - 侧边栏菜单分组间距增大,分组间分隔线 - 制度公示员工阅读签收:PolicyReadRecord模型、portal路由、管理端阅读统计 - 修复Policies.tsx民主程序推进bug(字段名/API路径/参数) - 用工文本模板变量名英文转中文显示 - 通知类型TYPE_LABELS补全(RISK_ALERT/SOCIAL_INS/OVERTIME_ALERT/PAYSLIP_READY) - 通知示例数据补充 - h2标题统一为text-sm font-medium - 新增run.md
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
import { useState } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
import { CalendarCheck, CheckCircle, Clock, AlertCircle, Upload } from 'lucide-react'
|
||||
import api from '../lib/api'
|
||||
import Card from '../components/ui/Card'
|
||||
import Button from '../components/ui/Button'
|
||||
import EmptyState from '../components/ui/EmptyState'
|
||||
|
||||
const STATUS_CONFIG: Record<string, { label: string; color: string; bg: string; icon: typeof CheckCircle }> = {
|
||||
PENDING: { label: '待确认', color: 'text-amber-700', bg: 'bg-amber-100', icon: Clock },
|
||||
CONFIRMED: { label: '已确认', color: 'text-green-700', bg: 'bg-green-100', icon: CheckCircle },
|
||||
DISPUTED: { label: '有异议', color: 'text-red-700', bg: 'bg-red-100', icon: AlertCircle },
|
||||
}
|
||||
|
||||
/**
|
||||
* 考勤确认管理页面
|
||||
*/
|
||||
export default function Attendance() {
|
||||
const queryClient = useQueryClient()
|
||||
const [month, setMonth] = useState(new Date().toISOString().slice(0, 7))
|
||||
|
||||
const { data: list, isLoading } = useQuery<any>({
|
||||
queryKey: ['attendance', month],
|
||||
queryFn: async () => {
|
||||
const res = await api.get(`/attendance?month=${month}`) as any
|
||||
return res.data
|
||||
},
|
||||
})
|
||||
|
||||
const { data: stats } = useQuery<any>({
|
||||
queryKey: ['attendance-stats', month],
|
||||
queryFn: async () => {
|
||||
const res = await api.get(`/attendance/stats?month=${month}`) as any
|
||||
return res.data
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CalendarCheck className="h-5 w-5 text-primary" />
|
||||
<h1 className="text-base font-semibold">考勤确认</h1>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-gray-500">月度考勤数据确认与异议管理</p>
|
||||
</div>
|
||||
<input
|
||||
type="month"
|
||||
value={month}
|
||||
onChange={e => setMonth(e.target.value)}
|
||||
className="px-3 py-1.5 text-sm border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
{stats && (
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{[
|
||||
{ label: '总计', value: stats.total, color: 'text-gray-700' },
|
||||
{ label: '待确认', value: stats.pending, color: 'text-amber-600' },
|
||||
{ label: '已确认', value: stats.confirmed, color: 'text-green-600' },
|
||||
{ label: '有异议', value: stats.disputed, color: 'text-red-600' },
|
||||
].map(s => (
|
||||
<Card key={s.label} className="text-center">
|
||||
<div className={`text-lg font-bold ${s.color}`}>{s.value}</div>
|
||||
<div className="text-xs text-gray-500">{s.label}</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<div className="text-center py-8 text-gray-500">加载中...</div>
|
||||
) : !list || list.length === 0 ? (
|
||||
<EmptyState title="本月暂无考勤确认记录" description="请先批量导入考勤数据" />
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{list.map((item: any) => {
|
||||
const config = STATUS_CONFIG[item.status] || STATUS_CONFIG.PENDING
|
||||
const StatusIcon = config.icon
|
||||
return (
|
||||
<Card key={item.id}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-gray-50 flex-shrink-0">
|
||||
<CalendarCheck className="w-4 h-4 text-gray-600" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">{item.employee?.name}</span>
|
||||
<span className="text-xs text-gray-500">{item.employee?.department}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-xs text-gray-500 mt-0.5">
|
||||
<span>出勤 {item.workDays} 天</span>
|
||||
<span>工作日加班 {item.weekdayHours}h</span>
|
||||
<span>周末加班 {item.weekendHours}h</span>
|
||||
<span>法定加班 {item.holidayHours}h</span>
|
||||
<span className="text-gray-700">加班费 ¥{item.overtimePay?.toFixed(2)}</span>
|
||||
</div>
|
||||
{item.disputeNote && (
|
||||
<div className="text-xs text-red-600 mt-1">异议说明:{item.disputeNote}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`flex items-center gap-1 px-2 py-1 rounded-lg ${config.bg} ${config.color} flex-shrink-0`}>
|
||||
<StatusIcon className="w-3.5 h-3.5" />
|
||||
<span className="text-xs font-medium">{config.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user