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 }) } }, }) }, })