519 lines
17 KiB
JavaScript
519 lines
17 KiB
JavaScript
const api = require('../../utils/api')
|
|
|
|
const pad = value => {
|
|
const text = String(value)
|
|
return text.length > 1 ? text : `0${text}`
|
|
}
|
|
|
|
const parseDateValue = value => {
|
|
const text = String(value || '')
|
|
if (text.endsWith('Z') || /[+-]\d{2}:\d{2}$/.test(text)) {
|
|
return new Date(text)
|
|
}
|
|
return new Date(text.replace(/-/g, '/').replace('T', ' '))
|
|
}
|
|
|
|
const formatDate = value => {
|
|
const date = parseDateValue(value)
|
|
if (Number.isNaN(date.getTime())) {
|
|
return ''
|
|
}
|
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
|
}
|
|
|
|
const formatTimeValue = value => {
|
|
const date = parseDateValue(value)
|
|
if (Number.isNaN(date.getTime())) {
|
|
return [0, 0, 0]
|
|
}
|
|
return [date.getHours(), date.getMinutes(), date.getSeconds()]
|
|
}
|
|
|
|
const formatTimeText = value => {
|
|
const parts = formatTimeValue(value)
|
|
return `${pad(parts[0])}:${pad(parts[1])}:${pad(parts[2])}`
|
|
}
|
|
|
|
const formatTimePartsText = parts => `${pad(parts[0])}:${pad(parts[1])}:${pad(parts[2])}`
|
|
|
|
const buildDateTimeValue = (date, timeValue) => (
|
|
date ? `${date}T${pad(timeValue[0])}:${pad(timeValue[1])}:${pad(timeValue[2])}` : ''
|
|
)
|
|
|
|
const timeOptions = [
|
|
Array.from({ length: 24 }, (_, index) => pad(index)),
|
|
Array.from({ length: 60 }, (_, index) => pad(index)),
|
|
Array.from({ length: 60 }, (_, index) => pad(index)),
|
|
]
|
|
|
|
const productKey = product => `${String(product.projectNo || '').trim()}||${String(product.productName || '').trim()}||${String(product.process || '').trim()}`
|
|
|
|
const blockDisplay = block => block.moldDisplayName || block.moldName || block.deviceNo
|
|
|
|
const productToItem = (product, startAt = '', availableOptions = null, equipmentOptions = [], deviceNo = '') => {
|
|
const selectedDeviceNo = deviceNo || ''
|
|
const deviceIndex = selectedDeviceNo
|
|
? Math.max(0, equipmentOptions.findIndex(item => item.deviceNo === selectedDeviceNo))
|
|
: -1
|
|
return {
|
|
productIndex: product ? 0 : -1,
|
|
attendancePointName: product ? product.attendancePointName : '',
|
|
deviceIndex,
|
|
deviceNo: selectedDeviceNo,
|
|
availableOptions: availableOptions || null,
|
|
startAt,
|
|
startAtText: startAt ? `${formatDate(startAt)} ${formatTimeText(startAt)}` : '',
|
|
projectNo: product ? product.projectNo : '',
|
|
productName: product ? product.productName : '',
|
|
materialCode: product ? product.materialCode : '',
|
|
materialName: product ? product.materialName : '',
|
|
rawMaterialBatchNo: '',
|
|
rawMaterialBatchOptions: [],
|
|
rawMaterialBatchIndex: -1,
|
|
rawMaterialBatchError: false,
|
|
process: product ? product.process : '',
|
|
stampingMethod: product ? product.stampingMethod : '',
|
|
operatorCount: product ? product.operatorCount : 1,
|
|
isMultiPerson: product ? !!product.isMultiPerson : false,
|
|
isMisc: product ? !!product.isMisc : false,
|
|
standardBeat: product ? product.standardBeat : 0,
|
|
standardWorkload: product ? product.standardWorkload : 0,
|
|
goodQty: product && product.isMisc ? 0 : '',
|
|
defectQty: '',
|
|
remark: '',
|
|
deviceError: false,
|
|
}
|
|
}
|
|
|
|
const REPORTING_LIMIT_MINUTES = 15
|
|
|
|
Page({
|
|
data: {
|
|
sessionId: '',
|
|
draft: null,
|
|
remainingText: '',
|
|
remainingSeconds: 0,
|
|
reminderShown: false,
|
|
confirmVisible: false,
|
|
confirmSummary: null,
|
|
submitting: false,
|
|
continuing: false,
|
|
addStartVisible: false,
|
|
addStartForm: null,
|
|
timeOptions,
|
|
},
|
|
onLoad(options) {
|
|
this.setData({ sessionId: options.sessionId || '' })
|
|
this.load()
|
|
},
|
|
onUnload() {
|
|
this.clearCountdown()
|
|
},
|
|
async load() {
|
|
try {
|
|
const draft = await api.buildReportDraft(this.data.sessionId)
|
|
this.setData({ draft }, () => {
|
|
this.showReportReminder()
|
|
this.startCountdown(draft)
|
|
})
|
|
} catch (error) {
|
|
if (!this.handleExpiredReport(error)) {
|
|
wx.showToast({ title: error.message, icon: 'none' })
|
|
}
|
|
}
|
|
},
|
|
handleExpiredReport(error) {
|
|
const message = error && error.message ? error.message : ''
|
|
if (message.indexOf('超过15分钟') === -1) {
|
|
return false
|
|
}
|
|
wx.showModal({
|
|
title: '报工已超时',
|
|
content: '请重新扫码点击重新报工,下班时间会更新为重新报工时间。',
|
|
showCancel: false,
|
|
success: () => {
|
|
wx.navigateBack({ delta: 1 })
|
|
},
|
|
})
|
|
return true
|
|
},
|
|
showReportReminder() {
|
|
if (this.data.reminderShown) {
|
|
return
|
|
}
|
|
this.setData({ reminderShown: true })
|
|
wx.showModal({
|
|
title: '填报提醒',
|
|
content: `报工填报时限为${REPORTING_LIMIT_MINUTES}分钟,请尽快完成填报`,
|
|
showCancel: false,
|
|
})
|
|
},
|
|
parseTime(value) {
|
|
if (!value) {
|
|
return 0
|
|
}
|
|
const text = String(value)
|
|
if (text.endsWith('Z') || /[+-]\d{2}:\d{2}$/.test(text)) {
|
|
return new Date(text).getTime()
|
|
}
|
|
return new Date(text.replace(/-/g, '/').replace('T', ' ')).getTime()
|
|
},
|
|
formatRemaining(seconds) {
|
|
const safeSeconds = Math.max(0, Number(seconds || 0))
|
|
const minutes = Math.floor(safeSeconds / 60)
|
|
const second = safeSeconds % 60
|
|
const minuteText = minutes < 10 ? `0${minutes}` : `${minutes}`
|
|
const secondText = second < 10 ? `0${second}` : `${second}`
|
|
return `${minuteText}:${secondText}`
|
|
},
|
|
clearCountdown() {
|
|
if (this.countdownTimer) {
|
|
clearInterval(this.countdownTimer)
|
|
this.countdownTimer = null
|
|
}
|
|
},
|
|
startCountdown(draft) {
|
|
this.clearCountdown()
|
|
const endTime = this.parseTime(draft && draft.endAt)
|
|
if (!endTime) {
|
|
this.setData({ remainingText: '', remainingSeconds: 0 })
|
|
return
|
|
}
|
|
const deadline = endTime + REPORTING_LIMIT_MINUTES * 60 * 1000
|
|
const tick = () => {
|
|
const remainingSeconds = Math.max(0, Math.ceil((deadline - Date.now()) / 1000))
|
|
this.setData({
|
|
remainingSeconds,
|
|
remainingText: this.formatRemaining(remainingSeconds),
|
|
})
|
|
if (remainingSeconds <= 0) {
|
|
this.clearCountdown()
|
|
}
|
|
}
|
|
tick()
|
|
this.countdownTimer = setInterval(tick, 1000)
|
|
},
|
|
itemOptions(block, item) {
|
|
return item.availableOptions || block.options || []
|
|
},
|
|
onProductChange(e) {
|
|
const blockIndex = e.currentTarget.dataset.blockIndex
|
|
const itemIndex = e.currentTarget.dataset.itemIndex
|
|
const productIndex = Number(e.detail.value)
|
|
const block = this.data.draft.deviceBlocks[blockIndex]
|
|
const currentItem = block.items[itemIndex] || {}
|
|
const options = this.itemOptions(block, currentItem)
|
|
const product = options[productIndex]
|
|
if (!product) {
|
|
return
|
|
}
|
|
const item = productToItem(
|
|
product,
|
|
currentItem.startAt || block.startAt || this.data.draft.startAt,
|
|
currentItem.availableOptions || null,
|
|
block.equipmentOptions || [],
|
|
currentItem.deviceNo,
|
|
)
|
|
item.productIndex = productIndex
|
|
item.deviceError = !!currentItem.deviceError && !item.deviceNo
|
|
|
|
this.setData({
|
|
[`draft.deviceBlocks[${blockIndex}].items[${itemIndex}]`]: item,
|
|
})
|
|
},
|
|
onDeviceChange(e) {
|
|
const blockIndex = e.currentTarget.dataset.blockIndex
|
|
const itemIndex = e.currentTarget.dataset.itemIndex
|
|
const deviceIndex = Number(e.detail.value)
|
|
const equipment = this.data.draft.deviceBlocks[blockIndex].equipmentOptions[deviceIndex]
|
|
if (!equipment) {
|
|
return
|
|
}
|
|
this.setData({
|
|
[`draft.deviceBlocks[${blockIndex}].items[${itemIndex}].deviceIndex`]: deviceIndex,
|
|
[`draft.deviceBlocks[${blockIndex}].items[${itemIndex}].deviceNo`]: equipment.deviceNo,
|
|
[`draft.deviceBlocks[${blockIndex}].items[${itemIndex}].deviceError`]: false,
|
|
})
|
|
},
|
|
onRawMaterialBatchChange(e) {
|
|
const blockIndex = e.currentTarget.dataset.blockIndex
|
|
const itemIndex = e.currentTarget.dataset.itemIndex
|
|
const batchIndex = Number(e.detail.value)
|
|
const item = this.data.draft.deviceBlocks[blockIndex].items[itemIndex]
|
|
const selected = (item.rawMaterialBatchOptions || [])[batchIndex]
|
|
if (!selected) {
|
|
return
|
|
}
|
|
this.setData({
|
|
[`draft.deviceBlocks[${blockIndex}].items[${itemIndex}].rawMaterialBatchIndex`]: batchIndex,
|
|
[`draft.deviceBlocks[${blockIndex}].items[${itemIndex}].rawMaterialBatchNo`]: selected,
|
|
[`draft.deviceBlocks[${blockIndex}].items[${itemIndex}].rawMaterialBatchError`]: false,
|
|
})
|
|
},
|
|
onQtyInput(e) {
|
|
const blockIndex = e.currentTarget.dataset.blockIndex
|
|
const itemIndex = e.currentTarget.dataset.itemIndex
|
|
const field = e.currentTarget.dataset.field
|
|
const updates = {
|
|
[`draft.deviceBlocks[${blockIndex}].items[${itemIndex}].${field}`]: e.detail.value,
|
|
}
|
|
if (field === 'changeoverCount') {
|
|
updates[`draft.deviceBlocks[${blockIndex}].items[${itemIndex}].changeoverCountError`] = false
|
|
}
|
|
this.setData(updates)
|
|
},
|
|
addItem(e) {
|
|
const blockIndex = e.currentTarget.dataset.blockIndex
|
|
const block = this.data.draft.deviceBlocks[blockIndex]
|
|
if (block.options.length < 2) {
|
|
return
|
|
}
|
|
|
|
const usedKeys = new Set(
|
|
(block.items || [])
|
|
.filter(item => item.productName)
|
|
.map(item => productKey(item)),
|
|
)
|
|
const availableOptions = (block.options || []).filter(option => !usedKeys.has(productKey(option)))
|
|
if (!availableOptions.length) {
|
|
wx.showToast({ title: '该模具没有可新增的产品', icon: 'none' })
|
|
return
|
|
}
|
|
const previousItem = block.items[block.items.length - 1] || {}
|
|
const defaultStartAt = previousItem.startAt || block.startAt || this.data.draft.startAt
|
|
this.setData({
|
|
addStartVisible: true,
|
|
addStartForm: {
|
|
blockIndex,
|
|
availableOptions,
|
|
previousStartAt: defaultStartAt,
|
|
date: formatDate(defaultStartAt),
|
|
timeValue: formatTimeValue(defaultStartAt),
|
|
timeText: formatTimeText(defaultStartAt),
|
|
},
|
|
})
|
|
},
|
|
onAddStartDateChange(e) {
|
|
this.setData({
|
|
'addStartForm.date': e.detail.value,
|
|
})
|
|
},
|
|
onAddStartTimeChange(e) {
|
|
const timeValue = e.detail.value.map(value => Number(value))
|
|
this.setData({
|
|
'addStartForm.timeValue': timeValue,
|
|
'addStartForm.timeText': formatTimePartsText(timeValue),
|
|
})
|
|
},
|
|
cancelAddStart() {
|
|
this.setData({
|
|
addStartVisible: false,
|
|
addStartForm: null,
|
|
})
|
|
},
|
|
confirmAddStart() {
|
|
const form = this.data.addStartForm
|
|
if (!form) {
|
|
return
|
|
}
|
|
const block = this.data.draft.deviceBlocks[form.blockIndex]
|
|
const startAt = buildDateTimeValue(form.date, form.timeValue)
|
|
const startTime = this.parseTime(startAt)
|
|
const blockStartTime = this.parseTime(block.startAt || this.data.draft.startAt)
|
|
const blockEndTime = this.parseTime(block.endAt || this.data.draft.endAt)
|
|
const previousStartTime = this.parseTime(form.previousStartAt)
|
|
if (!startTime || startTime < blockStartTime || startTime > blockEndTime) {
|
|
wx.showToast({ title: '开始时间需在本设备工作时间内', icon: 'none' })
|
|
return
|
|
}
|
|
if (previousStartTime && startTime < previousStartTime) {
|
|
wx.showToast({ title: '开始时间不能早于上一产品', icon: 'none' })
|
|
return
|
|
}
|
|
const item = productToItem(
|
|
form.availableOptions && form.availableOptions[0],
|
|
startAt,
|
|
form.availableOptions || [],
|
|
block.equipmentOptions || [],
|
|
)
|
|
const items = block.items.concat(item)
|
|
this.setData({
|
|
[`draft.deviceBlocks[${form.blockIndex}].items`]: items,
|
|
addStartVisible: false,
|
|
addStartForm: null,
|
|
})
|
|
},
|
|
removeItem(e) {
|
|
const blockIndex = e.currentTarget.dataset.blockIndex
|
|
const itemIndex = e.currentTarget.dataset.itemIndex
|
|
const block = this.data.draft.deviceBlocks[blockIndex]
|
|
if (block.items.length <= 1) {
|
|
return
|
|
}
|
|
const items = block.items.filter((_, index) => index !== itemIndex)
|
|
this.setData({
|
|
[`draft.deviceBlocks[${blockIndex}].items`]: items,
|
|
})
|
|
},
|
|
noop() {},
|
|
validateDraft(draft) {
|
|
const blocks = draft.deviceBlocks || []
|
|
for (let blockIndex = 0; blockIndex < blocks.length; blockIndex += 1) {
|
|
const block = blocks[blockIndex]
|
|
const items = block.items || []
|
|
for (let itemIndex = 0; itemIndex < items.length; itemIndex += 1) {
|
|
const item = items[itemIndex]
|
|
if (item.isMisc) {
|
|
continue
|
|
}
|
|
if ((block.options || []).length > 1 && (!item.productName || item.productIndex < 0)) {
|
|
wx.showToast({
|
|
title: `${blockDisplay(block)}缺少产品信息`,
|
|
icon: 'none',
|
|
})
|
|
return false
|
|
}
|
|
if (!item.deviceNo) {
|
|
this.setData({
|
|
[`draft.deviceBlocks[${blockIndex}].items[${itemIndex}].deviceError`]: true,
|
|
})
|
|
wx.showToast({
|
|
title: `${blockDisplay(block)}请选择设备号`,
|
|
icon: 'none',
|
|
})
|
|
return false
|
|
}
|
|
if (!item.startAt) {
|
|
wx.showToast({
|
|
title: `${blockDisplay(block)}请选择开始工作时间`,
|
|
icon: 'none',
|
|
})
|
|
return false
|
|
}
|
|
if ((item.rawMaterialBatchOptions || []).length && !item.rawMaterialBatchNo) {
|
|
this.setData({
|
|
[`draft.deviceBlocks[${blockIndex}].items[${itemIndex}].rawMaterialBatchError`]: true,
|
|
})
|
|
wx.showToast({
|
|
title: `${blockDisplay(block)}请选择材料库存批次号`,
|
|
icon: 'none',
|
|
})
|
|
return false
|
|
}
|
|
if (item.isContinuousDie) {
|
|
const text = String(item.changeoverCount || '').trim()
|
|
const value = Number(text)
|
|
if (!text || !Number.isInteger(value) || value < 0) {
|
|
this.setData({
|
|
[`draft.deviceBlocks[${blockIndex}].items[${itemIndex}].changeoverCountError`]: true,
|
|
})
|
|
wx.showToast({
|
|
title: `${blockDisplay(block)}请填写非负整数换料次数`,
|
|
icon: 'none',
|
|
})
|
|
return false
|
|
}
|
|
}
|
|
const goodQty = Number(item.goodQty || 0)
|
|
const defectQty = Number(item.defectQty || 0)
|
|
if (!Number.isFinite(goodQty) || goodQty < 0 || !Number.isFinite(defectQty) || defectQty < 0) {
|
|
wx.showToast({
|
|
title: `${blockDisplay(block)}产出数量不能为负数`,
|
|
icon: 'none',
|
|
})
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
},
|
|
buildConfirmSummary(draft) {
|
|
const totalGood = draft.deviceBlocks.reduce((sum, block) => (
|
|
sum + block.items.reduce((inner, item) => inner + (item.isMisc ? 0 : Number(item.goodQty || 0)), 0)
|
|
), 0)
|
|
const totalDefect = draft.deviceBlocks.reduce((sum, block) => (
|
|
sum + block.items.reduce((inner, item) => inner + (item.isMisc ? 0 : Number(item.defectQty || 0)), 0)
|
|
), 0)
|
|
const hasMisc = draft.deviceBlocks.some(block => (block.items || []).some(item => item.isMisc))
|
|
return {
|
|
deviceText: draft.deviceBlocks.map(block => blockDisplay(block)).join('、'),
|
|
totalGood,
|
|
totalDefect,
|
|
note: hasMisc ? '包含处理杂活,提交后由管理员补充杂活事项' : '提交后进入管理员审核',
|
|
}
|
|
},
|
|
submit() {
|
|
if (this.data.continuing) {
|
|
return
|
|
}
|
|
const draft = this.data.draft
|
|
if (!this.validateDraft(draft)) {
|
|
return
|
|
}
|
|
this.setData({
|
|
confirmVisible: true,
|
|
confirmSummary: this.buildConfirmSummary(draft),
|
|
})
|
|
},
|
|
cancelConfirm() {
|
|
if (this.data.submitting) {
|
|
return
|
|
}
|
|
this.setData({ confirmVisible: false })
|
|
},
|
|
async confirmSubmit() {
|
|
if (this.data.submitting || this.data.continuing) {
|
|
return
|
|
}
|
|
this.setData({ submitting: true })
|
|
wx.showLoading({ title: '提交中' })
|
|
try {
|
|
const report = await api.submitReport(this.data.draft)
|
|
this.setData({ confirmVisible: false })
|
|
wx.reLaunch({
|
|
url: `/pages/reportResult/reportResult?reportId=${report.id}`,
|
|
})
|
|
} catch (error) {
|
|
if (!this.handleExpiredReport(error)) {
|
|
wx.showToast({ title: error.message, icon: 'none' })
|
|
}
|
|
} finally {
|
|
wx.hideLoading()
|
|
this.setData({ submitting: false })
|
|
}
|
|
},
|
|
continueWork() {
|
|
if (this.data.submitting || this.data.continuing) {
|
|
return
|
|
}
|
|
wx.showModal({
|
|
title: '继续上班',
|
|
content: '确认取消本次下班报工填报,继续保持上班状态?当前填写内容不会保存。',
|
|
confirmText: '继续上班',
|
|
cancelText: '返回填写',
|
|
success: async res => {
|
|
if (!res.confirm) {
|
|
return
|
|
}
|
|
this.setData({ continuing: true })
|
|
wx.showLoading({ title: '处理中' })
|
|
try {
|
|
await api.continueWork(this.data.sessionId)
|
|
this.clearCountdown()
|
|
wx.showToast({ title: '已继续上班', icon: 'success' })
|
|
setTimeout(() => {
|
|
wx.reLaunch({
|
|
url: '/pages/index/index',
|
|
})
|
|
}, 500)
|
|
} catch (error) {
|
|
wx.showToast({ title: error.message, icon: 'none' })
|
|
} finally {
|
|
wx.hideLoading()
|
|
this.setData({ continuing: false })
|
|
}
|
|
},
|
|
})
|
|
},
|
|
})
|