feat: TurboHR 14项优化与功能增强

- #1 Dashboard风险提醒增加立刻办理按钮
- #2 Calendar月份选择器改为input month
- #3 Termination增加7种解聘原因法律依据和操作步骤
- #5 合同审查支持PDF TXT格式
- #6 AI合同审查prompt优化为具体修改建议
- #7 知识库添加更新机制说明
- #9 SpecialStatus员工选择改用all-lite接口
- #10 Termination增加详细法律条款引用
- #11 Money发薪批次增加社保公积金合计列
- #12 EmployeeAttachment扩展文件类型
- #13 花名册增加女职工干部工人选项加退休提醒
- #14 新增公司备用文件上传模块
This commit is contained in:
selfrelease
2026-08-01 13:47:49 +08:00
parent d9e2c610cf
commit f1a02f0439
20 changed files with 542 additions and 32 deletions
+2
View File
@@ -41,6 +41,7 @@ const CalendarPage = lazy(() => import('./pages/Calendar'))
const WorkProcess = lazy(() => import('./pages/WorkProcess'))
const MyAttendance = lazy(() => import('./pages/portal/MyAttendance'))
const SpecialStatus = lazy(() => import('./pages/SpecialStatus'))
const CompanyFiles = lazy(() => import('./pages/CompanyFiles'))
// Sprint 4-5 新增页面
const EmployeeHome = lazy(() => import('./pages/portal/EmployeeHome'))
@@ -189,6 +190,7 @@ export default function App() {
<Route path="/tools/annual-value" element={<ProtectedRoute><AdminLayout><AnnualValueReport /></AdminLayout></ProtectedRoute>} />
<Route path="/work-process" element={<ProtectedRoute><AdminLayout><WorkProcess /></AdminLayout></ProtectedRoute>} />
<Route path="/special-status" element={<ProtectedRoute><AdminLayout><SpecialStatus /></AdminLayout></ProtectedRoute>} />
<Route path="/company-files" element={<ProtectedRoute><AdminLayout><CompanyFiles /></AdminLayout></ProtectedRoute>} />
<Route path="/risk-center" element={<ProtectedRoute><AdminLayout><RiskCenter /></AdminLayout></ProtectedRoute>} />
<Route path="/salary-dashboard" element={<ProtectedRoute><AdminLayout><SalaryDashboard /></AdminLayout></ProtectedRoute>} />
@@ -77,6 +77,7 @@ const navGroups: NavGroup[] = [
{ path: '/templates', label: '文本模板', icon: BookMarked },
{ path: '/notifications', label: '通知管理', icon: Bell },
{ path: '/audit', label: '操作日志', icon: ScrollText },
{ path: '/company-files', label: '公司文件', icon: Building2 },
{ path: '/settings', label: '设置', icon: Settings },
],
},
@@ -27,6 +27,7 @@ const QUICK_PAGES: SearchResult[] = [
{ type: 'page', id: 'risk-center', title: '风险中心', link: '/risk-center', icon: 'alert' },
{ type: 'page', id: 'salary-dashboard', title: '薪酬分析', link: '/salary-dashboard', icon: 'chart' },
{ type: 'page', id: 'policies', title: '规章制度', link: '/policies', icon: 'file' },
{ type: 'page', id: 'company-files', title: '公司文件', link: '/company-files', icon: 'building' },
{ type: 'page', id: 'settings', title: '设置', link: '/settings', icon: 'gear' },
]
+8 -2
View File
@@ -1463,9 +1463,9 @@ function ReviewTab() {
<Select value={docType} onChange={(e) => setDocType(e.target.value)} className="w-40">
{REVIEW_DOC_TYPES.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
</Select>
<input ref={fileInputRef} type="file" accept=".docx,.doc" onChange={handleFileUpload} className="hidden" />
<input ref={fileInputRef} type="file" accept=".docx,.txt,.pdf" onChange={handleFileUpload} className="hidden" />
<Button size="sm" variant="secondary" onClick={() => fileInputRef.current?.click()} disabled={uploading}>
{uploading ? (<><Loader2 className="w-4 h-4 animate-spin mr-1" />...</>) : (<><FileText className="w-4 h-4 mr-1" /> .docx </>)}
{uploading ? (<><Loader2 className="w-4 h-4 animate-spin mr-1" />...</>) : (<><FileText className="w-4 h-4 mr-1" /></>)}
</Button>
{fileName && <span className="text-xs text-gray-500 truncate max-w-[200px]">{fileName}</span>}
</div>
@@ -1807,6 +1807,12 @@ function KnowledgeTab() {
return (
<div className="space-y-3">
<div className="bg-blue-50 border border-blue-200 rounded-md p-3 text-xs text-blue-700 space-y-1">
<div className="font-medium">📖 </div>
<div>· </div>
<div>· AI </div>
<div>· </div>
</div>
<div className="flex items-center justify-between">
<span className="text-xs text-gray-500"> {knowledgeList?.length || 0} </span>
<div className="flex gap-2">
+6 -1
View File
@@ -173,7 +173,12 @@ export default function Calendar() {
<Button variant="secondary" size="sm" onClick={prevMonth}>
<ChevronLeft className="w-4 h-4" />
</Button>
<span className="text-sm font-medium min-w-[80px] text-center">{calendarMonth}</span>
<input
type="month"
value={calendarMonth}
onChange={(e) => setCalendarMonth(e.target.value)}
className="text-sm font-medium rounded-md border border-input bg-background px-2 py-1 min-w-[120px] text-center"
/>
<Button variant="secondary" size="sm" onClick={nextMonth}>
<ChevronRight className="w-4 h-4" />
</Button>
+172
View File
@@ -0,0 +1,172 @@
import { useState, useRef } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { Building2, Upload, Trash2, FileText, AlertCircle, Calendar } from 'lucide-react'
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 EmptyState from '../components/ui/EmptyState'
const FILE_TYPES = [
{ value: 'BUSINESS_LICENSE', label: '营业执照' },
{ value: 'WORK_HOURS', label: '工时备案' },
{ value: 'HR_POLICY', label: '制度文件' },
{ value: 'LABOR_CONTRACT_TEMPLATE', label: '合同模板' },
{ value: 'OTHER', label: '其他' },
]
const fileTypeLabels: Record<string, string> = Object.fromEntries(FILE_TYPES.map(t => [t.value, t.label]))
export default function CompanyFiles() {
const queryClient = useQueryClient()
const fileInputRef = useRef<HTMLInputElement>(null)
const [fileType, setFileType] = useState('BUSINESS_LICENSE')
const [remark, setRemark] = useState('')
const [expiryDate, setExpiryDate] = useState('')
const [filterType, setFilterType] = useState('')
const { data: files, isLoading } = useQuery<any[]>({
queryKey: ['company-files', filterType],
queryFn: async () => {
const res = await api.get('/company-files', { params: filterType ? { fileType: filterType } : {} }) as any
return res.data || []
},
})
const addMutation = useMutation({
mutationFn: (data: any) => api.post('/company-files', data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['company-files'] })
toast.success('文件上传成功')
setRemark('')
setExpiryDate('')
},
onError: () => toast.error('上传失败'),
})
const deleteMutation = useMutation({
mutationFn: (id: string) => api.delete(`/company-files/${id}`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['company-files'] })
toast.success('已删除')
},
})
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
if (file.size > 10 * 1024 * 1024) {
toast.error('文件不能超过 10MB')
return
}
const reader = new FileReader()
reader.onload = () => {
addMutation.mutate({
fileName: file.name,
fileType,
fileUrl: reader.result as string,
fileSize: file.size,
remark: remark || undefined,
expiryDate: expiryDate || undefined,
})
}
reader.readAsDataURL(file)
e.target.value = ''
}
const fmtSize = (bytes: number) => {
if (bytes < 1024) return `${bytes}B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`
return `${(bytes / 1024 / 1024).toFixed(1)}MB`
}
return (
<div className="space-y-4">
<div className="flex items-center gap-2">
<Building2 className="h-5 w-5 text-primary" />
<h1 className="text-lg font-semibold"></h1>
</div>
<Card>
<div className="p-4 space-y-3">
<div className="text-sm text-gray-500"></div>
<div className="flex flex-wrap items-end gap-3">
<div>
<Label></Label>
<Select value={fileType} onChange={(e) => setFileType(e.target.value)} className="w-36">
{FILE_TYPES.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
</Select>
</div>
<div>
<Label></Label>
<Input value={remark} onChange={(e) => setRemark(e.target.value)} placeholder="如:2024年营业执照" className="w-48" />
</div>
<div>
<Label></Label>
<Input type="date" value={expiryDate} onChange={(e) => setExpiryDate(e.target.value)} className="w-40" />
</div>
<input ref={fileInputRef} type="file" className="hidden" onChange={handleFileUpload} />
<Button onClick={() => fileInputRef.current?.click()} disabled={addMutation.isPending}>
<Upload className="w-4 h-4 mr-1" />
{addMutation.isPending ? '上传中...' : '上传文件'}
</Button>
</div>
</div>
</Card>
<Card>
<div className="p-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-medium"></h2>
<Select value={filterType} onChange={(e) => setFilterType(e.target.value)} className="w-32 text-xs">
<option value=""></option>
{FILE_TYPES.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
</Select>
</div>
{isLoading ? (
<div className="text-center py-8 text-gray-500">...</div>
) : !files || files.length === 0 ? (
<EmptyState title="暂无文件" description="点击上方上传按钮添加公司文件" />
) : (
<div className="space-y-2">
{files.map((f: any) => {
const isExpired = f.expiryDate && new Date(f.expiryDate) < new Date()
const isExpiringSoon = f.expiryDate && !isExpired && (new Date(f.expiryDate).getTime() - new Date().getTime()) < 30 * 24 * 60 * 60 * 1000
return (
<div key={f.id} className="flex items-center gap-3 p-3 rounded-md border hover:bg-gray-50">
<FileText className="w-5 h-5 text-gray-400 flex-shrink-0" />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium truncate">{f.fileName}</span>
<span className="text-xs px-1.5 py-0.5 rounded bg-gray-100 text-gray-600">{fileTypeLabels[f.fileType] || f.fileType}</span>
{isExpired && <span className="text-xs px-1.5 py-0.5 rounded bg-red-50 text-danger flex items-center gap-0.5"><AlertCircle className="w-3 h-3" /></span>}
{isExpiringSoon && <span className="text-xs px-1.5 py-0.5 rounded bg-amber-50 text-warning flex items-center gap-0.5"><Calendar className="w-3 h-3" /></span>}
</div>
<div className="text-xs text-gray-400 mt-0.5">
{fmtSize(f.fileSize)}
{f.remark && <span className="ml-2">· {f.remark}</span>}
{f.expiryDate && <span className="ml-2">· {f.expiryDate.slice(0, 10)}</span>}
<span className="ml-2">· {new Date(f.createdAt).toLocaleDateString('zh-CN')}</span>
</div>
</div>
<a href={f.fileUrl} download={f.fileName} className="text-xs text-primary hover:underline flex items-center gap-1">
<FileText className="w-3.5 h-3.5" />
</a>
<button
onClick={() => deleteMutation.mutate(f.id)}
className="p-1.5 rounded hover:bg-red-50 text-gray-400 hover:text-danger"
title="删除"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
)
})}
</div>
)}
</div>
</Card>
</div>
)
}
+11 -1
View File
@@ -380,7 +380,7 @@ function AddEmployeeModal({ open, onClose, onSubmit, loading, error }: {
function EmployeeDetailDrawer({ employeeId, onClose }: { employeeId: string; onClose: () => void }) {
const queryClient = useQueryClient()
const fileInputRef = useRef<HTMLInputElement>(null)
const [fileType, setFileType] = useState<'ID_CARD' | 'BANK_CARD' | 'CONTRACT_SCAN' | 'EDUCATION' | 'OTHER'>('ID_CARD')
const [fileType, setFileType] = useState<'ID_CARD' | 'BANK_CARD' | 'CONTRACT_SCAN' | 'EDUCATION' | 'TERMINATION_DOC' | 'RETIREMENT_DOC' | 'INJURY_CERT' | 'MEDICAL_CERT' | 'PREGNANCY_CERT' | 'OTHER'>('ID_CARD')
const [previewUrl, setPreviewUrl] = useState<string | null>(null)
const { data: employee } = useQuery<any>({
@@ -447,6 +447,11 @@ function EmployeeDetailDrawer({ employeeId, onClose }: { employeeId: string; onC
BANK_CARD: '银行卡',
CONTRACT_SCAN: '合同附件',
EDUCATION: '学历证书',
TERMINATION_DOC: '解除文件',
RETIREMENT_DOC: '退休档案',
INJURY_CERT: '工伤认定',
MEDICAL_CERT: '医疗期证明',
PREGNANCY_CERT: '三期证明',
OTHER: '其他',
}
@@ -523,6 +528,11 @@ function EmployeeDetailDrawer({ employeeId, onClose }: { employeeId: string; onC
<option value="BANK_CARD"></option>
<option value="CONTRACT_SCAN"></option>
<option value="EDUCATION"></option>
<option value="TERMINATION_DOC"></option>
<option value="RETIREMENT_DOC">退</option>
<option value="INJURY_CERT"></option>
<option value="MEDICAL_CERT"></option>
<option value="PREGNANCY_CERT"></option>
<option value="OTHER"></option>
</Select>
<input
+7
View File
@@ -25,6 +25,7 @@ const TODO_ICON_CONFIG: Record<string, { icon: typeof FileText; color: string; b
TERMINATION: { icon: ShieldAlert, color: 'text-red-600', bg: 'bg-red-50' },
MONTHLY: { icon: Calendar, color: 'text-purple-600', bg: 'bg-purple-50' },
ONBOARDING: { icon: UserPlus, color: 'text-cyan-600', bg: 'bg-cyan-50' },
RETIREMENT: { icon: Clock, color: 'text-orange-600', bg: 'bg-orange-50' },
}
function TodoIcon({ type }: { type: string; level: string }) {
@@ -981,6 +982,12 @@ export default function Dashboard() {
</Link>
</div>
<div className="flex items-center gap-1">
<Link
to={todo.actionUrl}
className="px-2 py-1 rounded text-xs font-medium bg-primary/10 text-primary hover:bg-primary/20 transition-colors whitespace-nowrap"
>
</Link>
<button
onClick={() => resolveMutation.mutate(todo.id)}
disabled={resolveMutation.isPending}
+4
View File
@@ -377,6 +377,8 @@ function BatchManager() {
<th className="py-2 px-3"></th>
<th className="py-2 px-3 text-right"></th>
<th className="py-2 px-3 text-right"></th>
<th className="py-2 px-3 text-right"></th>
<th className="py-2 px-3 text-right"></th>
<th className="py-2 px-3 text-right"></th>
<th className="py-2 px-3 text-right"></th>
<th className="py-2 px-3"></th>
@@ -424,6 +426,8 @@ function BatchManager() {
</td>
<td className="py-2.5 px-3 text-right text-sm">{batch.employeeCount}</td>
<td className="py-2.5 px-3 text-right text-sm font-medium text-primary">¥{fmt(batch.totalPay)}</td>
<td className="py-2.5 px-3 text-right text-sm text-amber-600">¥{fmt((batch.totalSocialOrg || 0) + (batch.totalSocialEmp || 0))}</td>
<td className="py-2.5 px-3 text-right text-sm text-cyan-600">¥{fmt((batch.totalHousingOrg || 0) + (batch.totalHousingEmp || 0))}</td>
<td className="py-2.5 px-3 text-right text-sm text-danger">¥{fmt(batch.totalTax)}</td>
<td className="py-2.5 px-3 text-right text-sm font-bold text-safe">¥{fmt(batch.totalNetPay)}</td>
<td className="py-2.5 px-3">
+2 -2
View File
@@ -159,8 +159,8 @@ export default function SpecialStatus() {
const fetchEmployees = async () => {
try {
const res = await api.get('/employees', { params: { pageSize: 999 } }) as any
setEmployees(res.data.list || [])
const res = await api.get('/employees/all-lite') as any
setEmployees(res.data || [])
} catch {
// 忽略
}
+123 -7
View File
@@ -17,13 +17,105 @@ import Pagination from '../components/ui/Pagination'
const fmt = (n: number) => (n || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
const REASONS = [
{ value: 'NEGOTIATED', label: '协商解除(双方同意分开了)', legalBasis: '《劳动合同法》第36条' },
{ value: 'FAULT', label: '员工犯错被辞退(严重违纪/失职等)', legalBasis: '《劳动合同法》第39条' },
{ value: 'NONFAULT', label: '员工没犯错但干不了(生病/不胜任等)', legalBasis: '《劳动合同法》第40条' },
{ value: 'LAYOFF', label: '公司裁员(经营困难/技术调整等)', legalBasis: '《劳动合同法》第41条' },
{ value: 'EXPIRED', label: '合同到期不续签', legalBasis: '《劳动合同法》第44条、第46条' },
{ value: 'ILLEGAL', label: '违法解除(赔偿金×2', legalBasis: '《劳动合同法》第87条' },
{ value: 'RESIGNATION', label: '员工主动离职', legalBasis: '《劳动合同法》第37条' },
{
value: 'NEGOTIATED',
label: '协商解除(双方同意分开了)',
legalBasis: '《劳动合同法》第36条',
legalDetail: '用人单位与劳动者协商一致,可以解除劳动合同。需支付经济补偿(N),工作满1年支付1个月工资。',
steps: [
'第一步:与员工进行协商沟通,达成解除意向',
'第二步:签订《协商解除劳动合同协议书》,明确解除日期、补偿金额、支付方式',
'第三步:按协议约定支付经济补偿金',
'第四步:出具《解除劳动合同证明书》',
'第五步:办理工作交接、社保减员、档案转移',
],
},
{
value: 'FAULT',
label: '员工犯错被辞退(严重违纪/失职等)',
legalBasis: '《劳动合同法》第39条',
legalDetail: '劳动者有下列情形之一的,用人单位可以解除劳动合同:(1)在试用期间被证明不符合录用条件的;(2)严重违反用人单位的规章制度的;(3)严重失职,营私舞弊,给用人单位造成重大损害的;(4)同时与其他用人单位建立劳动关系,对完成本单位的工作任务造成严重影响,或经提出拒不改正的;(5)以欺诈、胁迫等手段致使劳动合同无效的;(6)被依法追究刑事责任的。无需支付经济补偿。',
steps: [
'第一步:收集并固化证据(违纪事实、制度依据、证人证言等)',
'第二步:确认规章制度经过民主程序制定并已向员工公示(签收记录)',
'第三步:将解除理由通知工会,工会提出意见后书面回复工会',
'第四步:向员工送达《解除劳动合同通知书》,注明解除依据和事实',
'第五步:办理工作交接、社保减员、档案转移',
'⚠ 注意:证据不足或制度未公示可能导致违法解除,建议咨询律师',
],
},
{
value: 'NONFAULT',
label: '员工没犯错但干不了(生病/不胜任等)',
legalBasis: '《劳动合同法》第40条',
legalDetail: '有下列情形之一的,用人单位提前三十日以书面形式通知劳动者本人或者额外支付一个月工资后,可以解除劳动合同:(1)劳动者患病或非因工负伤,在规定的医疗期满后不能从事原工作,也不能从事由用人单位另行安排的工作的;(2)劳动者不能胜任工作,经过培训或者调整工作岗位,仍不能胜任工作的。需支付经济补偿(N)+代通知金(1个月)。',
steps: [
'第一步(医疗期满):确认医疗期已满,安排劳动能力鉴定',
'第一步(不胜任):进行绩效考核,确认不胜任事实',
'第二步(医疗期满):另行安排合适岗位,员工仍不能胜任',
'第二步(不胜任):进行培训或调岗,再次考核仍不胜任',
'第三步:提前30天书面通知员工,或额外支付1个月代通知金',
'第四步:支付经济补偿金(N)',
'第五步:出具解除证明,办理交接、社保减员',
],
},
{
value: 'LAYOFF',
label: '公司裁员(经营困难/技术调整等)',
legalBasis: '《劳动合同法》第41条',
legalDetail: '有下列情形之一,需要裁减人员二十人以上或不足二十人但占企业职工总数百分之十以上的,用人单位提前三十日向工会或全体职工说明情况,听取意见后,向劳动行政部门报告,可以裁减人员:(1)依照企业破产法规定进行重整的;(2)生产经营发生严重困难的;(3)企业转产、重大技术革新或经营方式调整,经变更劳动合同仍需裁减人员的。需支付经济补偿(N)。',
steps: [
'第一步:确认符合法定裁员情形,准备相关证明材料',
'第二步:提前30天向工会或全体职工说明情况(书面会议记录)',
'第三步:听取工会或职工意见,形成意见处理方案',
'第四步:向当地劳动行政部门报告裁员方案',
'第五步:确定裁员名单(优先留用法定保护人员)',
'第六步:向员工送达解除通知,支付经济补偿金(N)',
'第七步:办理交接、社保减员、档案转移',
],
},
{
value: 'EXPIRED',
label: '合同到期不续签',
legalBasis: '《劳动合同法》第44条、第46条',
legalDetail: '劳动合同期满,劳动合同终止。除用人单位维持或提高劳动合同约定条件续订劳动合同,劳动者不同意续订的情形外,用人单位应当向劳动者支付经济补偿(N)。连续订立两次固定期限劳动合同后续订的,应当订立无固定期限劳动合同。',
steps: [
'第一步:确认合同到期日期,提前评估是否续签',
'第二步:如不续签,提前通知员工(建议提前30天)',
'第三步:确认是否属于法定应续签无固定期限合同的情形',
'第四步:支付经济补偿金(N),工作满1年支付1个月工资',
'第五步:出具《终止劳动合同证明书》',
'第六步:办理交接、社保减员、档案转移',
],
},
{
value: 'ILLEGAL',
label: '违法解除(赔偿金×2',
legalBasis: '《劳动合同法》第87条',
legalDetail: '用人单位违反本法规定解除或终止劳动合同的,应当依照本法第47条规定的经济补偿标准的二倍向劳动者支付赔偿金(2N)。',
steps: [
'第一步:确认违法解除事实(未走法定程序、证据不足等)',
'第二步:与员工协商赔偿金额,尽量达成一致',
'第三步:如员工要求继续履行合同,应恢复劳动关系',
'第四步:如无法恢复,支付2倍经济补偿作为赔偿金',
'第五步:办理交接、社保减员、档案转移',
'⚠ 建议:违法解除风险极高,操作前务必咨询专业律师',
],
},
{
value: 'RESIGNATION',
label: '员工主动离职',
legalBasis: '《劳动合同法》第37条',
legalDetail: '劳动者提前三十日以书面形式通知用人单位,可以解除劳动合同。劳动者在试用期内提前三日通知用人单位,可以解除劳动合同。无需支付经济补偿。',
steps: [
'第一步:接收员工书面辞职信(确认日期和签字)',
'第二步:确认提前30天通知(试用期3天)',
'第三步:安排工作交接计划',
'第四步:结算未发放工资、未休年假补偿等',
'第五步:出具《解除劳动合同证明书》',
'第六步:办理社保减员、档案转移',
],
},
]
const STEPS = ['选择员工', '解聘方式', '合规检查', '费用结算', '工作交接', '确认提交']
@@ -1087,6 +1179,30 @@ export default function Termination() {
)
})}
</div>
{/* 法律条款详情 + 操作步骤 */}
{reason && (() => {
const r = REASONS.find((x) => x.value === reason)
if (!r) return null
return (
<div className="border border-gray-200 rounded-md p-4 space-y-3 bg-gray-50/50">
<div>
<div className="text-xs font-medium text-gray-700 flex items-center gap-1.5">
<Shield className="w-3.5 h-3.5 text-primary" />
{r.legalBasis}
</div>
<p className="text-xs text-gray-600 mt-1 leading-relaxed">{r.legalDetail}</p>
</div>
<div className="border-t border-gray-200 pt-2">
<div className="text-xs font-medium text-gray-700 mb-1.5">📋 </div>
<ol className="space-y-1">
{r.steps.map((s, i) => (
<li key={i} className={`text-xs leading-relaxed ${s.includes('⚠') ? 'text-amber-600 font-medium' : 'text-gray-600'}`}>{s}</li>
))}
</ol>
</div>
</div>
)
})()}
<div>
<Label></Label>
<Input type="date" value={terminationDate} onChange={(e) => setTerminationDate(e.target.value)} />
+3 -3
View File
@@ -85,7 +85,7 @@ export interface DashboardData {
}
urgentRisk: {
id: string
type: 'CONTRACT' | 'SALARY' | 'TERMINATION' | 'MONTHLY' | 'ONBOARDING'
type: 'CONTRACT' | 'SALARY' | 'TERMINATION' | 'MONTHLY' | 'ONBOARDING' | 'RETIREMENT'
level: 'high' | 'medium' | 'low'
priority: 'URGENT' | 'HIGH' | 'MEDIUM' | 'LOW'
title: string
@@ -98,7 +98,7 @@ export interface DashboardData {
} | null
todos: {
id: string
type: 'CONTRACT' | 'SALARY' | 'TERMINATION' | 'MONTHLY' | 'ONBOARDING'
type: 'CONTRACT' | 'SALARY' | 'TERMINATION' | 'MONTHLY' | 'ONBOARDING' | 'RETIREMENT'
level: 'high' | 'medium' | 'low'
priority: 'URGENT' | 'HIGH' | 'MEDIUM' | 'LOW'
title: string
@@ -113,7 +113,7 @@ export interface DashboardData {
}[]
resolvedTodos: {
id: string
type: 'CONTRACT' | 'SALARY' | 'TERMINATION' | 'MONTHLY' | 'ONBOARDING'
type: 'CONTRACT' | 'SALARY' | 'TERMINATION' | 'MONTHLY' | 'ONBOARDING' | 'RETIREMENT'
level: 'high' | 'medium' | 'low'
title: string
description: string