3f39c56f4b
- 部署脚本 deploy.sh:一键部署到 154.8.162.18,端口3400,Nginx conf.d - 应用名改为「企业用工专家」(登录/注册/侧边栏/员工端等6处) - 移动端适配修复:Attendance/Dashboard/Settings/Contracts 6处grid响应式 - 帮助系统:HelpModal 组件 + TopNav 帮助图标 + RAG语义搜索集成 - 后端 rag.service.ts 新增23条帮助种子数据 + searchHelp() - 前端搜索调用 /ai/rag/help-search,RAG不可用时回退关键词匹配 - 网页图标 favicon.svg(indigo主色+建筑人形图案) - index.html 标题改为「企业用工专家」
120 lines
5.1 KiB
TypeScript
120 lines
5.1 KiB
TypeScript
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-2 sm: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>
|
||
)
|
||
}
|