From 5c3f3503ea35a49f1f2bfb56cce7e26da65701b6 Mon Sep 17 00:00:00 2001 From: selfrelease Date: Sat, 15 Aug 2026 15:22:06 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=8A=B1=E5=90=8D=E5=86=8C=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E8=81=8C=E5=8A=A1=E7=AD=9B=E9=80=89=20+=20=E5=8E=BB?= =?UTF-8?q?=E6=8E=89=E8=A7=A3=E8=81=98=E9=A1=B5=E6=96=B0=E5=BB=BA=E6=8C=89?= =?UTF-8?q?=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端: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> --- backend/src/routes/roster.routes.ts | 19 +++++++++++++++++++ frontend/src/pages/Roster.tsx | 27 ++++++++++++++++++++++++--- frontend/src/pages/Termination.tsx | 7 +------ 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/backend/src/routes/roster.routes.ts b/backend/src/routes/roster.routes.ts index db2a0ce..1531da6 100644 --- a/backend/src/routes/roster.routes.ts +++ b/backend/src/routes/roster.routes.ts @@ -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 } }, diff --git a/frontend/src/pages/Roster.tsx b/frontend/src/pages/Roster.tsx index 0bf5d40..373a691 100644 --- a/frontend/src/pages/Roster.tsx +++ b/frontend/src/pages/Roster.tsx @@ -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(null) const { data: rosterData, isLoading, isError, error, refetch } = useQuery({ - 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({ + 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() { {departmentList?.map((d: string) => )} + {hasActiveFilters && ( )} @@ -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' diff --git a/frontend/src/pages/Termination.tsx b/frontend/src/pages/Termination.tsx index 7056d32..4528f09 100644 --- a/frontend/src/pages/Termination.tsx +++ b/frontend/src/pages/Termination.tsx @@ -803,11 +803,6 @@ export default function Termination() {

规范处理离职、解聘审批及补偿核算流程

- {view === 'list' && ( - - )} {view !== 'list' && (