From 1d954e144a487017f5fd65f2676f53e4b115eb5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E8=AF=97=E6=95=8F?= <3130004661@qq.com> Date: Thu, 14 Aug 2025 14:38:21 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E5=9B=A2=E9=98=9F=E6=88=90=E5=91=98=E6=97=B6=E7=9A=84=E6=A8=A1?= =?UTF-8?q?=E7=B3=8A=E6=9F=A5=E6=89=BE=E5=8A=9F=E8=83=BD=E4=B8=8E=E8=BF=9B?= =?UTF-8?q?=E5=BA=A6=E7=AE=A1=E7=90=86=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/system/user.ts | 5 + src/types/auto-imports.d.ts | 2 +- .../construction-personnel.vue | 127 +- .../personnel-dispatch/index.vue | 40 +- .../projects/progress/index.vue | 1766 +++++++++++++---- 5 files changed, 1516 insertions(+), 424 deletions(-) diff --git a/src/apis/system/user.ts b/src/apis/system/user.ts index 59257e1..479f7f4 100644 --- a/src/apis/system/user.ts +++ b/src/apis/system/user.ts @@ -63,3 +63,8 @@ export function resetUserPwd(data: any, id: string) { export function updateUserRole(data: { roleIds: string[] }, id: string) { return http.patch(`${BASE_URL}/${id}/role`, data) } + +/** @desc 按姓名模糊搜索用户 */ +export function searchUserByName(name: string) { + return http.get(`${BASE_URL}/searchByName`, { name }) +} diff --git a/src/types/auto-imports.d.ts b/src/types/auto-imports.d.ts index 369aad4..eab6be6 100644 --- a/src/types/auto-imports.d.ts +++ b/src/types/auto-imports.d.ts @@ -70,6 +70,6 @@ declare global { // for type re-export declare global { // @ts-ignore - export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' + export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue' import('vue') } diff --git a/src/views/project-management/personnel-dispatch/construction-personnel.vue b/src/views/project-management/personnel-dispatch/construction-personnel.vue index c25741e..ef46a07 100644 --- a/src/views/project-management/personnel-dispatch/construction-personnel.vue +++ b/src/views/project-management/personnel-dispatch/construction-personnel.vue @@ -180,7 +180,17 @@
- +
@@ -304,7 +314,7 @@ \ No newline at end of file diff --git a/src/views/project-management/personnel-dispatch/index.vue b/src/views/project-management/personnel-dispatch/index.vue index 2451c01..a0bcbd8 100644 --- a/src/views/project-management/personnel-dispatch/index.vue +++ b/src/views/project-management/personnel-dispatch/index.vue @@ -79,7 +79,7 @@ 负责人: {{ project.manager || '未指定' }}
-
+
团队: {{ project.teamSize > 0 ? project.teamSize + '人' : '未设置' }}
@@ -215,7 +215,7 @@ 负责人: {{ project.manager || '未指定' }}
-
+
团队: {{ project.teamSize > 0 ? project.teamSize + '人' : '未设置' }}
@@ -351,7 +351,7 @@ 负责人: {{ project.manager || '未指定' }}
-
+
团队: {{ project.teamSize > 0 ? project.teamSize + '人' : '未设置' }}
@@ -487,7 +487,7 @@ 负责人: {{ project.manager || '未指定' }}
-
+
团队: {{ project.teamSize > 0 ? project.teamSize + '人' : '未设置' }}
@@ -623,7 +623,7 @@ 负责人: {{ project.manager || '未指定' }}
-
+
团队: {{ project.teamSize > 0 ? project.teamSize + '人' : '未设置' }}
@@ -759,7 +759,7 @@ 负责人: {{ project.manager || '未指定' }}
-
+
团队: {{ project.teamSize > 0 ? project.teamSize + '人' : '未设置' }}
@@ -895,7 +895,7 @@ 负责人: {{ project.manager || '未指定' }}
-
+
团队: {{ project.teamSize > 0 ? project.teamSize + '人' : '未设置' }}
@@ -1031,7 +1031,7 @@ 负责人: {{ project.manager || '未指定' }}
-
+
团队: {{ project.teamSize > 0 ? project.teamSize + '人' : '未设置' }}
@@ -1180,10 +1180,10 @@ 负责人: {{ project.manager || '未指定' }}
-
- - 团队: {{ project.teamSize > 0 ? project.teamSize + '人' : '未设置' }} -
+
+ + 团队: {{ project.teamSize > 0 ? project.teamSize + '人' : '未设置' }} +
项目进度 @@ -1327,7 +1327,7 @@ 负责人: {{ project.manager || '未指定' }}
-
+
团队: {{ project.teamSize > 0 ? project.teamSize + '人' : '未设置' }}
@@ -1474,7 +1474,7 @@ 负责人: {{ project.manager || '未指定' }}
-
+
团队: {{ project.teamSize > 0 ? project.teamSize + '人' : '未设置' }}
@@ -1621,7 +1621,7 @@ 负责人: {{ project.manager || '未指定' }}
-
+
团队: {{ project.teamSize > 0 ? project.teamSize + '人' : '未设置' }}
@@ -1915,7 +1915,7 @@ 负责人: {{ project.manager || '未指定' }}
-
+
团队: {{ project.teamSize > 0 ? project.teamSize + '人' : '未设置' }}
@@ -2062,7 +2062,7 @@ 负责人: {{ project.manager || '未指定' }}
-
+
团队: {{ project.teamSize > 0 ? project.teamSize + '人' : '未设置' }}
@@ -3041,11 +3041,11 @@ const openSiteManagement = (project: any) => { } // 人员管理 -const openPersonnelManagement = () => { - if (currentProject.value && currentProject.value.id) { +const openPersonnelManagement = (project: any) => { + if (project && project.id) { router.push({ path: '/project-management/personnel-dispatch/construction-personnel', - query: { projectId: currentProject.value.id } + query: { projectId: project.id } }) } else { Message.error('项目信息不完整,无法进入团队成员管理') diff --git a/src/views/project-management/projects/progress/index.vue b/src/views/project-management/projects/progress/index.vue index 812e92c..ae5a12d 100644 --- a/src/views/project-management/projects/progress/index.vue +++ b/src/views/project-management/projects/progress/index.vue @@ -1,476 +1,1448 @@ - - \ No newline at end of file + From ee5534d0520cab84a171eddae8a135f399d5c497 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E8=AF=97=E6=95=8F?= <3130004661@qq.com> Date: Thu, 14 Aug 2025 20:23:28 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E5=88=9D=E6=AD=A5=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E8=BF=9B=E5=BA=A6=E7=AE=A1=E7=90=86=E5=92=8C=E4=BA=BA=E5=91=98?= =?UTF-8?q?=E7=BB=84=E7=BB=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/project/personnel-organization.ts | 54 + src/apis/project/type.ts | 130 ++ src/router/route.ts | 12 +- src/types/components.d.ts | 63 + .../projects/personnel-organization/index.vue | 1385 +++++++++++++++++ .../projects/progress/index.vue | 86 +- 6 files changed, 1686 insertions(+), 44 deletions(-) create mode 100644 src/apis/project/personnel-organization.ts create mode 100644 src/views/project-management/projects/personnel-organization/index.vue diff --git a/src/apis/project/personnel-organization.ts b/src/apis/project/personnel-organization.ts new file mode 100644 index 0000000..0d07776 --- /dev/null +++ b/src/apis/project/personnel-organization.ts @@ -0,0 +1,54 @@ +import type * as T from './type' +import http from '@/utils/http' + +const BASE_URL = '/project/personnel-organization' + +/** @desc 获取项目列表 */ +export function getProjectList(query?: T.ProjectQuery) { + return http.get>('/project/list', query) +} + +/** @desc 获取项目人员组织信息 */ +export function getPersonnelOrganization(projectId: string | number) { + return http.get(`${BASE_URL}/${projectId}`) +} + +/** @desc 获取工种列表 */ +export function getWorkTypeList() { + return http.get('/work-type/list') +} + +/** @desc 获取施工人员列表 */ +export function getConstructorList(query: T.ConstructorQuery) { + return http.get>('/constructor/list', query) +} + +/** @desc 获取施工人员技能证书 */ +export function getConstructorCertifications(constructorId: string | number) { + return http.get(`/constructor/${constructorId}/certifications`) +} + +/** @desc 分配施工人员到项目 */ +export function assignConstructorToProject(data: T.AssignConstructorReq) { + return http.post(`${BASE_URL}/assign`, data) +} + +/** @desc 移除项目施工人员 */ +export function removeConstructorFromProject(data: T.RemoveConstructorReq) { + return http.delete(`${BASE_URL}/remove`, data) +} + +/** @desc 更新施工人员分配信息 */ +export function updateConstructorAssignment(data: T.UpdateAssignmentReq) { + return http.put(`${BASE_URL}/update`, data) +} + +/** @desc 获取项目人员统计 */ +export function getPersonnelStats(projectId: string | number) { + return http.get(`${BASE_URL}/${projectId}/stats`) +} + +/** @desc 导出项目人员组织 */ +export function exportPersonnelOrganization(projectId: string | number) { + return http.download(`${BASE_URL}/${projectId}/export`) +} diff --git a/src/apis/project/type.ts b/src/apis/project/type.ts index 5d5f527..ea6c210 100644 --- a/src/apis/project/type.ts +++ b/src/apis/project/type.ts @@ -327,7 +327,137 @@ export interface UpdateRequirementStatusForm { status: 'pending' | 'recruiting' | 'completed' } +// ==================== 人员组织相关类型 ==================== +/** 工种响应 */ +export interface WorkTypeResp { + id: string | number + name: string + code: string + description?: string + requiredCertifications?: string[] + sort?: number +} + +/** 施工人员响应 */ +export interface ConstructorResp { + id: string | number + name: string + phone: string + email?: string + avatar?: string + workTypes: string[] // 工种列表 + certifications: CertificationResp[] + experience: number // 工作经验(年) + status: 'ACTIVE' | 'INACTIVE' | 'BUSY' + rating: number // 评分 + joinDate: string + remark?: string +} + +/** 证书响应 */ +export interface CertificationResp { + id: string | number + name: string + type: string + code: string + issueDate: string + expiryDate: string + issuingAuthority: string + status: 'VALID' | 'EXPIRED' | 'EXPIRING_SOON' + imageUrl?: string +} + +/** 施工人员查询参数 */ +export interface ConstructorQuery extends PageQuery { + name?: string + workType?: string + status?: string + hasCertification?: boolean + experienceMin?: number + experienceMax?: number +} + +/** 人员组织响应 */ +export interface PersonnelOrganizationResp { + projectId: string | number + projectName: string + workTypeAssignments: WorkTypeAssignmentResp[] + totalPersonnel: number + assignedPersonnel: number + availablePersonnel: number +} + +/** 工种分配响应 */ +export interface WorkTypeAssignmentResp { + workTypeId: string | number + workTypeName: string + workTypeCode: string + requiredCount: number + assignedCount: number + constructors: ConstructorAssignmentResp[] +} + +/** 施工人员分配响应 */ +export interface ConstructorAssignmentResp { + constructorId: string | number + constructorName: string + phone: string + avatar?: string + workTypes: string[] + certifications: CertificationResp[] + experience: number + rating: number + assignmentDate: string + status: 'ASSIGNED' | 'WORKING' | 'COMPLETED' + remark?: string +} + +/** 分配施工人员请求 */ +export interface AssignConstructorReq { + projectId: string | number + workTypeId: string | number + constructorIds: (string | number)[] + assignmentDate?: string + remark?: string +} + +/** 移除施工人员请求 */ +export interface RemoveConstructorReq { + projectId: string | number + constructorIds: (string | number)[] + reason?: string +} + +/** 更新分配信息请求 */ +export interface UpdateAssignmentReq { + projectId: string | number + constructorId: string | number + workTypeId?: string | number + status?: string + remark?: string +} + +/** 人员统计响应 */ +export interface PersonnelStatsResp { + totalPersonnel: number + assignedPersonnel: number + availablePersonnel: number + workTypeStats: { + workTypeName: string + requiredCount: number + assignedCount: number + shortageCount: number + }[] + certificationStats: { + certificationType: string + count: number + }[] + experienceStats: { + range: string + count: number + }[] +} /** 分页查询基础参数 */ export interface PageQuery { diff --git a/src/router/route.ts b/src/router/route.ts index 05536b6..1156b79 100644 --- a/src/router/route.ts +++ b/src/router/route.ts @@ -804,7 +804,17 @@ export const systemRoutes: RouteRecordRaw[] = [ hidden: false, }, }, - + { + path: '/project-management/projects/personnel-organization', + name: 'PersonnelOrganization', + component: () => import('@/views/project-management/projects/personnel-organization/index.vue'), + meta: { + title: '人员组织', + icon: 'user-group', + hidden: false, + }, + }, + { path: '/project-management/projects/device', name: 'DeviceManagement', diff --git a/src/types/components.d.ts b/src/types/components.d.ts index 7fa6b1b..a427ece 100644 --- a/src/types/components.d.ts +++ b/src/types/components.d.ts @@ -7,7 +7,70 @@ export {} declare module 'vue' { export interface GlobalComponents { + ApprovalAssistant: typeof import('./../components/ApprovalAssistant/index.vue')['default'] + ApprovalMessageItem: typeof import('./../components/NotificationCenter/ApprovalMessageItem.vue')['default'] + Avatar: typeof import('./../components/Avatar/index.vue')['default'] + Breadcrumb: typeof import('./../components/Breadcrumb/index.vue')['default'] + CellCopy: typeof import('./../components/CellCopy/index.vue')['default'] + Chart: typeof import('./../components/Chart/index.vue')['default'] + CircularProgress: typeof import('./../components/CircularProgress/index.vue')['default'] + ColumnSetting: typeof import('./../components/GiTable/src/components/ColumnSetting.vue')['default'] + CronForm: typeof import('./../components/GenCron/CronForm/index.vue')['default'] + CronModal: typeof import('./../components/GenCron/CronModal/index.vue')['default'] + DateRangePicker: typeof import('./../components/DateRangePicker/index.vue')['default'] + DayForm: typeof import('./../components/GenCron/CronForm/component/day-form.vue')['default'] + FilePreview: typeof import('./../components/FilePreview/index.vue')['default'] + GiCellAvatar: typeof import('./../components/GiCell/GiCellAvatar.vue')['default'] + GiCellGender: typeof import('./../components/GiCell/GiCellGender.vue')['default'] + GiCellStatus: typeof import('./../components/GiCell/GiCellStatus.vue')['default'] + GiCellTag: typeof import('./../components/GiCell/GiCellTag.vue')['default'] + GiCellTags: typeof import('./../components/GiCell/GiCellTags.vue')['default'] + GiCodeView: typeof import('./../components/GiCodeView/index.vue')['default'] + GiDot: typeof import('./../components/GiDot/index.tsx')['default'] + GiEditTable: typeof import('./../components/GiEditTable/GiEditTable.vue')['default'] + GiFooter: typeof import('./../components/GiFooter/index.vue')['default'] + GiForm: typeof import('./../components/GiForm/src/GiForm.vue')['default'] + GiIconBox: typeof import('./../components/GiIconBox/index.vue')['default'] + GiIconSelector: typeof import('./../components/GiIconSelector/index.vue')['default'] + GiIframe: typeof import('./../components/GiIframe/index.vue')['default'] + GiOption: typeof import('./../components/GiOption/index.vue')['default'] + GiOptionItem: typeof import('./../components/GiOptionItem/index.vue')['default'] + GiPageLayout: typeof import('./../components/GiPageLayout/index.vue')['default'] + GiSpace: typeof import('./../components/GiSpace/index.vue')['default'] + GiSplitButton: typeof import('./../components/GiSplitButton/index.vue')['default'] + GiSplitPane: typeof import('./../components/GiSplitPane/index.vue')['default'] + GiSplitPaneFlexibleBox: typeof import('./../components/GiSplitPane/components/GiSplitPaneFlexibleBox.vue')['default'] + GiSvgIcon: typeof import('./../components/GiSvgIcon/index.vue')['default'] + GiTable: typeof import('./../components/GiTable/src/GiTable.vue')['default'] + GiTag: typeof import('./../components/GiTag/index.tsx')['default'] + GiThemeBtn: typeof import('./../components/GiThemeBtn/index.vue')['default'] + HourForm: typeof import('./../components/GenCron/CronForm/component/hour-form.vue')['default'] + Icon403: typeof import('./../components/icons/Icon403.vue')['default'] + Icon404: typeof import('./../components/icons/Icon404.vue')['default'] + Icon500: typeof import('./../components/icons/Icon500.vue')['default'] + IconBorders: typeof import('./../components/icons/IconBorders.vue')['default'] + IconTableSize: typeof import('./../components/icons/IconTableSize.vue')['default'] + IconTreeAdd: typeof import('./../components/icons/IconTreeAdd.vue')['default'] + IconTreeReduce: typeof import('./../components/icons/IconTreeReduce.vue')['default'] + ImageImport: typeof import('./../components/ImageImport/index.vue')['default'] + ImageImportWizard: typeof import('./../components/ImageImportWizard/index.vue')['default'] + IndustrialImageList: typeof import('./../components/IndustrialImageList/index.vue')['default'] + JsonPretty: typeof import('./../components/JsonPretty/index.vue')['default'] + MinuteForm: typeof import('./../components/GenCron/CronForm/component/minute-form.vue')['default'] + MonthForm: typeof import('./../components/GenCron/CronForm/component/month-form.vue')['default'] + NotificationCenter: typeof import('./../components/NotificationCenter/index.vue')['default'] + ParentView: typeof import('./../components/ParentView/index.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] + SecondForm: typeof import('./../components/GenCron/CronForm/component/second-form.vue')['default'] + SplitPanel: typeof import('./../components/SplitPanel/index.vue')['default'] + TextCopy: typeof import('./../components/TextCopy/index.vue')['default'] + TurbineGrid: typeof import('./../components/TurbineGrid/index.vue')['default'] + UserSelect: typeof import('./../components/UserSelect/index.vue')['default'] + Verify: typeof import('./../components/Verify/index.vue')['default'] + VerifyPoints: typeof import('./../components/Verify/Verify/VerifyPoints.vue')['default'] + VerifySlide: typeof import('./../components/Verify/Verify/VerifySlide.vue')['default'] + WeekForm: typeof import('./../components/GenCron/CronForm/component/week-form.vue')['default'] + YearForm: typeof import('./../components/GenCron/CronForm/component/year-form.vue')['default'] } } diff --git a/src/views/project-management/projects/personnel-organization/index.vue b/src/views/project-management/projects/personnel-organization/index.vue new file mode 100644 index 0000000..ae2284d --- /dev/null +++ b/src/views/project-management/projects/personnel-organization/index.vue @@ -0,0 +1,1385 @@ + + + + + diff --git a/src/views/project-management/projects/progress/index.vue b/src/views/project-management/projects/progress/index.vue index ae5a12d..153b22b 100644 --- a/src/views/project-management/projects/progress/index.vue +++ b/src/views/project-management/projects/progress/index.vue @@ -49,9 +49,9 @@ 📅 {{ formatShortDate(project.endDate) }} -
+
@@ -68,9 +68,9 @@

{{ selectedProject.projectName }}

- - {{ getStatusText(selectedProject.status) }} - + + {{ getStatusText(selectedProject.status) }} + {{ selectedProject.projectCode }}
@@ -626,37 +626,37 @@ const availableWeeks = ref([ // 时间轴周数 - 简化版本 const timelineWeeks = computed(() => { try { - const weeks = [] + const weeks = [] const startDate = new Date('2025-01-06') - - for (let i = 0; i < 4; i++) { + + for (let i = 0; i < 4; i++) { const weekStart = dayjs.add(startDate, i * 7, 'day') - const days = [] - - for (let j = 0; j < 7; j++) { + const days = [] + + for (let j = 0; j < 7; j++) { const currentDate = dayjs.add(weekStart, j, 'day') const dayNumber = currentDate.getDate() const weekday = ['日', '一', '二', '三', '四', '五', '六'][currentDate.getDay()] const isWeekend = currentDate.getDay() === 0 || currentDate.getDay() === 6 const isToday = dayjs.isSame(currentDate, new Date(), 'day') - - days.push({ - date: dayjs.format(currentDate, 'YYYY-MM-DD'), - dayNumber, - weekday, - isWeekend, - isToday - }) - } - weeks.push({ - value: `week${i + 1}`, - label: `第${i + 1}周`, - days + days.push({ + date: dayjs.format(currentDate, 'YYYY-MM-DD'), + dayNumber, + weekday, + isWeekend, + isToday }) } - return weeks + weeks.push({ + value: `week${i + 1}`, + label: `第${i + 1}周`, + days + }) + } + + return weeks } catch (error) { console.error('计算时间轴出错:', error) return [] @@ -666,7 +666,7 @@ const timelineWeeks = computed(() => { // 计算属性 const getProjectCountByStatus = (status: string) => { try { - return projects.value.filter(p => getStatusKey(p.status) === status).length + return projects.value.filter(p => getStatusKey(p.status) === status).length } catch (error) { console.error('计算项目数量出错:', error) return 0 @@ -675,7 +675,7 @@ const getProjectCountByStatus = (status: string) => { const getProjectsByStatus = (status: string) => { try { - return projects.value.filter(p => getStatusKey(p.status) === status) + return projects.value.filter(p => getStatusKey(p.status) === status) } catch (error) { console.error('筛选项目出错:', error) return [] @@ -684,10 +684,10 @@ const getProjectsByStatus = (status: string) => { const getTotalWorkload = () => { try { - if (!selectedProject.value) return 0 - return projectTasks.value - .filter(task => !task.isSubTask) - .reduce((total, task) => total + (task.workDays || 0), 0) + if (!selectedProject.value) return 0 + return projectTasks.value + .filter(task => !task.isSubTask) + .reduce((total, task) => total + (task.workDays || 0), 0) } catch (error) { console.error('计算工作量出错:', error) return 0 @@ -796,8 +796,8 @@ const isTaskActiveOnDay = (task: any, date: string) => { const isTaskCompletedOnDay = (task: any, date: string) => { try { - if (task.progress >= 100) return true - + if (task.progress >= 100) return true + const taskStart = new Date(task.startDate) const taskEnd = new Date(task.endDate) const currentDate = new Date(date) @@ -805,10 +805,10 @@ const isTaskCompletedOnDay = (task: any, date: string) => { if (!dayjs.isBetween(currentDate, taskStart, taskEnd, 'day', '[]')) return false const totalDays = dayjs.diff(taskEnd, taskStart, 'day') + 1 - const completedDays = Math.floor((task.progress / 100) * totalDays) + const completedDays = Math.floor((task.progress / 100) * totalDays) const daysFromStart = dayjs.diff(currentDate, taskStart, 'day') + 1 - - return daysFromStart <= completedDays + + return daysFromStart <= completedDays } catch (error) { console.error('检查任务完成状态出错:', error) return false @@ -820,10 +820,10 @@ const getTaskBarStyle = (task: any, date: string) => { const taskStart = new Date(task.startDate) const currentDate = new Date(date) const daysFromStart = dayjs.diff(currentDate, taskStart, 'day') - - return { - left: `${daysFromStart * 40}px`, - width: '40px' + + return { + left: `${daysFromStart * 40}px`, + width: '40px' } } catch (error) { console.error('计算任务条样式出错:', error) @@ -840,9 +840,9 @@ onMounted(() => { console.log('项目数据:', projects.value) console.log('任务数据:', projectTasks.value) - // 默认选择第一个项目 - if (projects.value.length > 0) { - selectProject(projects.value[0]) + // 默认选择第一个项目 + if (projects.value.length > 0) { + selectProject(projects.value[0]) console.log('已选择默认项目:', projects.value[0]) } else { console.warn('没有可用的项目数据') @@ -963,7 +963,7 @@ onMounted(() => { margin-bottom: 4px; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; } - + .project-manager { font-size: 12px; color: #86909c; From 4ea535b82f0f0db332eed463869a6d9293e7c112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E8=AF=97=E6=95=8F?= <3130004661@qq.com> Date: Fri, 15 Aug 2025 09:46:31 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BA=BA=E5=91=98?= =?UTF-8?q?=E7=BB=84=E7=BB=87=E6=9F=A5=E7=9C=8B=E5=B7=A5=E4=BD=9C=E7=BB=84?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/project/type.ts | 165 +++++ src/apis/project/workGroup.ts | 81 +++ .../projects/personnel-organization/index.vue | 606 ++++++++++++++++-- 3 files changed, 792 insertions(+), 60 deletions(-) create mode 100644 src/apis/project/workGroup.ts diff --git a/src/apis/project/type.ts b/src/apis/project/type.ts index ea6c210..7840bcc 100644 --- a/src/apis/project/type.ts +++ b/src/apis/project/type.ts @@ -117,6 +117,86 @@ export interface ProjectKanbanStats { teamLeaderCount: string } +// ==================== 工作组相关类型 ==================== + +/** 工作组类型 */ +export interface WorkGroup { + id: string + name: string + description: string + projectId: string + status: 'ACTIVE' | 'INACTIVE' | 'COMPLETED' + workTypeGroups: WorkTypeGroup[] + memberCount: number + requiredCount: number + members?: WorkGroupMember[] + createTime: string + updateTime?: string +} + +/** 工种分组类型 */ +export interface WorkTypeGroup { + id?: string + name: string + requiredCount: number + priority: 'HIGH' | 'MEDIUM' | 'LOW' + assignedCount?: number +} + +/** 工作组成员类型 */ +export interface WorkGroupMember { + id: string + name: string + role: string + avatar?: string + phone?: string + email?: string + workTypes?: string[] + joinTime?: string +} + +/** 工作组查询参数 */ +export interface WorkGroupQuery { + projectId?: string + name?: string + status?: string + workType?: string +} + +/** 工作组分页查询参数 */ +export interface WorkGroupPageQuery extends WorkGroupQuery, PageQuery {} + +/** 创建工作组请求参数 */ +export interface CreateWorkGroupReq { + name: string + description?: string + projectId: string + workTypeGroups: WorkTypeGroup[] +} + +/** 更新工作组请求参数 */ +export interface UpdateWorkGroupReq { + id: string + name?: string + description?: string + status?: string + workTypeGroups?: WorkTypeGroup[] +} + +/** 分配工人到工作组请求参数 */ +export interface AssignWorkerReq { + workGroupId: string + workerIds: string[] + workTypeName: string +} + +/** 从工作组移除工人请求参数 */ +export interface RemoveWorkerReq { + workGroupId: string + workerId: string + workTypeName: string +} + /** 项目看板数据 */ export interface ProjectKanbanData { inProgressProjects: ProjectCard[] @@ -471,4 +551,89 @@ export interface PageRes { total: number page: number pageSize: number +} + +// ==================== 工作组相关类型 ==================== + +/** 工作组类型 */ +export interface WorkGroup { + id: string + name: string + description: string + projectId: string + status: 'ACTIVE' | 'INACTIVE' | 'COMPLETED' + workTypeGroups: WorkTypeGroup[] + memberCount: number + requiredCount: number + members?: WorkGroupMember[] + createTime: string + updateTime?: string +} + +/** 工种分组类型 */ +export interface WorkTypeGroup { + id?: string + name: string + requiredCount: number + priority: 'HIGH' | 'MEDIUM' | 'LOW' + assignedCount?: number +} + +/** 工作组成员类型 */ +export interface WorkGroupMember { + id: string + name: string + role: string + avatar?: string + phone?: string + email?: string + workTypes?: string[] + joinTime?: string +} + +/** 工作组查询参数 */ +export interface WorkGroupQuery { + projectId?: string + name?: string + status?: string + workType?: string +} + +/** 工作组分页查询参数 */ +export interface WorkGroupPageQuery extends WorkGroupQuery, PageQuery {} + +/** 创建工作组请求参数 */ +export interface CreateWorkGroupReq { + name: string + description?: string + projectId: string + workTypeGroups: WorkTypeGroup[] +} + +/** 更新工作组请求参数 */ +export interface UpdateWorkGroupReq { + id: string + name?: string + description?: string + status?: string + workTypeGroups?: WorkTypeGroup[] +} + +/** 分配工人到工作组请求参数 */ +export interface AssignWorkerReq { + workGroupId: string + workerIds: string[] + workTypeName: string +} + +/** 从工作组移除工人请求参数 */ +export interface RemoveWorkerReq { + workGroupId: string + workerId: string + workTypeName: string +} + +// 扩展项目类型,包含工作组信息 +export interface ProjectWithWorkGroups extends ProjectResp { + workGroups?: WorkGroup[] } \ No newline at end of file diff --git a/src/apis/project/workGroup.ts b/src/apis/project/workGroup.ts new file mode 100644 index 0000000..3a6985f --- /dev/null +++ b/src/apis/project/workGroup.ts @@ -0,0 +1,81 @@ +import request from '@/utils/http' +import type { + WorkGroup, + WorkTypeGroup, + WorkGroupPageQuery, + CreateWorkGroupReq, + UpdateWorkGroupReq, + AssignWorkerReq, + RemoveWorkerReq, + PageRes +} from './type' + +/** + * 获取工作组列表 + */ +export function getWorkGroupList(params: WorkGroupPageQuery) { + return request.get>('/api/work-groups', params) +} + +/** + * 根据项目ID获取工作组列表 + */ +export function getWorkGroupsByProject(projectId: string) { + return request.get(`/api/projects/${projectId}/work-groups`) +} + +/** + * 获取工作组详情 + */ +export function getWorkGroupDetail(id: string) { + return request.get(`/api/work-groups/${id}`) +} + +/** + * 创建工作组 + */ +export function createWorkGroup(data: CreateWorkGroupReq) { + return request.post('/api/work-groups', data) +} + +/** + * 更新工作组 + */ +export function updateWorkGroup(id: string, data: UpdateWorkGroupReq) { + return request.put(`/api/work-groups/${id}`, data) +} + +/** + * 删除工作组 + */ +export function deleteWorkGroup(id: string) { + return request.del(`/api/work-groups/${id}`) +} + +/** + * 分配工人到工作组 + */ +export function assignWorkerToGroup(data: AssignWorkerReq) { + return request.post('/api/work-groups/assign-worker', data) +} + +/** + * 从工作组移除工人 + */ +export function removeWorkerFromGroup(data: RemoveWorkerReq) { + return request.post('/api/work-groups/remove-worker', data) +} + +/** + * 更新工作组状态 + */ +export function updateWorkGroupStatus(id: string, status: string) { + return request.patch(`/api/work-groups/${id}/status`, { status }) +} + +/** + * 获取工作组统计信息 + */ +export function getWorkGroupStats(projectId?: string) { + return request.get('/api/work-groups/stats', { projectId }) +} diff --git a/src/views/project-management/projects/personnel-organization/index.vue b/src/views/project-management/projects/personnel-organization/index.vue index ae2284d..6a42055 100644 --- a/src/views/project-management/projects/personnel-organization/index.vue +++ b/src/views/project-management/projects/personnel-organization/index.vue @@ -1,5 +1,5 @@ @@ -436,13 +526,57 @@ import { IconCheck, IconFilter, IconClose, - IconSave + IconSave, + IconEye } from '@arco-design/web-vue/es/icon' import type { ProjectResp, ConstructorResp, - CertificationResp + CertificationResp, + WorkGroup, + WorkTypeGroup, + WorkGroupMember, + ProjectWithWorkGroups } from '@/apis/project/type' +import { + getWorkGroupsByProject, + createWorkGroup as createWorkGroupAPI, + assignWorkerToGroup, + removeWorkerFromGroup +} from '@/apis/project/workGroup' + +// 移除重复的类型定义,使用导入的类型 +// interface WorkGroup { +// id: string +// name: string +// description: string +// projectId: string +// status: 'ACTIVE' | 'INACTIVE' | 'COMPLETED' +// workTypeGroups: WorkTypeGroup[] +// memberCount: number +// requiredCount: number +// members?: WorkGroupMember[] +// createTime: string +// } + +// interface WorkTypeGroup { +// name: string +// requiredCount: number +// priority: 'HIGH' | 'MEDIUM' | 'LOW' +// } + +// interface WorkGroupMember { +// id: string +// name: string +// role: string +// avatar?: string +// phone?: string +// } + +// 扩展项目类型,包含工作组信息 +// interface ProjectWithWorkGroups extends ProjectResp { +// workGroups?: WorkGroup[] +// } defineOptions({ name: 'PersonnelOrganization' }) @@ -450,11 +584,13 @@ defineOptions({ name: 'PersonnelOrganization' }) const currentStep = ref(1) // 项目数据 -const projectList = ref([]) -const selectedProject = ref(null) +const projectList = ref([]) +const selectedProject = ref(null) +const selectedProjectForDetail = ref(null) // 工作组表单 const workGroupForm = reactive({ + id: '', // 添加工作组ID字段 name: '', description: '', projectId: '', @@ -483,6 +619,7 @@ const workerAssignments = ref>({}) // 弹窗状态 const certificateModalVisible = ref(false) const selectedWorkerCertificates = ref([]) +const workGroupDetailModalVisible = ref(false) // 拖拽状态 const draggedWorker = ref(null) @@ -499,7 +636,7 @@ const getGroupMembers = (groupName: string) => { // 方法 const loadProjectList = async () => { try { - // 使用模拟数据 + // 使用模拟数据,包含工作组信息 projectList.value = [ { projectId: '1', @@ -507,7 +644,44 @@ const loadProjectList = async () => { projectName: '风电场A区建设项目', farmName: '风电场A区', status: 1, - statusLabel: '进行中' + statusLabel: '进行中', + workGroups: [ + { + id: 'wg1', + name: '高空作业组', + description: '负责塔上登高作业', + projectId: '1', + status: 'ACTIVE', + workTypeGroups: [ + { name: '塔上登高作业', requiredCount: 5, priority: 'HIGH' } + ], + memberCount: 3, + requiredCount: 5, + members: [ + { id: 'm1', name: '张登高', role: '组长', avatar: '' }, + { id: 'm2', name: '李高空', role: '组员', avatar: '' }, + { id: 'm3', name: '王高空', role: '组员', avatar: '' } + ], + createTime: '2024-01-15' + }, + { + id: 'wg2', + name: '地勤保障组', + description: '负责地面保障工作', + projectId: '1', + status: 'ACTIVE', + workTypeGroups: [ + { name: '地勤人员', requiredCount: 3, priority: 'MEDIUM' } + ], + memberCount: 2, + requiredCount: 3, + members: [ + { id: 'm4', name: '赵地勤', role: '组长', avatar: '' }, + { id: 'm5', name: '孙地勤', role: '组员', avatar: '' } + ], + createTime: '2024-01-16' + } + ] }, { projectId: '2', @@ -515,7 +689,29 @@ const loadProjectList = async () => { projectName: '风电场B区维护项目', farmName: '风电场B区', status: 2, - statusLabel: '已完成' + statusLabel: '已完成', + workGroups: [ + { + id: 'wg3', + name: '维护保养组', + description: '负责设备维护保养', + projectId: '2', + status: 'COMPLETED', + workTypeGroups: [ + { name: '机械工程师', requiredCount: 2, priority: 'HIGH' }, + { name: '电气工程师', requiredCount: 2, priority: 'HIGH' } + ], + memberCount: 4, + requiredCount: 4, + members: [ + { id: 'm6', name: '周机械', role: '机械工程师', avatar: '' }, + { id: 'm7', name: '吴电气', role: '电气工程师', avatar: '' }, + { id: 'm8', name: '郑机械', role: '机械工程师', avatar: '' }, + { id: 'm9', name: '王电气', role: '电气工程师', avatar: '' } + ], + createTime: '2024-01-10' + } + ] }, { projectId: '3', @@ -523,9 +719,21 @@ const loadProjectList = async () => { projectName: '风电场C区升级项目', farmName: '风电场C区', status: 0, - statusLabel: '准备中' + statusLabel: '准备中', + workGroups: [] } ] + + // 为每个项目加载工作组信息 + for (const project of projectList.value) { + try { + const workGroups = await getWorkGroupsByProject(project.projectId) + project.workGroups = workGroups + } catch (error) { + console.warn(`加载项目 ${project.projectName} 的工作组失败:`, error) + // 保持模拟数据作为后备 + } + } } catch (error) { Message.error('加载项目列表失败') } @@ -662,11 +870,11 @@ const loadWorkers = async () => { } } -const selectProject = (project: ProjectResp) => { +const selectProject = (project: ProjectWithWorkGroups) => { selectedProject.value = project } -const enterWorkGroupCreation = (project: ProjectResp) => { +const enterWorkGroupCreation = (project: ProjectWithWorkGroups) => { selectedProject.value = project workGroupForm.projectId = project.projectId currentStep.value = 2 @@ -696,7 +904,7 @@ const goBackToStep2 = () => { currentStep.value = 2 } -const createWorkGroup = () => { +const createWorkGroup = async () => { if (!workGroupForm.name || !workGroupForm.projectId) { Message.warning('请填写工作组名称和关联项目') return @@ -708,11 +916,38 @@ const createWorkGroup = () => { return } - currentStep.value = 3 - loadWorkers() + try { + // 调用API创建工作组 + const newWorkGroup = await createWorkGroupAPI({ + name: workGroupForm.name, + description: workGroupForm.description, + projectId: workGroupForm.projectId, + workTypeGroups: workGroupForm.workTypeGroups + }) + + Message.success('工作组创建成功') + + // 保存新创建的工作组ID + workGroupForm.id = newWorkGroup.id + + // 更新项目的工作组列表 + if (selectedProject.value) { + if (!selectedProject.value.workGroups) { + selectedProject.value.workGroups = [] + } + selectedProject.value.workGroups.push(newWorkGroup) + } + + currentStep.value = 3 + loadWorkers() + } catch (error) { + Message.error('创建工作组失败') + console.error('创建工作组错误:', error) + } } const resetWorkGroupForm = () => { + workGroupForm.id = '' // 重置工作组ID workGroupForm.name = '' workGroupForm.description = '' workGroupForm.projectId = '' @@ -797,16 +1032,34 @@ const handleDrop = (event: DragEvent, group: any) => { Message.warning('该工种分组人数已满') return } - + workerAssignments.value[groupName].push(draggedWorker.value) Message.success(`已将 ${draggedWorker.value.name} 分配到 ${groupName}`) } -const removeFromGroup = (member: ConstructorResp, groupName: string) => { - const index = workerAssignments.value[groupName].findIndex(m => m.id === member.id) - if (index > -1) { - workerAssignments.value[groupName].splice(index, 1) +const removeFromGroup = async (member: ConstructorResp, groupName: string) => { + try { + if (!workGroupForm.id) { + Message.error('工作组ID不存在') + return + } + + await removeWorkerFromGroup({ + workGroupId: workGroupForm.id, + workerId: member.id, + workTypeName: groupName + }) + + // 从本地分配数据中移除 + const index = workerAssignments.value[groupName].findIndex(m => m.id === member.id) + if (index > -1) { + workerAssignments.value[groupName].splice(index, 1) + } + Message.success(`已从 ${groupName} 移除 ${member.name}`) + } catch (error) { + Message.error('移除工人失败') + console.error('移除工人错误:', error) } } @@ -831,14 +1084,51 @@ const saveAssignment = async () => { Message.warning('请至少分配一名工人') return } + + if (!workGroupForm.id) { + Message.error('工作组ID不存在') + return + } + + // 调用API保存分配结果 + const assignmentPromises = [] + for (const [workTypeName, workers] of Object.entries(workerAssignments.value)) { + if (workers.length > 0) { + const workerIds = workers.map(w => w.id) + assignmentPromises.push( + assignWorkerToGroup({ + workGroupId: workGroupForm.id, + workerIds, + workTypeName + }) + ) + } + } + + await Promise.all(assignmentPromises) Message.success('分配保存成功') - // 这里可以调用API保存分配结果 + + // 刷新项目的工作组信息 + if (selectedProject.value) { + try { + const workGroups = await getWorkGroupsByProject(selectedProject.value.projectId) + selectedProject.value.workGroups = workGroups + } catch (error) { + console.warn('刷新工作组信息失败:', error) + } + } } catch (error) { Message.error('保存分配失败') + console.error('保存分配错误:', error) } } +const viewProjectWorkGroups = (project: ProjectWithWorkGroups) => { + selectedProjectForDetail.value = project + workGroupDetailModalVisible.value = true +} + // 工具方法 const getStatusColor = (status: number) => { const statusMap: Record = { @@ -885,9 +1175,27 @@ const getCertificateStatusText = (status: string) => { return statusMap[status] || status } +const getWorkGroupStatusColor = (status: string) => { + const statusMap: Record = { + 'ACTIVE': 'green', + 'INACTIVE': 'red', + 'COMPLETED': 'gray' + } + return statusMap[status] || 'gray' +} + +const getWorkGroupStatusText = (status: string) => { + const statusMap: Record = { + 'ACTIVE': '进行中', + 'INACTIVE': '已暂停', + 'COMPLETED': '已完成' + } + return statusMap[status] || status +} + // 生命周期 onMounted(async () => { - await loadProjectList() + await loadProjectList() }) @@ -896,16 +1204,7 @@ onMounted(async () => { padding: 20px; background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); min-height: 100vh; - overflow-y: auto; - height: auto; - position: relative; - z-index: 1; - box-sizing: border-box; -} - -// 确保页面可以滚动 -:deep(.gi-page-layout__body) { - overflow: auto !important; + overflow-y: auto; // 添加垂直滚动 } // 流程导航样式 @@ -918,6 +1217,7 @@ onMounted(async () => { background: white; border-radius: 16px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); + flex-shrink: 0; // 防止压缩 } .flow-step { @@ -958,8 +1258,7 @@ onMounted(async () => { .step-content { max-width: 1200px; margin: 0 auto; - overflow-y: auto; - height: auto; + margin-bottom: 30px; // 添加底部间距 } // 项目选择卡片 @@ -1023,7 +1322,7 @@ onMounted(async () => { color: white; font-size: 20px; } - + .project-info { margin-bottom: 20px; @@ -1033,13 +1332,13 @@ onMounted(async () => { font-weight: 600; color: #1d2129; } - + .project-code { margin: 0 0 4px 0; font-size: 14px; color: #86909c; } - + .project-farm { margin: 0; font-size: 14px; @@ -1047,8 +1346,49 @@ onMounted(async () => { } } +.project-work-groups { + margin-top: 20px; + padding-top: 16px; + border-top: 1px solid #e9ecef; + + .work-groups-header { + display: flex; + align-items: center; + gap: 8px; + font-size: 16px; + font-weight: 600; + color: #1d2129; + margin-bottom: 12px; + } + + .work-groups-list { + display: flex; + flex-wrap: wrap; + gap: 12px; + } + + .work-group-preview { + background: #f8f9fa; + border: 1px solid #e9ecef; + border-radius: 8px; + padding: 8px 12px; + font-size: 14px; + color: #1d2129; + display: flex; + align-items: center; + gap: 8px; + } + + .more-work-groups { + font-size: 14px; + color: #86909c; + font-weight: 500; + } +} + .project-actions { text-align: center; + margin-top: 20px; } // 工作组创建表单 @@ -1103,19 +1443,17 @@ onMounted(async () => { display: grid; grid-template-columns: 280px 1fr 320px; gap: 24px; - min-height: calc(100vh - 200px); - height: auto; + min-height: 600px; // 设置最小高度而不是固定高度 } .filter-sidebar, .work-groups-sidebar { .filter-card, .work-groups-card { - min-height: 100%; - height: auto; + height: 100%; border-radius: 16px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); - + .card-title { display: flex; align-items: center; @@ -1134,8 +1472,7 @@ onMounted(async () => { .worker-table-section { .worker-table-card { - min-height: 100%; - height: auto; + height: 100%; border-radius: 16px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); @@ -1145,7 +1482,7 @@ onMounted(async () => { gap: 8px; font-size: 16px; font-weight: 600; - + .worker-count { color: #86909c; font-weight: 400; @@ -1155,9 +1492,8 @@ onMounted(async () => { } .worker-table { - min-height: calc(100vh - 300px); - max-height: calc(100vh - 300px); - overflow-y: auto; + max-height: 500px; // 设置最大高度 + overflow-y: auto; // 添加垂直滚动 padding: 16px; } @@ -1230,7 +1566,7 @@ onMounted(async () => { justify-content: space-between; align-items: center; margin-bottom: 8px; - + .label { font-size: 12px; color: #86909c; @@ -1254,12 +1590,11 @@ onMounted(async () => { // 工种分组 .work-groups-content { - min-height: calc(100vh - 300px); - max-height: calc(100vh - 300px); - overflow-y: auto; + max-height: 400px; // 设置最大高度 + overflow-y: auto; // 添加垂直滚动 padding: 16px; } - + .work-group-drop-zone { background: white; border: 2px dashed #d9d9d9; @@ -1267,26 +1602,26 @@ onMounted(async () => { padding: 16px; margin-bottom: 16px; transition: all 0.3s ease; - + &:hover { border-color: #667eea; background: #f8f9ff; } } - + .work-group-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; - + h4 { margin: 0; font-size: 16px; font-weight: 600; color: #1d2129; } - + .work-group-stats { font-size: 14px; font-weight: 600; @@ -1299,7 +1634,7 @@ onMounted(async () => { margin: 0 4px; color: #86909c; } - + .required { color: #1890ff; } @@ -1318,7 +1653,7 @@ onMounted(async () => { background: #f8f9fa; border-radius: 8px; margin-bottom: 8px; - + .member-name { flex: 1; font-size: 14px; @@ -1382,4 +1717,155 @@ onMounted(async () => { color: #4e5969; } } + +// 工作组详情弹窗 +.work-group-detail { + .project-info-section { + margin-bottom: 20px; + padding-bottom: 16px; + border-bottom: 1px solid #e9ecef; + + h3 { + margin: 0 0 8px 0; + font-size: 18px; + font-weight: 600; + color: #1d2129; + } + + .project-code { + font-size: 14px; + color: #86909c; + } + } + + .work-groups-section { + h4 { + margin: 0 0 16px 0; + font-size: 16px; + font-weight: 600; + color: #1d2129; + } + + .work-groups-detail-list { + .work-group-detail-item { + background: #f8f9fa; + border: 1px solid #e9ecef; + border-radius: 8px; + padding: 16px; + margin-bottom: 16px; + + .work-group-detail-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + + h5 { + margin: 0; + font-size: 15px; + font-weight: 600; + color: #1d2129; + } + + .work-group-detail-stats { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + color: #86909c; + + .member-count { + color: #52c41a; + } + } + } + + .work-group-detail-members { + .members-header { + margin-bottom: 12px; + font-size: 14px; + color: #1d2129; + font-weight: 500; + } + + .members-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); + gap: 12px; + } + + .member-detail-item { + display: flex; + align-items: center; + gap: 12px; + + .member-detail-info { + .member-name { + font-size: 14px; + color: #1d2129; + font-weight: 600; + } + .member-role { + font-size: 12px; + color: #86909c; + } + } + } + } + + .work-group-detail-description { + p { + margin: 0; + font-size: 13px; + color: #4e5969; + } + } + } + } + } +} + +// 响应式设计 +@media (max-width: 1200px) { + .worker-assignment-layout { + grid-template-columns: 250px 1fr 280px; + gap: 16px; + } +} + +@media (max-width: 992px) { + .worker-assignment-layout { + grid-template-columns: 1fr; + gap: 16px; + } + + .filter-sidebar, + .work-groups-sidebar { + order: 2; + } + + .worker-table-section { + order: 1; + } +} + +@media (max-width: 768px) { + .personnel-organization-container { + padding: 16px; + } + + .project-grid { + grid-template-columns: 1fr; + gap: 16px; + } + + .process-flow { + flex-direction: column; + gap: 16px; + + .flow-arrow { + transform: rotate(90deg); + } + } +}