init: AI HR Compliance Assistant

This commit is contained in:
freedakgmail
2026-07-23 12:34:43 +08:00
commit 820579e98d
81 changed files with 19327 additions and 0 deletions
+116
View File
@@ -0,0 +1,116 @@
import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Link } from 'react-router-dom'
import { DollarSign, Check } 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 portalApi = api.create({ baseURL: '/api/v1/portal' })
portalApi.interceptors.request.use((config: any) => {
const token = localStorage.getItem('portalToken')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
export default function Payslip() {
const queryClient = useQueryClient()
const [month, setMonth] = useState(new Date().toISOString().slice(0, 7))
const { data, isLoading } = useQuery<any>({
queryKey: ['payslip', month],
queryFn: async () => {
const res = await portalApi.get('/payslip', { params: { month } }) as any
return res.data?.data ?? null
},
})
const confirmMutation = useMutation({
mutationFn: (id: string) => portalApi.post(`/payslip/${id}/confirm`),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['payslip'] }),
})
const employee = JSON.parse(localStorage.getItem('portalEmployee') || '{}')
return (
<div className="min-h-screen bg-gray-50 px-4 py-6">
<div className="max-w-md mx-auto">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-2">
<DollarSign className="w-6 h-6 text-primary" />
<h1 className="text-lg font-semibold"></h1>
</div>
<div className="flex items-center gap-3">
<span className="text-sm text-gray-500">{employee.name}</span>
<Link to="/portal/contract" className="text-sm text-primary"></Link>
</div>
</div>
<div className="mb-4">
<input
type="month"
value={month}
onChange={(e) => setMonth(e.target.value)}
className="px-3 py-2 rounded-md border border-gray-300 text-sm"
/>
</div>
<Card>
{isLoading ? (
<div className="text-center py-8 text-gray-400">...</div>
) : !data ? (
<EmptyState title="暂无工资条" description={`该月份(${month})暂无工资记录`} />
) : (
<div className="space-y-3">
<div className="flex justify-between text-sm">
<span className="text-gray-500"></span>
<span className="font-medium">¥{Number(data.baseSalary).toLocaleString()}</span>
</div>
{data.overtimePay > 0 && (
<div className="space-y-1">
<div className="flex justify-between text-sm">
<span className="text-gray-500"></span>
<span className="font-medium">¥{Number(data.overtimePay).toLocaleString()}</span>
</div>
</div>
)}
{data.allowance > 0 && (
<div className="flex justify-between text-sm">
<span className="text-gray-500"></span>
<span className="font-medium">¥{Number(data.allowance).toLocaleString()}</span>
</div>
)}
{data.deduction > 0 && (
<div className="flex justify-between text-sm">
<span className="text-gray-500"></span>
<span className="font-medium text-danger">-¥{Number(data.deduction).toLocaleString()}</span>
</div>
)}
<div className="border-t pt-3">
<div className="flex justify-between">
<span className="font-medium"></span>
<span className="text-xl font-bold text-primary">¥{Number(data.totalPay).toLocaleString()}</span>
</div>
</div>
{data.confirmedAt ? (
<div className="flex items-center gap-2 text-sm text-safe">
<Check className="w-4 h-4" /> {new Date(data.confirmedAt).toLocaleString()}
</div>
) : (
<Button
className="w-full"
onClick={() => confirmMutation.mutate(data.id)}
disabled={confirmMutation.isPending}
>
{confirmMutation.isPending ? '确认中...' : '确认已阅'}
</Button>
)}
</div>
)}
</Card>
</div>
</div>
)
}