Files
TurboHR/frontend/src/pages/Attendance.tsx
T
selfrelease 3f39c56f4b feat: 部署到服务器+移动端适配+帮助系统RAG集成+网页图标
- 部署脚本 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 标题改为「企业用工专家」
2026-07-27 10:10:07 +08:00

120 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
)
}