fix: portal端token过期后自动跳转登录页,避免显示空数据

- portalAxios 401 响应拦截器:清除 token 并跳转 /portal/login
- PortalLayoutWrapper:检查 token 是否存在,不存在则跳转登录页
- 排除 /portal/login 和 /portal/auto-login 页面避免重复跳转

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
selfrelease
2026-08-17 15:06:46 +08:00
parent c66ea290eb
commit 5196d7c80a
23 changed files with 812 additions and 74 deletions
+8
View File
@@ -19,6 +19,7 @@ const ForgotPassword = lazyRetry(() => import('./pages/auth/ForgotPassword'))
const Dashboard = lazyRetry(() => import('./pages/Dashboard'))
const Money = lazyRetry(() => import('./pages/Money'))
const SocialInsurance = lazyRetry(() => import('./pages/SocialInsurance'))
const Overtime = lazyRetry(() => import('./pages/Overtime'))
const Roster = lazyRetry(() => import('./pages/Roster'))
const OrgChart = lazyRetry(() => import('./pages/OrgChart'))
const SupportDashboard = lazyRetry(() => import('./pages/support/SupportDashboard'))
@@ -141,6 +142,12 @@ function PlatformLayout({ children }: { children: React.ReactNode }) {
}
function PortalLayoutWrapper({ children, showNav = true }: { children: React.ReactNode; showNav?: boolean }) {
// 检查 portal token 是否存在,不存在则跳转登录页
const portalToken = localStorage.getItem('portalToken')
if (!portalToken) {
window.location.href = '/portal/login'
return null
}
if (!showNav) {
return (
<div className="min-h-screen bg-surface pt-safe pb-safe">
@@ -192,6 +199,7 @@ export default function App() {
<Route path="/support" element={<ProtectedRoute><AdminLayout><SupportDashboard /></AdminLayout></ProtectedRoute>} />
<Route path="/money" element={<ProtectedRoute><AdminLayout><Money /></AdminLayout></ProtectedRoute>} />
<Route path="/social" element={<ProtectedRoute><AdminLayout><SocialInsurance /></AdminLayout></ProtectedRoute>} />
<Route path="/overtime" element={<ProtectedRoute><AdminLayout><Overtime /></AdminLayout></ProtectedRoute>} />
<Route path="/termination" element={<ProtectedRoute><AdminLayout><Termination /></AdminLayout></ProtectedRoute>} />
<Route path="/ai-assistant" element={<ProtectedRoute><AdminLayout><AIAssistant /></AdminLayout></ProtectedRoute>} />
<Route path="/settings" element={<ProtectedRoute><AdminLayout><Settings /></AdminLayout></ProtectedRoute>} />
@@ -16,7 +16,7 @@ import {
ChevronDown, ChevronRight,
Building2, CalendarDays, ClipboardList, Heart, CalendarClock,
Gift, PenTool, Umbrella, GraduationCap, TrendingUp, AlertTriangle,
DollarSign,
DollarSign, Clock,
} from 'lucide-react'
import { settingsApi } from '../../lib/api-services'
@@ -64,6 +64,7 @@ const navGroups: NavGroup[] = [
items: [
{ path: '/money', label: '薪税管理', icon: Calculator },
{ path: '/social', label: '社保公积金', icon: Shield },
{ path: '/overtime', label: '加班费计算', icon: Clock },
{ path: '/commission-bonus', label: '提成奖金', icon: DollarSign },
{ path: '/salary-dashboard', label: '薪酬分析', icon: BarChart3 },
],
+27 -1
View File
@@ -294,6 +294,12 @@ export const attendanceApi = {
/** 批量确认 */
batchConfirm: (data: { month: string; all?: boolean; ids?: string[] }) =>
post('/attendance/batch-confirm', data).then(unwrap<any>()),
/** 批量创建/更新考勤确认记录 */
batchCreate: (data: { month: string; items: any[] }) =>
post('/attendance/batch', data).then(unwrap<any>()),
/** 更新考勤确认扣款金额 */
updateDeduction: (id: string, deductionAmount: number) =>
put(`/attendance/confirmations/${id}/deduction`, { deductionAmount }).then(unwrap<any>()),
/** 单条确认 */
confirm: (data: { employeeId: string; month: string }) =>
post('/attendance/confirm', data).then(unwrap<any>()),
@@ -480,6 +486,15 @@ export const payrollApi = {
/** 获取提成奖金到批次 */
fetchBonusToBatch: (batchId: string) =>
post(`/payroll2/batches/${batchId}/fetch-bonus`).then(unwrap<any>()),
/** 获取绩效工资到批次(按考核系数计算) */
fetchPerformanceToBatch: (batchId: string) =>
post(`/payroll2/batches/${batchId}/fetch-performance`).then(unwrap<any>()),
/** 获取违纪扣款到批次 */
fetchDisciplinaryToBatch: (batchId: string) =>
post(`/payroll2/batches/${batchId}/fetch-disciplinary`).then(unwrap<any>()),
/** 获取考勤扣款到批次 */
fetchAttendanceDeductionToBatch: (batchId: string) =>
post(`/payroll2/batches/${batchId}/fetch-attendance-deduction`).then(unwrap<any>()),
/** 加班费配置 */
overtimeConfig: () =>
get('/payroll/overtime/config').then(unwrap<any>()),
@@ -1096,9 +1111,20 @@ portalAxios.interceptors.request.use((config: any) => {
return config
})
// 与管理端 api 实例一致:response interceptor 返回 response.data(后端 JSON body
// 401 时清除 token 并跳转登录页(避免 token 过期后显示空数据)
portalAxios.interceptors.response.use(
(response) => response.data,
(error) => Promise.reject(error),
(error) => {
if (error?.response?.status === 401) {
localStorage.removeItem('portalToken')
localStorage.removeItem('portalEmployee')
// 避免在登录页重复跳转
if (!window.location.pathname.includes('/portal/login') && !window.location.pathname.includes('/portal/auto-login')) {
window.location.href = '/portal/login'
}
}
return Promise.reject(error)
},
)
const portalGet = ((url: string, config?: any) => portalAxios.get(url, config)) as any
const portalPost = ((url: string, data?: any, config?: any) => portalAxios.post(url, data, config)) as any
+21 -2
View File
@@ -110,7 +110,7 @@ function ConfirmTab({ onGoToTab }: { onGoToTab?: (tab: string) => void }) {
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
const [searchQuery, setSearchQuery] = useState('')
const [editItem, setEditItem] = useState<any>(null)
const [editForm, setEditForm] = useState({ workDays: 0, lateCount: 0, earlyLeaveCount: 0, absentDays: 0, leaveDays: 0, overtimeHours: 0, overtimePay: 0 })
const [editForm, setEditForm] = useState({ workDays: 0, lateCount: 0, earlyLeaveCount: 0, absentDays: 0, leaveDays: 0, overtimeHours: 0, overtimePay: 0, deductionAmount: 0 })
const fileInputRef = useRef<HTMLInputElement>(null)
const pageSize = usePageSize()
const [page, setPage] = useState(1)
@@ -193,7 +193,19 @@ function ConfirmTab({ onGoToTab }: { onGoToTab?: (tab: string) => void }) {
const editMutation = useMutation({
mutationFn: async (data: any) => {
return await attendanceApi.manualCorrect(data)
// 通过批量创建接口更新考勤确认记录(upsert 语义)
return await attendanceApi.batchCreate({
month: data.month,
items: [{
employeeId: data.employeeId,
workDays: data.workDays,
weekdayHours: 0,
weekendHours: 0,
holidayHours: 0,
overtimePay: data.overtimePay,
deductionAmount: data.deductionAmount || 0,
}],
})
},
onSuccess: () => {
toast.success('考勤记录已修改')
@@ -421,6 +433,7 @@ function ConfirmTab({ onGoToTab }: { onGoToTab?: (tab: string) => void }) {
<span> {item.weekendHours}h</span>
<span> {item.holidayHours}h</span>
<span className="text-gray-700"> ¥{item.overtimePay?.toFixed(2)}</span>
{item.deductionAmount > 0 && <span className="text-danger"> ¥{item.deductionAmount?.toFixed(2)}</span>}
</div>
{item.disputeNote && (
<div className="text-xs text-red-600 mt-1">{item.disputeNote}</div>
@@ -449,6 +462,7 @@ function ConfirmTab({ onGoToTab }: { onGoToTab?: (tab: string) => void }) {
leaveDays: item.leaveDays || 0,
overtimeHours: (item.weekdayHours || 0) + (item.weekendHours || 0) + (item.holidayHours || 0),
overtimePay: item.overtimePay || 0,
deductionAmount: item.deductionAmount || 0,
})
}}
>
@@ -510,6 +524,11 @@ function ConfirmTab({ onGoToTab }: { onGoToTab?: (tab: string) => void }) {
<input type="number" min="0" step="0.01" className="w-full px-2 py-1.5 text-sm border rounded-md" value={editForm.overtimePay}
onChange={(e) => setEditForm({ ...editForm, overtimePay: Number(e.target.value) })} />
</div>
<div className="col-span-2">
<label className="text-xs text-gray-500"></label>
<input type="number" min="0" step="0.01" className="w-full px-2 py-1.5 text-sm border rounded-md" value={editForm.deductionAmount}
onChange={(e) => setEditForm({ ...editForm, deductionAmount: Number(e.target.value) })} placeholder="迟到/早退/旷工等扣款金额" />
</div>
</div>
<div className="flex justify-end gap-2 mt-4">
<Button size="sm" variant="secondary" onClick={() => setEditItem(null)}></Button>
+27 -3
View File
@@ -240,6 +240,8 @@ function AddEmployeeModal({ open, onClose, onSubmit, loading, error }: {
position: '',
hireDate: '',
monthlySalary: '',
baseSalary: '',
performanceSalary: '0',
gender: '男' as '男' | '女',
phone: '',
isPregnant: false,
@@ -261,6 +263,8 @@ function AddEmployeeModal({ open, onClose, onSubmit, loading, error }: {
position: form.position || undefined,
hireDate: new Date(form.hireDate).toISOString(),
monthlySalary: form.monthlySalary,
baseSalary: form.baseSalary,
performanceSalary: form.performanceSalary || '0',
gender: form.gender,
phone: form.phone || undefined,
isPregnant: form.isPregnant,
@@ -276,6 +280,8 @@ function AddEmployeeModal({ open, onClose, onSubmit, loading, error }: {
contractYears: form.contractYears,
probationMonths: form.probationMonths,
probationSalary: form.probationSalary,
baseSalary: parseFloat(form.baseSalary) || 0,
performanceSalary: parseFloat(form.performanceSalary) || 0,
}
}
onSubmit(data)
@@ -317,9 +323,27 @@ function AddEmployeeModal({ open, onClose, onSubmit, loading, error }: {
</div>
</div>
<div>
<Label> *</Label>
<Input type="number" value={form.monthlySalary} onChange={(e) => setForm({ ...form, monthlySalary: e.target.value })} placeholder="元" />
<div className="grid grid-cols-3 gap-3">
<div>
<Label> *</Label>
<Input type="number" value={form.baseSalary} onChange={(e) => {
const base = parseFloat(e.target.value) || 0
const perf = parseFloat(form.performanceSalary) || 0
setForm({ ...form, baseSalary: e.target.value, monthlySalary: base + perf > 0 ? String(base + perf) : '' })
}} placeholder="元" />
</div>
<div>
<Label></Label>
<Input type="number" value={form.performanceSalary} onChange={(e) => {
const perf = parseFloat(e.target.value) || 0
const base = parseFloat(form.baseSalary) || 0
setForm({ ...form, performanceSalary: e.target.value, monthlySalary: base + perf > 0 ? String(base + perf) : '' })
}} placeholder="可为0" />
</div>
<div>
<Label></Label>
<div className="text-xs text-gray-600 py-1.5">¥{(parseFloat(form.baseSalary) || 0) + (parseFloat(form.performanceSalary) || 0)}</div>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
+4 -7
View File
@@ -1,15 +1,14 @@
import { useState, Suspense } from 'react'
import { useSearchParams } from 'react-router-dom'
import { Layers, Wallet, LayoutTemplate, Clock, Receipt, Loader2 } from 'lucide-react'
import { Layers, Wallet, LayoutTemplate, Receipt, Loader2 } from 'lucide-react'
import PageGuide from '../components/ui/PageGuide'
import { lazyRetry } from '../lib/lazyRetry'
const BatchManager = lazyRetry(() => import('./money/BatchTab').then(m => ({ default: m.BatchManager })))
const TemplateManager = lazyRetry(() => import('./money/TemplateTab').then(m => ({ default: m.TemplateManager })))
const OvertimeCalculator = lazyRetry(() => import('./money/OvertimeTab').then(m => ({ default: m.OvertimeCalculator })))
const PayslipManager = lazyRetry(() => import('./money/PayslipTab').then(m => ({ default: m.PayslipManager })))
type Tab = 'batch' | 'template' | 'overtime' | 'payslip'
type Tab = 'batch' | 'template' | 'payslip'
export default function Money() {
const [searchParams] = useSearchParams()
@@ -20,18 +19,17 @@ export default function Money() {
const tabs: { key: Tab; label: string; icon: React.ReactNode }[] = [
{ key: 'batch', label: '发薪批次', icon: <Layers className="w-4 h-4" /> },
{ key: 'template', label: '薪酬模版', icon: <LayoutTemplate className="w-4 h-4" /> },
{ key: 'overtime', label: '加班费计算', icon: <Clock className="w-4 h-4" /> },
{ key: 'payslip', label: '工资条管理', icon: <Receipt className="w-4 h-4" /> },
]
return (
<div className="space-y-4">
<PageGuide> </PageGuide>
<PageGuide> </PageGuide>
<div className="flex items-center gap-2">
<Wallet className="w-5 h-5 text-primary" />
<div>
<h1 className="text-base font-semibold"></h1>
<p className="mt-1 text-sm text-gray-500"></p>
<p className="mt-1 text-sm text-gray-500"></p>
</div>
</div>
@@ -53,7 +51,6 @@ export default function Money() {
<Suspense fallback={<div className="flex items-center justify-center py-12"><Loader2 className="w-5 h-5 animate-spin text-gray-400" /></div>}>
{tab === 'batch' && <BatchManager />}
{tab === 'template' && <TemplateManager />}
{tab === 'overtime' && <OvertimeCalculator />}
{tab === 'payslip' && <PayslipManager filterEmployeeId={initialEmployeeId} />}
</Suspense>
</div>
+17
View File
@@ -0,0 +1,17 @@
import { Suspense } from 'react'
import { Loader2 } from 'lucide-react'
import { lazyRetry } from '../lib/lazyRetry'
const OvertimeCalculator = lazyRetry(() => import('./money/OvertimeTab').then(m => ({ default: m.OvertimeCalculator })))
/**
* 加班费计算独立页面
* 从薪税管理子标签拆出,作为一级菜单项
*/
export default function Overtime() {
return (
<Suspense fallback={<div className="flex items-center justify-center py-12"><Loader2 className="w-5 h-5 animate-spin text-gray-400" /></div>}>
<OvertimeCalculator />
</Suspense>
)
}
+79 -1
View File
@@ -3,7 +3,7 @@ import { usePageSize } from '../../hooks/usePageSize'
import { toast } from 'sonner'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useConfirm } from '../../hooks/useConfirm'
import { Calculator, AlertCircle, Info, Upload, Layers, Settings as SettingsIcon, Archive, Plus, Trash2, AlertTriangle, Download, FileText, X, ChevronLeft, Clock, Users, TrendingDown, TrendingUp, BadgeCheck, DollarSign } from 'lucide-react'
import { Calculator, AlertCircle, Info, Upload, Layers, Settings as SettingsIcon, Archive, Plus, Trash2, AlertTriangle, Download, FileText, X, ChevronLeft, Clock, Users, TrendingDown, TrendingUp, BadgeCheck, DollarSign, CalendarCheck } from 'lucide-react'
import { Stepper, type Step } from '../../components/ui/Stepper'
import { InlineAlert } from '../../components/ui/InlineAlert'
import PageGuide from '../../components/ui/PageGuide'
@@ -597,6 +597,54 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
},
})
const fetchPerformanceMutation = useMutation({
mutationFn: () => payrollApi.fetchPerformanceToBatch(batchId),
onSuccess: (res: any) => {
queryClient.invalidateQueries({ queryKey: ['batch-detail'] })
queryClient.invalidateQueries({ queryKey: ['batches'] })
if (res.data?.filled > 0) {
toast.success(res.data.message)
} else {
toast.info(res.data?.message || '无绩效工资数据')
}
},
onError: () => {
toast.error('获取绩效工资失败')
},
})
const fetchDisciplinaryMutation = useMutation({
mutationFn: () => payrollApi.fetchDisciplinaryToBatch(batchId),
onSuccess: (res: any) => {
queryClient.invalidateQueries({ queryKey: ['batch-detail'] })
queryClient.invalidateQueries({ queryKey: ['batches'] })
if (res.data?.filled > 0) {
toast.success(res.data.message)
} else {
toast.info(res.data?.message || '无违纪扣款数据')
}
},
onError: () => {
toast.error('获取违纪扣款失败')
},
})
const fetchAttendanceDeductionMutation = useMutation({
mutationFn: () => payrollApi.fetchAttendanceDeductionToBatch(batchId),
onSuccess: (res: any) => {
queryClient.invalidateQueries({ queryKey: ['batch-detail'] })
queryClient.invalidateQueries({ queryKey: ['batches'] })
if (res.data?.filled > 0) {
toast.success(res.data.message)
} else {
toast.info(res.data?.message || '无考勤扣款数据')
}
},
onError: () => {
toast.error('获取考勤扣款失败')
},
})
const importPayrollMutation = useMutation({
mutationFn: async (file: File) => {
const token = useAuthStore.getState().accessToken
@@ -825,6 +873,36 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
<DollarSign className="w-4 h-4 mr-1" />
{fetchBonusMutation.isPending ? '获取中...' : '获取提成奖金'}
</Button>
<Button
variant="secondary"
size="sm"
onClick={() => fetchPerformanceMutation.mutate()}
disabled={fetchPerformanceMutation.isPending || isArchived}
title="按考核等级系数计算绩效工资(A=1.2/B=1.0/C=0.8/D=0.6),无考核记录按1.0"
>
<TrendingUp className="w-4 h-4 mr-1" />
{fetchPerformanceMutation.isPending ? '获取中...' : '获取绩效工资'}
</Button>
<Button
variant="secondary"
size="sm"
onClick={() => fetchDisciplinaryMutation.mutate()}
disabled={fetchDisciplinaryMutation.isPending || isArchived}
title="按批次月份从违纪记录中汇总扣款金额(action=DEDUCTION"
>
<AlertTriangle className="w-4 h-4 mr-1" />
{fetchDisciplinaryMutation.isPending ? '获取中...' : '获取违纪扣款'}
</Button>
<Button
variant="secondary"
size="sm"
onClick={() => fetchAttendanceDeductionMutation.mutate()}
disabled={fetchAttendanceDeductionMutation.isPending || isArchived}
title="按批次月份从考勤确认中获取扣款金额(需先在考勤确认中填写)"
>
<CalendarCheck className="w-4 h-4 mr-1" />
{fetchAttendanceDeductionMutation.isPending ? '获取中...' : '获取考勤扣款'}
</Button>
<Button
size="sm"
onClick={async () => {
+23 -2
View File
@@ -89,6 +89,8 @@ export default function BasicInfo({ profile, employeeId, attachments }: { profil
phone: profile.phone || '',
hireDate: profile.hireDate?.toString().slice(0, 10) || '',
monthlySalary: profile.monthlySalary || '',
baseSalary: profile.baseSalary != null ? profile.baseSalary : '',
performanceSalary: profile.performanceSalary != null ? profile.performanceSalary : '0',
emergencyContact: profile.emergencyContact || '',
emergencyPhone: profile.emergencyPhone || '',
address: profile.address || '',
@@ -171,6 +173,8 @@ export default function BasicInfo({ profile, employeeId, attachments }: { profil
phone: form.phone || undefined,
hireDate: new Date(form.hireDate).toISOString(),
monthlySalary: String(form.monthlySalary),
baseSalary: String(form.baseSalary),
performanceSalary: String(form.performanceSalary || '0'),
emergencyContact: form.emergencyContact || undefined,
emergencyPhone: form.emergencyPhone || undefined,
address: form.address || undefined,
@@ -235,7 +239,9 @@ export default function BasicInfo({ profile, employeeId, attachments }: { profil
: []),
]
const salaryFields = [
{ label: '月工资', value: `¥${fmt(profile.monthlySalary)}` },
{ label: '月工资', value: profile.baseSalary != null || profile.performanceSalary != null
? `¥${fmt(profile.monthlySalary)}(基本 ¥${fmt(profile.baseSalary || 0)} + 绩效 ¥${fmt(profile.performanceSalary || 0)}`
: `¥${fmt(profile.monthlySalary)}` },
{ label: '紧急联系人', value: profile.emergencyContact || '未填写' },
{ label: '紧急联系电话', value: profile.emergencyPhone || '未填写' },
{ label: '住址', value: profile.address || '未填写' },
@@ -378,7 +384,22 @@ export default function BasicInfo({ profile, employeeId, attachments }: { profil
<div><Label></Label><Select value={form.education} onChange={(e) => setForm({ ...form, education: e.target.value })}><option value=""></option><option value="博士"></option><option value="硕士"></option><option value="本科"></option><option value="大专"></option><option value="高中"></option><option value="其他"></option></Select></div>
<div><Label>/</Label><Input value={form.position} onChange={(e) => setForm({ ...form, position: e.target.value })} placeholder="如:前端工程师" /></div>
<div><Label></Label><Input type="date" value={form.hireDate} onChange={(e) => setForm({ ...form, hireDate: e.target.value })} /></div>
<div><Label></Label><Input type="number" value={form.monthlySalary} onChange={(e) => setForm({ ...form, monthlySalary: Number(e.target.value) })} /></div>
<div>
<Label></Label>
<Input type="number" value={form.baseSalary} onChange={(e) => {
const base = parseFloat(e.target.value) || 0
const perf = parseFloat(form.performanceSalary) || 0
setForm({ ...form, baseSalary: e.target.value, monthlySalary: base + perf })
}} placeholder="元" />
</div>
<div>
<Label></Label>
<Input type="number" value={form.performanceSalary} onChange={(e) => {
const perf = parseFloat(e.target.value) || 0
const base = parseFloat(form.baseSalary) || 0
setForm({ ...form, performanceSalary: e.target.value, monthlySalary: base + perf })
}} placeholder="可为0" />
</div>
<div><Label></Label><Input value={form.emergencyContact} onChange={(e) => setForm({ ...form, emergencyContact: e.target.value })} placeholder="选填" /></div>
<div><Label></Label><Input value={form.emergencyPhone} onChange={(e) => setForm({ ...form, emergencyPhone: e.target.value })} placeholder="选填" /></div>
<div className="md:col-span-2"><Label></Label><Input value={form.address} onChange={(e) => setForm({ ...form, address: e.target.value })} placeholder="选填" /></div>
+8 -2
View File
@@ -10,11 +10,11 @@ import Button from "../../components/ui/Button"
import { Input, Label, Select } from "../../components/ui/Input"
import { FileText, AlertTriangle, X, Paperclip, Trash2, Info, Download, PenTool } from "lucide-react"
export default function ContractInfo({ employeeId, contracts, hireDate }: { employeeId: string; contracts: any[]; hireDate: string }) {
export default function ContractInfo({ employeeId, contracts, hireDate, employee }: { employeeId: string; contracts: any[]; hireDate: string; employee?: any }) {
const queryClient = useQueryClient()
const confirm = useConfirm()
const [showForm, setShowForm] = useState(false)
const [form, setForm] = useState({ contractType: 'FIXED', signDate: '', startDate: '', endDate: '', contractYears: 3, probationMonths: 0, probationSalary: 0, signMethod: 'PAPER' as 'PAPER' | 'ELECTRONIC', attachmentUrl: '', attachments: [] as { name: string; url: string }[], electronicContractNo: '', electronicContractUrl: '' })
const [form, setForm] = useState({ contractType: 'FIXED', signDate: '', startDate: '', endDate: '', contractYears: 3, probationMonths: 0, probationSalary: 0, baseSalary: 0, performanceSalary: 0, signMethod: 'PAPER' as 'PAPER' | 'ELECTRONIC', attachmentUrl: '', attachments: [] as { name: string; url: string }[], electronicContractNo: '', electronicContractUrl: '' })
const contractFileRef = useRef<HTMLInputElement>(null)
const supplementFileRefs = useRef<Record<string, HTMLInputElement | null>>({})
const [previewUrl, setPreviewUrl] = useState<string | null>(null)
@@ -200,6 +200,8 @@ export default function ContractInfo({ employeeId, contracts, hireDate }: { empl
endDate: '',
probationMonths: 0,
probationSalary: 0,
baseSalary: employee?.baseSalary != null ? employee.baseSalary : 0,
performanceSalary: employee?.performanceSalary != null ? employee.performanceSalary : 0,
signMethod: 'PAPER',
attachmentUrl: '',
attachments: [],
@@ -250,6 +252,8 @@ export default function ContractInfo({ employeeId, contracts, hireDate }: { empl
<div><Label></Label><Input type="number" value={form.probationSalary} onChange={(e) => setForm({ ...form, probationSalary: parseInt(e.target.value) || 0 })} /></div>
</>
)}
<div><Label></Label><Input type="number" value={form.baseSalary} onChange={(e) => setForm({ ...form, baseSalary: parseFloat(e.target.value) || 0 })} placeholder="元/月" /></div>
<div><Label></Label><Input type="number" value={form.performanceSalary} onChange={(e) => setForm({ ...form, performanceSalary: parseFloat(e.target.value) || 0 })} placeholder="可为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' })}>
@@ -324,6 +328,8 @@ export default function ContractInfo({ employeeId, contracts, hireDate }: { empl
<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.baseSalary || 0}/</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.performanceSalary || 0}/</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>
@@ -13,7 +13,7 @@ import { useAuthStore } from '../../store/authStore'
export default function DisciplinaryInfo({ employeeId, records }: { employeeId: string; records: any[] }) {
const queryClient = useQueryClient()
const [showForm, setShowForm] = useState(false)
const [form, setForm] = useState({ violationDate: '', violationType: 'LATE', description: '', severity: 'WARNING', action: 'ORAL_WARNING', actionDetail: '', employeeAck: false, ackDate: '', ackMethod: 'SIGN', witness: '' })
const [form, setForm] = useState({ violationDate: '', violationType: 'LATE', description: '', severity: 'WARNING', action: 'ORAL_WARNING', actionDetail: '', deductionAmount: 0, employeeAck: false, ackDate: '', ackMethod: 'SIGN', witness: '' })
const createMutation = useMutation({
mutationFn: (data: any) => rosterApi.createDisciplinary(employeeId, data),
@@ -56,7 +56,10 @@ export default function DisciplinaryInfo({ employeeId, records }: { employeeId:
{Object.entries(actionMap).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
</Select>
</div>
<div><Label></Label><Input value={form.actionDetail} onChange={(e) => setForm({ ...form, actionDetail: e.target.value })} placeholder="扣款金额/降职说明等" /></div>
<div><Label></Label><Input value={form.actionDetail} onChange={(e) => setForm({ ...form, actionDetail: e.target.value })} placeholder="降职说明等" /></div>
{form.action === 'DEDUCTION' && (
<div><Label></Label><Input type="number" value={form.deductionAmount} onChange={(e) => setForm({ ...form, deductionAmount: Number(e.target.value) || 0 })} placeholder="扣款金额,用于薪资批次导入" /></div>
)}
<div><Label></Label><Input value={form.witness} onChange={(e) => setForm({ ...form, witness: e.target.value })} /></div>
<div className="flex items-center gap-2 pt-6">
<input type="checkbox" id="empAck" checked={form.employeeAck} onChange={(e) => setForm({ ...form, employeeAck: e.target.checked })} />
@@ -90,6 +93,7 @@ export default function DisciplinaryInfo({ employeeId, records }: { employeeId:
<div className="flex items-center gap-2 text-xs">
<span className="text-gray-400"></span>
<span className="px-2 py-0.5 rounded bg-blue-50 text-blue-600">{actionMap[r.action] || r.action}</span>
{r.deductionAmount > 0 && <span className="text-danger font-medium"> ¥{r.deductionAmount.toFixed(2)}</span>}
{r.actionDetail && <span className="text-gray-500">{r.actionDetail}</span>}
</div>
<div className="flex items-center gap-4 text-xs pt-1 border-t">
@@ -133,7 +133,7 @@ export default function DisciplinaryRecords() {
</span>
</td>
<td className="py-2 pr-4 text-gray-600">{ACTION_LABELS[r.action] || r.action}</td>
<td className="py-2 pr-4 max-w-xs truncate text-gray-500 text-xs" title={r.actionDetail}>{r.actionDetail || '-'}</td>
<td className="py-2 pr-4 max-w-xs truncate text-gray-500 text-xs" title={r.actionDetail}>{r.actionDetail || '-'}{r.deductionAmount > 0 && <span className="text-danger"> ¥{r.deductionAmount.toFixed(2)}</span>}</td>
<td className="py-2 pr-4">
{r.employeeAck ? (
<span className="text-xs text-green-600"></span>
@@ -226,6 +226,7 @@ function DisciplinaryForm({ employees, record, onSubmit, onClose }: {
severity: record?.severity || 'WARNING',
action: record?.action || 'ORAL_WARNING',
actionDetail: record?.actionDetail || '',
deductionAmount: record?.deductionAmount || 0,
witness: record?.witness || '',
})
@@ -289,8 +290,14 @@ function DisciplinaryForm({ employees, record, onSubmit, onClose }: {
</div>
<div>
<Label></Label>
<textarea value={form.actionDetail} onChange={(e) => setForm({ ...form, actionDetail: e.target.value })} rows={3} className="w-full px-3 py-2 text-sm border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary" placeholder="详细说明处罚执行情况,如扣款金额、书面警告文号、降职后岗位等" />
<textarea value={form.actionDetail} onChange={(e) => setForm({ ...form, actionDetail: e.target.value })} rows={3} className="w-full px-3 py-2 text-sm border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary" placeholder="详细说明处罚执行情况,如书面警告文号、降职后岗位等" />
</div>
{form.action === 'DEDUCTION' && (
<div>
<Label></Label>
<Input type="number" value={form.deductionAmount} onChange={(e) => setForm({ ...form, deductionAmount: Number(e.target.value) || 0 })} placeholder="扣款金额,用于薪资批次导入" />
</div>
)}
<div>
<Label></Label>
<Input value={form.witness} onChange={(e) => setForm({ ...form, witness: e.target.value })} placeholder="见证人(选填)" />
@@ -62,7 +62,7 @@ export default function EmployeeProfile({ employeeId, onBack }: { employeeId: st
{(activeTab) => (
<>
{activeTab === 'basic' && <BasicInfo profile={profile} employeeId={employeeId} attachments={profile.attachments} />}
{activeTab === 'contract' && <ContractInfo employeeId={employeeId} contracts={profile.contracts} hireDate={profile.hireDate} />}
{activeTab === 'contract' && <ContractInfo employeeId={employeeId} contracts={profile.contracts} hireDate={profile.hireDate} employee={profile} />}
{activeTab === 'payslip' && <PayslipSocialInfo payslips={profile.payslips} socialInsRecords={profile.socialInsRecords} housingFundRecords={profile.housingFundRecords} monthlyProcessRecords={profile.monthlyProcessRecords} employeeId={employeeId} />}
{activeTab === 'disciplinary' && <DisciplinaryInfo employeeId={employeeId} records={profile.disciplinaryRecords} />}
{activeTab === 'attendance' && <AttendanceOvertimeInfo employeeId={employeeId} attendanceRecords={profile.attendanceRecords} overtimeRecords={profile.overtimeRecords} trainingRecords={profile.trainingRecords} />}
+145 -33
View File
@@ -20,19 +20,25 @@ export function SalaryChangeModal({ employee, onClose, onSubmit, loading, error
const todayStr = new Date().toISOString().slice(0, 10)
const [form, setForm] = useState({
newSalary: '',
newBaseSalary: '',
newPerformanceSalary: '',
effectiveDate: todayStr,
reason: '',
})
const handleSubmit = () => {
const base = parseFloat(form.newBaseSalary) || 0
const perf = parseFloat(form.newPerformanceSalary) || 0
onSubmit({
newSalary: parseFloat(form.newSalary),
newSalary: base + perf,
baseSalary: base,
performanceSalary: perf,
effectiveDate: new Date(form.effectiveDate).toISOString(),
reason: form.reason || undefined,
})
}
const canSubmit = form.newSalary && parseFloat(form.newSalary) > 0 && form.effectiveDate
const canSubmit = form.newBaseSalary && parseFloat(form.newBaseSalary) > 0 && form.effectiveDate
return (
<Modal open onClose={onClose} title={`调薪 - ${employee.name}`}>
@@ -43,20 +49,39 @@ export function SalaryChangeModal({ employee, onClose, onSubmit, loading, error
<div className="text-xs text-gray-600 py-1.5">{employee.name} - {employee.department}</div>
</div>
<div>
<Label></Label>
<div className="text-xs text-gray-600 py-1.5">¥{fmt(employee.monthlySalary)}</div>
<Label></Label>
<div className="text-xs text-gray-600 py-1.5">
{employee.baseSalary != null || employee.performanceSalary != null
? `基本 ¥${fmt(employee.baseSalary || 0)} / 绩效 ¥${fmt(employee.performanceSalary || 0)} / 合计 ¥${fmt(employee.monthlySalary)}`
: `¥${fmt(employee.monthlySalary)}`}
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="grid grid-cols-3 gap-3">
<div>
<Label> *</Label>
<Input type="number" value={form.newSalary} onChange={(e) => setForm({ ...form, newSalary: e.target.value })} placeholder="元" />
<Label> *</Label>
<Input type="number" value={form.newBaseSalary} onChange={(e) => {
const base = parseFloat(e.target.value) || 0
const perf = parseFloat(form.newPerformanceSalary) || 0
setForm({ ...form, newBaseSalary: e.target.value, newSalary: String(base + perf) })
}} placeholder="元" />
</div>
<div>
<Label></Label>
<Input type="number" value={form.newPerformanceSalary} onChange={(e) => {
const perf = parseFloat(e.target.value) || 0
const base = parseFloat(form.newBaseSalary) || 0
setForm({ ...form, newPerformanceSalary: e.target.value, newSalary: String(base + perf) })
}} placeholder="可为0" />
</div>
<div>
<Label> *</Label>
<Input type="date" value={form.effectiveDate} onChange={(e) => setForm({ ...form, effectiveDate: e.target.value })} />
</div>
</div>
{(parseFloat(form.newBaseSalary) || 0) + (parseFloat(form.newPerformanceSalary) || 0) > 0 && (
<div className="text-xs text-gray-500">¥{(parseFloat(form.newBaseSalary) || 0) + (parseFloat(form.newPerformanceSalary) || 0)}</div>
)}
<div>
<Label></Label>
<Input value={form.reason} onChange={(e) => setForm({ ...form, reason: e.target.value })} placeholder="如:年度调薪、晋升加薪" />
@@ -336,19 +361,27 @@ export function ConfirmModal({ employee, onClose, onSubmit, loading, error }: {
error: any
}) {
const originalSalary = employee.monthlySalary ? Number(employee.monthlySalary) : null
const originalBaseSalary = employee.baseSalary != null ? Number(employee.baseSalary) : null
const originalPerformanceSalary = employee.performanceSalary != null ? Number(employee.performanceSalary) : null
const [form, setForm] = useState({
confirmDate: new Date().toISOString().slice(0, 10),
regularSalary: originalSalary ? String(originalSalary) : '',
regularBaseSalary: originalBaseSalary != null ? String(originalBaseSalary) : '',
regularPerformanceSalary: originalPerformanceSalary != null ? String(originalPerformanceSalary) : '0',
})
/** 转正薪资与原薪资是否不同 */
const salaryChanged = form.regularSalary !== '' && Number(form.regularSalary) !== originalSalary
const handleSubmit = () => {
const base = parseFloat(form.regularBaseSalary) || 0
const perf = parseFloat(form.regularPerformanceSalary) || 0
onSubmit({
employeeId: employee.id,
confirmDate: new Date(form.confirmDate).toISOString(),
regularSalary: form.regularSalary ? Number(form.regularSalary) : undefined,
regularSalary: base + perf,
baseSalary: base,
performanceSalary: perf,
})
}
@@ -366,16 +399,33 @@ export function ConfirmModal({ employee, onClose, onSubmit, loading, error }: {
<Label> *</Label>
<Input type="date" value={form.confirmDate} onChange={(e) => setForm({ ...form, confirmDate: e.target.value })} />
</div>
<div>
<Label>{originalSalary ? `(原薪资 ¥${originalSalary}` : ''}</Label>
<Input type="number" value={form.regularSalary} onChange={(e) => setForm({ ...form, regularSalary: e.target.value })} placeholder={originalSalary ? `不填则保持原薪资 ¥${originalSalary}` : '请输入转正薪资'} />
<div className="text-xs text-gray-400 mt-1">
{originalSalary
? (salaryChanged
? <span className="text-amber-600"> </span>
: '填写后将更新员工月工资并记录薪资变更')
: '填写后将更新员工月工资并记录薪资变更'}
<div className="grid grid-cols-2 gap-3">
<div>
<Label> *</Label>
<Input type="number" value={form.regularBaseSalary} onChange={(e) => {
const base = parseFloat(e.target.value) || 0
const perf = parseFloat(form.regularPerformanceSalary) || 0
setForm({ ...form, regularBaseSalary: e.target.value, regularSalary: String(base + perf) })
}} placeholder="元" />
</div>
<div>
<Label></Label>
<Input type="number" value={form.regularPerformanceSalary} onChange={(e) => {
const perf = parseFloat(e.target.value) || 0
const base = parseFloat(form.regularBaseSalary) || 0
setForm({ ...form, regularPerformanceSalary: e.target.value, regularSalary: String(base + perf) })
}} placeholder="可为0" />
</div>
</div>
{(parseFloat(form.regularBaseSalary) || 0) + (parseFloat(form.regularPerformanceSalary) || 0) > 0 && (
<div className="text-xs text-gray-500">¥{(parseFloat(form.regularBaseSalary) || 0) + (parseFloat(form.regularPerformanceSalary) || 0)}</div>
)}
<div className="text-xs text-gray-400 mt-1">
{originalSalary
? (salaryChanged
? <span className="text-amber-600"> </span>
: '填写后将更新员工月工资并记录薪资变更')
: '填写后将更新员工月工资并记录薪资变更'}
</div>
{error && (
<div className="text-xs text-danger">
@@ -438,6 +488,9 @@ export function RehireModal({ employee, onClose, onSubmit, loading, error }: {
contractYears: 3,
probationMonths: 0,
probationSalary: 0,
baseSalary: employee.baseSalary != null ? String(employee.baseSalary) : '',
performanceSalary: employee.performanceSalary != null ? String(employee.performanceSalary) : '0',
monthlySalary: employee.monthlySalary ? String(employee.monthlySalary) : '',
socialInsBase: '', socialInsStartMonth: '',
housingFundBase: '', housingFundStartMonth: '',
})
@@ -470,11 +523,14 @@ export function RehireModal({ employee, onClose, onSubmit, loading, error }: {
})()
const monthlySalaryNum = employee?.monthlySalary || 0
const baseSalaryNum = parseFloat(form.baseSalary) || 0
const probationSalaryError = (() => {
if (form.probationMonths <= 0) return ''
if (form.probationSalary <= 0) return '有试用期时试用期工资必填'
if (monthlySalaryNum > 0 && form.probationSalary < monthlySalaryNum * 0.8) {
return `试用期工资不得低于转正工资的80%(最低¥${(monthlySalaryNum * 0.8).toFixed(0)}`
// 试用期工资不得低于基本工资的80%
const refSalary = baseSalaryNum > 0 ? baseSalaryNum : monthlySalaryNum
if (refSalary > 0 && form.probationSalary < refSalary * 0.8) {
return `试用期工资不得低于基本工资的80%(最低¥${(refSalary * 0.8).toFixed(0)}`
}
return ''
})()
@@ -518,9 +574,14 @@ export function RehireModal({ employee, onClose, onSubmit, loading, error }: {
}
const handleSubmit = () => {
const base = parseFloat(form.baseSalary) || 0
const perf = parseFloat(form.performanceSalary) || 0
const data: any = {
hireDate: new Date(form.hireDate).toISOString(),
department: form.department,
baseSalary: base,
performanceSalary: perf,
monthlySalary: base + perf,
socialInsBase: form.socialInsBase ? parseFloat(form.socialInsBase) : undefined,
socialInsStartMonth: form.socialInsStartMonth || undefined,
housingFundBase: form.housingFundBase ? parseFloat(form.housingFundBase) : undefined,
@@ -535,6 +596,8 @@ export function RehireModal({ employee, onClose, onSubmit, loading, error }: {
contractYears: form.contractYears,
probationMonths: form.probationMonths,
probationSalary: form.probationSalary,
baseSalary: base,
performanceSalary: perf,
}
}
onSubmit(data)
@@ -569,13 +632,35 @@ export function RehireModal({ employee, onClose, onSubmit, loading, error }: {
<Label> *</Label>
<Input type="date" value={form.hireDate} onChange={(e) => handleHireDateChange(e.target.value)} />
</div>
<div className="grid grid-cols-3 gap-3">
<div>
<Label> *</Label>
<Input type="number" value={form.baseSalary} onChange={(e) => {
const base = parseFloat(e.target.value) || 0
const perf = parseFloat(form.performanceSalary) || 0
setForm({ ...form, baseSalary: e.target.value, monthlySalary: String(base + perf) })
}} placeholder="元" />
</div>
<div>
<Label></Label>
<Input type="number" value={form.performanceSalary} onChange={(e) => {
const perf = parseFloat(e.target.value) || 0
const base = parseFloat(form.baseSalary) || 0
setForm({ ...form, performanceSalary: e.target.value, monthlySalary: String(base + perf) })
}} placeholder="可为0" />
</div>
<div>
<Label></Label>
<div className="text-xs text-gray-600 py-1.5">¥{(parseFloat(form.baseSalary) || 0) + (parseFloat(form.performanceSalary) || 0)}</div>
</div>
</div>
<div className="border-t pt-3">
<Label></Label>
<div className="text-xs text-gray-400 mb-2"></div>
<div className="text-xs text-gray-400 mb-2"></div>
<div className="grid grid-cols-4 gap-3">
<div>
<Label></Label>
<Input type="number" value={form.socialInsBase} onChange={(e) => setForm({ ...form, socialInsBase: e.target.value })} onFocus={(e) => e.target.select()} placeholder={employee?.monthlySalary || '默认为月工资'} />
<Input type="number" value={form.socialInsBase} onChange={(e) => setForm({ ...form, socialInsBase: e.target.value })} onFocus={(e) => e.target.select()} placeholder={form.monthlySalary || '默认为月薪总额'} />
</div>
<div>
<Label></Label>
@@ -583,7 +668,7 @@ export function RehireModal({ employee, onClose, onSubmit, loading, error }: {
</div>
<div>
<Label></Label>
<Input type="number" value={form.housingFundBase} onChange={(e) => setForm({ ...form, housingFundBase: e.target.value })} onFocus={(e) => e.target.select()} placeholder={employee?.monthlySalary || '默认为月工资'} />
<Input type="number" value={form.housingFundBase} onChange={(e) => setForm({ ...form, housingFundBase: e.target.value })} onFocus={(e) => e.target.select()} placeholder={form.monthlySalary || '默认为月薪总额'} />
</div>
<div>
<Label></Label>
@@ -641,8 +726,8 @@ export function RehireModal({ employee, onClose, onSubmit, loading, error }: {
<div>
<Label>{form.probationMonths > 0 ? ' *' : ''}</Label>
<Input type="number" value={form.probationSalary} onChange={(e) => setForm({ ...form, probationSalary: parseFloat(e.target.value) || 0 })} disabled={form.probationMonths <= 0} />
{monthlySalaryNum > 0 && form.probationMonths > 0 && (
<div className="text-xs text-gray-400 mt-0.5">80%¥{(monthlySalaryNum * 0.8).toFixed(0)}</div>
{baseSalaryNum > 0 && form.probationMonths > 0 && (
<div className="text-xs text-gray-400 mt-0.5">80%¥{(baseSalaryNum * 0.8).toFixed(0)}</div>
)}
{probationSalaryError && <div className="text-xs text-danger mt-0.5">{probationSalaryError}</div>}
</div>
@@ -704,6 +789,7 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
} catch {}
return {
name: '', department: '', departmentId: '', position: '', hireDate: todayStr, monthlySalary: '',
baseSalary: '', performanceSalary: '0',
idCardNumber: '', gender: '男' as '男' | '女', femaleWorkerType: '' as '' | 'CADRE' | 'WORKER', phone: '',
city: '北京', education: '', status: 'ACTIVE',
contractType: 'FIXED' as 'FIXED' | 'UNFIXED' | 'UNSIGNED',
@@ -900,11 +986,14 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
})()
const monthlySalaryNum = parseFloat(form.monthlySalary) || 0
const baseSalaryNum = parseFloat(form.baseSalary) || 0
const probationSalaryError = (() => {
if (form.probationMonths <= 0) return ''
if (form.probationSalary <= 0) return '有试用期时试用期工资必填'
if (monthlySalaryNum > 0 && form.probationSalary < monthlySalaryNum * 0.8) {
return `试用期工资不得低于转正工资的80%(最低¥${(monthlySalaryNum * 0.8).toFixed(0)}`
// 试用期工资不得低于基本工资的80%
const refSalary = baseSalaryNum > 0 ? baseSalaryNum : monthlySalaryNum
if (refSalary > 0 && form.probationSalary < refSalary * 0.8) {
return `试用期工资不得低于基本工资的80%(最低¥${(refSalary * 0.8).toFixed(0)}`
}
return ''
})()
@@ -944,7 +1033,10 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
name: form.name, department: form.department, departmentId: form.departmentId || undefined,
position: form.position || undefined,
hireDate: new Date(form.hireDate).toISOString(),
monthlySalary: form.monthlySalary, gender: form.gender,
monthlySalary: form.monthlySalary,
baseSalary: form.baseSalary,
performanceSalary: form.performanceSalary || '0',
gender: form.gender,
femaleWorkerType: form.gender === '女' && form.femaleWorkerType ? form.femaleWorkerType : undefined,
idCardNumber: form.idCardNumber || undefined,
phone: form.phone || undefined,
@@ -971,7 +1063,7 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
onSubmit(data)
}
const canSubmit = form.name && form.department && form.hireDate && form.monthlySalary
const canSubmit = form.name && form.department && form.hireDate && form.baseSalary
&& form.idCardNumber.length >= 18
&& ageWarning?.type !== 'BLOCK'
&& (form.contractType === 'UNSIGNED' || form.startDate)
@@ -979,7 +1071,7 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
// 超龄人员不得签订劳动合同
&& (!isOverage || ['LABOR', 'INTERNSHIP', 'UNSIGNED'].includes(form.contractType))
const isDirty = !!(form.name || form.department || form.idCardNumber || form.monthlySalary || form.phone)
const isDirty = !!(form.name || form.department || form.idCardNumber || form.baseSalary || form.phone)
useUnsavedChanges(isDirty)
return (
@@ -1022,7 +1114,27 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
</div>
<div className="grid grid-cols-4 gap-4">
<div><Label> *</Label><Input type="date" value={form.hireDate} onChange={(e) => handleHireDateChange(e.target.value)} /></div>
<div><Label> *</Label><Input type="number" value={form.monthlySalary} onChange={(e) => setForm({ ...form, monthlySalary: e.target.value })} placeholder="元" /></div>
<div>
<Label> *</Label>
<Input type="number" value={form.baseSalary} onChange={(e) => {
const base = e.target.value
const perf = parseFloat(form.performanceSalary) || 0
const total = (parseFloat(base) || 0) + perf
setForm({ ...form, baseSalary: base, monthlySalary: total > 0 ? String(total) : '' })
}} placeholder="元" />
</div>
<div>
<Label></Label>
<Input type="number" value={form.performanceSalary} onChange={(e) => {
const perf = e.target.value
const base = parseFloat(form.baseSalary) || 0
const total = base + (parseFloat(perf) || 0)
setForm({ ...form, performanceSalary: perf, monthlySalary: total > 0 ? String(total) : '' })
}} placeholder="可为0" />
{(parseFloat(form.baseSalary) || 0) + (parseFloat(form.performanceSalary) || 0) > 0 && (
<div className="text-xs text-gray-400 mt-0.5"> ¥{(parseFloat(form.baseSalary) || 0) + (parseFloat(form.performanceSalary) || 0)}</div>
)}
</div>
<div><Label></Label><Input value={form.phone} onChange={(e) => {
const phone = e.target.value.replace(/\D/g, '').slice(0, 11)
setForm({ ...form, phone })
@@ -1051,7 +1163,7 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
{isNoSocialContract ? (
<span className="text-xs text-amber-600">/</span>
) : (
<span className="text-xs text-gray-500"></span>
<span className="text-xs text-gray-500"></span>
)}
</div>
{/* 账户选择(选部门后自动带出,可手动调整) */}
@@ -1193,8 +1305,8 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
<div>
<Label>{form.probationMonths > 0 ? ' *' : ''}</Label>
<Input type="number" value={form.probationSalary} onChange={(e) => setForm({ ...form, probationSalary: parseFloat(e.target.value) || 0 })} disabled={form.probationMonths <= 0} />
{monthlySalaryNum > 0 && form.probationMonths > 0 && (
<div className="text-xs text-gray-500 mt-1">80%¥{(monthlySalaryNum * 0.8).toFixed(0)}</div>
{baseSalaryNum > 0 && form.probationMonths > 0 && (
<div className="text-xs text-gray-500 mt-1">80%¥{(baseSalaryNum * 0.8).toFixed(0)}</div>
)}
{probationSalaryError && <div className="text-xs text-danger mt-1">{probationSalaryError}</div>}
</div>