342 lines
11 KiB
JavaScript
342 lines
11 KiB
JavaScript
const api = require('../../utils/api')
|
|
|
|
const EXPANDED_MONTH_WIDTH = 390
|
|
const COLLAPSED_MONTH_WIDTH = 84
|
|
const END_SPACER_WIDTH = 120
|
|
const ALL_ATTENDANCE_POINTS = '__ALL_ATTENDANCE_POINTS__'
|
|
|
|
const monthLabel = month => `${month}月`
|
|
|
|
const formatQty = value => {
|
|
const number = Number(value || 0)
|
|
if (!Number.isFinite(number)) {
|
|
return '0'
|
|
}
|
|
return Number.isInteger(number) ? String(number) : String(Math.round(number * 100) / 100)
|
|
}
|
|
|
|
const sanitizeQty = value => {
|
|
const text = String(value || '').replace(/[^\d.]/g, '')
|
|
const parts = text.split('.')
|
|
if (parts.length <= 1) {
|
|
return parts[0]
|
|
}
|
|
return `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}`
|
|
}
|
|
|
|
Page({
|
|
data: {
|
|
years: [],
|
|
yearLabels: [],
|
|
yearIndex: 0,
|
|
selectedYear: '',
|
|
currentYear: '',
|
|
currentMonth: '',
|
|
months: [],
|
|
rawRows: [],
|
|
rows: [],
|
|
pointOptions: [{ label: '全部考勤点', value: ALL_ATTENDANCE_POINTS }],
|
|
pointLabels: ['全部考勤点'],
|
|
pointIndex: 0,
|
|
selectedPointValue: ALL_ATTENDANCE_POINTS,
|
|
selectedPointLabel: '全部考勤点',
|
|
monthTableWidth: 0,
|
|
monthEndSpacerWidth: 0,
|
|
totalReported: 0,
|
|
totalReconciled: 0,
|
|
totalReturn: 0,
|
|
monthScrollLeft: 0,
|
|
loading: false,
|
|
refreshing: false,
|
|
savingKey: '',
|
|
},
|
|
onLoad() {
|
|
this.loadYears()
|
|
},
|
|
async onPullDownRefresh() {
|
|
this.setData({ refreshing: true })
|
|
try {
|
|
await this.loadLedger()
|
|
} finally {
|
|
this.setData({ refreshing: false })
|
|
if (wx.stopPullDownRefresh) {
|
|
wx.stopPullDownRefresh()
|
|
}
|
|
}
|
|
},
|
|
async loadYears() {
|
|
this.setData({ loading: true })
|
|
try {
|
|
const result = await api.listReconciliationYears()
|
|
const years = result.years && result.years.length ? result.years : [result.currentYear]
|
|
const selectedYear = result.currentYear || years[years.length - 1]
|
|
const yearIndex = Math.max(0, years.findIndex(year => Number(year) === Number(selectedYear)))
|
|
this.setData({
|
|
years,
|
|
yearLabels: years.map(year => `${year}年`),
|
|
currentYear: result.currentYear,
|
|
selectedYear,
|
|
yearIndex,
|
|
})
|
|
await this.loadLedger()
|
|
} catch (error) {
|
|
wx.showToast({ title: error.message || '加载年份失败', icon: 'none' })
|
|
} finally {
|
|
this.setData({ loading: false })
|
|
}
|
|
},
|
|
buildMonths(year, currentYear, currentMonth) {
|
|
const visibleCount = Number(year) === Number(currentYear) ? Number(currentMonth || 1) : 12
|
|
return Array.from({ length: visibleCount }, (_, index) => {
|
|
const month = index + 1
|
|
const expanded = Number(year) === Number(currentYear)
|
|
? month === Number(currentMonth)
|
|
: true
|
|
return {
|
|
month,
|
|
label: monthLabel(month),
|
|
expanded,
|
|
width: expanded ? EXPANDED_MONTH_WIDTH : COLLAPSED_MONTH_WIDTH,
|
|
}
|
|
})
|
|
},
|
|
buildRows(rawRows, months) {
|
|
return (rawRows || []).map(row => ({
|
|
attendancePointName: row.attendancePointName || '',
|
|
productName: row.productName,
|
|
uniqueKey: row.uniqueKey || `${row.attendancePointName || ''}||${row.productName}`,
|
|
months: months.map(monthMeta => {
|
|
const source = (row.months || []).find(item => Number(item.month) === Number(monthMeta.month)) || {}
|
|
const reported = Number(source.reportedGoodQty || 0)
|
|
const reconciled = Number(source.reconciledGoodQty || 0)
|
|
const returnQty = Number(source.returnQty || 0)
|
|
return {
|
|
...monthMeta,
|
|
reportedGoodQty: reported,
|
|
reconciledGoodQty: reconciled,
|
|
returnQty,
|
|
reportedGoodQtyText: formatQty(reported),
|
|
reconciledGoodQtyInput: formatQty(reconciled),
|
|
returnQtyInput: formatQty(returnQty),
|
|
}
|
|
}),
|
|
}))
|
|
},
|
|
buildPointOptions(rawRows) {
|
|
const seen = {}
|
|
const options = [{ label: '全部考勤点', value: ALL_ATTENDANCE_POINTS }]
|
|
;(rawRows || []).forEach(row => {
|
|
const value = row.attendancePointName || ''
|
|
if (seen[value]) {
|
|
return
|
|
}
|
|
seen[value] = true
|
|
options.push({
|
|
label: value || '未设置考勤点',
|
|
value,
|
|
})
|
|
})
|
|
return options
|
|
},
|
|
getPointFilterState(rawRows) {
|
|
const pointOptions = this.buildPointOptions(rawRows)
|
|
const currentValue = this.data.selectedPointValue || ALL_ATTENDANCE_POINTS
|
|
const matchedIndex = pointOptions.findIndex(item => item.value === currentValue)
|
|
const pointIndex = matchedIndex >= 0 ? matchedIndex : 0
|
|
return {
|
|
pointOptions,
|
|
pointLabels: pointOptions.map(item => item.label),
|
|
pointIndex,
|
|
selectedPointValue: pointOptions[pointIndex].value,
|
|
selectedPointLabel: pointOptions[pointIndex].label,
|
|
}
|
|
},
|
|
getFilteredRows(rawRows) {
|
|
const selectedPointValue = this.data.selectedPointValue || ALL_ATTENDANCE_POINTS
|
|
if (selectedPointValue === ALL_ATTENDANCE_POINTS) {
|
|
return rawRows || []
|
|
}
|
|
return (rawRows || []).filter(row => (row.attendancePointName || '') === selectedPointValue)
|
|
},
|
|
rpxToPx(value) {
|
|
let windowWidth = 375
|
|
try {
|
|
const info = wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync()
|
|
windowWidth = Number(info && info.windowWidth) || windowWidth
|
|
} catch (error) {
|
|
windowWidth = 375
|
|
}
|
|
return Math.round(Number(value || 0) * windowWidth / 750)
|
|
},
|
|
getCurrentMonthScrollLeft(months, year, currentYear, currentMonth) {
|
|
if (Number(year) !== Number(currentYear)) {
|
|
return 0
|
|
}
|
|
const targetMonth = Number(currentMonth || 1)
|
|
const offsetRpx = (months || []).reduce((sum, item) => {
|
|
if (Number(item.month) >= targetMonth) {
|
|
return sum
|
|
}
|
|
return sum + Number(item.width || 0)
|
|
}, 0)
|
|
return this.rpxToPx(offsetRpx)
|
|
},
|
|
alignToCurrentMonth(months, year, currentYear, currentMonth) {
|
|
const scrollLeft = this.getCurrentMonthScrollLeft(months, year, currentYear, currentMonth)
|
|
if (!scrollLeft) {
|
|
this.setData({ monthScrollLeft: 0 })
|
|
return
|
|
}
|
|
const currentScrollLeft = Number(this.data.monthScrollLeft || 0)
|
|
this.setData({ monthScrollLeft: currentScrollLeft === scrollLeft ? scrollLeft - 1 : scrollLeft })
|
|
setTimeout(() => {
|
|
this.setData({ monthScrollLeft: scrollLeft })
|
|
}, 80)
|
|
},
|
|
applyMonthView(rawRows, months) {
|
|
const rows = this.buildRows(this.getFilteredRows(rawRows), months)
|
|
const monthEndSpacerWidth = Number(this.data.selectedYear) === Number(this.data.currentYear)
|
|
? END_SPACER_WIDTH
|
|
: 0
|
|
const monthTableWidth = months.reduce((sum, item) => sum + Number(item.width || 0), 0) + monthEndSpacerWidth
|
|
const totals = rows.reduce((result, row) => {
|
|
row.months.forEach(month => {
|
|
result.reported += Number(month.reportedGoodQty || 0)
|
|
result.reconciled += Number(month.reconciledGoodQty || 0)
|
|
result.returnQty += Number(month.returnQty || 0)
|
|
})
|
|
return result
|
|
}, { reported: 0, reconciled: 0, returnQty: 0 })
|
|
this.setData({
|
|
rows,
|
|
months,
|
|
monthTableWidth,
|
|
monthEndSpacerWidth,
|
|
totalReported: formatQty(totals.reported),
|
|
totalReconciled: formatQty(totals.reconciled),
|
|
totalReturn: formatQty(totals.returnQty),
|
|
})
|
|
},
|
|
async loadLedger() {
|
|
if (!this.data.selectedYear) {
|
|
return
|
|
}
|
|
this.setData({ loading: true })
|
|
try {
|
|
const result = await api.listReconciliationLedger(this.data.selectedYear)
|
|
const months = this.buildMonths(result.year, result.currentYear, result.currentMonth)
|
|
this.setData({
|
|
selectedYear: result.year,
|
|
currentYear: result.currentYear,
|
|
currentMonth: result.currentMonth,
|
|
rawRows: result.rows || [],
|
|
...this.getPointFilterState(result.rows || []),
|
|
})
|
|
this.applyMonthView(result.rows || [], months)
|
|
this.alignToCurrentMonth(months, result.year, result.currentYear, result.currentMonth)
|
|
} catch (error) {
|
|
wx.showToast({ title: error.message || '加载账本失败', icon: 'none' })
|
|
} finally {
|
|
this.setData({ loading: false })
|
|
}
|
|
},
|
|
onYearChange(e) {
|
|
const yearIndex = Number(e.detail.value)
|
|
const selectedYear = this.data.years[yearIndex]
|
|
this.setData({ yearIndex, selectedYear })
|
|
this.loadLedger()
|
|
},
|
|
onPointChange(e) {
|
|
const pointIndex = Number(e.detail.value)
|
|
const selected = this.data.pointOptions[pointIndex] || this.data.pointOptions[0]
|
|
this.setData({
|
|
pointIndex,
|
|
selectedPointValue: selected ? selected.value : ALL_ATTENDANCE_POINTS,
|
|
selectedPointLabel: selected ? selected.label : '全部考勤点',
|
|
})
|
|
this.applyMonthView(this.data.rawRows, this.data.months)
|
|
},
|
|
toggleMonth(e) {
|
|
const index = Number(e.currentTarget.dataset.index)
|
|
const months = this.data.months.map((item, itemIndex) => {
|
|
if (itemIndex !== index) {
|
|
return item
|
|
}
|
|
const expanded = !item.expanded
|
|
return {
|
|
...item,
|
|
expanded,
|
|
width: expanded ? EXPANDED_MONTH_WIDTH : COLLAPSED_MONTH_WIDTH,
|
|
}
|
|
})
|
|
this.applyMonthView(this.data.rawRows, months)
|
|
},
|
|
onQtyInput(e) {
|
|
const rowIndex = Number(e.currentTarget.dataset.rowIndex)
|
|
const monthIndex = Number(e.currentTarget.dataset.monthIndex)
|
|
const field = e.currentTarget.dataset.field || 'reconciledGoodQty'
|
|
const value = sanitizeQty(e.detail.value)
|
|
this.setData({
|
|
[`rows[${rowIndex}].months[${monthIndex}].${field}Input`]: value,
|
|
})
|
|
},
|
|
updateRawCell(attendancePointName, productName, month, field, value) {
|
|
const rawRows = this.data.rawRows.map(row => {
|
|
if (row.productName !== productName || row.attendancePointName !== attendancePointName) {
|
|
return row
|
|
}
|
|
return {
|
|
...row,
|
|
months: row.months.map(item => (
|
|
Number(item.month) === Number(month)
|
|
? { ...item, [field]: value }
|
|
: item
|
|
)),
|
|
}
|
|
})
|
|
this.setData({ rawRows })
|
|
return rawRows
|
|
},
|
|
async saveCell(e) {
|
|
const rowIndex = Number(e.currentTarget.dataset.rowIndex)
|
|
const monthIndex = Number(e.currentTarget.dataset.monthIndex)
|
|
const field = e.currentTarget.dataset.field || 'reconciledGoodQty'
|
|
const inputField = `${field}Input`
|
|
const row = this.data.rows[rowIndex]
|
|
const month = row && row.months ? row.months[monthIndex] : null
|
|
if (!row || !month) {
|
|
return
|
|
}
|
|
const inputText = sanitizeQty(e.detail.value)
|
|
const value = Number(inputText || 0)
|
|
if (!Number.isFinite(value) || value < 0) {
|
|
wx.showToast({ title: '只能填写数字', icon: 'none' })
|
|
return
|
|
}
|
|
const roundedValue = Math.round(value * 100) / 100
|
|
const key = `${row.attendancePointName}-${row.productName}-${month.month}-${field}`
|
|
if (roundedValue === Number(month[field] || 0)) {
|
|
this.setData({
|
|
[`rows[${rowIndex}].months[${monthIndex}].${inputField}`]: formatQty(roundedValue),
|
|
})
|
|
return
|
|
}
|
|
this.setData({ savingKey: key })
|
|
try {
|
|
await api.saveReconciliationEntry({
|
|
year: this.data.selectedYear,
|
|
month: month.month,
|
|
attendancePointName: row.attendancePointName,
|
|
productName: row.productName,
|
|
[field]: roundedValue,
|
|
})
|
|
const rawRows = this.updateRawCell(row.attendancePointName, row.productName, month.month, field, roundedValue)
|
|
this.applyMonthView(rawRows, this.data.months)
|
|
} catch (error) {
|
|
wx.showToast({ title: error.message || '保存失败', icon: 'none' })
|
|
} finally {
|
|
this.setData({ savingKey: '' })
|
|
}
|
|
},
|
|
})
|