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:
@@ -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 },
|
||||
],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user