feat: 系统优化Phase2 - 面包屑导航/侧边栏间距/制度公示阅读签收/模板变量中文化/通知类型补全
- 面包屑导航组件,集成至TopNav header - 侧边栏菜单分组间距增大,分组间分隔线 - 制度公示员工阅读签收:PolicyReadRecord模型、portal路由、管理端阅读统计 - 修复Policies.tsx民主程序推进bug(字段名/API路径/参数) - 用工文本模板变量名英文转中文显示 - 通知类型TYPE_LABELS补全(RISK_ALERT/SOCIAL_INS/OVERTIME_ALERT/PAYSLIP_READY) - 通知示例数据补充 - h2标题统一为text-sm font-medium - 新增run.md
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
import { useState, useRef } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
|
||||
import api from "../../lib/api"
|
||||
import Card from "../../components/ui/Card"
|
||||
import Button from "../../components/ui/Button"
|
||||
import { Input, Label, Select } from "../../components/ui/Input"
|
||||
import Modal from "../../components/ui/Modal"
|
||||
import { Users, FileText, AlertTriangle, Calendar, GraduationCap, TrendingUp, Scale, X, Plus, Paperclip, Trash2, Printer, Calculator, Shield, Info, Check, Eye, Download, UserX, UserPlus, Briefcase, FileSignature, DollarSign, Building2, RotateCcw, History } from "lucide-react"
|
||||
import { fmt } from "./shared"
|
||||
|
||||
export default function ContractInfo({ employeeId, contracts, hireDate }: { employeeId: string; contracts: any[]; hireDate: string }) {
|
||||
const queryClient = useQueryClient()
|
||||
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: '', electronicContractNo: '', electronicContractUrl: '' })
|
||||
const contractFileRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const addContractMutation = useMutation({
|
||||
mutationFn: (data: any) => api.post('/employees/contracts', { ...data, employeeId }),
|
||||
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['roster-profile'] }); setShowForm(false) },
|
||||
})
|
||||
|
||||
const handleContractFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
// 文件类型校验
|
||||
const allowedTypes = ['application/pdf', 'image/jpeg', 'image/jpg', 'image/png', 'image/heic']
|
||||
const allowedExts = ['.pdf', '.jpg', '.jpeg', '.png', '.heic']
|
||||
const ext = file.name.toLowerCase().substring(file.name.lastIndexOf('.'))
|
||||
if (!allowedTypes.includes(file.type) && !allowedExts.includes(ext)) {
|
||||
toast.error('不支持的文件格式,请上传 PDF、JPG、PNG 或 HEIC 格式')
|
||||
return
|
||||
}
|
||||
|
||||
// 文件大小校验(10MB)
|
||||
const maxSize = 10 * 1024 * 1024
|
||||
if (file.size > maxSize) {
|
||||
const formatSize = (bytes: number) => bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(0)}KB` : `${(bytes / 1024 / 1024).toFixed(1)}MB`
|
||||
toast.error(`文件过大,请上传小于 10MB 的文件(当前: ${formatSize(file.size)})`)
|
||||
return
|
||||
}
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = (event) => {
|
||||
setForm({ ...form, attachmentUrl: event.target?.result as string })
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
|
||||
const typeMap: Record<string, string> = { FIXED: '固定期限', UNFIXED: '无固定期限', UNSIGNED: '未签订' }
|
||||
|
||||
// 按劳动合同法自动判断续签和合同类型建议
|
||||
const contractAdvice = (() => {
|
||||
if (!contracts?.length) return null
|
||||
const fixedContracts = contracts.filter((c: any) => c.contractType === 'FIXED')
|
||||
const latestContract = contracts[0]
|
||||
const isRenewal = !!latestContract?.endDate
|
||||
const renewalCount = (latestContract?.renewalCount || 0)
|
||||
|
||||
// 连续订立二次固定期限劳动合同,第三次应订立无固定期限
|
||||
const shouldUnfixed = fixedContracts.length >= 2
|
||||
|
||||
// 连续工作满十年
|
||||
const yearsSinceHire = hireDate ? (Date.now() - new Date(hireDate).getTime()) / (365.25 * 24 * 60 * 60 * 1000) : 0
|
||||
const shouldUnfixedByTenure = yearsSinceHire >= 10
|
||||
|
||||
if (shouldUnfixed || shouldUnfixedByTenure) {
|
||||
return {
|
||||
isRenewal,
|
||||
renewalCount: isRenewal ? renewalCount + 1 : renewalCount,
|
||||
suggestedType: 'UNFIXED',
|
||||
reason: shouldUnfixed
|
||||
? `已连续签订${fixedContracts.length}次固定期限合同,按《劳动合同法》第十四条应订立无固定期限合同`
|
||||
: `连续工作满${Math.floor(yearsSinceHire)}年,按《劳动合同法》第十四条应订立无固定期限合同`,
|
||||
}
|
||||
}
|
||||
|
||||
if (isRenewal) {
|
||||
return {
|
||||
isRenewal: true,
|
||||
renewalCount: renewalCount + 1,
|
||||
suggestedType: 'FIXED',
|
||||
reason: `本次为第${renewalCount + 1}次续签`,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
})()
|
||||
|
||||
const handleShowForm = () => {
|
||||
if (contractAdvice?.suggestedType) {
|
||||
setForm({
|
||||
...form,
|
||||
contractType: contractAdvice.suggestedType,
|
||||
signDate: new Date().toISOString().slice(0, 10),
|
||||
startDate: contractAdvice.isRenewal && contracts[0]?.endDate
|
||||
? contracts[0].endDate.toString().slice(0, 10)
|
||||
: new Date().toISOString().slice(0, 10),
|
||||
endDate: '',
|
||||
probationMonths: 0,
|
||||
probationSalary: 0,
|
||||
signMethod: 'PAPER',
|
||||
attachmentUrl: '',
|
||||
electronicContractNo: '',
|
||||
electronicContractUrl: '',
|
||||
})
|
||||
}
|
||||
setShowForm(!showForm)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-xs font-medium">劳动合同({contracts?.length || 0}份)</h2>
|
||||
<Button size="sm" onClick={handleShowForm}>新增合同</Button>
|
||||
</div>
|
||||
|
||||
{contractAdvice && !showForm && (
|
||||
<div className="px-3 py-2 rounded-md bg-blue-50 text-blue-700 text-xs flex items-start gap-2">
|
||||
<Info className="w-4 h-4 mt-0.5 shrink-0" />
|
||||
<span>{contractAdvice.reason},建议选择「{contractAdvice.suggestedType === 'UNFIXED' ? '无固定期限' : '固定期限'}」</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showForm && (
|
||||
<Card>
|
||||
{contractAdvice && (
|
||||
<div className="px-3 py-2 mb-3 rounded-md bg-amber-50 text-amber-700 text-xs flex items-start gap-2">
|
||||
<AlertTriangle className="w-4 h-4 mt-0.5 shrink-0" />
|
||||
<span>{contractAdvice.reason}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div><Label>合同类型</Label>
|
||||
<Select value={form.contractType} onChange={(e) => setForm({ ...form, contractType: e.target.value })}>
|
||||
<option value="FIXED">固定期限</option>
|
||||
<option value="UNFIXED">无固定期限</option>
|
||||
</Select>
|
||||
</div>
|
||||
<div><Label>签订日期</Label><Input type="date" value={form.signDate} onChange={(e) => setForm({ ...form, signDate: e.target.value })} /></div>
|
||||
<div><Label>合同开始日期 *</Label><Input type="date" value={form.startDate} onChange={(e) => setForm({ ...form, startDate: e.target.value })} /></div>
|
||||
{form.contractType === 'FIXED' && (
|
||||
<div><Label>合同结束日期</Label><Input type="date" value={form.endDate} onChange={(e) => setForm({ ...form, endDate: e.target.value })} /></div>
|
||||
)}
|
||||
{form.contractType === 'FIXED' && !contractAdvice?.isRenewal && (
|
||||
<>
|
||||
<div><Label>试用期(月)</Label><Input type="number" value={form.probationMonths} onChange={(e) => setForm({ ...form, probationMonths: parseInt(e.target.value) || 0 })} min={0} max={6} /></div>
|
||||
<div><Label>试用期工资</Label><Input type="number" value={form.probationSalary} onChange={(e) => setForm({ ...form, probationSalary: parseInt(e.target.value) || 0 })} /></div>
|
||||
</>
|
||||
)}
|
||||
<div className="md:col-span-2 border-t pt-3">
|
||||
<Label>签订方式</Label>
|
||||
<Select value={form.signMethod} onChange={(e) => setForm({ ...form, signMethod: e.target.value as 'PAPER' | 'ELECTRONIC' })}>
|
||||
<option value="PAPER">纸质签署</option>
|
||||
<option value="ELECTRONIC">电子签署</option>
|
||||
</Select>
|
||||
</div>
|
||||
{form.signMethod === 'PAPER' && (
|
||||
<div className="md:col-span-2">
|
||||
<Label>合同扫描件 *</Label>
|
||||
<input ref={contractFileRef} type="file" className="hidden" onChange={handleContractFileUpload} />
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={() => contractFileRef.current?.click()}>
|
||||
<Paperclip className="w-4 h-4 mr-1" />上传扫描件
|
||||
</Button>
|
||||
{form.attachmentUrl && <span className="text-xs text-safe">✓ 已上传</span>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{form.signMethod === 'ELECTRONIC' && (
|
||||
<>
|
||||
<div><Label>电子合同编号 *</Label><Input value={form.electronicContractNo} onChange={(e) => setForm({ ...form, electronicContractNo: e.target.value })} placeholder="如 E-2026-001" /></div>
|
||||
<div><Label>电子合同链接 *</Label><Input value={form.electronicContractUrl} onChange={(e) => setForm({ ...form, electronicContractUrl: e.target.value })} placeholder="https://..." /></div>
|
||||
</>
|
||||
)}
|
||||
<div className="md:col-span-2 flex gap-2">
|
||||
<Button onClick={() => addContractMutation.mutate(form)} disabled={
|
||||
addContractMutation.isPending || !form.startDate ||
|
||||
(form.signMethod === 'PAPER' && !form.attachmentUrl) ||
|
||||
(form.signMethod === 'ELECTRONIC' && (!form.electronicContractNo || !form.electronicContractUrl))
|
||||
}>
|
||||
{addContractMutation.isPending ? '保存中...' : '保存'}
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={() => setShowForm(false)}>取消</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{!contracts?.length ? (
|
||||
<Card><div className="text-center py-8 text-gray-400">暂无合同记录</div></Card>
|
||||
) : contracts.map((c) => (
|
||||
<Card key={c.id}>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="grid md:grid-cols-3 gap-x-6 gap-y-3 flex-1 text-xs">
|
||||
<div className="flex justify-between"><span className="text-gray-500 shrink-0">合同类型</span><span className="font-medium text-right truncate ml-2">{typeMap[c.contractType] || c.contractType}</span></div>
|
||||
<div className="flex justify-between"><span className="text-gray-500 shrink-0">签订日期</span><span className="font-medium text-right truncate ml-2">{c.signDate ? c.signDate.toString().slice(0, 10) : '未签订'}</span></div>
|
||||
<div className="flex justify-between"><span className="text-gray-500 shrink-0">合同开始</span><span className="font-medium text-right truncate ml-2">{c.startDate?.toString().slice(0, 10)}</span></div>
|
||||
<div className="flex justify-between"><span className="text-gray-500 shrink-0">合同结束</span><span className="font-medium text-right truncate ml-2">{c.endDate ? c.endDate.toString().slice(0, 10) : '无固定期限'}</span></div>
|
||||
<div className="flex justify-between"><span className="text-gray-500 shrink-0">合同期限</span><span className="font-medium text-right truncate ml-2">{c.contractYears}年</span></div>
|
||||
<div className="flex justify-between"><span className="text-gray-500 shrink-0">试用期</span><span className="font-medium text-right truncate ml-2">{c.probationMonths}个月(¥{c.probationSalary})</span></div>
|
||||
<div className="flex justify-between"><span className="text-gray-500 shrink-0">签订方式</span><span className="font-medium text-right truncate ml-2">{c.signMethod === 'PAPER' ? '纸质' : '电子'}</span></div>
|
||||
<div className="flex justify-between"><span className="text-gray-500 shrink-0">续签次数</span><span className="font-medium text-right truncate ml-2">{c.renewalCount}</span></div>
|
||||
{c.signMethod === 'PAPER' && (
|
||||
<div className="flex justify-between md:col-span-3">
|
||||
<span className="text-gray-500">合同扫描件</span>
|
||||
{c.attachmentUrl ? (
|
||||
<a href={c.attachmentUrl} target="_blank" rel="noopener noreferrer" className="text-primary hover:underline flex items-center gap-1">
|
||||
<Paperclip className="w-3 h-3" />查看扫描件
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-gray-400">未上传</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{c.signMethod === 'ELECTRONIC' && (
|
||||
<>
|
||||
{c.electronicContractNo && <div className="flex justify-between"><span className="text-gray-500">电子合同编号</span><span className="font-medium">{c.electronicContractNo}</span></div>}
|
||||
{c.electronicContractUrl && (
|
||||
<div className="flex justify-between md:col-span-3">
|
||||
<span className="text-gray-500">电子合同</span>
|
||||
<a href={c.electronicContractUrl} target="_blank" rel="noopener noreferrer" className="text-primary hover:underline flex items-center gap-1">
|
||||
<FileText className="w-3 h-3" />查看电子合同
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user