JhHardwareWRS/pages/review/review.js
2026-07-25 21:12:17 +08:00

892 lines
34 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const api = require('../../utils/api')
const pad = value => {
const text = String(value)
return text.length > 1 ? text : `0${text}`
}
const datePart = value => {
const date = new Date(value)
if (Number.isNaN(date.getTime())) {
return ''
}
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
}
const timePart = value => {
const date = new Date(value)
if (Number.isNaN(date.getTime())) {
return ''
}
return `${pad(date.getHours())}:${pad(date.getMinutes())}`
}
const dateTimeValue = (date, time) => (date && time ? `${date}T${time}:00` : '')
const productChoiceKey = item => `${item.attendancePointName || ''}||${item.projectNo}||${item.productName}`
const optionKey = item => `${item.attendancePointName || ''}||${item.projectNo}||${item.productName}||${item.process || ''}||${api.normalizeDeviceNo(item.deviceNo)}`
const segmentKey = item => `${api.normalizeDeviceNo(item.moldName || item.productName || item.deviceNo)}||${String(item.process || '').trim()}`
const round2 = value => Math.round(Number(value || 0) * 100) / 100
const withCompareClasses = item => {
const standardWorkload = item.isCleaning
? 0
: (item.isMisc ? 0 : (api.calculateStandardWorkload(item.allocatedMinutes, item.standardBeat) || Number(item.standardWorkload || 0)))
const beatComparison = (item.isCleaning || item.isMisc) ? api.classifyBeat(0, 0) : api.classifyBeat(item.actualBeat, item.standardBeat)
const workloadComparison = (item.isCleaning || item.isMisc) ? api.classifyWorkload(0, 0) : api.classifyWorkload(item.goodQty, standardWorkload)
return {
...item,
standardWorkload,
beatCompareClass: beatComparison.className,
beatReason: beatComparison.reason,
workloadCompareClass: workloadComparison.className,
workloadReason: workloadComparison.reason,
}
}
const correctionOrCurrent = (corrections, key, currentValue) => (
corrections && corrections[key] !== undefined ? corrections[key] : currentValue
)
const itemActualBeat = item => {
const outputQty = Number(item.goodQty || 0) + Number(item.defectQty || 0)
return outputQty > 0 ? round2(Number(item.allocatedMinutes || 0) * 60 / outputQty) : 0
}
Page({
data: {
user: null,
rows: [],
page: 1,
pageInput: '1',
totalPages: 1,
focusReportId: '',
refreshing: false,
scrollTop: 0,
expandedReportId: '',
detailExpandedAll: false,
filterVoided: true,
productModalVisible: false,
productModalRows: [],
productModalKeyword: '',
productModalPage: 1,
productModalPageInput: '1',
productModalTotalPages: 1,
productModalContext: null,
productModalLoading: false,
},
onLoad(options) {
this.setData({ focusReportId: options.reportId || '' })
},
onShow() {
this.load()
},
noop() {},
onScroll(e) {
this.currentScrollTop = e.detail.scrollTop || 0
},
toggleDetails() {
this.setData({
detailExpandedAll: !this.data.detailExpandedAll,
expandedReportId: '',
})
},
queryReportTop(id) {
return new Promise(resolve => {
this.createSelectorQuery()
.select(`#review-report-${id}`)
.boundingClientRect(rect => {
resolve(rect ? rect.top : null)
})
.exec()
})
},
restoreReportPosition(id, beforeTop) {
if (beforeTop === null || beforeTop === undefined) {
return
}
wx.nextTick(() => {
this.queryReportTop(id).then(afterTop => {
if (afterTop === null || afterTop === undefined) {
return
}
const nextScrollTop = Math.max(
0,
(this.currentScrollTop || 0) + afterTop - beforeTop,
)
this.currentScrollTop = nextScrollTop
this.setData({ scrollTop: nextScrollTop })
})
})
},
toggleReportDetail(e) {
const id = String(e.currentTarget.dataset.id || '')
const switchingReport = this.data.detailExpandedAll
|| (!!this.data.expandedReportId && this.data.expandedReportId !== id)
this.queryReportTop(id).then(beforeTop => {
this.setData({
detailExpandedAll: false,
expandedReportId: this.data.expandedReportId === id ? '' : id,
}, () => {
if (switchingReport) {
this.restoreReportPosition(id, beforeTop)
}
})
})
},
async load(page = this.data.page) {
const user = api.getCurrentUser()
try {
let result
if (this.data.focusReportId) {
const report = await api.getReport(this.data.focusReportId)
result = {
rows: (report.status === 'pending' || report.canEdit || report.isVoided) ? [report] : [],
page: 1,
totalPages: 1,
}
} else {
result = await api.listReports({
status: 'pending',
page,
pageSize: 6,
includeVoided: !this.data.filterVoided,
})
}
const rows = await this.buildReviewRows(result.rows || [])
const defaultExpandedId = this.data.focusReportId && rows.length ? String(rows[0].id) : ''
this.setData({
user,
rows,
page: result.page,
pageInput: String(result.page || 1),
totalPages: result.totalPages,
expandedReportId: defaultExpandedId,
detailExpandedAll: false,
})
} catch (error) {
this.setData({ user, rows: [] })
wx.showToast({ title: error.message, icon: 'none' })
}
},
async onPullDownRefresh() {
this.setData({ refreshing: true })
try {
await this.load(this.data.page)
} finally {
this.setData({ refreshing: false })
if (wx.stopPullDownRefresh) {
wx.stopPullDownRefresh()
}
}
},
async buildReviewRows(rows) {
const hydrated = []
for (const row of rows) {
const editItems = []
const editDeviceSegments = (row.deviceSegments || []).map((segment, index) => this.makeEditDeviceSegment(segment, index))
for (const detail of row.items || []) {
const editItem = await this.buildReviewItem(detail)
const itemKey = segmentKey(editItem)
editItem.deviceSegments = editDeviceSegments.filter(segment => segmentKey(segment) === itemKey)
editItems.push(editItem)
}
hydrated.push({
...row,
timeRangeText: row.isCleaning ? '清洗报工' : `${row.startAtText}${row.endAtText}`,
editStartDate: datePart(row.startAt),
editStartTime: timePart(row.startAt),
editEndDate: datePart(row.endAt),
editEndTime: timePart(row.endAt),
editReviewRemark: row.reviewRemark || '',
editItems,
})
}
return hydrated
},
async buildReviewItem(detail) {
const [products, equipmentOptions] = await Promise.all([
this.loadProductOptions(detail.productName, detail.attendancePointName),
detail.isMisc ? Promise.resolve([]) : this.loadEquipmentOptions(detail.attendancePointName, !!detail.isCleaning),
])
return this.makeEditItem(detail, products, equipmentOptions)
},
async loadProductOptions(productName, attendancePointName = '', keyword = '') {
try {
const result = await api.listProducts({
productName: keyword ? '' : productName,
keyword,
attendancePointName,
page: 1,
pageSize: 100,
})
return result.rows || []
} catch (error) {
return []
}
},
async loadEquipmentOptions(attendancePointName = '', isCleaning = false) {
try {
const result = await api.listEquipment({
attendancePointName,
deviceType: isCleaning ? '清洗设备' : '冲压设备',
page: 1,
pageSize: 100,
})
return result.rows || []
} catch (error) {
return []
}
},
makeEditItem(detail, products, equipmentOptions = []) {
const corrections = detail.corrections || {}
const current = {
attendancePointName: detail.attendancePointName || '',
projectNo: detail.projectNo,
productName: detail.productName,
process: detail.process || '',
stampingMethod: detail.stampingMethod || '',
deviceNo: detail.deviceNo,
materialCode: detail.materialCode || '',
materialName: detail.materialName || '',
rawMaterialBatchNo: detail.rawMaterialBatchNo || '',
standardBeat: detail.standardBeat || 0,
standardWorkload: detail.standardWorkload || 0,
isCleaning: !!detail.isCleaning,
isContinuousDie: !!detail.isContinuousDie,
isMisc: !!detail.isMisc,
isMultiPerson: !!detail.isMultiPerson,
operatorCount: detail.operatorCount || 1,
changeoverCount: detail.changeoverCount || 0,
remark: detail.remark || '',
}
const originalProduct = {
attendancePointName: current.attendancePointName,
projectNo: correctionOrCurrent(corrections, 'project_no', current.projectNo),
productName: correctionOrCurrent(corrections, 'product_name', current.productName),
process: correctionOrCurrent(corrections, 'process_name', current.process) || '',
materialCode: current.materialCode,
materialName: current.materialName,
stampingMethod: correctionOrCurrent(corrections, 'stamping_method', current.stampingMethod) || '',
operatorCount: current.operatorCount,
standardBeat: current.standardBeat,
isCleaning: current.isCleaning,
isContinuousDie: current.isContinuousDie,
isMisc: current.isMisc,
isMultiPerson: current.isMultiPerson,
}
const optionMap = {}
;(products || []).forEach(product => {
optionMap[optionKey(product)] = product
})
optionMap[optionKey(current)] = current
const options = Object.keys(optionMap).map(key => optionMap[key])
const productMap = {}
options.forEach(product => {
productMap[productChoiceKey(product)] = {
attendancePointName: product.attendancePointName || current.attendancePointName,
projectNo: product.projectNo,
productName: product.productName,
}
})
const productChoices = Object.keys(productMap).map(key => productMap[key])
const productLabels = productChoices.map(item => `${item.projectNo} · ${item.productName}`)
let productIndex = productChoices.findIndex(item => item.projectNo === current.projectNo && item.productName === current.productName)
if (productIndex < 0) {
productIndex = 0
}
const selectedProduct = productChoices[productIndex] || current
const processOptions = options.filter(item => (
item.attendancePointName === selectedProduct.attendancePointName
&& item.projectNo === selectedProduct.projectNo
&& item.productName === selectedProduct.productName
))
const processLabels = processOptions.map(item => item.process || '未填写工序')
let processIndex = processOptions.findIndex(item => (item.process || '') === current.process)
if (processIndex < 0) {
processIndex = 0
}
const selectedOption = processOptions[processIndex] || current
const selectedDeviceNo = api.normalizeDeviceNo(detail.deviceNo)
const deviceIndex = selectedDeviceNo
? equipmentOptions.findIndex(item => api.normalizeDeviceNo(item.deviceNo) === selectedDeviceNo)
: -1
return withCompareClasses({
id: detail.id,
attendancePointName: current.attendancePointName,
startAt: detail.startAt || '',
isCleaning: !!detail.isCleaning,
isContinuousDie: !!selectedOption.isContinuousDie,
isMisc: !!detail.isMisc,
isMultiPerson: !!selectedOption.isMultiPerson,
operatorCount: selectedOption.operatorCount || detail.operatorCount || 1,
changeoverCount: detail.changeoverCount || 0,
changeoverCountError: false,
remark: detail.remark || '',
overLimitStatus: detail.overLimitStatus || 'not_checked',
overLimitType: detail.overLimitType || '',
overLimitReason: detail.overLimitReason || '',
overLimitCheckedAt: detail.overLimitCheckedAt || '',
overLimitCheckSource: detail.overLimitCheckSource || '',
overLimitBatchNo: detail.overLimitBatchNo || '',
overLimitProductGrossWeightKg: detail.overLimitProductGrossWeightKg ?? '',
overLimitIssuedWeightKg: detail.overLimitIssuedWeightKg ?? '',
overLimitMaxReportableQty: detail.overLimitMaxReportableQty ?? '',
overLimitPreviousGoodQty: detail.overLimitPreviousGoodQty ?? '',
overLimitCurrentCumulativeQty: detail.overLimitCurrentCumulativeQty ?? '',
overLimitCurrentReportQty: detail.overLimitCurrentReportQty ?? '',
overLimitMaxReportableQtyText: detail.overLimitMaxReportableQtyText || '-',
overLimitPreviousGoodQtyText: detail.overLimitPreviousGoodQtyText || '-',
overLimitCurrentCumulativeQtyText: detail.overLimitCurrentCumulativeQtyText || '-',
overLimitCurrentReportQtyText: detail.overLimitCurrentReportQtyText || '-',
hasOverLimitMaxReportableQty: !!detail.hasOverLimitMaxReportableQty,
hasOverLimitPreviousGoodQty: !!detail.hasOverLimitPreviousGoodQty,
hasOverLimit: !!detail.hasOverLimit,
hasOverLimitNotice: !!detail.hasOverLimitNotice,
deviceError: false,
deviceNo: detail.deviceNo,
deviceIndex,
equipmentOptions,
projectNo: selectedOption.projectNo,
productName: selectedOption.productName,
process: selectedOption.process || '',
stampingMethod: selectedOption.stampingMethod || current.stampingMethod || '',
rawMaterialBatchNo: detail.rawMaterialBatchNo || '',
productSearchKeyword: '',
productOptions: options,
productChoices,
productLabels,
productIndex,
processOptions,
processLabels,
processIndex,
originalProduct,
goodQty: detail.goodQty,
defectQty: detail.defectQty,
allocatedMinutes: detail.allocatedMinutes || 0,
workTimeText: detail.workTimeText || '0秒',
actualBeat: detail.actualBeat || 0,
standardBeat: selectedOption.standardBeat || detail.standardBeat || 0,
standardWorkload: selectedOption.standardWorkload || detail.standardWorkload || 0,
deviceSegments: detail.deviceSegments || [],
})
},
productToEditItem(item, product) {
const selectedDeviceNo = api.normalizeDeviceNo(item.deviceNo)
const deviceIndex = selectedDeviceNo
? (item.equipmentOptions || []).findIndex(equipment => api.normalizeDeviceNo(equipment.deviceNo) === selectedDeviceNo)
: -1
const nextItem = {
...item,
attendancePointName: item.attendancePointName,
projectNo: product.projectNo || item.projectNo,
productName: product.productName || item.productName,
materialCode: product.materialCode || '',
materialName: product.materialName || '',
process: product.process || '',
stampingMethod: product.stampingMethod || '',
standardBeat: Number(product.standardBeat || 0),
standardWorkload: api.calculateStandardWorkload(item.allocatedMinutes, product.standardBeat),
operatorCount: product.operatorCount || 1,
isCleaning: !!product.isCleaning,
isContinuousDie: !!product.isContinuousDie,
isMisc: !!product.isMisc,
isMultiPerson: !!product.isMultiPerson,
changeoverCount: product.isContinuousDie ? (item.changeoverCount || 0) : 0,
changeoverCountError: false,
actualBeat: itemActualBeat(item),
deviceIndex,
productOptions: [product],
productChoices: [product],
productLabels: [`${product.projectNo || ''} · ${product.productName || ''}`],
productIndex: 0,
processOptions: [product],
processLabels: [product.process || '未填写工序'],
processIndex: 0,
}
return withCompareClasses(nextItem)
},
replaceEditProduct(reportIndex, itemIndex, product) {
const item = this.data.rows[reportIndex].editItems[itemIndex]
if (!item || !product) {
return
}
this.replaceEditItem(reportIndex, itemIndex, this.productToEditItem(item, product))
},
async openProductModal(e) {
const reportIndex = Number(e.currentTarget.dataset.reportIndex)
const itemIndex = Number(e.currentTarget.dataset.itemIndex)
const row = this.data.rows[reportIndex]
const item = row && row.editItems ? row.editItems[itemIndex] : null
if (!item) {
wx.showToast({ title: '报工明细不存在', icon: 'none' })
return
}
this.setData({
productModalVisible: true,
productModalContext: { reportIndex, itemIndex },
productModalRows: [],
productModalKeyword: '',
productModalPage: 1,
productModalPageInput: '1',
productModalTotalPages: 1,
})
await this.loadProductModal(1)
},
closeProductModal() {
this.setData({
productModalVisible: false,
productModalRows: [],
productModalKeyword: '',
productModalContext: null,
})
},
onDeviceChange(e) {
const reportIndex = Number(e.currentTarget.dataset.reportIndex)
const itemIndex = Number(e.currentTarget.dataset.itemIndex)
const deviceIndex = Number(e.detail.value)
const item = this.data.rows[reportIndex].editItems[itemIndex]
const equipment = (item.equipmentOptions || [])[deviceIndex]
if (!equipment) {
return
}
this.setData({
[`rows[${reportIndex}].editItems[${itemIndex}].deviceIndex`]: deviceIndex,
[`rows[${reportIndex}].editItems[${itemIndex}].deviceNo`]: equipment.deviceNo,
[`rows[${reportIndex}].editItems[${itemIndex}].deviceError`]: false,
})
},
async loadProductModal(page = this.data.productModalPage) {
const context = this.data.productModalContext
if (!context) {
return
}
const item = this.data.rows[context.reportIndex].editItems[context.itemIndex]
this.setData({ productModalLoading: true })
try {
const result = await api.listProducts({
keyword: this.data.productModalKeyword,
attendancePointName: item.attendancePointName,
page,
pageSize: 8,
})
this.setData({
productModalRows: result.rows || [],
productModalPage: result.page,
productModalPageInput: String(result.page || 1),
productModalTotalPages: result.totalPages,
})
} catch (error) {
wx.showToast({ title: error.message || '加载产品失败', icon: 'none' })
} finally {
this.setData({ productModalLoading: false })
}
},
onProductModalKeywordInput(e) {
this.setData({ productModalKeyword: e.detail.value })
this.loadProductModal(1)
},
replaceProductFromModal(e) {
const index = Number(e.currentTarget.dataset.index)
const context = this.data.productModalContext
const product = this.data.productModalRows[index]
if (!context || !product) {
return
}
const item = this.data.rows[context.reportIndex].editItems[context.itemIndex]
if (product.isMisc) {
wx.showToast({ title: '处理杂活不能替换到普通报工', icon: 'none' })
return
}
if (!!item.isCleaning !== !!product.isCleaning) {
wx.showToast({ title: item.isCleaning ? '清洗报工只能替换清洗工序' : '普通报工不能替换为清洗工序', icon: 'none' })
return
}
this.replaceEditProduct(context.reportIndex, context.itemIndex, product)
this.closeProductModal()
},
productModalPrevPage() {
if (this.data.productModalPage > 1) {
this.loadProductModal(this.data.productModalPage - 1)
}
},
productModalNextPage() {
if (this.data.productModalPage < this.data.productModalTotalPages) {
this.loadProductModal(this.data.productModalPage + 1)
}
},
onProductModalPageInput(e) {
this.setData({ productModalPageInput: e.detail.value })
},
jumpProductModalPage(e) {
wx.hideKeyboard()
const inputValue = e && e.detail && e.detail.value !== undefined ? e.detail.value : this.data.productModalPageInput
const target = Math.max(1, Math.min(Number(inputValue) || this.data.productModalPage, this.data.productModalTotalPages))
if (target === this.data.productModalPage) {
this.setData({ productModalPageInput: String(this.data.productModalPage || 1) })
return
}
this.loadProductModal(target)
},
async restoreProduct(e) {
const reportIndex = Number(e.currentTarget.dataset.reportIndex)
const itemIndex = Number(e.currentTarget.dataset.itemIndex)
const item = this.data.rows[reportIndex].editItems[itemIndex]
if (!item || !item.originalProduct) {
return
}
const original = item.originalProduct
wx.showLoading({ title: '恢复中' })
try {
const products = await this.loadProductOptions(original.productName, item.attendancePointName)
const product = (products || []).find(option => (
option.projectNo === original.projectNo
&& option.productName === original.productName
&& (option.process || '') === (original.process || '')
)) || original
this.replaceEditProduct(reportIndex, itemIndex, {
...product,
projectNo: original.projectNo,
productName: original.productName,
process: original.process || '',
stampingMethod: product.stampingMethod || original.stampingMethod || '',
materialCode: product.materialCode || original.materialCode || '',
materialName: product.materialName || original.materialName || '',
operatorCount: product.operatorCount || original.operatorCount || 1,
standardBeat: product.standardBeat === undefined ? original.standardBeat : product.standardBeat,
isCleaning: product.isCleaning === undefined ? original.isCleaning : product.isCleaning,
isContinuousDie: product.isContinuousDie === undefined ? original.isContinuousDie : product.isContinuousDie,
isMisc: product.isMisc === undefined ? original.isMisc : product.isMisc,
isMultiPerson: product.isMultiPerson === undefined ? original.isMultiPerson : product.isMultiPerson,
changeoverCount: (product.isContinuousDie === undefined ? original.isContinuousDie : product.isContinuousDie) ? (item.changeoverCount || 0) : 0,
})
} catch (error) {
wx.showToast({ title: error.message || '恢复失败', icon: 'none' })
} finally {
wx.hideLoading()
}
},
makeEditDeviceSegment(segment, index) {
const editScanDate = datePart(segment.scannedAt)
const editScanTime = timePart(segment.scannedAt)
return {
...segment,
label: index === 0 ? '开始模具' : '换模具',
editScanDate,
editScanTime,
originalScanDate: editScanDate,
originalScanTime: editScanTime,
}
},
replaceEditItem(reportIndex, itemIndex, item) {
this.setData({
[`rows[${reportIndex}].editItems[${itemIndex}]`]: item,
})
},
onDeviceSegmentFieldChange(e) {
const reportIndex = e.currentTarget.dataset.reportIndex
const itemIndex = e.currentTarget.dataset.itemIndex
const segmentIndex = e.currentTarget.dataset.segmentIndex
const field = e.currentTarget.dataset.field
this.setData({
[`rows[${reportIndex}].editItems[${itemIndex}].deviceSegments[${segmentIndex}].${field}`]: e.detail.value,
})
},
onReportFieldChange(e) {
const reportIndex = e.currentTarget.dataset.reportIndex
const field = e.currentTarget.dataset.field
this.setData({
[`rows[${reportIndex}].${field}`]: e.detail.value,
})
},
onItemInput(e) {
const reportIndex = e.currentTarget.dataset.reportIndex
const itemIndex = e.currentTarget.dataset.itemIndex
const field = e.currentTarget.dataset.field
const value = e.detail.value
const updates = {
[`rows[${reportIndex}].editItems[${itemIndex}].${field}`]: value,
}
if (field === 'goodQty' || field === 'defectQty') {
const item = {
...this.data.rows[reportIndex].editItems[itemIndex],
[field]: value,
}
const outputQty = Number(item.goodQty || 0) + Number(item.defectQty || 0)
updates[`rows[${reportIndex}].editItems[${itemIndex}].actualBeat`] = outputQty > 0
? round2(Number(item.allocatedMinutes || 0) * 60 / outputQty)
: 0
const nextItem = {
...item,
actualBeat: updates[`rows[${reportIndex}].editItems[${itemIndex}].actualBeat`],
}
const compared = withCompareClasses(nextItem)
updates[`rows[${reportIndex}].editItems[${itemIndex}].standardWorkload`] = compared.standardWorkload
updates[`rows[${reportIndex}].editItems[${itemIndex}].beatCompareClass`] = compared.beatCompareClass
updates[`rows[${reportIndex}].editItems[${itemIndex}].beatReason`] = compared.beatReason
updates[`rows[${reportIndex}].editItems[${itemIndex}].workloadCompareClass`] = compared.workloadCompareClass
updates[`rows[${reportIndex}].editItems[${itemIndex}].workloadReason`] = compared.workloadReason
}
if (field === 'changeoverCount') {
updates[`rows[${reportIndex}].editItems[${itemIndex}].changeoverCountError`] = false
}
this.setData(updates)
},
async reloadItemOptions(e) {
const reportIndex = e.currentTarget.dataset.reportIndex
const itemIndex = e.currentTarget.dataset.itemIndex
const item = this.data.rows[reportIndex].editItems[itemIndex]
wx.showLoading({ title: '匹配中' })
try {
const keyword = String(item.productSearchKeyword || '').trim()
const products = await this.loadProductOptions(item.productName, item.attendancePointName, keyword)
this.replaceEditItem(reportIndex, itemIndex, this.makeEditItem(item, products))
} finally {
wx.hideLoading()
}
},
onProductSearchInput(e) {
const reportIndex = e.currentTarget.dataset.reportIndex
const itemIndex = e.currentTarget.dataset.itemIndex
this.setData({
[`rows[${reportIndex}].editItems[${itemIndex}].productSearchKeyword`]: e.detail.value,
})
},
onProductChange(e) {
const reportIndex = e.currentTarget.dataset.reportIndex
const itemIndex = e.currentTarget.dataset.itemIndex
const item = this.data.rows[reportIndex].editItems[itemIndex]
const productIndex = Number(e.detail.value)
const selectedProduct = item.productChoices[productIndex]
const processOptions = item.productOptions.filter(option => (
option.attendancePointName === selectedProduct.attendancePointName
&& option.projectNo === selectedProduct.projectNo
&& option.productName === selectedProduct.productName
))
const nextOption = processOptions[0] || selectedProduct
this.replaceEditItem(reportIndex, itemIndex, withCompareClasses({
...item,
productIndex,
attendancePointName: item.attendancePointName,
projectNo: selectedProduct.projectNo,
productName: selectedProduct.productName,
processOptions,
processLabels: processOptions.map(option => option.process || '未填写工序'),
processIndex: 0,
process: nextOption.process || '',
stampingMethod: nextOption.stampingMethod || '',
deviceNo: item.deviceNo,
standardBeat: nextOption.standardBeat || 0,
standardWorkload: nextOption.standardWorkload || 0,
operatorCount: nextOption.operatorCount || 1,
isMultiPerson: !!nextOption.isMultiPerson,
}))
},
onProcessChange(e) {
const reportIndex = e.currentTarget.dataset.reportIndex
const itemIndex = e.currentTarget.dataset.itemIndex
const item = this.data.rows[reportIndex].editItems[itemIndex]
const processIndex = Number(e.detail.value)
const selected = item.processOptions[processIndex]
if (!selected) {
return
}
this.replaceEditItem(reportIndex, itemIndex, withCompareClasses({
...item,
processIndex,
attendancePointName: item.attendancePointName,
deviceNo: item.deviceNo,
projectNo: selected.projectNo,
productName: selected.productName,
process: selected.process || '',
stampingMethod: selected.stampingMethod || '',
standardBeat: selected.standardBeat || 0,
standardWorkload: selected.standardWorkload || 0,
operatorCount: selected.operatorCount || 1,
isMultiPerson: !!selected.isMultiPerson,
}))
},
async approve(e) {
const id = e.currentTarget.dataset.id
const user = this.data.user
if (!user || user.role !== 'admin') {
wx.showToast({ title: '无审核权限', icon: 'none' })
return
}
const row = this.data.rows.find(item => String(item.id) === String(id))
if (!row) {
wx.showToast({ title: '报工记录不存在', icon: 'none' })
return
}
if (row.isVoided) {
wx.showToast({ title: '作废记录不能编辑', icon: 'none' })
return
}
if (!row.canEdit) {
wx.showToast({ title: '已超过可编辑期限', icon: 'none' })
return
}
const invalidItem = (row.editItems || []).find(item => (
Number(item.goodQty || 0) < 0
|| Number(item.defectQty || 0) < 0
|| !Number.isFinite(Number(item.goodQty || 0))
|| !Number.isFinite(Number(item.defectQty || 0))
))
if (invalidItem) {
wx.showToast({ title: '产出数量不能为负数', icon: 'none' })
return
}
const invalidChangeoverItemIndex = (row.editItems || []).findIndex(item => {
if (!item.isContinuousDie) {
return false
}
const value = Number(item.changeoverCount)
return !Number.isInteger(value) || value < 0
})
if (invalidChangeoverItemIndex >= 0) {
this.setData({
[`rows[${this.data.rows.findIndex(item => String(item.id) === String(id))}].editItems[${invalidChangeoverItemIndex}].changeoverCountError`]: true,
})
wx.showToast({ title: '请填写非负整数换料次数', icon: 'none' })
return
}
const missingDeviceItemIndex = (row.editItems || []).findIndex(item => !item.isMisc && !api.normalizeDeviceNo(item.deviceNo))
if (missingDeviceItemIndex >= 0) {
this.setData({
[`rows[${this.data.rows.findIndex(item => String(item.id) === String(id))}].editItems[${missingDeviceItemIndex}].deviceError`]: true,
})
wx.showToast({ title: '请选择设备号', icon: 'none' })
return
}
await this.submitApprove(id, row, user)
},
async submitApprove(id, row, user) {
const deviceCorrections = (row.editItems || []).flatMap(item => item.deviceSegments || [])
.filter(segment => (
segment.editScanDate !== segment.originalScanDate
|| segment.editScanTime !== segment.originalScanTime
))
.map(segment => ({
id: segment.id,
scannedAt: dateTimeValue(segment.editScanDate, segment.editScanTime),
}))
const invalidDeviceCorrection = deviceCorrections.find(item => !item.scannedAt)
if (invalidDeviceCorrection) {
wx.showToast({ title: '换模具时间不能为空', icon: 'none' })
return
}
const patch = {
startAt: row.isCleaning ? null : dateTimeValue(row.editStartDate, row.editStartTime),
endAt: row.isCleaning ? null : dateTimeValue(row.editEndDate, row.editEndTime),
deviceCorrections: row.isCleaning ? [] : deviceCorrections,
reviewRemark: row.editReviewRemark || '',
itemCorrections: (row.editItems || []).map(item => ({
id: item.id,
device_no: api.normalizeDeviceNo(item.deviceNo),
project_no: item.projectNo,
product_name: item.productName,
raw_material_batch_no: item.rawMaterialBatchNo || null,
process_name: item.process || '',
changeover_count: item.isContinuousDie ? Number(item.changeoverCount || 0) : 0,
good_qty: Number(item.goodQty || 0),
defect_qty: Number(item.defectQty || 0),
scrap_qty: 0,
remark: item.remark || null,
})),
}
try {
await api.approveReport(id, user, patch)
wx.showToast({ title: row.status === 'approved' ? '已保存更改' : '已审核通过', icon: 'success' })
if (this.data.focusReportId) {
setTimeout(() => {
wx.navigateBack({
delta: 1,
fail: () => {
wx.redirectTo({
url: '/pages/records/records?mode=pending&status=pending',
})
},
})
}, 500)
return
}
await this.load(this.data.page)
} catch (error) {
wx.showToast({ title: error.message, icon: 'none' })
}
},
prevPage() {
if (this.data.page > 1) {
this.load(this.data.page - 1)
}
},
nextPage() {
if (this.data.page < this.data.totalPages) {
this.load(this.data.page + 1)
}
},
onPageInput(e) {
this.setData({ pageInput: e.detail.value })
},
jumpToPage(e) {
wx.hideKeyboard()
const inputValue = e && e.detail && e.detail.value !== undefined ? e.detail.value : this.data.pageInput
const target = Math.max(1, Math.min(Number(inputValue) || this.data.page, this.data.totalPages))
if (target === this.data.page) {
this.setData({ pageInput: String(this.data.page || 1) })
return
}
this.load(target)
},
toggleFilterVoided() {
this.setData({ filterVoided: !this.data.filterVoided, page: 1 })
this.load(1)
},
onFilterVoidedChange(e) {
this.setData({ filterVoided: !!e.detail.value, page: 1 })
this.load(1)
},
async voidReport(e) {
const id = e.currentTarget.dataset.id
wx.showModal({
title: '确认作废',
content: '作废的记录将在30天后被系统彻底删除彻底删除前可随时撤销作废是否确定删除',
cancelText: '否',
confirmText: '是',
confirmColor: '#c5221f',
success: async res => {
if (!res.confirm) {
return
}
wx.showLoading({ title: '处理中' })
try {
await api.voidReport(id)
wx.showToast({ title: '已作废', icon: 'success' })
await this.load(this.data.page)
} catch (error) {
wx.showToast({ title: error.message || '作废失败', icon: 'none' })
} finally {
wx.hideLoading()
}
},
})
},
async unvoidReport(e) {
const id = e.currentTarget.dataset.id
wx.showLoading({ title: '处理中' })
try {
await api.unvoidReport(id)
wx.showToast({ title: '已撤销作废', icon: 'success' })
await this.load(this.data.page)
} catch (error) {
wx.showToast({ title: error.message || '撤销失败', icon: 'none' })
} finally {
wx.hideLoading()
}
},
})