JhHardwareWRS/pages/reconciliationLedger/reconciliationLedger.js
2026-06-24 15:19:14 +08:00

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