diff --git a/src/api/production-master-data/shift-management.js b/src/api/production-master-data/shift-management.js index 4c4a12a7..4e68c526 100644 --- a/src/api/production-master-data/shift-management.js +++ b/src/api/production-master-data/shift-management.js @@ -1,4 +1,5 @@ import { request } from '@/api/_service' +import qs from 'qs' const BASE = 'system_settings/organization/production_shift_management/' @@ -6,8 +7,12 @@ function params (method, data = {}) { return { method: `system_settings_organization_production_shift_management_${method}`, platform: 'background', ...data } } +function stringifyParams (method, data = {}) { + return qs.stringify(params(method, data), { arrayFormat: 'brackets', allowDots: true, encode: false }) +} + export function getShiftAll (data) { return request({ url: BASE + 'all', method: 'get', params: params('all', data) }) } -export function getShiftList (data) { return request({ url: BASE + 'list', method: 'get', params: params('list', data) }) } +export function getShiftList (data) { return request({ url: BASE + 'list?' + stringifyParams('list', data), method: 'get', params: {} }) } export function createShift (data) { return request({ url: BASE + 'create', method: 'post', data: params('create', data) }) } export function editShift (data) { return request({ url: BASE + 'edit', method: 'put', data: params('edit', data) }) } export function deleteShift (data) { return request({ url: BASE + 'delete', method: 'delete', data: params('delete', data) }) } diff --git a/src/api/production-master-data/team-management.js b/src/api/production-master-data/team-management.js index f88868aa..95cb27f2 100644 --- a/src/api/production-master-data/team-management.js +++ b/src/api/production-master-data/team-management.js @@ -1,4 +1,5 @@ import { request } from '@/api/_service' +import qs from 'qs' const BASE = 'system_settings/organization/production_team_manage/' const MEMBER_BASE = 'system_settings/organization/production_members_manage/' @@ -11,8 +12,12 @@ function memberParams (method, data = {}) { return { method: `system_settings_organization_production_members_manage_${method}`, platform: 'background', ...data } } +function stringifyParams (method, data = {}) { + return qs.stringify(params(method, data), { arrayFormat: 'brackets', allowDots: true, encode: false }) +} + export function getTeamAll (data) { return request({ url: BASE + 'all', method: 'get', params: params('all', data) }) } -export function getTeamList (data) { return request({ url: BASE + 'list', method: 'get', params: params('list', data) }) } +export function getTeamList (data) { return request({ url: BASE + 'list?' + stringifyParams('list', data), method: 'get', params: {} }) } export function createTeam (data) { return request({ url: BASE + 'create', method: 'post', data: params('create', data) }) } export function editTeam (data) { return request({ url: BASE + 'edit', method: 'put', data: params('edit', data) }) } export function deleteTeam (data) { return request({ url: BASE + 'delete', method: 'delete', data: params('delete', data) }) } diff --git a/src/locales/en.json b/src/locales/en.json index 849195e4..21fbc664 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -641,6 +641,10 @@ "select_shift_end_time": "Please select shift end time", "production_team_binding": "Team Binding", "production_team_can_only_bind_one_shift": "One team can only bind one shift", + "rotation_count": "Rotation Count", + "rotation_unit": "Rotation Unit", + "please_select_valid_time": "Please select a valid time", + "shift_time_conflict": "Shift time range conflicts", "please_enter_shift_plan_name": "Please enter shift plan name", "please_enter_shift_plan_code": "Please enter shift plan code", "please_enter_shift_name_row": "Please enter shift name, row: ", diff --git a/src/locales/zh-chs.json b/src/locales/zh-chs.json index b829815f..43f7318c 100644 --- a/src/locales/zh-chs.json +++ b/src/locales/zh-chs.json @@ -641,6 +641,10 @@ "select_shift_end_time": "请选择班次结束时间", "production_team_binding": "生产班组绑定", "production_team_can_only_bind_one_shift": "一个生产班组只能绑定一个班次", + "rotation_count": "轮转次数", + "rotation_unit": "轮转单位", + "please_select_valid_time": "请选择有效时间", + "shift_time_conflict": "班次时间段存在冲突", "please_enter_shift_plan_name": "请输入班次计划名称", "please_enter_shift_plan_code": "请输入班次计划编码", "please_enter_shift_name_row": "请输入班次名称,行号:", diff --git a/src/views/production-master-data/team-model/shift-management/index.vue b/src/views/production-master-data/team-model/shift-management/index.vue index cac53884..539db76c 100644 --- a/src/views/production-master-data/team-model/shift-management/index.vue +++ b/src/views/production-master-data/team-model/shift-management/index.vue @@ -38,8 +38,8 @@ {{ $t(key('add_shift')) }} - - + + @@ -49,7 +49,7 @@ {{ $t(key('select_file')) }}{{ $t(key('download_template')) }} - + {{ $t(key('cancel')) }}{{ $t(key('confirm')) }} @@ -122,7 +122,12 @@ export default { methods: { defaultFormData () { return { name: '', code: '', time_range: '', status: 1, remark: '', weekly_rest_days: [6, 7], cycle: { unit: 'day', length: 0 }, productionTeamIds: [] } }, async loadTeams () { const res = await getTeamAll({}); this.teamOptions = (res && res.data) || res || [] }, - async fetchData () { this.loading = true; try { const res = await getShiftList({ ...this.search, page_no: this.pagination.current, page_size: this.pagination.size }); const { list, total } = readPageData(res); this.tableData = list; this.pagination.total = total } finally { this.loading = false } }, + buildSearchParams () { + const params = { ...this.search } + if (!Array.isArray(params.create_time) || !params.create_time.length) delete params.create_time + return params + }, + async fetchData () { this.loading = true; try { const res = await getShiftList({ ...this.buildSearchParams(), page_no: this.pagination.current, page_size: this.pagination.size }); const { list, total } = readPageData(res); this.tableData = list; this.pagination.total = total } finally { this.loading = false } }, onSearch () { this.pagination.current = 1; this.fetchData() }, onReset () { this.search = { name: '', code: '', create_time: '' }; this.pagination.current = 1; this.fetchData() }, onPageChange (page) { this.pagination.current = page.current; this.pagination.size = page.size; this.fetchData() }, @@ -130,16 +135,47 @@ export default { openEdit (row) { this.handleType = 'edit'; this.dialogTitle = this.key('edit_shift_plan'); this.editId = row.id; this.formData = { name: row.name, code: row.code, time_range: [row.start_time, row.finish_time], status: Number(row.status), remark: row.remark || '', weekly_rest_days: safeJson(row.weekly_rest_days, []), cycle: safeJson(row.cycle, { unit: 'day', length: 0 }), productionTeamIds: safeJson(row.production_team_ids, []) }; this.shiftsData = safeJson(row.shifts, []); this.dialogVisible = true }, addShiftDetail () { this.shiftsData.push({ name: '', start_time: '', finish_time: '', production_team_id: '' }) }, changeProductionTeam (value, index) { if (!value) return; if (this.shiftsData.some((item, i) => i !== index && item.production_team_id === value)) { this.shiftsData[index].production_team_id = ''; this.$message.warning(this.$t(this.key('production_team_can_only_bind_one_shift'))) } }, + changeShiftTime (value, index, field) { + if (!value) { + this.$message.warning(this.$t(this.key('please_select_valid_time'))) + this.shiftsData[index][field] = '' + return + } + const current = this.shiftsData[index] + if (!current.start_time || !current.finish_time) return + if (this.hasShiftTimeConflict(current, index)) { + this.$message.error(this.$t(this.key('shift_time_conflict'))) + this.shiftsData[index][field] = '' + } + }, + toMinute (value) { + const [hour, minute] = String(value || '').split(':').map(Number) + return hour * 60 + minute + }, + getShiftRanges (shift) { + const start = this.toMinute(shift.start_time) + const finish = this.toMinute(shift.finish_time) + if (finish < start) return [[start, 24 * 60], [0, finish]] + return [[start, finish]] + }, + hasShiftTimeConflict (current, currentIndex) { + const currentRanges = this.getShiftRanges(current) + return this.shiftsData.some((item, index) => { + if (index === currentIndex || !item.start_time || !item.finish_time) return false + const ranges = this.getShiftRanges(item) + return currentRanges.some(([start, finish]) => ranges.some(([itemStart, itemFinish]) => start < itemFinish && itemStart < finish)) + }) + }, validateShifts () { for (let i = 0; i < this.shiftsData.length; i++) { const item = this.shiftsData[i]; if (!item.name) return this.$t(this.key('please_enter_shift_name_row')) + (i + 1); if (!item.start_time) return this.$t(this.key('please_select_shift_start_time_row')) + (i + 1); if (!item.finish_time) return this.$t(this.key('please_select_shift_end_time_row')) + (i + 1) } return '' }, - submitDialog () { this.$refs.form.validate(async valid => { if (!valid) return; const error = this.validateShifts(); if (error) { this.$message.error(error); return } this.submitting = true; try { const payload = { ...this.formData, start_time: this.formData.time_range[0], finish_time: this.formData.time_range[1], is_shift: Number(this.formData.cycle.length) > 0 ? 1 : 0, rest_enabled: this.formData.weekly_rest_days.length !== 7 ? 1 : 0, shiftsData: JSON.stringify(this.shiftsData) }; if (this.handleType === 'create') await createShift(payload); else await editShift({ ...payload, id: this.editId }); this.$message.success(this.$t(this.key('operation_successful'))); this.closeDialog(); this.fetchData() } finally { this.submitting = false } }) }, + submitDialog () { this.$refs.form.validate(async valid => { if (!valid) return; const error = this.validateShifts(); if (error) { this.$message.error(error); return } if (this.shiftsData.some((item, index) => this.hasShiftTimeConflict(item, index))) { this.$message.error(this.$t(this.key('shift_time_conflict'))); return } this.submitting = true; try { const payload = { ...this.formData, start_time: this.formData.time_range[0], finish_time: this.formData.time_range[1], is_shift: Number(this.formData.cycle.length) > 0 ? 1 : 0, rest_enabled: this.formData.weekly_rest_days.length !== 7 ? 1 : 0, shiftsData: JSON.stringify(this.shiftsData) }; if (this.handleType === 'create') await createShift(payload); else await editShift({ ...payload, id: this.editId }); this.$message.success(this.$t(this.key('operation_successful'))); this.closeDialog(); this.fetchData() } finally { this.submitting = false } }) }, closeDialog () { this.dialogVisible = false; this.formData = this.defaultFormData(); this.shiftsData = []; this.editId = '' }, async handleDelete (row) { const cancelled = await this.$confirmAction({ message: this.key('delete_department_confirm_message'), title: this.key('prompt'), confirmButtonText: this.key('confirm'), cancelButtonText: this.key('cancel') }, () => deleteShift({ id: [row.id] })); if (cancelled) return; this.$message.success(this.$t(this.key('operation_successful'))); this.fetchData() }, async handleBatchDelete () { if (!this.selectedRows.length) { this.$message.error(this.$t(this.key('please_select_table_data'))); return } const cancelled = await this.$confirmAction({ message: this.key('batch_delete_confirm_message'), title: this.key('prompt'), confirmButtonText: this.key('confirm'), cancelButtonText: this.key('cancel') }, () => deleteShift({ id: this.selectedRows.map(item => item.id) })); if (cancelled) return; this.$message.success(this.$t(this.key('operation_successful'))); this.fetchData() }, openImport () { this.importFileList = []; this.importRows = []; this.importVisible = true }, async downloadTemplate () { this.importLoading = true; try { const res = await getShiftImportTemplate({}); downloadRename(res, 'xlsx', this.$t(this.key('shift_plan_data_import_template'))) } finally { this.importLoading = false } }, - async onImportFileChange (file) { if (!file || !/\.(xls|xlsx)$/i.test(file.name)) { this.$message.error(this.$t(this.key('upload_format_error'))); return } this.importFileList = [file]; this.importTableLoading = true; try { const rows = await readExcel(file.raw); this.importRows = rows.map(row => ({ name: row[this.$t(this.key('shift_plan_name'))], code: row[this.$t(this.key('shift_plan_code'))], start_time: row[this.$t(this.key('start_time'))], finish_time: row[this.$t(this.key('end_time'))], status: row[this.$t(this.key('status'))], shift_name: row[this.$t(this.key('shift_name'))], shifts: { name: row[this.$t(this.key('shift_name'))], start_time: row[this.$t(this.key('shift_start_time'))], finish_time: row[this.$t(this.key('shift_end_time'))], production_team_id: row[this.$t(this.key('production_team_binding'))] } })) } finally { this.importTableLoading = false } }, + async onImportFileChange (file) { if (!file || !/\.(xls|xlsx)$/i.test(file.name)) { this.$message.error(this.$t(this.key('upload_format_error'))); return } this.importFileList = [file]; this.importTableLoading = true; try { const rows = await readExcel(file.raw); this.importRows = rows.map(row => { const shift = { name: row[this.$t(this.key('shift_name'))], start_time: row[this.$t(this.key('shift_start_time'))], finish_time: row[this.$t(this.key('shift_end_time'))], production_team_id: row[this.$t(this.key('production_team_binding'))] }; return { name: row[this.$t(this.key('shift_plan_name'))], code: row[this.$t(this.key('shift_plan_code'))], start_time: row[this.$t(this.key('start_time'))], finish_time: row[this.$t(this.key('end_time'))], production_team_ids: row[this.$t(this.key('production_team'))], cycle: { length: row[this.$t(this.key('rotation_count'))], unit: row[this.$t(this.key('rotation_unit'))] }, cycle_length: row[this.$t(this.key('rotation_count'))], cycle_unit: row[this.$t(this.key('rotation_unit'))], weekly_rest_days: row[this.$t(this.key('rest_day_setting'))], status: row[this.$t(this.key('status'))], shift_name: shift.name, shift_start_time: shift.start_time, shift_finish_time: shift.finish_time, production_team_id: shift.production_team_id, shifts: shift } }) } finally { this.importTableLoading = false } }, async submitImport () { if (!this.importRows.length) { this.$message.error(this.$t(this.key('please_import_department_data'))); return } await importShiftData({ import_data: JSON.stringify(this.importRows) }); this.$message.success(this.$t(this.key('operation_successful'))); this.importVisible = false; this.fetchData() }, - async handleExport () { const cancelled = await this.$confirmAction({ message: this.key('export_confirm_message'), title: this.key('prompt'), confirmButtonText: this.key('confirm'), cancelButtonText: this.key('cancel') }, () => exportShiftTask({ ...this.search, action: 'download' })); if (cancelled) return; this.$message.success(this.$t(this.key('download_task_created'))) } + async handleExport () { const cancelled = await this.$confirmAction({ message: this.key('export_confirm_message'), title: this.key('prompt'), confirmButtonText: this.key('confirm'), cancelButtonText: this.key('cancel') }, () => exportShiftTask({ ...this.buildSearchParams(), action: 'download' })); if (cancelled) return; this.$message.success(this.$t(this.key('download_task_created'))) } } } diff --git a/src/views/production-master-data/team-model/team-management/index.vue b/src/views/production-master-data/team-model/team-management/index.vue index 1371e0bd..c3b40cd3 100644 --- a/src/views/production-master-data/team-model/team-management/index.vue +++ b/src/views/production-master-data/team-model/team-management/index.vue @@ -62,6 +62,17 @@ + {{ $t(key('cancel')) }} {{ $t(key('confirm')) }} @@ -134,6 +145,7 @@ export default { leaderIndex: undefined, formData: { name: '', area_id: '', line_id: '' }, membersData: [], + memberPagination: { current: 1, size: 8, total: 0 }, importVisible: false, importFileList: [], importRows: [], @@ -195,10 +207,15 @@ export default { }, onSearchAreaChange (areaId) { this.search.line_id = ''; this.loadLines(areaId, 'searchLineOptions') }, onFormAreaChange (areaId) { this.formData.line_id = ''; this.loadLines(areaId, 'formLineOptions') }, + buildSearchParams () { + const params = { ...this.search } + if (!Array.isArray(params.create_time) || !params.create_time.length) delete params.create_time + return params + }, async fetchData () { this.loading = true try { - const res = await getTeamList({ ...this.search, page_no: this.pagination.current, page_size: this.pagination.size }) + const res = await getTeamList({ ...this.buildSearchParams(), page_no: this.pagination.current, page_size: this.pagination.size }) const { list, total } = readPageData(res) this.tableData = list this.pagination.total = total @@ -212,12 +229,30 @@ export default { this.handleType = 'edit'; this.dialogTitle = this.key('edit_team'); this.editId = row.id this.formData = { name: row.name, area_id: row.area_id, line_id: row.line_id } await this.loadLines(row.area_id, 'formLineOptions') - const res = await getTeamMemberList({ production_team_id: row.id, page_no: 1, page_size: 10000 }) + this.memberPagination.current = 1 + await this.loadMembers() + this.dialogVisible = true + }, + async loadMembers () { + const res = await getTeamMemberList({ + production_team_id: this.editId, + page_no: this.memberPagination.current, + page_size: this.memberPagination.size + }) const { list } = readPageData(res) this.membersData = list.map(item => ({ ...item, is_main: Number(item.is_main) })) + this.memberPagination.total = readPageData(res).total this.leaderIndex = this.membersData.findIndex(item => Number(item.is_main) === 1) if (this.leaderIndex < 0) this.leaderIndex = undefined - this.dialogVisible = true + }, + onMemberSizeChange (size) { + this.memberPagination.size = size + this.memberPagination.current = 1 + if (this.editId) this.loadMembers() + }, + onMemberCurrentChange (current) { + this.memberPagination.current = current + if (this.editId) this.loadMembers() }, addMember () { this.membersData.push({ user_id: '', is_main: 0 }) }, onLeaderChange (val, index) { @@ -228,7 +263,12 @@ export default { else if (Number(val) === 1) this.leaderIndex = index }, async deleteMember (row, index) { - if (row.id) { await deleteTeamMember({ id: [row.id] }); this.$message.success(this.$t(this.key('operation_successful'))) } + if (row.id) { + await deleteTeamMember({ id: [row.id] }) + this.$message.success(this.$t(this.key('operation_successful'))) + await this.loadMembers() + return + } this.membersData.splice(index, 1) }, submitDialog () { @@ -244,7 +284,15 @@ export default { } finally { this.submitting = false } }) }, - closeDialog () { this.dialogVisible = false; this.formData = { name: '', area_id: '', line_id: '' }; this.membersData = []; this.formLineOptions = []; this.leaderIndex = undefined; this.editId = '' }, + closeDialog () { + this.dialogVisible = false + this.formData = { name: '', area_id: '', line_id: '' } + this.membersData = [] + this.formLineOptions = [] + this.memberPagination = { current: 1, size: 8, total: 0 } + this.leaderIndex = undefined + this.editId = '' + }, async handleDelete (row) { const cancelled = await this.$confirmAction({ message: this.key('delete_team_confirm_message'), title: this.key('prompt'), confirmButtonText: this.key('confirm'), cancelButtonText: this.key('cancel') }, () => deleteTeam({ id: [row.id] })) if (cancelled) return @@ -273,7 +321,7 @@ export default { this.$message.success(this.$t(this.key('operation_successful'))); this.importVisible = false; this.fetchData() }, async handleExport () { - const cancelled = await this.$confirmAction({ message: this.key('export_confirm_message'), title: this.key('prompt'), confirmButtonText: this.key('confirm'), cancelButtonText: this.key('cancel') }, () => exportTeamTask({ ...this.search, action: 'download' })) + const cancelled = await this.$confirmAction({ message: this.key('export_confirm_message'), title: this.key('prompt'), confirmButtonText: this.key('confirm'), cancelButtonText: this.key('cancel') }, () => exportTeamTask({ ...this.buildSearchParams(), action: 'download' })) if (cancelled) return this.$message.success(this.$t(this.key('download_task_created'))) } @@ -283,5 +331,6 @@ export default {