feat: 花名册增加职务筛选 + 去掉解聘页新建按钮
- 后端:roster list 接口增加 position 筛选参数 - 后端:新增 /roster/positions 接口返回在职员工职务去重列表 - 前端:花名册筛选栏增加"全部职务"下拉框 - 前端:导出也支持 position 参数 - 前端:去掉解聘补偿页面的"新建解聘"按钮,解聘统一从花名册发起 - 前端:空状态提示改为"请在花名册中选择员工发起解聘" Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -46,6 +46,21 @@ router.get('/departments', authMiddleware, async (req: AuthRequest, res, next) =
|
||||
}
|
||||
})
|
||||
|
||||
/** 花名册可选职务列表(从在职员工中提取去重) */
|
||||
router.get('/positions', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const employees = await prisma.employee.findMany({
|
||||
where: { orgId: req.user!.orgId, status: 'ACTIVE' },
|
||||
select: { position: true },
|
||||
distinct: 'position',
|
||||
})
|
||||
const positions = employees.map((e) => e.position).filter(Boolean).sort()
|
||||
res.json({ success: true, data: positions })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 花名册列表(含汇总信息,支持分页和过滤)
|
||||
router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
@@ -55,6 +70,7 @@ router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
const status = req.query.status as string // ACTIVE | PRE_HIRE | RESIGNED
|
||||
const contractStatus = req.query.contractStatus as string // active | expiring | expired | unsigned | etc.
|
||||
const department = req.query.department as string
|
||||
const position = req.query.position as string
|
||||
const skip = (page - 1) * pageSize
|
||||
|
||||
// 使用本地日期午夜,避免时区问题导致当天入职被误判为预入职
|
||||
@@ -71,6 +87,9 @@ router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
if (department) {
|
||||
whereBase.department = department
|
||||
}
|
||||
if (position) {
|
||||
whereBase.position = position
|
||||
}
|
||||
if (search && !isIdCardSearch) {
|
||||
whereBase.OR = [
|
||||
{ name: { contains: search } },
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { useConfirm } from '../hooks/useConfirm'
|
||||
import { Users, Plus, Check, UserX, UserPlus, DollarSign, Building2, RotateCcw, Upload, Download, Phone, MapPin, Search, Settings2, CheckCircle, FileText } from 'lucide-react'
|
||||
import { rosterApi, employeeApi, terminationApi, workProcessApi } from '../lib/api-services'
|
||||
import api from '../lib/api'
|
||||
import { copyToClipboard } from '../lib/clipboard'
|
||||
import { useAuthStore } from '../store/authStore'
|
||||
import { useDebouncedValue } from '../hooks/useDebouncedValue'
|
||||
@@ -98,13 +99,14 @@ export default function Roster() {
|
||||
const [filterStatus, setFilterStatus] = useState('')
|
||||
const [filterContractStatus, setFilterContractStatus] = useState('')
|
||||
const [filterDepartment, setFilterDepartment] = useState('')
|
||||
const [filterPosition, setFilterPosition] = useState('')
|
||||
const [showBatchTerminateModal, setShowBatchTerminateModal] = useState(false)
|
||||
const [batchTerminateDate, setBatchTerminateDate] = useState(() => new Date().toISOString().slice(0, 10))
|
||||
const [batchTerminateReason, setBatchTerminateReason] = useState('NEGOTIATED')
|
||||
const [terminatePreviewData, setTerminatePreviewData] = useState<any>(null)
|
||||
|
||||
const { data: rosterData, isLoading, isError, error, refetch } = useQuery<any>({
|
||||
queryKey: ['roster', page, pageSize, debouncedSearch, filterStatus, filterContractStatus, filterDepartment],
|
||||
queryKey: ['roster', page, pageSize, debouncedSearch, filterStatus, filterContractStatus, filterDepartment, filterPosition],
|
||||
queryFn: async () => {
|
||||
const params: any = { page, pageSize }
|
||||
if (debouncedSearch) params.search = debouncedSearch
|
||||
@@ -112,7 +114,8 @@ export default function Roster() {
|
||||
if (filterStatus === 'PROBATION') params.status = 'ACTIVE'
|
||||
if (filterContractStatus) params.contractStatus = filterContractStatus
|
||||
if (filterDepartment) params.department = filterDepartment
|
||||
const res = await rosterApi.list({ search: debouncedSearch, page, pageSize, status: filterStatus === 'PROBATION' ? 'ACTIVE' : filterStatus || undefined, contractStatus: filterContractStatus || undefined, department: filterDepartment || undefined } as any) as any
|
||||
if (filterPosition) params.position = filterPosition
|
||||
const res = await rosterApi.list({ search: debouncedSearch, page, pageSize, status: filterStatus === 'PROBATION' ? 'ACTIVE' : filterStatus || undefined, contractStatus: filterContractStatus || undefined, department: filterDepartment || undefined, position: filterPosition || undefined } as any) as any
|
||||
return res
|
||||
},
|
||||
})
|
||||
@@ -425,10 +428,11 @@ export default function Roster() {
|
||||
setFilterStatus('')
|
||||
setFilterContractStatus('')
|
||||
setFilterDepartment('')
|
||||
setFilterPosition('')
|
||||
setPage(1)
|
||||
}
|
||||
|
||||
const hasActiveFilters = search || filterStatus || filterContractStatus || filterDepartment
|
||||
const hasActiveFilters = search || filterStatus || filterContractStatus || filterDepartment || filterPosition
|
||||
|
||||
/** 全局合同风险统计(来自后端,与风险中心同口径) */
|
||||
const globalRiskStats = rosterData?.globalRiskStats
|
||||
@@ -457,6 +461,14 @@ export default function Roster() {
|
||||
},
|
||||
})
|
||||
|
||||
const { data: positionList } = useQuery<string[]>({
|
||||
queryKey: ['roster-positions'],
|
||||
queryFn: async () => {
|
||||
const res = await api.get('/roster/positions')
|
||||
return res.data as string[]
|
||||
},
|
||||
})
|
||||
|
||||
const { isVisible: colVisible, toggle: colToggle } = useRosterColumns()
|
||||
const [showColSettings, setShowColSettings] = useState(false)
|
||||
|
||||
@@ -521,6 +533,14 @@ export default function Roster() {
|
||||
<option value="">全部部门</option>
|
||||
{departmentList?.map((d: string) => <option key={d} value={d}>{d}</option>)}
|
||||
</select>
|
||||
<select
|
||||
value={filterPosition}
|
||||
onChange={(e) => { setFilterPosition(e.target.value); setPage(1) }}
|
||||
className="h-9 rounded-md border border-gray-200 bg-white px-3 text-sm text-gray-700 shadow-sm outline-none transition focus:border-primary focus:ring-2 focus:ring-primary/10"
|
||||
>
|
||||
<option value="">全部职务</option>
|
||||
{positionList?.map((p: string) => <option key={p} value={p}>{p}</option>)}
|
||||
</select>
|
||||
{hasActiveFilters && (
|
||||
<button onClick={clearFilters} className="h-9 px-2 text-sm text-gray-400 transition hover:text-gray-700">清除筛选</button>
|
||||
)}
|
||||
@@ -536,6 +556,7 @@ export default function Roster() {
|
||||
if (debouncedSearch) params.set('search', debouncedSearch)
|
||||
if (filterStatus) params.set('status', filterStatus)
|
||||
if (filterDepartment) params.set('department', filterDepartment)
|
||||
if (filterPosition) params.set('position', filterPosition)
|
||||
if (filterContractStatus) params.set('contractStatus', filterContractStatus)
|
||||
const token = useAuthStore.getState().accessToken
|
||||
const baseURL = import.meta.env.DEV ? 'http://localhost:3000/api/v1' : '/api/v1'
|
||||
|
||||
@@ -803,11 +803,6 @@ export default function Termination() {
|
||||
<p className="mt-1 text-sm text-gray-500">规范处理离职、解聘审批及补偿核算流程</p>
|
||||
</div>
|
||||
</div>
|
||||
{view === 'list' && (
|
||||
<Button size="sm" onClick={handleNewTermination}>
|
||||
<Plus className="w-4 h-4 mr-1" />新建解聘
|
||||
</Button>
|
||||
)}
|
||||
{view !== 'list' && (
|
||||
<Button variant="secondary" size="sm" onClick={() => { setView('list'); resetWizard() }}>
|
||||
<ChevronLeft className="w-4 h-4 mr-1" />返回列表
|
||||
@@ -896,7 +891,7 @@ export default function Termination() {
|
||||
<EmptyState
|
||||
icon={<List className="w-8 h-8 text-gray-400" />}
|
||||
title="暂无解聘记录"
|
||||
description="点击「新建解聘」开始创建"
|
||||
description="请在花名册中选择员工发起解聘"
|
||||
/>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
|
||||
Reference in New Issue
Block a user