feat: 20260805 系统优化 - 身份证复制fallback/薪税日期筛选/社保版本修复/证据链导出/违纪证明/医疗期政策/绩效类型评级/合同作废/帮助更新
This commit is contained in:
@@ -3,6 +3,7 @@ import { useState, useRef } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
||||
import { attachmentApi, employeeApi } from '../../lib/api-services'
|
||||
import { copyToClipboard } from '../../lib/clipboard'
|
||||
import Card from "../../components/ui/Card"
|
||||
import Button from "../../components/ui/Button"
|
||||
import { Input, Label, Select } from "../../components/ui/Input"
|
||||
@@ -203,9 +204,7 @@ export default function BasicInfo({ profile, employeeId, attachments }: { profil
|
||||
className="text-gray-400 hover:text-primary transition-colors shrink-0"
|
||||
title="复制身份证号"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(profile.idCardNumber)
|
||||
.then(() => toast.success('已复制身份证号'))
|
||||
.catch(() => toast.error('复制失败'))
|
||||
copyToClipboard(profile.idCardNumber, '已复制身份证号')
|
||||
}}
|
||||
>
|
||||
<Copy className="w-3 h-3" />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useRef } from "react"
|
||||
import api from '../../lib/api'
|
||||
import { toast } from "sonner"
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
||||
import { employeeApi, esignApi } from '../../lib/api-services'
|
||||
@@ -14,7 +15,59 @@ export default function ContractInfo({ employeeId, contracts, hireDate }: { empl
|
||||
const [showForm, setShowForm] = useState(false)
|
||||
const [form, setForm] = useState({ contractType: 'FIXED', signDate: '', startDate: '', endDate: '', contractYears: 3, probationMonths: 0, probationSalary: 0, signMethod: 'PAPER' as 'PAPER' | 'ELECTRONIC', attachmentUrl: '', attachments: [] as { name: string; url: string }[], electronicContractNo: '', electronicContractUrl: '' })
|
||||
const contractFileRef = useRef<HTMLInputElement>(null)
|
||||
const supplementFileRefs = useRef<Record<string, HTMLInputElement | null>>({})
|
||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null)
|
||||
const [previewName, setPreviewName] = useState<string>('附件')
|
||||
|
||||
const uploadAttachmentMutation = useMutation({
|
||||
mutationFn: async ({ contractId, attachmentUrl }: { contractId: string; attachmentUrl: string }) => {
|
||||
await api.patch(`/employees/contracts/${contractId}/attachment`, { attachmentUrl })
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['roster-profile'] })
|
||||
toast.success('附件已上传')
|
||||
},
|
||||
onError: () => toast.error('上传失败'),
|
||||
})
|
||||
|
||||
const handleSupplementUpload = (e: React.ChangeEvent<HTMLInputElement>, contractId: string, existingAtts: { name: string; url: string }[]) => {
|
||||
const files = e.target.files
|
||||
if (!files || files.length === 0) return
|
||||
const allowedExts = ['.pdf', '.jpg', '.jpeg', '.png', '.heic', '.gif', '.bmp', '.webp', '.doc', '.docx', '.xls', '.xlsx', '.tiff', '.tif']
|
||||
const maxSize = 10 * 1024 * 1024
|
||||
const validFiles: File[] = []
|
||||
for (const file of Array.from(files)) {
|
||||
const ext = file.name.toLowerCase().substring(file.name.lastIndexOf('.'))
|
||||
if (!allowedExts.includes(ext)) {
|
||||
toast.error(`不支持的文件格式: ${file.name}`)
|
||||
continue
|
||||
}
|
||||
if (file.size > maxSize) {
|
||||
toast.error(`文件过大: ${file.name}(最大 10MB)`)
|
||||
continue
|
||||
}
|
||||
validFiles.push(file)
|
||||
}
|
||||
if (validFiles.length === 0) return
|
||||
const promises = validFiles.map(file => new Promise<{ name: string; url: string }>((resolve) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = (event) => {
|
||||
resolve({ name: file.name, url: event.target?.result as string })
|
||||
}
|
||||
reader.onerror = () => {
|
||||
toast.error(`读取文件失败: ${file.name}`)
|
||||
resolve({ name: file.name, url: '' })
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}))
|
||||
Promise.all(promises).then(atts => {
|
||||
const validAtts = atts.filter(a => a.url)
|
||||
if (validAtts.length === 0) return
|
||||
const merged = [...existingAtts, ...validAtts]
|
||||
uploadAttachmentMutation.mutate({ contractId, attachmentUrl: JSON.stringify(merged) })
|
||||
})
|
||||
e.target.value = ''
|
||||
}
|
||||
|
||||
const addContractMutation = useMutation({
|
||||
mutationFn: async (data: any) => {
|
||||
@@ -43,7 +96,7 @@ export default function ContractInfo({ employeeId, contracts, hireDate }: { empl
|
||||
|
||||
const deleteContractMutation = useMutation({
|
||||
mutationFn: (contractId: string) => employeeApi.removeContract(contractId),
|
||||
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['roster-profile'] }); toast.success('合同已删除') },
|
||||
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['roster-profile'] }); toast.success('合同已作废') },
|
||||
})
|
||||
|
||||
const handleContractFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -261,6 +314,7 @@ export default function ContractInfo({ employeeId, contracts, hireDate }: { empl
|
||||
{(() => {
|
||||
let atts: { name: string; url: string }[] = []
|
||||
try {
|
||||
if (!c.attachmentUrl) throw new Error('empty')
|
||||
const parsed = JSON.parse(c.attachmentUrl)
|
||||
atts = Array.isArray(parsed) ? parsed : [{ name: '附件', url: c.attachmentUrl }]
|
||||
} catch {
|
||||
@@ -270,19 +324,45 @@ export default function ContractInfo({ employeeId, contracts, hireDate }: { empl
|
||||
atts = [{ name: `附件.${ext}`, url: c.attachmentUrl }]
|
||||
}
|
||||
}
|
||||
if (atts.length === 0) return <span className="text-gray-400 ml-2">未上传</span>
|
||||
return (
|
||||
<div className="mt-1 space-y-1">
|
||||
{atts.length === 0 && <span className="text-gray-400 ml-2">未上传</span>}
|
||||
{atts.map((att, idx) => (
|
||||
<div key={idx} className="flex items-center justify-between text-xs bg-gray-50 rounded px-2 py-1">
|
||||
<button onClick={() => setPreviewUrl(att.url)} className="text-primary hover:underline flex items-center gap-1 truncate">
|
||||
<button onClick={() => { setPreviewName(att.name); setPreviewUrl(att.url) }} className="text-primary hover:underline flex items-center gap-1 truncate">
|
||||
<Paperclip className="w-3 h-3 shrink-0" />{att.name}
|
||||
</button>
|
||||
<a href={att.url} download={att.name} className="text-gray-400 hover:text-primary ml-2 shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
className="text-gray-400 hover:text-primary ml-2 shrink-0"
|
||||
title="下载附件"
|
||||
onClick={() => {
|
||||
const dataToBlobUrl = (dataUrl: string) => {
|
||||
try {
|
||||
const arr = dataUrl.split(',')
|
||||
const mime = arr[0].match(/:(.*?);/)?.[1] || 'application/octet-stream'
|
||||
const bstr = atob(arr[1])
|
||||
const u8 = new Uint8Array(bstr.length)
|
||||
for (let i = 0; i < bstr.length; i++) u8[i] = bstr.charCodeAt(i)
|
||||
return URL.createObjectURL(new Blob([u8], { type: mime }))
|
||||
} catch { return dataUrl }
|
||||
}
|
||||
const blobUrl = att.url.startsWith('data:') ? dataToBlobUrl(att.url) : att.url
|
||||
const a = document.createElement('a')
|
||||
a.href = blobUrl
|
||||
a.download = att.name
|
||||
a.click()
|
||||
if (blobUrl !== att.url) URL.revokeObjectURL(blobUrl)
|
||||
}}
|
||||
>
|
||||
<Download className="w-3 h-3" />
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<input id={`contract-file-${c.id}`} type="file" multiple className="hidden" onChange={(e) => handleSupplementUpload(e, c.id, atts)} />
|
||||
<button type="button" onClick={() => document.getElementById(`contract-file-${c.id}`)?.click()} disabled={uploadAttachmentMutation.isPending} className="inline-flex items-center justify-center font-medium rounded-md transition-colors bg-gray-100 text-gray-700 hover:bg-gray-200 px-3 py-1.5 text-xs">
|
||||
<Paperclip className="w-3 h-3 mr-1" />{atts.length > 0 ? '补充上传' : '上传附件'}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
@@ -298,9 +378,9 @@ export default function ContractInfo({ employeeId, contracts, hireDate }: { empl
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={async () => { if (await confirm({ title: '删除合同', message: '确定删除此合同记录?' })) deleteContractMutation.mutate(c.id) }}
|
||||
onClick={async () => { if (await confirm({ title: '作废合同', message: '确定作废此合同记录?作废后记录将保留但不再生效。' })) deleteContractMutation.mutate(c.id) }}
|
||||
className="text-gray-400 hover:text-danger shrink-0 ml-2 mt-1"
|
||||
title="删除合同"
|
||||
title="作废合同"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
@@ -332,9 +412,16 @@ export default function ContractInfo({ employeeId, contracts, hireDate }: { empl
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b">
|
||||
<span className="text-sm font-medium">附件预览</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<a href={blobUrl} download="附件" className="text-xs text-primary hover:underline flex items-center gap-1">
|
||||
<button type="button" onClick={() => {
|
||||
const a = document.createElement('a')
|
||||
a.href = blobUrl
|
||||
a.download = previewName
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
}} className="text-xs text-primary hover:underline flex items-center gap-1">
|
||||
<Download className="w-3 h-3" />下载
|
||||
</a>
|
||||
</button>
|
||||
<button onClick={() => { if (blobUrl !== previewUrl) URL.revokeObjectURL(blobUrl); setPreviewUrl(null) }} className="text-gray-400 hover:text-gray-600">
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
@@ -349,9 +436,16 @@ export default function ContractInfo({ employeeId, contracts, hireDate }: { empl
|
||||
<div className="text-center space-y-3">
|
||||
<FileText className="w-12 h-12 text-gray-300 mx-auto" />
|
||||
<p className="text-sm text-gray-500">此文件格式不支持在线预览</p>
|
||||
<a href={blobUrl} download="附件" className="inline-flex items-center gap-1 text-sm text-primary hover:underline">
|
||||
<button type="button" onClick={() => {
|
||||
const a = document.createElement('a')
|
||||
a.href = blobUrl
|
||||
a.download = previewName
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
}} className="inline-flex items-center gap-1 text-sm text-primary hover:underline">
|
||||
<Download className="w-4 h-4" />点击下载查看
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,9 @@ import { rosterApi } from '../../lib/api-services'
|
||||
import Card from "../../components/ui/Card"
|
||||
import Button from "../../components/ui/Button"
|
||||
import { Input, Label, Select } from "../../components/ui/Input"
|
||||
import { AlertTriangle, Check } from "lucide-react"
|
||||
import { AlertTriangle, Check, Download } from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
import { useAuthStore } from '../../store/authStore'
|
||||
|
||||
// ========== 违纪记录管理 ==========
|
||||
|
||||
@@ -104,7 +106,34 @@ export default function DisciplinaryInfo({ employeeId, records }: { employeeId:
|
||||
{r.ackMethod && <span className="text-gray-400">确认方式:{r.ackMethod === 'SIGN' ? '签字' : r.ackMethod === 'ELECTRONIC' ? '电子' : '拒绝'}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={() => deleteMutation.mutate(r.id)} className="text-xs text-gray-300 hover:text-danger shrink-0">删除</button>
|
||||
<div className="flex items-center gap-2">
|
||||
{r.employeeAck && (
|
||||
<button
|
||||
onClick={async () => {
|
||||
try {
|
||||
const token = useAuthStore.getState().accessToken
|
||||
const res = await fetch(`/api/v1/roster/${employeeId}/disciplinary/${r.id}/certificate`, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
})
|
||||
if (!res.ok) throw new Error('导出失败')
|
||||
const blob = await res.blob()
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `违纪确认证明_${r.violationDate?.toString().slice(0, 10)}.doc`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
} catch {
|
||||
toast.error('下载失败')
|
||||
}
|
||||
}}
|
||||
className="text-xs text-primary hover:underline flex items-center gap-1 shrink-0"
|
||||
>
|
||||
<Download className="w-3 h-3" />下载确认证明
|
||||
</button>
|
||||
)}
|
||||
<button onClick={() => deleteMutation.mutate(r.id)} className="text-xs text-gray-300 hover:text-danger shrink-0">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Search, Plus, Edit2, Trash2, X } from 'lucide-react'
|
||||
import { Search, Plus, Edit2, Trash2, X, Download } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { rosterApi, employeeApi } from '../../lib/api-services'
|
||||
import api from '../../lib/api'
|
||||
@@ -139,6 +139,35 @@ export default function DisciplinaryRecords() {
|
||||
</td>
|
||||
<td className="py-2 pr-4">
|
||||
<div className="flex gap-1">
|
||||
{r.employeeAck && (
|
||||
<button
|
||||
title="下载违纪确认证明"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const { useAuthStore } = await import('../../store/authStore')
|
||||
const token = useAuthStore.getState().accessToken
|
||||
const res = await fetch(`/api/v1/roster/${r.employeeId}/disciplinary/${r.id}/certificate`, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
})
|
||||
if (!res.ok) throw new Error('导出失败')
|
||||
const blob = await res.blob()
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
const cd = res.headers.get('content-disposition') || ''
|
||||
const fname = cd.match(/filename\*=UTF-8''(.+)/)?.[1] || cd.match(/filename="(.+?)"/)?.[1] || '违纪确认证明.doc'
|
||||
a.download = decodeURIComponent(fname)
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
} catch { toast.error('下载失败') }
|
||||
}}
|
||||
className="p-1 hover:bg-gray-100 rounded"
|
||||
>
|
||||
<Download className="w-3.5 h-3.5 text-primary" />
|
||||
</button>
|
||||
)}
|
||||
<button onClick={() => setEditRecord(r)} className="p-1 hover:bg-gray-100 rounded">
|
||||
<Edit2 className="w-3.5 h-3.5 text-gray-500" />
|
||||
</button>
|
||||
|
||||
@@ -11,7 +11,7 @@ import { AlertTriangle, Check } from "lucide-react"
|
||||
export default function PerformanceInfo({ employeeId, records }: { employeeId: string; records: any[] }) {
|
||||
const queryClient = useQueryClient()
|
||||
const [showForm, setShowForm] = useState(false)
|
||||
const [form, setForm] = useState({ period: '', score: 80, grade: 'B', result: 'QUALIFIED', summary: '', improvementPlan: '', employeeAck: false, ackDate: '', reviewer: '' })
|
||||
const [form, setForm] = useState({ period: '', periodType: 'MONTHLY' as 'MONTHLY' | 'QUARTERLY' | 'YEARLY', score: 80, grade: 'B', result: 'QUALIFIED', summary: '', improvementPlan: '', employeeAck: false, ackDate: '', reviewer: '' })
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: any) => rosterApi.performance(employeeId, data),
|
||||
@@ -25,6 +25,19 @@ export default function PerformanceInfo({ employeeId, records }: { employeeId: s
|
||||
|
||||
const resultMap: Record<string, string> = { EXCELLENT: '优秀', QUALIFIED: '合格', NEED_IMPROVE: '需改进', UNQUALIFIED: '不胜任' }
|
||||
|
||||
// 根据得分自动计算等级和结果
|
||||
const scoreToGrade = (score: number): { grade: string; result: string } => {
|
||||
if (score >= 90) return { grade: 'A', result: 'EXCELLENT' }
|
||||
if (score >= 80) return { grade: 'B', result: 'QUALIFIED' }
|
||||
if (score >= 60) return { grade: 'C', result: 'NEED_IMPROVE' }
|
||||
return { grade: 'D', result: 'UNQUALIFIED' }
|
||||
}
|
||||
|
||||
const handleScoreChange = (score: number) => {
|
||||
const { grade, result } = scoreToGrade(score)
|
||||
setForm({ ...form, score, grade, result })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
@@ -35,14 +48,21 @@ export default function PerformanceInfo({ employeeId, records }: { employeeId: s
|
||||
{showForm && (
|
||||
<Card>
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div><Label>考核周期</Label><Input value={form.period} onChange={(e) => setForm({ ...form, period: e.target.value })} placeholder="如 2026-07 或 2026-Q3" /></div>
|
||||
<div><Label>考核得分</Label><Input type="number" value={form.score} onChange={(e) => setForm({ ...form, score: Number(e.target.value) })} /></div>
|
||||
<div><Label>等级</Label>
|
||||
<div><Label>考核类型</Label>
|
||||
<Select value={form.periodType} onChange={(e) => setForm({ ...form, periodType: e.target.value as any })}>
|
||||
<option value="MONTHLY">月度考核</option>
|
||||
<option value="QUARTERLY">季度考核</option>
|
||||
<option value="YEARLY">年度考核</option>
|
||||
</Select>
|
||||
</div>
|
||||
<div><Label>考核周期</Label><Input value={form.period} onChange={(e) => setForm({ ...form, period: e.target.value })} placeholder={form.periodType === 'MONTHLY' ? '如 2026-07' : form.periodType === 'QUARTERLY' ? '如 2026-Q3' : '如 2026'} /></div>
|
||||
<div><Label>考核得分</Label><Input type="number" value={form.score} onChange={(e) => handleScoreChange(Number(e.target.value))} /></div>
|
||||
<div><Label>等级(由得分自动计算)</Label>
|
||||
<Select value={form.grade} onChange={(e) => setForm({ ...form, grade: e.target.value })}>
|
||||
<option value="A">A</option><option value="B">B</option><option value="C">C</option><option value="D">D</option>
|
||||
</Select>
|
||||
</div>
|
||||
<div><Label>考核结果</Label>
|
||||
<div><Label>考核结果(由得分自动计算)</Label>
|
||||
<Select value={form.result} onChange={(e) => setForm({ ...form, result: e.target.value })}>
|
||||
{Object.entries(resultMap).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
|
||||
</Select>
|
||||
|
||||
@@ -181,6 +181,7 @@ function PerformanceForm({ employees, record, onSubmit, onClose }: {
|
||||
const [form, setForm] = useState({
|
||||
employeeId: record?.employeeId || '',
|
||||
period: record?.period || new Date().toISOString().slice(0, 7),
|
||||
periodType: record?.periodType || 'MONTHLY',
|
||||
score: record?.score || 80,
|
||||
grade: record?.grade || 'B',
|
||||
result: record?.result || 'QUALIFIED',
|
||||
@@ -189,6 +190,18 @@ function PerformanceForm({ employees, record, onSubmit, onClose }: {
|
||||
reviewer: record?.reviewer || '',
|
||||
})
|
||||
|
||||
const scoreToGrade = (score: number): { grade: string; result: string } => {
|
||||
if (score >= 90) return { grade: 'A', result: 'EXCELLENT' }
|
||||
if (score >= 80) return { grade: 'B', result: 'QUALIFIED' }
|
||||
if (score >= 60) return { grade: 'C', result: 'NEED_IMPROVE' }
|
||||
return { grade: 'D', result: 'UNQUALIFIED' }
|
||||
}
|
||||
|
||||
const handleScoreChange = (score: number) => {
|
||||
const { grade, result } = scoreToGrade(score)
|
||||
setForm({ ...form, score, grade, result })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/30 flex items-center justify-center z-50" onClick={onClose}>
|
||||
<div className="bg-white rounded-lg p-6 w-full max-w-md max-h-[90vh] overflow-y-auto" onClick={e => e.stopPropagation()}>
|
||||
@@ -197,6 +210,16 @@ function PerformanceForm({ employees, record, onSubmit, onClose }: {
|
||||
<button onClick={onClose}><X className="w-4 h-4 text-gray-400" /></button>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{!record && (
|
||||
<div>
|
||||
<Label>考核类型</Label>
|
||||
<Select value={form.periodType} onChange={(e) => setForm({ ...form, periodType: e.target.value })}>
|
||||
<option value="MONTHLY">月度考核</option>
|
||||
<option value="QUARTERLY">季度考核</option>
|
||||
<option value="YEARLY">年度考核</option>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
{!record && (
|
||||
<div>
|
||||
<Label>员工</Label>
|
||||
@@ -210,15 +233,15 @@ function PerformanceForm({ employees, record, onSubmit, onClose }: {
|
||||
)}
|
||||
<div>
|
||||
<Label>考核周期</Label>
|
||||
<Input type="month" value={form.period} onChange={(e) => setForm({ ...form, period: e.target.value })} />
|
||||
<Input type={form.periodType === 'YEARLY' ? 'number' : 'month'} value={form.period} onChange={(e) => setForm({ ...form, period: e.target.value })} placeholder={form.periodType === 'YEARLY' ? '如 2026' : undefined} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label>得分</Label>
|
||||
<Input type="number" min={0} max={100} value={form.score} onChange={(e) => setForm({ ...form, score: Number(e.target.value) })} />
|
||||
<Input type="number" min={0} max={100} value={form.score} onChange={(e) => handleScoreChange(Number(e.target.value))} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>等级</Label>
|
||||
<Label>等级(自动计算)</Label>
|
||||
<Select value={form.grade} onChange={(e) => setForm({ ...form, grade: e.target.value })}>
|
||||
<option value="A">A</option>
|
||||
<option value="B">B</option>
|
||||
@@ -228,7 +251,7 @@ function PerformanceForm({ employees, record, onSubmit, onClose }: {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>考核结果</Label>
|
||||
<Label>考核结果(自动计算)</Label>
|
||||
<Select value={form.result} onChange={(e) => setForm({ ...form, result: e.target.value })}>
|
||||
<option value="EXCELLENT">优秀</option>
|
||||
<option value="QUALIFIED">合格</option>
|
||||
|
||||
Reference in New Issue
Block a user