611 lines
26 KiB
TypeScript
611 lines
26 KiB
TypeScript
/**
|
||
* 员工特殊状态台账页面
|
||
* 管理三期(孕期/产期/哺乳期)、工伤、医疗期等特殊状态的跟踪和提醒
|
||
*/
|
||
import { useEffect, useState } from 'react'
|
||
import { Search, Plus, Edit2, Trash2, AlertTriangle, Clock, Baby, HeartPulse, Activity, X } from 'lucide-react'
|
||
import api from '../lib/api'
|
||
import { Input, Select, Label } from '../components/ui/Input'
|
||
import Button from '../components/ui/Button'
|
||
|
||
interface SpecialStatus {
|
||
id: string
|
||
type: string
|
||
status: string
|
||
startDate: string | null
|
||
endDate: string | null
|
||
actualEndDate: string | null
|
||
expectedDueDate: string | null
|
||
maternityLeaveStart: string | null
|
||
maternityLeaveEnd: string | null
|
||
nursingEndDate: string | null
|
||
injuryDate: string | null
|
||
injuryDescription: string | null
|
||
certificationDate: string | null
|
||
certificationNo: string | null
|
||
disabilityLevel: number | null
|
||
assessmentDate: string | null
|
||
medicalMonths: number | null
|
||
medicalPeriodEnd: string | null
|
||
description: string | null
|
||
attachments: any[] | null
|
||
timeline: any[] | null
|
||
reminderDate: string | null
|
||
alertLevel: 'red' | 'yellow' | 'green'
|
||
employee: {
|
||
id: string; name: string; department: string; phone: string; gender: string; status: string
|
||
}
|
||
}
|
||
|
||
interface Stats {
|
||
total: number
|
||
active: number
|
||
pending: number
|
||
redAlert: number
|
||
yellowAlert: number
|
||
byType: { type: string; label: string; count: number }[]
|
||
}
|
||
|
||
const TYPE_LABELS: Record<string, string> = {
|
||
PREGNANCY: '三期',
|
||
WORK_INJURY: '工伤',
|
||
MEDICAL_PERIOD: '医疗期',
|
||
OTHER: '其他',
|
||
}
|
||
|
||
const TYPE_ICONS: Record<string, typeof Baby> = {
|
||
PREGNANCY: Baby,
|
||
WORK_INJURY: Activity,
|
||
MEDICAL_PERIOD: HeartPulse,
|
||
OTHER: AlertTriangle,
|
||
}
|
||
|
||
const TYPE_COLORS: Record<string, string> = {
|
||
PREGNANCY: 'bg-pink-50 text-pink-700 border-pink-200',
|
||
WORK_INJURY: 'bg-red-50 text-red-700 border-red-200',
|
||
MEDICAL_PERIOD: 'bg-orange-50 text-orange-700 border-orange-200',
|
||
OTHER: 'bg-gray-50 text-gray-700 border-gray-200',
|
||
}
|
||
|
||
const STATUS_LABELS: Record<string, string> = {
|
||
ACTIVE: '进行中',
|
||
PENDING: '待处理',
|
||
RESOLVED: '已结束',
|
||
}
|
||
|
||
const STATUS_COLORS: Record<string, string> = {
|
||
ACTIVE: 'bg-emerald-50 text-emerald-700',
|
||
PENDING: 'bg-amber-50 text-amber-700',
|
||
RESOLVED: 'bg-gray-100 text-gray-500',
|
||
}
|
||
|
||
const ALERT_STYLES: Record<string, { border: string; badge: string; text: string }> = {
|
||
red: { border: 'border-l-4 border-l-red-500', badge: 'bg-red-100 text-red-700', text: 'text-red-600' },
|
||
yellow: { border: 'border-l-4 border-l-amber-500', badge: 'bg-amber-100 text-amber-700', text: 'text-amber-600' },
|
||
green: { border: 'border-l-4 border-l-emerald-500', badge: 'bg-emerald-100 text-emerald-700', text: 'text-emerald-600' },
|
||
}
|
||
|
||
function formatDate(d: string | null): string {
|
||
if (!d) return '-'
|
||
return new Date(d).toLocaleDateString('zh-CN')
|
||
}
|
||
|
||
function daysUntil(d: string | null): number | null {
|
||
if (!d) return null
|
||
const diff = new Date(d).getTime() - Date.now()
|
||
return Math.ceil(diff / (1000 * 60 * 60 * 24))
|
||
}
|
||
|
||
export default function SpecialStatus() {
|
||
const [list, setList] = useState<SpecialStatus[]>([])
|
||
const [total, setTotal] = useState(0)
|
||
const [page, setPage] = useState(1)
|
||
const [pageSize] = useState(20)
|
||
const [search, setSearch] = useState('')
|
||
const [typeFilter, setTypeFilter] = useState('')
|
||
const [statusFilter, setStatusFilter] = useState('')
|
||
const [loading, setLoading] = useState(true)
|
||
const [stats, setStats] = useState<Stats | null>(null)
|
||
const [editOpen, setEditOpen] = useState(false)
|
||
const [editing, setEditing] = useState<SpecialStatus | null>(null)
|
||
const [deleteTarget, setDeleteTarget] = useState<SpecialStatus | null>(null)
|
||
const [employees, setEmployees] = useState<any[]>([])
|
||
const [form, setForm] = useState<any>(getDefaultForm())
|
||
|
||
function getDefaultForm() {
|
||
return {
|
||
employeeId: '',
|
||
type: 'PREGNANCY',
|
||
status: 'ACTIVE',
|
||
startDate: '',
|
||
endDate: '',
|
||
actualEndDate: '',
|
||
expectedDueDate: '',
|
||
injuryDate: '',
|
||
injuryDescription: '',
|
||
certificationDate: '',
|
||
certificationNo: '',
|
||
disabilityLevel: '',
|
||
assessmentDate: '',
|
||
medicalMonths: '',
|
||
description: '',
|
||
reminderDate: '',
|
||
}
|
||
}
|
||
|
||
const fetchList = async () => {
|
||
setLoading(true)
|
||
try {
|
||
const params: any = { page, pageSize }
|
||
if (search) params.search = search
|
||
if (typeFilter) params.type = typeFilter
|
||
if (statusFilter) params.status = statusFilter
|
||
const res = await api.get('/special-statuses', { params }) as any
|
||
setList(res.data.list)
|
||
setTotal(res.data.total)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
const fetchStats = async () => {
|
||
try {
|
||
const res = await api.get('/special-statuses/stats/overview') as any
|
||
setStats(res.data)
|
||
} catch {
|
||
// 忽略
|
||
}
|
||
}
|
||
|
||
const fetchEmployees = async () => {
|
||
try {
|
||
const res = await api.get('/employees', { params: { pageSize: 999 } }) as any
|
||
setEmployees(res.data.list || [])
|
||
} catch {
|
||
// 忽略
|
||
}
|
||
}
|
||
|
||
useEffect(() => { fetchList(); fetchStats() }, [page, typeFilter, statusFilter])
|
||
useEffect(() => { setPage(1) }, [search, typeFilter, statusFilter])
|
||
|
||
const handleOpenCreate = () => {
|
||
setEditing(null)
|
||
setForm(getDefaultForm())
|
||
setEditOpen(true)
|
||
fetchEmployees()
|
||
}
|
||
|
||
const handleOpenEdit = (item: SpecialStatus) => {
|
||
setEditing(item)
|
||
setForm({
|
||
employeeId: item.employee.id,
|
||
type: item.type,
|
||
status: item.status,
|
||
startDate: item.startDate ? item.startDate.split('T')[0] : '',
|
||
endDate: item.endDate ? item.endDate.split('T')[0] : '',
|
||
actualEndDate: item.actualEndDate ? item.actualEndDate.split('T')[0] : '',
|
||
expectedDueDate: item.expectedDueDate ? item.expectedDueDate.split('T')[0] : '',
|
||
injuryDate: item.injuryDate ? item.injuryDate.split('T')[0] : '',
|
||
injuryDescription: item.injuryDescription || '',
|
||
certificationDate: item.certificationDate ? item.certificationDate.split('T')[0] : '',
|
||
certificationNo: item.certificationNo || '',
|
||
disabilityLevel: item.disabilityLevel || '',
|
||
assessmentDate: item.assessmentDate ? item.assessmentDate.split('T')[0] : '',
|
||
medicalMonths: item.medicalMonths || '',
|
||
description: item.description || '',
|
||
reminderDate: item.reminderDate ? item.reminderDate.split('T')[0] : '',
|
||
})
|
||
setEditOpen(true)
|
||
fetchEmployees()
|
||
}
|
||
|
||
const handleSave = async () => {
|
||
if (!form.employeeId) { alert('请选择员工'); return }
|
||
try {
|
||
const data: any = { ...form }
|
||
// 空字符串转 null
|
||
Object.keys(data).forEach((k) => {
|
||
if (data[k] === '') data[k] = null
|
||
})
|
||
if (data.disabilityLevel) data.disabilityLevel = parseInt(data.disabilityLevel)
|
||
if (data.medicalMonths) data.medicalMonths = parseInt(data.medicalMonths)
|
||
|
||
if (editing) {
|
||
await api.put(`/special-statuses/${editing.id}`, data)
|
||
} else {
|
||
await api.post('/special-statuses', data)
|
||
}
|
||
setEditOpen(false)
|
||
fetchList()
|
||
fetchStats()
|
||
} catch (err: any) {
|
||
alert(err.response?.data?.error?.message || '保存失败')
|
||
}
|
||
}
|
||
|
||
const handleDelete = async () => {
|
||
if (!deleteTarget) return
|
||
try {
|
||
await api.delete(`/special-statuses/${deleteTarget.id}`)
|
||
setDeleteTarget(null)
|
||
fetchList()
|
||
fetchStats()
|
||
} catch (err: any) {
|
||
alert(err.response?.data?.error?.message || '删除失败')
|
||
}
|
||
}
|
||
|
||
const totalPages = Math.ceil(total / pageSize)
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* 标题 */}
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-gray-900">特殊状态台账</h1>
|
||
<p className="text-sm text-gray-500 mt-1">三期/工伤/医疗期等特殊员工状态跟踪与提醒</p>
|
||
</div>
|
||
|
||
{/* 统计卡片 */}
|
||
{stats && (
|
||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3">
|
||
<div className="bg-white border border-gray-200 rounded-lg p-3">
|
||
<div className="text-xs text-gray-500">总记录</div>
|
||
<div className="text-xl font-bold text-gray-900">{stats.total}</div>
|
||
</div>
|
||
<div className="bg-white border border-gray-200 rounded-lg p-3">
|
||
<div className="text-xs text-gray-500">进行中</div>
|
||
<div className="text-xl font-bold text-emerald-600">{stats.active}</div>
|
||
</div>
|
||
<div className="bg-white border border-gray-200 rounded-lg p-3">
|
||
<div className="text-xs text-gray-500">待处理</div>
|
||
<div className="text-xl font-bold text-amber-600">{stats.pending}</div>
|
||
</div>
|
||
<div className="bg-white border border-red-200 rounded-lg p-3">
|
||
<div className="text-xs text-gray-500">紧急提醒</div>
|
||
<div className="text-xl font-bold text-red-600">{stats.redAlert}</div>
|
||
</div>
|
||
<div className="bg-white border border-amber-200 rounded-lg p-3">
|
||
<div className="text-xs text-gray-500">即将到期</div>
|
||
<div className="text-xl font-bold text-amber-600">{stats.yellowAlert}</div>
|
||
</div>
|
||
<div className="bg-white border border-gray-200 rounded-lg p-3">
|
||
<div className="text-xs text-gray-500 mb-1">类型分布</div>
|
||
<div className="text-xs text-gray-700">
|
||
{stats.byType.map((t) => (
|
||
<span key={t.type} className="mr-2">{t.label}: <b>{t.count}</b></span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 筛选栏 */}
|
||
<div className="flex flex-wrap gap-3 items-center">
|
||
<div className="relative flex-1 min-w-[200px]">
|
||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||
<Input
|
||
placeholder="搜索员工姓名、手机号..."
|
||
className="pl-10"
|
||
value={search}
|
||
onChange={(e) => setSearch(e.target.value)}
|
||
onKeyDown={(e) => e.key === 'Enter' && fetchList()}
|
||
/>
|
||
</div>
|
||
<Select className="w-32" value={typeFilter} onChange={(e) => setTypeFilter(e.target.value)}>
|
||
<option value="">全部类型</option>
|
||
<option value="PREGNANCY">三期</option>
|
||
<option value="WORK_INJURY">工伤</option>
|
||
<option value="MEDICAL_PERIOD">医疗期</option>
|
||
<option value="OTHER">其他</option>
|
||
</Select>
|
||
<Select className="w-32" value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)}>
|
||
<option value="">全部状态</option>
|
||
<option value="ACTIVE">进行中</option>
|
||
<option value="PENDING">待处理</option>
|
||
<option value="RESOLVED">已结束</option>
|
||
</Select>
|
||
<Button variant="secondary" onClick={fetchList}>搜索</Button>
|
||
<Button onClick={handleOpenCreate}><Plus className="w-4 h-4 mr-1" />新增</Button>
|
||
</div>
|
||
|
||
{/* 列表 */}
|
||
{loading ? (
|
||
<div className="text-center py-12 text-gray-400">加载中...</div>
|
||
) : list.length === 0 ? (
|
||
<div className="text-center py-12 text-gray-400">
|
||
<AlertTriangle className="w-12 h-12 mx-auto mb-3 text-gray-300" />
|
||
暂无特殊状态记录
|
||
</div>
|
||
) : (
|
||
<>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||
{list.map((item) => {
|
||
const Icon = TYPE_ICONS[item.type] || AlertTriangle
|
||
const alert = ALERT_STYLES[item.alertLevel] || ALERT_STYLES.green
|
||
const reminderDays = daysUntil(item.reminderDate)
|
||
return (
|
||
<div key={item.id} className={`bg-white border ${alert.border} border-gray-200 rounded-lg p-4`}>
|
||
{/* 头部 */}
|
||
<div className="flex items-start justify-between mb-3">
|
||
<div className="flex items-center gap-2">
|
||
<span className={`px-2 py-0.5 rounded text-xs font-medium border ${TYPE_COLORS[item.type] || TYPE_COLORS.OTHER}`}>
|
||
<Icon className="w-3 h-3 inline mr-1" />
|
||
{TYPE_LABELS[item.type] || item.type}
|
||
</span>
|
||
<span className={`px-2 py-0.5 rounded text-xs font-medium ${STATUS_COLORS[item.status] || STATUS_COLORS.RESOLVED}`}>
|
||
{STATUS_LABELS[item.status] || item.status}
|
||
</span>
|
||
</div>
|
||
<div className="flex gap-1">
|
||
<button onClick={() => handleOpenEdit(item)} className="p-1 rounded text-gray-400 hover:text-primary hover:bg-gray-100" title="编辑">
|
||
<Edit2 className="w-3.5 h-3.5" />
|
||
</button>
|
||
<button onClick={() => setDeleteTarget(item)} className="p-1 rounded text-gray-400 hover:text-red-500 hover:bg-gray-100" title="删除">
|
||
<Trash2 className="w-3.5 h-3.5" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 员工信息 */}
|
||
<div className="mb-3">
|
||
<span className="font-medium text-gray-900">{item.employee.name}</span>
|
||
<span className="text-sm text-gray-500 ml-2">{item.employee.department}</span>
|
||
{item.employee.status === 'RESIGNED' && (
|
||
<span className="ml-2 text-xs text-gray-400">已离职</span>
|
||
)}
|
||
</div>
|
||
|
||
{/* 关键日期 */}
|
||
<div className="space-y-1.5 text-sm">
|
||
{item.type === 'PREGNANCY' && (
|
||
<>
|
||
{item.expectedDueDate && <Row label="预产期" value={formatDate(item.expectedDueDate)} />}
|
||
{item.maternityLeaveStart && <Row label="产假开始" value={formatDate(item.maternityLeaveStart)} />}
|
||
{item.maternityLeaveEnd && <Row label="产假结束" value={formatDate(item.maternityLeaveEnd)} />}
|
||
{item.nursingEndDate && <Row label="哺乳期截止" value={formatDate(item.nursingEndDate)} />}
|
||
</>
|
||
)}
|
||
{item.type === 'WORK_INJURY' && (
|
||
<>
|
||
{item.injuryDate && <Row label="受伤日期" value={formatDate(item.injuryDate)} />}
|
||
{item.injuryDescription && <Row label="伤情描述" value={item.injuryDescription} />}
|
||
{item.certificationDate ? (
|
||
<Row label="认定日期" value={formatDate(item.certificationDate)} />
|
||
) : (
|
||
<Row label="认定日期" value="待认定" valueClass="text-amber-600" />
|
||
)}
|
||
{item.assessmentDate && <Row label="鉴定日期" value={formatDate(item.assessmentDate)} />}
|
||
{item.disabilityLevel && <Row label="伤残等级" value={`${item.disabilityLevel}级`} />}
|
||
</>
|
||
)}
|
||
{item.type === 'MEDICAL_PERIOD' && (
|
||
<>
|
||
{item.startDate && <Row label="开始日期" value={formatDate(item.startDate)} />}
|
||
{item.medicalMonths && <Row label="医疗期" value={`${item.medicalMonths}个月`} />}
|
||
{item.medicalPeriodEnd && <Row label="截止日期" value={formatDate(item.medicalPeriodEnd)} />}
|
||
</>
|
||
)}
|
||
{item.type === 'OTHER' && (
|
||
<>
|
||
{item.startDate && <Row label="开始日期" value={formatDate(item.startDate)} />}
|
||
{item.endDate && <Row label="预计结束" value={formatDate(item.endDate)} />}
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
{/* 提醒 */}
|
||
{item.status === 'ACTIVE' && reminderDays !== null && (
|
||
<div className={`mt-3 pt-3 border-t border-gray-100 flex items-center gap-1.5 text-xs ${alert.text}`}>
|
||
<Clock className="w-3.5 h-3.5" />
|
||
{reminderDays < 0 ? `已过期 ${Math.abs(reminderDays)} 天` : `距提醒日还有 ${reminderDays} 天`}
|
||
</div>
|
||
)}
|
||
{item.description && (
|
||
<div className="mt-2 text-xs text-gray-400 line-clamp-2">{item.description}</div>
|
||
)}
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
|
||
{/* 分页 */}
|
||
{totalPages > 1 && (
|
||
<div className="flex items-center justify-center gap-2 pt-4">
|
||
<Button variant="secondary" disabled={page <= 1} onClick={() => setPage(page - 1)}>上一页</Button>
|
||
<span className="text-sm text-gray-500">{page} / {totalPages}</span>
|
||
<Button variant="secondary" disabled={page >= totalPages} onClick={() => setPage(page + 1)}>下一页</Button>
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
|
||
{/* 新增/编辑弹窗 */}
|
||
{editOpen && (
|
||
<div className="fixed inset-0 bg-black/40 z-50 flex items-center justify-center p-4" onClick={() => setEditOpen(false)}>
|
||
<div className="bg-white border border-gray-200 rounded-lg p-6 w-full max-w-lg max-h-[90vh] overflow-y-auto" onClick={(e) => e.stopPropagation()}>
|
||
<div className="flex items-center justify-between mb-4">
|
||
<h2 className="text-lg font-semibold text-gray-900">{editing ? '编辑特殊状态' : '新增特殊状态'}</h2>
|
||
<button onClick={() => setEditOpen(false)} className="text-gray-400 hover:text-gray-600"><X className="w-5 h-5" /></button>
|
||
</div>
|
||
|
||
<div className="space-y-3">
|
||
{/* 员工选择 */}
|
||
<div>
|
||
<Label>员工</Label>
|
||
<Select value={form.employeeId} onChange={(e) => setForm({ ...form, employeeId: e.target.value })} disabled={!!editing}>
|
||
<option value="">请选择员工</option>
|
||
{employees.map((emp) => (
|
||
<option key={emp.id} value={emp.id}>{emp.name} - {emp.department}</option>
|
||
))}
|
||
</Select>
|
||
</div>
|
||
|
||
{/* 类型 + 状态 */}
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<Label>类型</Label>
|
||
<Select value={form.type} onChange={(e) => setForm({ ...form, type: e.target.value })}>
|
||
<option value="PREGNANCY">三期</option>
|
||
<option value="WORK_INJURY">工伤</option>
|
||
<option value="MEDICAL_PERIOD">医疗期</option>
|
||
<option value="OTHER">其他</option>
|
||
</Select>
|
||
</div>
|
||
<div>
|
||
<Label>状态</Label>
|
||
<Select value={form.status} onChange={(e) => setForm({ ...form, status: e.target.value })}>
|
||
<option value="ACTIVE">进行中</option>
|
||
<option value="PENDING">待处理</option>
|
||
<option value="RESOLVED">已结束</option>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 三期专用字段 */}
|
||
{form.type === 'PREGNANCY' && (
|
||
<div className="border-t border-gray-100 pt-3 space-y-3">
|
||
<h3 className="text-xs text-primary font-medium">三期信息</h3>
|
||
<div>
|
||
<Label>预产期</Label>
|
||
<Input type="date" value={form.expectedDueDate} onChange={(e) => setForm({ ...form, expectedDueDate: e.target.value })} />
|
||
<p className="text-xs text-gray-400 mt-1">填写预产期后,系统将自动计算产假起止和哺乳期截止日期</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 工伤专用字段 */}
|
||
{form.type === 'WORK_INJURY' && (
|
||
<div className="border-t border-gray-100 pt-3 space-y-3">
|
||
<h3 className="text-xs text-primary font-medium">工伤信息</h3>
|
||
<div>
|
||
<Label>受伤日期</Label>
|
||
<Input type="date" value={form.injuryDate} onChange={(e) => setForm({ ...form, injuryDate: e.target.value })} />
|
||
</div>
|
||
<div>
|
||
<Label>伤情描述</Label>
|
||
<Input value={form.injuryDescription} onChange={(e) => setForm({ ...form, injuryDescription: e.target.value })} placeholder="简要描述伤情" />
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<Label>认定日期</Label>
|
||
<Input type="date" value={form.certificationDate} onChange={(e) => setForm({ ...form, certificationDate: e.target.value })} />
|
||
</div>
|
||
<div>
|
||
<Label>认定书编号</Label>
|
||
<Input value={form.certificationNo} onChange={(e) => setForm({ ...form, certificationNo: e.target.value })} />
|
||
</div>
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<Label>伤残等级(1-10)</Label>
|
||
<Input type="number" min={1} max={10} value={form.disabilityLevel} onChange={(e) => setForm({ ...form, disabilityLevel: e.target.value })} placeholder="未鉴定则留空" />
|
||
</div>
|
||
<div>
|
||
<Label>鉴定日期</Label>
|
||
<Input type="date" value={form.assessmentDate} onChange={(e) => setForm({ ...form, assessmentDate: e.target.value })} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 医疗期专用字段 */}
|
||
{form.type === 'MEDICAL_PERIOD' && (
|
||
<div className="border-t border-gray-100 pt-3 space-y-3">
|
||
<h3 className="text-xs text-primary font-medium">医疗期信息</h3>
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<Label>开始日期</Label>
|
||
<Input type="date" value={form.startDate} onChange={(e) => setForm({ ...form, startDate: e.target.value })} />
|
||
</div>
|
||
<div>
|
||
<Label>医疗期月数</Label>
|
||
<Input type="number" value={form.medicalMonths} onChange={(e) => setForm({ ...form, medicalMonths: e.target.value })} placeholder="如3/6/9/12/24" />
|
||
</div>
|
||
</div>
|
||
<p className="text-xs text-gray-400">填写开始日期和月数后,系统将自动计算截止日期</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* 其他类型 */}
|
||
{form.type === 'OTHER' && (
|
||
<div className="border-t border-gray-100 pt-3 space-y-3">
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<Label>开始日期</Label>
|
||
<Input type="date" value={form.startDate} onChange={(e) => setForm({ ...form, startDate: e.target.value })} />
|
||
</div>
|
||
<div>
|
||
<Label>预计结束日期</Label>
|
||
<Input type="date" value={form.endDate} onChange={(e) => setForm({ ...form, endDate: e.target.value })} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 通用字段 */}
|
||
<div className="border-t border-gray-100 pt-3 space-y-3">
|
||
{form.status === 'RESOLVED' && (
|
||
<div>
|
||
<Label>实际结束日期</Label>
|
||
<Input type="date" value={form.actualEndDate} onChange={(e) => setForm({ ...form, actualEndDate: e.target.value })} />
|
||
</div>
|
||
)}
|
||
<div>
|
||
<Label>提醒日期</Label>
|
||
<Input type="date" value={form.reminderDate} onChange={(e) => setForm({ ...form, reminderDate: e.target.value })} />
|
||
<p className="text-xs text-gray-400 mt-1">系统将在此日期前7天和30天分别发出提醒</p>
|
||
</div>
|
||
<div>
|
||
<Label>备注</Label>
|
||
<Input value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} placeholder="补充说明" />
|
||
</div>
|
||
</div>
|
||
|
||
{/* 操作按钮 */}
|
||
<div className="flex gap-2 pt-3">
|
||
<Button className="flex-1" onClick={handleSave}>保存</Button>
|
||
<Button variant="secondary" onClick={() => setEditOpen(false)}>取消</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 删除确认 */}
|
||
{deleteTarget && (
|
||
<div className="fixed inset-0 bg-black/40 z-50 flex items-center justify-center p-4" onClick={() => setDeleteTarget(null)}>
|
||
<div className="bg-white border border-gray-200 rounded-lg p-6 w-full max-w-sm" onClick={(e) => e.stopPropagation()}>
|
||
<div className="flex items-center gap-3 mb-4">
|
||
<div className="w-10 h-10 rounded-full bg-red-50 flex items-center justify-center">
|
||
<Trash2 className="w-5 h-5 text-red-500" />
|
||
</div>
|
||
<div>
|
||
<h3 className="font-semibold text-gray-900">确认删除</h3>
|
||
<p className="text-sm text-gray-500">删除后无法恢复</p>
|
||
</div>
|
||
</div>
|
||
<p className="text-sm text-gray-600 mb-4">
|
||
确定要删除 <b>{deleteTarget.employee.name}</b> 的 <b>{TYPE_LABELS[deleteTarget.type]}</b> 记录吗?
|
||
</p>
|
||
<div className="flex gap-2">
|
||
<Button className="flex-1 bg-red-500 hover:bg-red-600" onClick={handleDelete}>删除</Button>
|
||
<Button variant="secondary" onClick={() => setDeleteTarget(null)}>取消</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
/** 信息行组件 */
|
||
function Row({ label, value, valueClass }: { label: string; value: string; valueClass?: string }) {
|
||
return (
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-gray-500 text-xs">{label}</span>
|
||
<span className={`text-gray-900 text-xs font-medium ${valueClass || ''}`}>{value}</span>
|
||
</div>
|
||
)
|
||
}
|