feat: 社保公积金版本管理 + 员工基数调整 + 加班费计算优化 + UI组件改进
- SocialInsuranceConfig 版本化(effectiveFrom/effectiveTo/isCurrent/adjustmentDone) - 社保配置版本管理接口(列表/新建/按月获取/当前版本) - 员工基数调整:预览全部在职员工、上年月均工资计算建议基数、可编辑表格、确认后批量保存 - payroll.service 按批次月份匹配对应版本社保配置 - risk.service / seed.ts 同步更新 - 前端社保tab重构为版本管理+调整+试算 - 加班费计算三步流程、CSV导入、批次导入 - UI组件、Dashboard、合同、薪酬等页面优化
This commit is contained in:
@@ -15,9 +15,9 @@ export default function Button({ variant = 'primary', size = 'md', className, ch
|
||||
'bg-primary text-white hover:bg-primary-dark': variant === 'primary',
|
||||
'bg-gray-100 text-gray-700 hover:bg-gray-200': variant === 'secondary',
|
||||
'bg-danger text-white hover:bg-red-700': variant === 'danger',
|
||||
'px-3 py-1.5 text-sm': size === 'sm',
|
||||
'px-4 py-2 text-sm': size === 'md',
|
||||
'px-6 py-3 text-base': size === 'lg',
|
||||
'px-2.5 py-1 text-xs': size === 'sm',
|
||||
'px-3 py-1.5 text-xs': size === 'md',
|
||||
'px-5 py-2 text-sm': size === 'lg',
|
||||
},
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,7 @@ import clsx from 'clsx'
|
||||
|
||||
export default function Card({ className, children, ...props }: HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div className={clsx('bg-white rounded-lg shadow-sm border border-gray-200 p-4', className)} {...props}>
|
||||
<div className={clsx('bg-white rounded-lg shadow-sm border border-gray-200 p-3', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -12,12 +12,12 @@ interface EmptyStateProps {
|
||||
|
||||
export default function EmptyState({ icon, title, description, actionLabel, onAction }: EmptyStateProps) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-center">
|
||||
<div className="text-gray-300 mb-4">
|
||||
{icon || <Inbox className="w-12 h-12" />}
|
||||
<div className="flex flex-col items-center justify-center py-10 text-center">
|
||||
<div className="text-gray-300 mb-3">
|
||||
{icon || <Inbox className="w-10 h-10" />}
|
||||
</div>
|
||||
<h3 className="text-base font-medium text-gray-900 mb-1">{title}</h3>
|
||||
{description && <p className="text-sm text-gray-500 mb-4">{description}</p>}
|
||||
<h3 className="text-sm font-medium text-gray-900 mb-1">{title}</h3>
|
||||
{description && <p className="text-xs text-gray-500 mb-3">{description}</p>}
|
||||
{actionLabel && onAction && (
|
||||
<Button onClick={onAction}>{actionLabel}</Button>
|
||||
)}
|
||||
|
||||
@@ -7,7 +7,7 @@ export const Input = forwardRef<HTMLInputElement, InputHTMLAttributes<HTMLInputE
|
||||
<input
|
||||
ref={ref}
|
||||
className={clsx(
|
||||
'w-full px-3 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-sm',
|
||||
'w-full px-2.5 py-1.5 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-xs',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -22,7 +22,7 @@ export const Select = forwardRef<HTMLSelectElement, SelectHTMLAttributes<HTMLSel
|
||||
<select
|
||||
ref={ref}
|
||||
className={clsx(
|
||||
'w-full px-3 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-sm bg-white',
|
||||
'w-full px-2.5 py-1.5 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-xs bg-white',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -34,5 +34,5 @@ export const Select = forwardRef<HTMLSelectElement, SelectHTMLAttributes<HTMLSel
|
||||
)
|
||||
|
||||
export function Label({ children, className }: { children: React.ReactNode; className?: string }) {
|
||||
return <label className={clsx('block text-sm font-medium text-gray-700 mb-1', className)}>{children}</label>
|
||||
return <label className={clsx('block text-xs font-medium text-gray-700 mb-0.5', className)}>{children}</label>
|
||||
}
|
||||
|
||||
@@ -29,14 +29,14 @@ export default function Modal({ open, onClose, title, children, className }: Mod
|
||||
<div className="fixed inset-0 bg-black/40" onClick={onClose} />
|
||||
<div className={clsx('relative bg-white rounded-lg shadow-xl w-full max-w-lg max-h-[90vh] overflow-y-auto', className)}>
|
||||
{title && (
|
||||
<div className="flex items-center justify-between px-5 py-3 border-b border-gray-200">
|
||||
<h3 className="font-medium text-gray-900">{title}</h3>
|
||||
<div className="flex items-center justify-between px-4 py-2.5 border-b border-gray-200">
|
||||
<h3 className="font-medium text-gray-900 text-sm">{title}</h3>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
|
||||
<X className="w-5 h-5" />
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-5">{children}</div>
|
||||
<div className="p-4">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
+10
-4
@@ -6,16 +6,22 @@
|
||||
body {
|
||||
@apply bg-surface text-gray-900 antialiased;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
* {
|
||||
@apply box-border;
|
||||
}
|
||||
|
||||
h1 { @apply text-base font-semibold; }
|
||||
h2 { @apply text-sm font-semibold; }
|
||||
h3 { @apply text-sm font-medium; }
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.btn {
|
||||
@apply inline-flex items-center justify-center px-4 py-2 rounded-md font-medium text-sm transition-colors disabled:opacity-50 disabled:cursor-not-allowed;
|
||||
@apply inline-flex items-center justify-center px-3 py-1.5 rounded font-medium text-xs transition-colors disabled:opacity-50 disabled:cursor-not-allowed;
|
||||
}
|
||||
.btn-primary {
|
||||
@apply btn bg-primary text-white hover:bg-primary-dark;
|
||||
@@ -27,12 +33,12 @@
|
||||
@apply btn bg-danger text-white hover:bg-red-700;
|
||||
}
|
||||
.card {
|
||||
@apply bg-white rounded-lg shadow-sm border border-gray-200 p-4;
|
||||
@apply bg-white rounded-lg shadow-sm border border-gray-200 p-3;
|
||||
}
|
||||
.input {
|
||||
@apply w-full px-3 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-sm;
|
||||
@apply w-full px-2.5 py-1.5 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-xs;
|
||||
}
|
||||
.label {
|
||||
@apply block text-sm font-medium text-gray-700 mb-1;
|
||||
@apply block text-xs font-medium text-gray-700 mb-0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function AIAssistant() {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-lg font-semibold">AI 合规顾问</h1>
|
||||
<h1 className="text-xs font-medium">AI 合规顾问</h1>
|
||||
|
||||
<div className="flex gap-1 border-b overflow-x-auto">
|
||||
{tabs.map((t) => {
|
||||
@@ -40,7 +40,7 @@ export default function AIAssistant() {
|
||||
<button
|
||||
key={t.key}
|
||||
onClick={() => setTab(t.key)}
|
||||
className={`flex items-center gap-1.5 px-4 py-2 text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${
|
||||
className={`flex items-center gap-1.5 px-4 py-2 text-xs font-medium border-b-2 transition-colors whitespace-nowrap ${
|
||||
tab === t.key ? 'border-primary text-primary' : 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
@@ -95,7 +95,7 @@ function ChatTab() {
|
||||
<div ref={scrollRef} className="flex-1 overflow-y-auto space-y-4 pb-4">
|
||||
{messages.map((msg, i) => (
|
||||
<div key={i} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
||||
<div className={`max-w-[80%] px-4 py-3 rounded-lg text-sm whitespace-pre-wrap ${
|
||||
<div className={`max-w-[80%] px-4 py-3 rounded-lg text-xs whitespace-pre-wrap ${
|
||||
msg.role === 'user' ? 'bg-primary text-white' : 'bg-gray-100 text-gray-800'
|
||||
}`}>
|
||||
{msg.content || (loading && i === messages.length - 1 ? '思考中...' : '')}
|
||||
@@ -111,7 +111,7 @@ function ChatTab() {
|
||||
<button
|
||||
key={q}
|
||||
onClick={() => send(q)}
|
||||
className="px-3 py-1.5 text-sm rounded-full border border-gray-300 text-gray-600 hover:bg-gray-50"
|
||||
className="px-3 py-1.5 text-xs rounded-full border border-gray-300 text-gray-600 hover:bg-gray-50"
|
||||
>
|
||||
{q}
|
||||
</button>
|
||||
@@ -167,7 +167,7 @@ function PredictTab() {
|
||||
<Loader2 className="w-5 h-5 animate-spin" /> 分析中...
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-gray-700 whitespace-pre-wrap">{result}</div>
|
||||
<div className="text-xs text-gray-700 whitespace-pre-wrap">{result}</div>
|
||||
)}
|
||||
<div className="mt-4">
|
||||
<Button variant="secondary" size="sm" onClick={fetchPrediction} disabled={loading}>刷新预测</Button>
|
||||
@@ -204,7 +204,7 @@ function ReviewTab() {
|
||||
</div>
|
||||
<Label>粘贴合同条款文本</Label>
|
||||
<textarea
|
||||
className="w-full px-3 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-sm min-h-[200px] resize-y"
|
||||
className="w-full px-3 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-xs min-h-[200px] resize-y"
|
||||
placeholder="粘贴劳动合同文本..."
|
||||
value={contractText}
|
||||
onChange={(e) => setContractText(e.target.value)}
|
||||
@@ -219,7 +219,7 @@ function ReviewTab() {
|
||||
{result && (
|
||||
<Card>
|
||||
<h3 className="font-medium mb-3">审查结果</h3>
|
||||
<div className="text-sm text-gray-700 whitespace-pre-wrap">{result}</div>
|
||||
<div className="text-xs text-gray-700 whitespace-pre-wrap">{result}</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
@@ -254,7 +254,7 @@ function CaseTab() {
|
||||
</div>
|
||||
<Label>描述你的争议情形</Label>
|
||||
<textarea
|
||||
className="w-full px-3 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-sm min-h-[150px] resize-y"
|
||||
className="w-full px-3 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-xs min-h-[150px] resize-y"
|
||||
placeholder="例如:员工入职3个月没签合同,现在要辞退他..."
|
||||
value={scenario}
|
||||
onChange={(e) => setScenario(e.target.value)}
|
||||
@@ -269,7 +269,7 @@ function CaseTab() {
|
||||
{result && (
|
||||
<Card>
|
||||
<h3 className="font-medium mb-3">分析结果</h3>
|
||||
<div className="text-sm text-gray-700 whitespace-pre-wrap">{result}</div>
|
||||
<div className="text-xs text-gray-700 whitespace-pre-wrap">{result}</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -61,15 +61,15 @@ export default function Compensation() {
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-lg font-semibold">补偿计算</h1>
|
||||
<div className="space-y-3">
|
||||
<h1 className="text-xs font-medium">补偿计算</h1>
|
||||
|
||||
<div className="flex gap-1 border-b">
|
||||
{tabs.map((t) => (
|
||||
<button
|
||||
key={t.key}
|
||||
onClick={() => setTab(t.key)}
|
||||
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
|
||||
className={`px-3 py-1.5 text-xs font-medium border-b-2 transition-colors ${
|
||||
tab === t.key ? 'border-primary text-primary' : 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
@@ -150,7 +150,7 @@ function SeveranceCalculator() {
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<Card>
|
||||
<h2 className="font-medium mb-4">填写信息</h2>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<EmployeeSelector employees={employees} selectedId={selectedEmpId} onSelect={handleSelectEmp} />
|
||||
<div>
|
||||
<Label>入职日期</Label>
|
||||
@@ -189,19 +189,19 @@ function SeveranceCalculator() {
|
||||
<h2 className="font-medium mb-4 flex items-center gap-2"><Calculator className="w-5 h-5" />计算结果</h2>
|
||||
{result ? (
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm text-gray-500">离职原因:<span className="text-gray-900">{result.reason}</span></div>
|
||||
<div className="text-sm text-gray-500">工作年限:<span className="text-gray-900">{result.years}年{result.remainingMonths}个月</span></div>
|
||||
<div className="text-xs text-gray-500">离职原因:<span className="text-gray-900">{result.reason}</span></div>
|
||||
<div className="text-xs text-gray-500">工作年限:<span className="text-gray-900">{result.years}年{result.remainingMonths}个月</span></div>
|
||||
{result.noComp ? (
|
||||
<div className="px-3 py-2 rounded-md bg-gray-50 text-gray-700 text-sm">
|
||||
<div className="px-3 py-2 rounded-md bg-gray-50 text-gray-700 text-xs">
|
||||
{result.reasonNote}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-sm text-gray-500">补偿月数:<span className="text-gray-900">{result.compMonths}个月</span></div>
|
||||
<div className="text-xs text-gray-500">补偿月数:<span className="text-gray-900">{result.compMonths}个月</span></div>
|
||||
{result.capped && (
|
||||
<div className="text-sm text-warning">⚠️ 工资超过社平3倍,已按三倍封顶且最多补偿12个月</div>
|
||||
<div className="text-xs text-warning">⚠️ 工资超过社平3倍,已按三倍封顶且最多补偿12个月</div>
|
||||
)}
|
||||
<div className="text-sm text-gray-500">计算基数:<span className="text-gray-900">¥{fmt(result.wage)}/月</span></div>
|
||||
<div className="text-xs text-gray-500">计算基数:<span className="text-gray-900">¥{fmt(result.wage)}/月</span></div>
|
||||
<div className="border-t pt-3 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">{result.isIllegal ? '经济补偿金' : '应付金额'}</span>
|
||||
@@ -211,7 +211,7 @@ function SeveranceCalculator() {
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium text-danger">违法解除赔偿金(×2)</span>
|
||||
<span className="text-xl font-bold text-danger">¥{fmt(result.totalPay)}</span>
|
||||
<span className="text-lg font-bold text-danger">¥{fmt(result.totalPay)}</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">({result.compMonths}个月 × ¥{fmt(result.wage)} × 2)</div>
|
||||
</>
|
||||
@@ -219,7 +219,7 @@ function SeveranceCalculator() {
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">{result.reason}</span>
|
||||
<span className="text-xl font-bold text-primary">¥{fmt(result.totalPay)}</span>
|
||||
<span className="text-lg font-bold text-primary">¥{fmt(result.totalPay)}</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">({result.compMonths}个月 × ¥{fmt(result.wage)})
|
||||
{result.noticePay > 0 && <span className="block">含代通知金 ¥{fmt(result.noticePay)}</span>}
|
||||
@@ -228,12 +228,12 @@ function SeveranceCalculator() {
|
||||
)}
|
||||
</div>
|
||||
{result.reasonNote && (
|
||||
<div className={`flex items-start gap-2 px-3 py-2 rounded-md text-sm ${result.isIllegal ? 'bg-red-50 text-red-700' : 'bg-blue-50 text-blue-700'}`}>
|
||||
<div className={`flex items-start gap-2 px-3 py-2 rounded-md text-xs ${result.isIllegal ? 'bg-red-50 text-red-700' : 'bg-blue-50 text-blue-700'}`}>
|
||||
<Info className="w-4 h-4 mt-0.5 shrink-0" />
|
||||
<span>{result.reasonNote}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-start gap-2 px-3 py-2 rounded-md bg-blue-50 text-blue-700 text-sm">
|
||||
<div className="flex items-start gap-2 px-3 py-2 rounded-md bg-blue-50 text-blue-700 text-xs">
|
||||
<Info className="w-4 h-4 mt-0.5 shrink-0" />
|
||||
<span>满1年补1个月,满6个月不满1年按1年算,不满6个月补半个月</span>
|
||||
</div>
|
||||
@@ -241,7 +241,7 @@ function SeveranceCalculator() {
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-gray-400 text-sm">填写信息后点击「计算」按钮</div>
|
||||
<div className="text-gray-400 text-xs">填写信息后点击「计算」按钮</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
@@ -303,7 +303,7 @@ function DoubleSalaryCalculator() {
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<Card>
|
||||
<h2 className="font-medium mb-4">填写信息</h2>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<EmployeeSelector employees={employees} selectedId={selectedEmpId} onSelect={handleSelectEmp} />
|
||||
<div>
|
||||
<Label>月工资(元)</Label>
|
||||
@@ -333,24 +333,24 @@ function DoubleSalaryCalculator() {
|
||||
<h2 className="font-medium mb-4 flex items-center gap-2"><AlertCircle className="w-5 h-5 text-warning" />计算结果</h2>
|
||||
{result ? (
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm text-gray-500">入职日期:<span className="text-gray-900">{hireDate}</span></div>
|
||||
<div className="text-sm text-gray-500">合同签订:<span className="text-gray-900">{hasContract ? contractDate || '未填写' : '未签订'}</span></div>
|
||||
<div className="text-sm text-gray-500">双倍工资起算:<span className="text-gray-900">{result.startDate.toISOString().slice(0, 10)}</span></div>
|
||||
<div className="text-sm text-gray-500">双倍工资截止:<span className="text-gray-900">{result.endDate.toISOString().slice(0, 10)}</span></div>
|
||||
<div className="text-xs text-gray-500">入职日期:<span className="text-gray-900">{hireDate}</span></div>
|
||||
<div className="text-xs text-gray-500">合同签订:<span className="text-gray-900">{hasContract ? contractDate || '未填写' : '未签订'}</span></div>
|
||||
<div className="text-xs text-gray-500">双倍工资起算:<span className="text-gray-900">{result.startDate.toISOString().slice(0, 10)}</span></div>
|
||||
<div className="text-xs text-gray-500">双倍工资截止:<span className="text-gray-900">{result.endDate.toISOString().slice(0, 10)}</span></div>
|
||||
<div className="border-t pt-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">需赔</span>
|
||||
<span className="text-xl font-bold text-danger">¥{fmt(result.totalPay)}</span>
|
||||
<span className="text-lg font-bold text-danger">¥{fmt(result.totalPay)}</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mt-1">({result.months}个月 × ¥{fmt(monthlyWage)})</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-2 px-3 py-2 rounded-md bg-blue-50 text-blue-700 text-sm">
|
||||
<div className="flex items-start gap-2 px-3 py-2 rounded-md bg-blue-50 text-blue-700 text-xs">
|
||||
<Info className="w-4 h-4 mt-0.5 shrink-0" />
|
||||
<span>法律规定:入职1个月没签合同,从第2个月起要付双倍工资,最多11个月</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-gray-400 text-sm">请填写入职日期</div>
|
||||
<div className="text-gray-400 text-xs">请填写入职日期</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -91,7 +91,7 @@ export default function Contracts() {
|
||||
) : (
|
||||
<>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="border-b text-left text-gray-500">
|
||||
<th className="py-2 px-3 font-medium">姓名</th>
|
||||
@@ -108,10 +108,19 @@ export default function Contracts() {
|
||||
<td className="py-3 px-3 text-gray-600">{emp.department}</td>
|
||||
<td className="py-3 px-3 text-gray-600">{emp.hireDate}</td>
|
||||
<td className="py-3 px-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Signal level={emp.riskLevel} />
|
||||
<span>{emp.contractStatusText}</span>
|
||||
</div>
|
||||
{(() => {
|
||||
const tagStyles: Record<string, string> = {
|
||||
expired: 'bg-red-50 text-danger',
|
||||
unsigned_over_year: 'bg-red-50 text-danger',
|
||||
unsigned_over_30: 'bg-red-50 text-danger',
|
||||
unsigned: 'bg-yellow-50 text-yellow-700',
|
||||
expiring: 'bg-yellow-50 text-yellow-700',
|
||||
active: 'bg-green-50 text-safe',
|
||||
unfixed: 'bg-blue-50 text-blue-700',
|
||||
}
|
||||
const style = tagStyles[emp.contractStatus] || 'bg-gray-100 text-gray-600'
|
||||
return <span className={`px-2 py-0.5 rounded text-xs ${style}`}>{emp.contractStatusText}</span>
|
||||
})()}
|
||||
</td>
|
||||
<td className="py-3 px-3">
|
||||
<div className="flex gap-1">
|
||||
@@ -135,7 +144,7 @@ export default function Contracts() {
|
||||
disabled={page === 1}
|
||||
onClick={() => setPage(p => p - 1)}
|
||||
>上一页</Button>
|
||||
<span className="text-sm text-gray-500">{page} / {data.totalPages}</span>
|
||||
<span className="text-xs text-gray-500">{page} / {data.totalPages}</span>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
@@ -221,7 +230,7 @@ function AddEmployeeModal({ open, onClose, onSubmit, loading, error }: {
|
||||
<Modal open={open} onClose={onClose} title="添加员工">
|
||||
<div className="space-y-4">
|
||||
{error && (
|
||||
<div className="px-3 py-2 rounded-md bg-red-50 text-red-700 text-sm">
|
||||
<div className="px-3 py-2 rounded-md bg-red-50 text-red-700 text-xs">
|
||||
{error.response?.data?.error?.message || '操作失败'}
|
||||
</div>
|
||||
)}
|
||||
@@ -264,15 +273,15 @@ function AddEmployeeModal({ open, onClose, onSubmit, loading, error }: {
|
||||
|
||||
{/* 特殊状态 */}
|
||||
<div className="flex gap-4">
|
||||
<label className="flex items-center gap-1.5 text-sm">
|
||||
<label className="flex items-center gap-1.5 text-xs">
|
||||
<input type="checkbox" checked={form.isPregnant} onChange={(e) => setForm({ ...form, isPregnant: e.target.checked })} />
|
||||
孕期/哺乳期
|
||||
</label>
|
||||
<label className="flex items-center gap-1.5 text-sm">
|
||||
<label className="flex items-center gap-1.5 text-xs">
|
||||
<input type="checkbox" checked={form.isInMedicalPeriod} onChange={(e) => setForm({ ...form, isInMedicalPeriod: e.target.checked })} />
|
||||
医疗期
|
||||
</label>
|
||||
<label className="flex items-center gap-1.5 text-sm">
|
||||
<label className="flex items-center gap-1.5 text-xs">
|
||||
<input type="checkbox" checked={form.isWorkInjured} onChange={(e) => setForm({ ...form, isWorkInjured: e.target.checked })} />
|
||||
工伤
|
||||
</label>
|
||||
@@ -392,7 +401,7 @@ function EmployeeDetailDrawer({ employeeId, onClose }: { employeeId: string; onC
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex justify-end">
|
||||
<div className="fixed inset-0 bg-black/40" onClick={onClose} />
|
||||
<div className="relative w-full max-w-md bg-white h-full overflow-y-auto shadow-xl">
|
||||
<div className="relative w-full max-w-2xl bg-white h-full overflow-y-auto shadow-xl">
|
||||
<div className="flex items-center justify-between px-5 py-3 border-b border-gray-200 sticky top-0 bg-white z-10">
|
||||
<h3 className="font-medium text-gray-900">员工详情</h3>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
|
||||
@@ -406,9 +415,9 @@ function EmployeeDetailDrawer({ employeeId, onClose }: { employeeId: string; onC
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-lg font-semibold">{emp.name}</h2>
|
||||
<span className="text-sm text-gray-500">{emp.department}</span>
|
||||
<span className="text-xs text-gray-500">{emp.department}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<div><span className="text-gray-400">入职日期:</span>{emp.hireDate?.slice(0, 10)}</div>
|
||||
<div><span className="text-gray-400">性别:</span>{emp.gender || '-'}</div>
|
||||
<div><span className="text-gray-400">手机:</span>{emp.phone || '-'}</div>
|
||||
@@ -425,13 +434,16 @@ function EmployeeDetailDrawer({ employeeId, onClose }: { employeeId: string; onC
|
||||
|
||||
{emp.contracts && emp.contracts.length > 0 && (
|
||||
<div className="border-t pt-3">
|
||||
<h3 className="font-medium text-sm mb-2">合同信息</h3>
|
||||
<div className="space-y-2 text-sm">
|
||||
<h3 className="font-medium text-xs mb-2">合同信息</h3>
|
||||
<div className="space-y-2 text-xs">
|
||||
{emp.contracts.map((c: any) => (
|
||||
<div key={c.id} className="bg-gray-50 rounded p-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Signal level={c.riskLevel || 'safe'} />
|
||||
<span>{c.contractType === 'FIXED' ? '固定期限' : c.contractType === 'UNFIXED' ? '无固定期限' : '未签'}</span>
|
||||
{(() => {
|
||||
const typeLabel = c.contractType === 'FIXED' ? '固定期限' : c.contractType === 'UNFIXED' ? '无固定期限' : '未签'
|
||||
const typeStyle = c.contractType === 'UNSIGNED' ? 'bg-red-50 text-danger' : 'bg-blue-50 text-blue-700'
|
||||
return <span className={`px-2 py-0.5 rounded text-xs ${typeStyle}`}>{typeLabel}</span>
|
||||
})()}
|
||||
</div>
|
||||
<div className="text-gray-500 text-xs mt-1">
|
||||
{c.startDate?.slice(0, 10)} ~ {c.endDate?.slice(0, 10) || '无固定期限'}
|
||||
@@ -446,13 +458,13 @@ function EmployeeDetailDrawer({ employeeId, onClose }: { employeeId: string; onC
|
||||
|
||||
<div className="border-t pt-3">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="font-medium text-sm flex items-center gap-1">
|
||||
<h3 className="font-medium text-xs flex items-center gap-1">
|
||||
<Paperclip className="w-4 h-4" /> 附件管理
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 mb-3">
|
||||
<Select value={fileType} onChange={(e) => setFileType(e.target.value as any)} className="text-sm">
|
||||
<Select value={fileType} onChange={(e) => setFileType(e.target.value as any)} className="text-xs">
|
||||
<option value="ID_CARD">身份证</option>
|
||||
<option value="BANK_CARD">银行卡</option>
|
||||
<option value="CONTRACT_SCAN">合同扫描件</option>
|
||||
@@ -478,7 +490,7 @@ function EmployeeDetailDrawer({ employeeId, onClose }: { employeeId: string; onC
|
||||
{attachments && attachments.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{attachments.map((att: any) => (
|
||||
<div key={att.id} className="flex items-center justify-between bg-gray-50 rounded p-2 text-sm">
|
||||
<div key={att.id} className="flex items-center justify-between bg-gray-50 rounded p-2 text-xs">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Paperclip className="w-4 h-4 text-gray-400 shrink-0" />
|
||||
<div className="min-w-0">
|
||||
@@ -498,7 +510,7 @@ function EmployeeDetailDrawer({ employeeId, onClose }: { employeeId: string; onC
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-gray-400 text-sm text-center py-4">暂无附件</div>
|
||||
<div className="text-gray-400 text-xs text-center py-4">暂无附件</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -103,11 +103,11 @@ export default function Dashboard() {
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold">{data.greeting}</h1>
|
||||
<p className="text-sm text-gray-500 mt-0.5">{payroll?.month} 月度总览</p>
|
||||
<h1 className="text-xs font-medium">{data.greeting}</h1>
|
||||
<p className="text-xs text-gray-500 mt-0.5">{payroll?.month} 月度总览</p>
|
||||
</div>
|
||||
<Button variant="secondary" size="sm" onClick={() => refetch()} disabled={isFetching}>
|
||||
<RefreshCw className={`w-4 h-4 mr-1 ${isFetching ? 'animate-spin' : ''}`} />
|
||||
@@ -123,7 +123,7 @@ export default function Dashboard() {
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
className={`flex items-center gap-1.5 px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium border-b-2 transition-colors ${
|
||||
activeTab === tab.key ? 'border-primary text-primary' : 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
@@ -141,16 +141,16 @@ export default function Dashboard() {
|
||||
|
||||
{/* 概览 Tab */}
|
||||
{activeTab === 'overview' && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||
{stats.map((stat) => {
|
||||
const Icon = stat.icon
|
||||
return (
|
||||
<Card key={stat.label} className="flex items-center gap-3">
|
||||
<Icon className={`w-8 h-8 ${stat.color}`} />
|
||||
<Card key={stat.label} className="flex items-center gap-2.5">
|
||||
<Icon className={`w-6 h-6 ${stat.color}`} />
|
||||
<div>
|
||||
<div className="text-xl font-bold">{stat.value}</div>
|
||||
<div className="text-base font-bold">{stat.value}</div>
|
||||
<div className="text-xs text-gray-500">{stat.label}</div>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -160,17 +160,17 @@ export default function Dashboard() {
|
||||
|
||||
{/* 本月工作动态 */}
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="font-semibold flex items-center gap-2"><Briefcase className="w-5 h-5" />本月工作动态</h2>
|
||||
<span className="text-sm text-gray-400">{activities?.month}</span>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="font-medium flex items-center gap-1.5"><Briefcase className="w-4 h-4" />本月工作动态</h2>
|
||||
<span className="text-xs text-gray-400">{activities?.month}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-2">
|
||||
{activityItems.map((item) => {
|
||||
const Icon = item.icon
|
||||
return (
|
||||
<div key={item.label} className="flex flex-col items-center p-3 rounded-lg bg-gray-50">
|
||||
<Icon className={`w-5 h-5 mb-1 ${item.color}`} />
|
||||
<div className="text-lg font-bold">{item.value}</div>
|
||||
<div key={item.label} className="flex flex-col items-center p-2 rounded-lg bg-gray-50">
|
||||
<Icon className={`w-4 h-4 mb-1 ${item.color}`} />
|
||||
<div className="text-xs font-bold">{item.value}</div>
|
||||
<div className="text-xs text-gray-500">{item.label}</div>
|
||||
</div>
|
||||
)
|
||||
@@ -180,18 +180,18 @@ export default function Dashboard() {
|
||||
|
||||
{/* 风险分布 */}
|
||||
<Card>
|
||||
<h2 className="font-semibold mb-4">风险分布</h2>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<h2 className="font-medium mb-3">风险分布</h2>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-primary">{data.riskDistribution.contract}</div>
|
||||
<div className="text-lg font-bold text-primary">{data.riskDistribution.contract}</div>
|
||||
<div className="text-xs text-gray-500 mt-1">合同风险</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-warning">{data.riskDistribution.salary}</div>
|
||||
<div className="text-lg font-bold text-warning">{data.riskDistribution.salary}</div>
|
||||
<div className="text-xs text-gray-500 mt-1">薪资风险</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-danger">{data.riskDistribution.termination}</div>
|
||||
<div className="text-lg font-bold text-danger">{data.riskDistribution.termination}</div>
|
||||
<div className="text-xs text-gray-500 mt-1">解聘风险</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -202,18 +202,18 @@ export default function Dashboard() {
|
||||
{/* 薪税 Tab */}
|
||||
{activeTab === 'payroll' && (
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="font-semibold flex items-center gap-2"><Calculator className="w-5 h-5" />本月薪税费用总览</h2>
|
||||
<Link to="/money" className="text-sm text-primary hover:underline flex items-center gap-1">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="font-medium flex items-center gap-1.5"><Calculator className="w-4 h-4" />本月薪税费用总览</h2>
|
||||
<Link to="/money" className="text-xs text-primary hover:underline flex items-center gap-1">
|
||||
查看明细 <ArrowRight className="w-3 h-3" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{payroll && payroll.payslipCount > 0 ? (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
{/* 工资构成 */}
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-600 mb-2">工资构成</div>
|
||||
<div className="text-xs font-medium text-gray-600 mb-1.5">工资构成</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||
{payrollItems.map((item) => {
|
||||
const Icon = item.icon
|
||||
@@ -223,7 +223,7 @@ export default function Dashboard() {
|
||||
<Icon className={`w-4 h-4 ${item.color}`} />
|
||||
<span className="text-xs text-gray-500">{item.label}</span>
|
||||
</div>
|
||||
<span className={`text-sm font-medium ${item.value < 0 ? 'text-danger' : ''}`}>{fmt(item.value)}</span>
|
||||
<span className={`text-xs font-medium ${item.value < 0 ? 'text-danger' : ''}`}>{fmt(item.value)}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
@@ -233,17 +233,17 @@ export default function Dashboard() {
|
||||
{/* 应发合计 */}
|
||||
<div className="flex items-center justify-between border-t border-b py-2">
|
||||
<span className="font-medium">应发合计</span>
|
||||
<span className="text-lg font-bold text-primary">{fmt(payroll.totalPay)}</span>
|
||||
<span className="text-base font-bold text-primary">{fmt(payroll.totalPay)}</span>
|
||||
</div>
|
||||
|
||||
{/* 扣减项 */}
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-600 mb-2">扣减项</div>
|
||||
<div className="text-xs font-medium text-gray-600 mb-1.5">扣减项</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{deductionItems.map((item) => (
|
||||
<div key={item.label} className="flex items-center justify-between p-2 rounded-md bg-red-50">
|
||||
<span className="text-xs text-gray-500">{item.label}</span>
|
||||
<span className="text-sm font-medium text-danger">{fmt(item.value)}</span>
|
||||
<span className="text-xs font-medium text-danger">{fmt(item.value)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -252,40 +252,40 @@ export default function Dashboard() {
|
||||
{/* 员工实发 */}
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<span className="font-medium flex items-center gap-2"><Wallet className="w-4 h-4 text-safe" />员工实发工资</span>
|
||||
<span className="text-lg font-bold text-safe">{fmt(payroll.empNetPay)}</span>
|
||||
<span className="text-base font-bold text-safe">{fmt(payroll.empNetPay)}</span>
|
||||
</div>
|
||||
|
||||
{/* 企业成本 */}
|
||||
<div className="border-t pt-3 space-y-2">
|
||||
<div className="text-sm font-medium text-gray-600 mb-1">企业用工成本</div>
|
||||
<div className="border-t pt-2 space-y-2">
|
||||
<div className="text-xs font-medium text-gray-600 mb-1">企业用工成本</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div className="flex items-center justify-between p-2 rounded-md bg-blue-50">
|
||||
<span className="text-xs text-gray-500 flex items-center gap-1"><Building2 className="w-3 h-3" />企业社保</span>
|
||||
<span className="text-sm font-medium text-blue-700">{fmt(payroll.socialOrg)}</span>
|
||||
<span className="text-xs font-medium text-blue-700">{fmt(payroll.socialOrg)}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-2 rounded-md bg-purple-50">
|
||||
<span className="text-xs text-gray-500 flex items-center gap-1"><Building2 className="w-3 h-3" />企业公积金</span>
|
||||
<span className="text-sm font-medium text-purple-700">{fmt(payroll.housingOrg)}</span>
|
||||
<span className="text-xs font-medium text-purple-700">{fmt(payroll.housingOrg)}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-2 rounded-md bg-green-50">
|
||||
<span className="text-xs text-gray-500 flex items-center gap-1"><Receipt className="w-3 h-3" />工资总额</span>
|
||||
<span className="text-sm font-medium text-green-700">{fmt(payroll.totalPay)}</span>
|
||||
<span className="text-xs font-medium text-green-700">{fmt(payroll.totalPay)}</span>
|
||||
</div>
|
||||
</div>
|
||||
{payroll.severancePay > 0 && (
|
||||
<div className="flex items-center justify-between p-2 rounded-md bg-orange-50">
|
||||
<span className="text-xs text-gray-500 flex items-center gap-1"><DollarSign className="w-3 h-3" />经济补偿金</span>
|
||||
<span className="text-sm font-medium text-orange-700">{fmt(payroll.severancePay)}</span>
|
||||
<span className="text-xs font-medium text-orange-700">{fmt(payroll.severancePay)}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<span className="font-medium flex items-center gap-2"><DollarSign className="w-4 h-4 text-danger" />企业总成本</span>
|
||||
<span className="text-lg font-bold text-danger">{fmt(payroll.orgTotalCost)}</span>
|
||||
<span className="text-base font-bold text-danger">{fmt(payroll.orgTotalCost)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 工资条确认状态 */}
|
||||
<div className="flex items-center gap-4 text-sm border-t pt-3">
|
||||
<div className="flex items-center gap-3 text-xs border-t pt-2">
|
||||
<span className="text-gray-500">工资条确认:</span>
|
||||
<span className="text-safe">已确认 {payroll.confirmedPayslips}</span>
|
||||
<span className="text-warning">未确认 {payroll.unconfirmedPayslips}</span>
|
||||
@@ -300,12 +300,12 @@ export default function Dashboard() {
|
||||
|
||||
{/* 风险提醒 Tab */}
|
||||
{(activeTab === 'risk' || activeTab === 'task') && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
{/* 待办列表 */}
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="font-semibold">{activeTab === 'risk' ? '风险提醒' : '月度任务'}</h2>
|
||||
<span className="text-sm text-gray-400">{filteredTodos.length} 项</span>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="font-medium">{activeTab === 'risk' ? '风险提醒' : '月度任务'}</h2>
|
||||
<span className="text-xs text-gray-400">{filteredTodos.length} 项</span>
|
||||
</div>
|
||||
|
||||
{filteredTodos.length === 0 ? (
|
||||
@@ -317,12 +317,12 @@ export default function Dashboard() {
|
||||
{filteredTodos.slice((todoPage - 1) * todoPageSize, todoPage * todoPageSize).map((todo) => (
|
||||
<div
|
||||
key={todo.id}
|
||||
className="flex items-center justify-between px-3 py-3 rounded-md hover:bg-gray-50 transition-colors"
|
||||
className="flex items-center justify-between px-2.5 py-2 rounded-md hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<Link to={todo.actionUrl} className="flex items-center gap-3 flex-1">
|
||||
<Link to={todo.actionUrl} className="flex items-center gap-2.5 flex-1">
|
||||
<TodoIcon type={todo.type} level={todo.level} />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm text-gray-800">{todo.title}</span>
|
||||
<span className="text-xs text-gray-800">{todo.title}</span>
|
||||
<span className="text-xs text-gray-400 flex items-center gap-1"><Clock className="w-3 h-3" />{todo.description}</span>
|
||||
</div>
|
||||
</Link>
|
||||
@@ -354,20 +354,20 @@ export default function Dashboard() {
|
||||
{/* 已办事项 */}
|
||||
{data.resolvedTodos && data.resolvedTodos.length > 0 && (
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="font-semibold flex items-center gap-2"><CheckSquare className="w-5 h-5 text-safe" />已办事项</h2>
|
||||
<span className="text-sm text-gray-400">{data.resolvedTodos.length} 项</span>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="font-medium flex items-center gap-1.5"><CheckSquare className="w-4 h-4 text-safe" />已办事项</h2>
|
||||
<span className="text-xs text-gray-400">{data.resolvedTodos.length} 项</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1.5">
|
||||
{data.resolvedTodos.map((todo) => (
|
||||
<div
|
||||
key={todo.id}
|
||||
className="flex items-center justify-between px-3 py-3 rounded-md bg-gray-50"
|
||||
className="flex items-center justify-between px-2.5 py-2 rounded-md bg-gray-50"
|
||||
>
|
||||
<Link to={todo.actionUrl} className="flex items-center gap-3 flex-1">
|
||||
<Link to={todo.actionUrl} className="flex items-center gap-2.5 flex-1">
|
||||
<TodoIcon type={todo.type} level={todo.level} />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm text-gray-600 line-through">{todo.title}</span>
|
||||
<span className="text-xs text-gray-600 line-through">{todo.title}</span>
|
||||
<span className="text-xs text-gray-400">{todo.description}</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
+797
-256
File diff suppressed because it is too large
Load Diff
+470
-155
File diff suppressed because it is too large
Load Diff
@@ -40,8 +40,8 @@ export default function Settings() {
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-lg font-semibold">系统设置</h1>
|
||||
<div className="space-y-3">
|
||||
<h1 className="text-xs font-medium">系统设置</h1>
|
||||
|
||||
<div className="flex gap-1 border-b">
|
||||
{sections.map((s) => {
|
||||
@@ -50,7 +50,7 @@ export default function Settings() {
|
||||
<button
|
||||
key={s.key}
|
||||
onClick={() => setActiveSection(s.key)}
|
||||
className={`flex items-center gap-1.5 px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium border-b-2 transition-colors ${
|
||||
activeSection === s.key ? 'border-primary text-primary' : 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
@@ -82,7 +82,7 @@ function OrgSettings({ orgData, onSave, saving }: { orgData: any; onSave: (data:
|
||||
return (
|
||||
<Card>
|
||||
<h2 className="font-medium mb-4">企业信息</h2>
|
||||
<div className="space-y-4 max-w-md">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label>企业名称</Label>
|
||||
<Input value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} placeholder="企业名称" />
|
||||
@@ -126,7 +126,7 @@ function UserSettings({ usersData }: { usersData: any }) {
|
||||
</Button>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="border-b text-left text-gray-500">
|
||||
<th className="py-2 px-3 font-medium">姓名</th>
|
||||
@@ -176,8 +176,8 @@ function AddUserModal({ open, onClose }: { open: boolean; onClose: () => void })
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose} title="添加用户">
|
||||
<div className="space-y-4">
|
||||
{error && <div className="px-3 py-2 rounded-md bg-red-50 text-red-700 text-sm">{error}</div>}
|
||||
<div className="space-y-3">
|
||||
{error && <div className="px-3 py-2 rounded-md bg-red-50 text-red-700 text-xs">{error}</div>}
|
||||
<div>
|
||||
<Label>姓名 *</Label>
|
||||
<Input value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} />
|
||||
@@ -225,17 +225,17 @@ function PlanSettings({ orgData }: { orgData: any }) {
|
||||
<Card key={p.key}>
|
||||
<div className={`px-4 py-3 rounded-t-lg ${plan === p.key ? 'bg-primary text-white' : 'bg-gray-50'}`}>
|
||||
<div className="font-medium">{p.label}</div>
|
||||
<div className={`text-lg font-bold ${plan === p.key ? 'text-white' : 'text-gray-900'}`}>{p.price}</div>
|
||||
<div className={`text-base font-bold ${plan === p.key ? 'text-white' : 'text-gray-900'}`}>{p.price}</div>
|
||||
</div>
|
||||
<div className="p-4 space-y-2">
|
||||
{p.features.map((f, i) => (
|
||||
<div key={i} className="text-sm text-gray-600 flex items-center gap-2">
|
||||
<div key={i} className="text-xs text-gray-600 flex items-center gap-2">
|
||||
<span className="text-safe">✓</span> {f}
|
||||
</div>
|
||||
))}
|
||||
<div className="pt-2">
|
||||
{plan === p.key ? (
|
||||
<div className="text-sm text-center text-primary font-medium">当前套餐</div>
|
||||
<div className="text-xs text-center text-primary font-medium">当前套餐</div>
|
||||
) : (
|
||||
<Button variant="secondary" className="w-full" size="sm">升级</Button>
|
||||
)}
|
||||
@@ -288,12 +288,12 @@ function NotificationSettings() {
|
||||
const logs = logsData?.items || []
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<Card>
|
||||
<h2 className="font-medium mb-4">通知设置</h2>
|
||||
<div className="space-y-4 max-w-md">
|
||||
<div className="space-y-3">
|
||||
<label className="flex items-center justify-between">
|
||||
<span className="text-sm">合同到期提醒</span>
|
||||
<span className="text-xs">合同到期提醒</span>
|
||||
<input type="checkbox" checked={form.contractExpiry ?? true} onChange={(e) => setForm({ ...form, contractExpiry: e.target.checked })} />
|
||||
</label>
|
||||
<div>
|
||||
@@ -301,19 +301,19 @@ function NotificationSettings() {
|
||||
<Input type="number" value={form.expiryDays ?? 30} onChange={(e) => setForm({ ...form, expiryDays: Number(e.target.value) })} />
|
||||
</div>
|
||||
<label className="flex items-center justify-between">
|
||||
<span className="text-sm">未签合同提醒</span>
|
||||
<span className="text-xs">未签合同提醒</span>
|
||||
<input type="checkbox" checked={form.contractUnsigned ?? true} onChange={(e) => setForm({ ...form, contractUnsigned: e.target.checked })} />
|
||||
</label>
|
||||
<label className="flex items-center justify-between">
|
||||
<span className="text-sm">加班超时提醒</span>
|
||||
<span className="text-xs">加班超时提醒</span>
|
||||
<input type="checkbox" checked={form.overtimeAlert ?? true} onChange={(e) => setForm({ ...form, overtimeAlert: e.target.checked })} />
|
||||
</label>
|
||||
<label className="flex items-center justify-between">
|
||||
<span className="text-sm">工资条发布通知</span>
|
||||
<span className="text-xs">工资条发布通知</span>
|
||||
<input type="checkbox" checked={form.payslipReady ?? true} onChange={(e) => setForm({ ...form, payslipReady: e.target.checked })} />
|
||||
</label>
|
||||
<div className="border-t pt-3 space-y-3">
|
||||
<div className="text-sm font-medium">月度事务提醒</div>
|
||||
<div className="text-xs font-medium">月度事务提醒</div>
|
||||
<div className="text-xs text-gray-400">设置每月截止日,到期后自动生成待办提醒</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
@@ -339,7 +339,7 @@ function NotificationSettings() {
|
||||
<Input value={form.wechatWebhook || ''} onChange={(e) => setForm({ ...form, wechatWebhook: e.target.value || null })} placeholder="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=..." />
|
||||
</div>
|
||||
<label className="flex items-center justify-between">
|
||||
<span className="text-sm">邮件通知</span>
|
||||
<span className="text-xs">邮件通知</span>
|
||||
<input type="checkbox" checked={form.emailNotify ?? false} onChange={(e) => setForm({ ...form, emailNotify: e.target.checked })} />
|
||||
</label>
|
||||
{form.emailNotify && (
|
||||
@@ -362,12 +362,12 @@ function NotificationSettings() {
|
||||
</Button>
|
||||
</div>
|
||||
{checkResult && (
|
||||
<div className="px-3 py-2 rounded-md bg-blue-50 text-blue-700 text-sm mb-3">{checkResult}</div>
|
||||
<div className="px-3 py-2 rounded-md bg-blue-50 text-blue-700 text-xs mb-3">{checkResult}</div>
|
||||
)}
|
||||
{logs.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{logs.map((log: any) => (
|
||||
<div key={log.id} className="text-sm border-b last:border-0 py-2">
|
||||
<div key={log.id} className="text-xs border-b last:border-0 py-2">
|
||||
<div className="font-medium">{log.title}</div>
|
||||
<div className="text-gray-500 text-xs mt-0.5">{log.content}</div>
|
||||
<div className="text-gray-400 text-xs mt-0.5">{new Date(log.createdAt).toLocaleString('zh-CN')}</div>
|
||||
@@ -375,7 +375,7 @@ function NotificationSettings() {
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-gray-400 text-sm text-center py-4">暂无通知记录</div>
|
||||
<div className="text-gray-400 text-xs text-center py-4">暂无通知记录</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -275,8 +275,8 @@ export default function Termination() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-lg font-semibold">解聘助手</h1>
|
||||
<div className="space-y-3">
|
||||
<h1 className="text-xs font-medium">解聘助手</h1>
|
||||
|
||||
{/* 进度条 */}
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -289,11 +289,11 @@ export default function Termination() {
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<div className="mb-2 text-sm text-gray-500">Step {step + 1}/5:{STEPS[step]}</div>
|
||||
<div className="mb-2 text-xs text-gray-500">Step {step + 1}/5:{STEPS[step]}</div>
|
||||
|
||||
{/* Step 1: 选择员工 */}
|
||||
{step === 0 && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label>选择员工</Label>
|
||||
<Select value={employeeId} onChange={(e) => setEmployeeId(e.target.value)}>
|
||||
@@ -304,7 +304,7 @@ export default function Termination() {
|
||||
</Select>
|
||||
</div>
|
||||
{selectedEmployee && (
|
||||
<div className="text-sm text-gray-600 bg-gray-50 p-3 rounded-md space-y-1">
|
||||
<div className="text-xs text-gray-600 bg-gray-50 p-3 rounded-md space-y-1">
|
||||
<div className="font-medium">{selectedEmployee.name}({selectedEmployee.department})</div>
|
||||
<div>入职日期:{selectedEmployee.hireDate?.toString().slice(0, 10)}</div>
|
||||
<div>月工资:¥{fmt(selectedEmployee.monthlySalary)}</div>
|
||||
@@ -330,11 +330,11 @@ export default function Termination() {
|
||||
)}
|
||||
{profile && suggestions.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">📋 解聘方式建议</div>
|
||||
<div className="text-xs font-medium">📋 解聘方式建议</div>
|
||||
{suggestions.map((s, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`px-3 py-2 rounded-md text-sm ${s.reason === '' ? 'bg-red-50 text-red-700' : 'bg-blue-50 text-blue-700'}`}
|
||||
className={`px-3 py-2 rounded-md text-xs ${s.reason === '' ? 'bg-red-50 text-red-700' : 'bg-blue-50 text-blue-700'}`}
|
||||
>
|
||||
<div className="font-medium">{s.label}</div>
|
||||
<div className="text-xs mt-0.5">{s.why}</div>
|
||||
@@ -343,17 +343,17 @@ export default function Termination() {
|
||||
</div>
|
||||
)}
|
||||
{employeeId && !profile && (
|
||||
<div className="text-sm text-gray-400">加载员工档案中...</div>
|
||||
<div className="text-xs text-gray-400">加载员工档案中...</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 2: 解聘方式 */}
|
||||
{step === 1 && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
{suggestions.length > 0 && (
|
||||
<div className="bg-blue-50 rounded-md p-3 space-y-1">
|
||||
<div className="text-sm font-medium text-blue-700">💡 系统建议</div>
|
||||
<div className="text-xs font-medium text-blue-700">💡 系统建议</div>
|
||||
{suggestions.filter((s) => s.reason).map((s, i) => (
|
||||
<div key={i} className="text-xs text-blue-600">
|
||||
{s.label}:{s.why}
|
||||
@@ -371,7 +371,7 @@ export default function Termination() {
|
||||
>
|
||||
<input type="radio" name="reason" value={r.value} checked={reason === r.value} onChange={(e) => setReason(e.target.value)} className="mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<div className="text-sm flex items-center gap-2">
|
||||
<div className="text-xs flex items-center gap-2">
|
||||
{r.label}
|
||||
{suggested && <span className="text-xs text-primary font-medium">推荐</span>}
|
||||
</div>
|
||||
@@ -392,12 +392,12 @@ export default function Termination() {
|
||||
{riskAssessment && riskAssessment.warnings.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{riskAssessment.warnings.map((w, i) => (
|
||||
<div key={i} className="flex items-center gap-2 px-3 py-2 rounded-md bg-red-50 text-red-700 text-sm">
|
||||
<div key={i} className="flex items-center gap-2 px-3 py-2 rounded-md bg-red-50 text-red-700 text-xs">
|
||||
<AlertTriangle className="w-4 h-4 shrink-0" />
|
||||
{w}
|
||||
</div>
|
||||
))}
|
||||
<label className="flex items-center gap-2 text-sm px-3 py-2 rounded-md bg-yellow-50 text-yellow-800">
|
||||
<label className="flex items-center gap-2 text-xs px-3 py-2 rounded-md bg-yellow-50 text-yellow-800">
|
||||
<input type="checkbox" checked={acknowledgeRisk} onChange={(e) => setAcknowledgeRisk(e.target.checked)} />
|
||||
我已了解风险,继续操作
|
||||
</label>
|
||||
@@ -416,7 +416,7 @@ export default function Termination() {
|
||||
checked={checklist[item.key] || false}
|
||||
onChange={(e) => setChecklist({ ...checklist, [item.key]: e.target.checked })}
|
||||
/>
|
||||
<span className="text-sm">{item.label}</span>
|
||||
<span className="text-xs">{item.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
@@ -424,15 +424,15 @@ export default function Termination() {
|
||||
|
||||
{/* Step 4: 费用结算 */}
|
||||
{step === 3 && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label>当地社平工资(选填)</Label>
|
||||
<Input type="number" value={socialAvgWage} onChange={(e) => setSocialAvgWage(Number(e.target.value) || 0)} placeholder="用于三倍封顶计算" />
|
||||
</div>
|
||||
{costResult && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
{/* 员工概况 */}
|
||||
<div className="text-sm text-gray-600 bg-gray-50 p-3 rounded-md space-y-1">
|
||||
<div className="text-xs text-gray-600 bg-gray-50 p-3 rounded-md space-y-1">
|
||||
<div className="font-medium">{selectedEmployee?.name}({selectedEmployee?.department})</div>
|
||||
<div>工作年限:{costResult.years}年{costResult.remainingMonths}个月</div>
|
||||
<div>月工资:¥{fmt(costResult.wage)}/月</div>
|
||||
@@ -443,7 +443,7 @@ export default function Termination() {
|
||||
|
||||
{/* 经济补偿金 / 赔偿金 */}
|
||||
{costResult.noComp ? (
|
||||
<div className="px-3 py-2 rounded-md bg-gray-50 text-gray-700 text-sm">
|
||||
<div className="px-3 py-2 rounded-md bg-gray-50 text-gray-700 text-xs">
|
||||
员工过错解除,无需支付经济补偿金
|
||||
</div>
|
||||
) : (
|
||||
@@ -452,22 +452,22 @@ export default function Termination() {
|
||||
<Calculator className="w-4 h-4" />
|
||||
{costResult.isIllegal ? '违法解除赔偿金' : '经济补偿金'}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">补偿月数:{costResult.cappedMonths}个月</div>
|
||||
<div className="text-sm text-gray-500">计算基数:¥{fmt(costResult.cappedWage)}/月</div>
|
||||
<div className="text-xs text-gray-500">补偿月数:{costResult.cappedMonths}个月</div>
|
||||
<div className="text-xs text-gray-500">计算基数:¥{fmt(costResult.cappedWage)}/月</div>
|
||||
{costResult.isIllegal && (
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-gray-500">经济补偿金</span>
|
||||
<span>¥{fmt(costResult.basePay)}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">{costResult.isIllegal ? '赔偿金(×2)' : '补偿金'}</span>
|
||||
<span className={`text-lg font-bold ${costResult.isIllegal ? 'text-danger' : 'text-primary'}`}>
|
||||
<span className={`text-base font-bold ${costResult.isIllegal ? 'text-danger' : 'text-primary'}`}>
|
||||
¥{fmt(costResult.severancePay)}
|
||||
</span>
|
||||
</div>
|
||||
{costResult.noticePay > 0 && (
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-gray-500">代通知金</span>
|
||||
<span>¥{fmt(costResult.noticePay)}</span>
|
||||
</div>
|
||||
@@ -491,12 +491,12 @@ export default function Termination() {
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
未签劳动合同双倍工资
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">双倍工资起算:{costResult.doubleStartDate}</div>
|
||||
<div className="text-sm text-gray-500">双倍工资截止:{costResult.doubleEndDate}</div>
|
||||
<div className="text-sm text-gray-500">赔偿月数:{costResult.doubleMonths}个月</div>
|
||||
<div className="text-xs text-gray-500">双倍工资起算:{costResult.doubleStartDate}</div>
|
||||
<div className="text-xs text-gray-500">双倍工资截止:{costResult.doubleEndDate}</div>
|
||||
<div className="text-xs text-gray-500">赔偿月数:{costResult.doubleMonths}个月</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">双倍工资赔偿</span>
|
||||
<span className="text-lg font-bold text-warning">¥{fmt(costResult.doublePay)}</span>
|
||||
<span className="text-base font-bold text-warning">¥{fmt(costResult.doublePay)}</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">({costResult.doubleMonths}个月 × ¥{fmt(costResult.wage)})</div>
|
||||
<div className="flex items-start gap-2 px-3 py-2 rounded-md bg-yellow-50 text-yellow-800 text-xs">
|
||||
@@ -510,12 +510,12 @@ export default function Termination() {
|
||||
<div className="border-t pt-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">合计应付</span>
|
||||
<span className="text-xl font-bold text-danger">¥{fmt(costResult.grandTotal)}</span>
|
||||
<span className="text-lg font-bold text-danger">¥{fmt(costResult.grandTotal)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!costResult.noComp && (
|
||||
<div className="flex items-start gap-2 px-3 py-2 rounded-md bg-blue-50 text-blue-700 text-sm">
|
||||
<div className="flex items-start gap-2 px-3 py-2 rounded-md bg-blue-50 text-blue-700 text-xs">
|
||||
<Info className="w-4 h-4 mt-0.5 shrink-0" />
|
||||
<span>满1年补1个月,满6个月不满1年按1年算,不满6个月补半个月</span>
|
||||
</div>
|
||||
@@ -527,18 +527,18 @@ export default function Termination() {
|
||||
|
||||
{/* Step 5: 解聘材料 */}
|
||||
{step === 4 && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
{saveMutation.isError ? (
|
||||
<div className="text-center py-8">
|
||||
<AlertTriangle className="w-12 h-12 text-danger mx-auto" />
|
||||
<div className="text-danger font-medium mt-2">保存失败</div>
|
||||
<div className="text-sm text-gray-500">{(saveMutation.error as any)?.response?.data?.error?.message || '请稍后重试'}</div>
|
||||
<div className="text-xs text-gray-500">{(saveMutation.error as any)?.response?.data?.error?.message || '请稍后重试'}</div>
|
||||
<Button onClick={() => setStep(3)} className="mt-4">返回修改</Button>
|
||||
</div>
|
||||
) : saveMutation.isPending ? (
|
||||
<div className="text-center py-8 text-gray-400">保存中...</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
{/* 成功提示 */}
|
||||
<div className="flex items-center gap-2 text-safe">
|
||||
<Check className="w-5 h-5" />
|
||||
@@ -556,11 +556,11 @@ export default function Termination() {
|
||||
</div>
|
||||
|
||||
{/* 1. 解聘通知书 */}
|
||||
<div className="border rounded-lg p-6 space-y-4 print:shadow-none">
|
||||
<div className="border rounded-lg p-6 space-y-3 print:shadow-none">
|
||||
<div className="text-center">
|
||||
<h2 className="text-lg font-bold">解除劳动合同通知书</h2>
|
||||
<h2 className="text-base font-bold">解除劳动合同通知书</h2>
|
||||
</div>
|
||||
<div className="text-sm text-gray-700 space-y-3">
|
||||
<div className="text-xs text-gray-700 space-y-3">
|
||||
<p><strong>{selectedEmployee?.name}</strong> 先生/女士:</p>
|
||||
<p>
|
||||
您于 <strong>{selectedEmployee?.hireDate?.toString().slice(0, 10)}</strong> 入职我公司{selectedEmployee?.department}部门。
|
||||
@@ -600,7 +600,7 @@ export default function Termination() {
|
||||
{costResult && (
|
||||
<div className="border rounded-lg p-4 space-y-2">
|
||||
<h3 className="font-medium flex items-center gap-2"><Calculator className="w-4 h-4" />费用结算明细</h3>
|
||||
<div className="text-sm space-y-1">
|
||||
<div className="text-xs space-y-1">
|
||||
<div className="flex justify-between"><span>工作年限</span><span>{costResult.years}年{costResult.remainingMonths}个月</span></div>
|
||||
<div className="flex justify-between"><span>月工资</span><span>¥{fmt(costResult.wage)}/月</span></div>
|
||||
{costResult.capped && <div className="text-warning">⚠️ 工资超过社平3倍,已按三倍封顶且最多补偿12个月</div>}
|
||||
@@ -623,7 +623,7 @@ export default function Termination() {
|
||||
{/* 3. 合规检查清单 */}
|
||||
<div className="border rounded-lg p-4 space-y-2">
|
||||
<h3 className="font-medium flex items-center gap-2"><Shield className="w-4 h-4" />合规检查清单</h3>
|
||||
<div className="text-sm space-y-1">
|
||||
<div className="text-xs space-y-1">
|
||||
{checklistItems?.map((item) => (
|
||||
<div key={item.key} className="flex items-center gap-2">
|
||||
<span className={checklist[item.key] ? 'text-safe' : 'text-danger'}>
|
||||
@@ -661,7 +661,7 @@ export default function Termination() {
|
||||
}, {})
|
||||
return Object.entries(grouped).map(([category, items]) => (
|
||||
<div key={category} className="space-y-1">
|
||||
<div className="text-sm font-medium text-gray-700">{category as string}</div>
|
||||
<div className="text-xs font-medium text-gray-700">{category as string}</div>
|
||||
{(items as any[]).map((e: any, i: number) => (
|
||||
<div key={i} className="text-xs text-gray-600 pl-4 border-l-2 border-gray-200 ml-1">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -677,7 +677,7 @@ export default function Termination() {
|
||||
})()}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-sm text-gray-400">加载证据链中...</div>
|
||||
<div className="text-xs text-gray-400">加载证据链中...</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -48,7 +48,7 @@ export default function ContractConfirm() {
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 px-4">
|
||||
<div className="max-w-sm w-full text-center">
|
||||
<Check className="w-16 h-16 text-safe mx-auto mb-4" />
|
||||
<h1 className="text-lg font-semibold mb-2">合同签署确认成功</h1>
|
||||
<h1 className="text-sm font-semibold mb-2">合同签署确认成功</h1>
|
||||
<p className="text-sm text-gray-500">已记录您的签署确认时间和 IP 地址。</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -60,7 +60,7 @@ export default function ContractConfirm() {
|
||||
<div className="max-w-md mx-auto">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<PenTool className="w-6 h-6 text-primary" />
|
||||
<h1 className="text-lg font-semibold">合同签署确认</h1>
|
||||
<h1 className="text-sm font-semibold">合同签署确认</h1>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
@@ -74,7 +74,7 @@ export default function ContractConfirm() {
|
||||
</Card>
|
||||
) : data ? (
|
||||
<Card>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm text-gray-600">
|
||||
{data.orgName} 与 {data.employeeName} 的劳动合同
|
||||
</div>
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function MyContract() {
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="w-6 h-6 text-primary" />
|
||||
<h1 className="text-lg font-semibold">我的劳动合同</h1>
|
||||
<h1 className="text-sm font-semibold">我的劳动合同</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-gray-500">{employee.name}</span>
|
||||
@@ -51,7 +51,7 @@ export default function MyContract() {
|
||||
) : !contract ? (
|
||||
<EmptyState title="暂无合同" description="HR 尚未录入您的合同信息" />
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
{/* 到期提醒 */}
|
||||
{daysToExpire !== null && daysToExpire <= 30 && daysToExpire >= 0 && (
|
||||
<div className="flex items-center gap-2 px-3 py-2 rounded-md bg-yellow-50 text-yellow-700 text-sm">
|
||||
|
||||
@@ -53,7 +53,7 @@ export default function Onboarding() {
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 px-4">
|
||||
<div className="max-w-sm w-full text-center">
|
||||
<Check className="w-16 h-16 text-safe mx-auto mb-4" />
|
||||
<h1 className="text-lg font-semibold mb-2">信息提交成功</h1>
|
||||
<h1 className="text-sm font-semibold mb-2">信息提交成功</h1>
|
||||
<p className="text-sm text-gray-500">HR 将审核您的信息,请耐心等待。</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,7 +65,7 @@ export default function Onboarding() {
|
||||
<div className="max-w-md mx-auto">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<ClipboardList className="w-6 h-6 text-primary" />
|
||||
<h1 className="text-lg font-semibold">入职信息填报</h1>
|
||||
<h1 className="text-sm font-semibold">入职信息填报</h1>
|
||||
</div>
|
||||
|
||||
{orgName && (
|
||||
@@ -79,7 +79,7 @@ export default function Onboarding() {
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label>姓名 *</Label>
|
||||
<Input value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} placeholder="请输入姓名" />
|
||||
|
||||
@@ -42,7 +42,7 @@ export default function Payslip() {
|
||||
<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>
|
||||
<h1 className="text-sm font-semibold">我的工资条</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-gray-500">{employee.name}</span>
|
||||
@@ -93,7 +93,7 @@ export default function Payslip() {
|
||||
<div className="border-t pt-3">
|
||||
<div className="flex justify-between">
|
||||
<span className="font-medium">应发合计</span>
|
||||
<span className="text-xl font-bold text-primary">¥{fmt(Number(data.totalPay))}</span>
|
||||
<span className="text-base font-bold text-primary">¥{fmt(Number(data.totalPay))}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ export default function PortalLogin() {
|
||||
<div className="w-full max-w-sm">
|
||||
<div className="flex items-center justify-center gap-2 mb-8">
|
||||
<Building2 className="w-8 h-8 text-primary" />
|
||||
<span className="text-xl font-bold">用工合规助手 — 员工端</span>
|
||||
<span className="text-base font-bold">用工合规助手 — 员工端</span>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
@@ -82,7 +82,7 @@ export default function PortalLogin() {
|
||||
{error && <div className="mb-4 px-3 py-2 rounded-md bg-red-50 text-red-700 text-sm">{error}</div>}
|
||||
|
||||
{mode === 'password' ? (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label>手机号</Label>
|
||||
<Input type="tel" placeholder="请输入手机号" value={phone} onChange={(e) => setPhone(e.target.value)} maxLength={11} />
|
||||
@@ -96,7 +96,7 @@ export default function PortalLogin() {
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label>手机号</Label>
|
||||
<Input type="tel" placeholder="请输入手机号" value={phone} onChange={(e) => setPhone(e.target.value)} maxLength={11} />
|
||||
|
||||
Reference in New Issue
Block a user