baseline: 薪酬逻辑收口到 shared/payroll.js 后的当前全量状态
这是第一个包含完整工作区的提交,也是后续「拆 SFC / 上 Vite」的回滚基线。 此前的提交均为 index.html 单文件历史快照,由 .bak 文件重建。 当前状态(提交前已实测): - index.html 内联薪酬算法已删除,改调 WHTPayroll - shared/payroll.js 为唯一薪酬实现,浏览器与 Node 共用 - 对外接口未变:aggregateMonth / isCutEdge / payDayKind 等保留同名薄包装 未入库内容见 .gitignore:依赖、Chrome 验证 profile、验证截图、 运行时 sqlite 数据与日志、时间戳备份、.trash。
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
// 探针:把「一个月的全部薪酬输出」压成一个可比较的扁平对象。
|
||||
//
|
||||
// 两个实现必须产出结构完全相同的对象:
|
||||
// probeApp(app) —— 从 index.html 跑出来的 setup() 返回值上读
|
||||
// probePure(sc) —— 只用 shared/payroll.js 算
|
||||
// 于是「重构有没有改变行为」就退化成一次 deepEqual。
|
||||
'use strict';
|
||||
|
||||
const P = require('../../shared/payroll.js');
|
||||
|
||||
// 浮点噪声必须先掐掉:19.5 × 22 这类乘法在不同写法下可能差 1e-13,
|
||||
// 那不是行为变化,却会让 deepEqual 全线报红。金额留 4 位足够(页面只显示 1 位)。
|
||||
const r4 = (n) => {
|
||||
const v = Number(n);
|
||||
if (!Number.isFinite(v)) return v === undefined ? null : String(v);
|
||||
return Math.round(v * 1e4) / 1e4;
|
||||
};
|
||||
|
||||
const STAT_KEYS = [
|
||||
'h1', 'h2', 'totalHours', 'workDays', 'dayShifts', 'nightShifts', 'restDays', 'leaveDays',
|
||||
'foundCount', 'missedCount', 'otherPenalties',
|
||||
'fullBonus', 'foundBonus', 'missedPenalty', 'adjustNet', 'reviewNet',
|
||||
'rawReview', 'reviewAmount', 'reviewDelta', 'basePay'
|
||||
];
|
||||
|
||||
const CFG_KEYS = [
|
||||
'rate', 'fullDays', 'fullBonusPerHour', 'foundBonus', 'missedPenalty',
|
||||
'cutoff', 'payDayA', 'payDayB', 'dayHours', 'nightHours', 'defaultShift'
|
||||
];
|
||||
|
||||
function pickStats(s) {
|
||||
const out = {};
|
||||
for (const k of STAT_KEYS) out[k] = r4(s[k]);
|
||||
out.finalized = s.finalized === true;
|
||||
out.adjustStatus = s.adjustStatus === undefined ? null : s.adjustStatus;
|
||||
return out;
|
||||
}
|
||||
|
||||
function pickCfg(c) {
|
||||
const out = {};
|
||||
for (const k of CFG_KEYS) out[k] = typeof c[k] === 'string' ? c[k] : r4(c[k]);
|
||||
return out;
|
||||
}
|
||||
|
||||
const pickPenalties = (list) =>
|
||||
Array.prototype.map.call(list, (d) => ({ day: d.day, reason: d.reason, amount: r4(d.amount) }));
|
||||
|
||||
/**
|
||||
* 拉回宿主 realm。
|
||||
* index.html 是在 vm 沙箱里跑的,它造出来的数组/对象带的是**沙箱的** Array.prototype,
|
||||
* 而 deepStrictEqual 会连原型一起比 —— 不做这一步,所有场景都会因为原型不同而全红,
|
||||
* 报错还长得像「结构相同但不相等」,非常容易误判成算法出了问题(实测踩过)。
|
||||
*/
|
||||
const hostify = (o) => JSON.parse(JSON.stringify(o));
|
||||
|
||||
/** 从跑起来的 index.html 应用实例上读。 */
|
||||
function probeApp(app) {
|
||||
const s = app.monthStats.value;
|
||||
const p = app.prevStats.value;
|
||||
const cfg = app.cfg.value;
|
||||
const days = new Date(app.currentYear.value, app.currentMonth.value + 1, 0).getDate();
|
||||
const allDays = Array.from({ length: days }, (_, i) => i + 1);
|
||||
|
||||
return hostify({
|
||||
cfg: pickCfg(cfg),
|
||||
cur: pickStats(s),
|
||||
prev: pickStats(p),
|
||||
prevLabel: { year: app.prevLabel.value.year, month: app.prevLabel.value.month },
|
||||
nextLabel: { year: app.nextLabel.value.year, month: app.nextLabel.value.month },
|
||||
flows: {
|
||||
thisA: r4(app.payoutThisA.value),
|
||||
thisB: r4(app.payoutThisB.value),
|
||||
nextB: r4(app.payoutNextB.value),
|
||||
earnedTotal: r4(app.earnedTotal.value),
|
||||
cashTotal: r4(app.cashTotal.value),
|
||||
carry: r4(app.earnedSplit.value.carry),
|
||||
paid: r4(app.earnedSplit.value.paid),
|
||||
floored: app.earnedFloored.value === true
|
||||
},
|
||||
penalties: pickPenalties(app.penaltyDetails.value),
|
||||
calendar: {
|
||||
days,
|
||||
cutEdge: allDays.filter((d) => app.isCutEdge(d)),
|
||||
payDayA: allDays.filter((d) => app.payDayKind(d) === 'a'),
|
||||
payDayB: allDays.filter((d) => app.payDayKind(d) === 'b'),
|
||||
penaltyFlag: allDays.filter((d) => app.hasPenalty(d))
|
||||
},
|
||||
misc: {
|
||||
fullDaysLeft: app.fullDaysLeft.value,
|
||||
payDayAPassed: app.payDayAPassed.value === true,
|
||||
isCurrentMonth: app.isCurrentMonth.value === true,
|
||||
recordCount: app.recordCount.value,
|
||||
boardInt: String(app.boardMain.value.int),
|
||||
boardFrac: String(app.boardMain.value.frac),
|
||||
boardSign: String(app.boardMain.value.sign),
|
||||
hourOptions: app.hourOptions.join(','),
|
||||
daysInMonth: app.daysInMonth.value,
|
||||
firstDayOfWeek: app.firstDayOfWeek.value
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 只用 shared/payroll.js 算,不碰 Vue、不碰 index.html。 */
|
||||
function probePure(sc, nowIso) {
|
||||
const cfg = P.normalizeSettings(sc.settings || {});
|
||||
const ctx = { records: sc.records || {}, monthAdjust: sc.adjust || {}, cfg };
|
||||
const cur = P.aggregateMonth(sc.year, sc.month, ctx);
|
||||
const prevL = P.prevMonthOf(sc.year, sc.month);
|
||||
const nextL = P.nextMonthOf(sc.year, sc.month);
|
||||
const prev = P.aggregateMonth(prevL.year, prevL.month, ctx);
|
||||
const flows = P.flowsOf({ cur, prev, cfg });
|
||||
const now = nowIso ? new Date(nowIso) : new Date();
|
||||
|
||||
const days = P.daysInMonth(sc.year, sc.month);
|
||||
const allDays = Array.from({ length: days }, (_, i) => i + 1);
|
||||
const records = sc.records || {};
|
||||
const prefix = P.monthPrefix(sc.year, sc.month);
|
||||
const hasPenalty = (day) => {
|
||||
const rec = records[prefix + P.pad2(day)];
|
||||
return !!rec && rec.status === 'work' &&
|
||||
(Number(rec.contraband_missed || 0) > 0 || Number(rec.other_penalty || 0) > 0);
|
||||
};
|
||||
const parts = P.moneyParts(flows.earnedTotal);
|
||||
|
||||
return {
|
||||
cfg: pickCfg(cfg),
|
||||
cur: pickStats(cur),
|
||||
prev: pickStats(prev),
|
||||
prevLabel: { year: prevL.year, month: prevL.month },
|
||||
nextLabel: { year: nextL.year, month: nextL.month },
|
||||
flows: {
|
||||
thisA: r4(flows.thisA),
|
||||
thisB: r4(flows.thisB),
|
||||
nextB: r4(flows.nextB),
|
||||
earnedTotal: r4(flows.earnedTotal),
|
||||
cashTotal: r4(flows.cashTotal),
|
||||
carry: r4(flows.carry),
|
||||
paid: r4(flows.split.paid),
|
||||
floored: flows.floored === true
|
||||
},
|
||||
penalties: pickPenalties(P.penaltyDetails(sc.year, sc.month, ctx)),
|
||||
calendar: {
|
||||
days,
|
||||
cutEdge: allDays.filter((d) => P.isCutEdge(d, cfg)),
|
||||
payDayA: allDays.filter((d) => P.payDayKind(d, cfg) === 'a'),
|
||||
payDayB: allDays.filter((d) => P.payDayKind(d, cfg) === 'b'),
|
||||
penaltyFlag: allDays.filter(hasPenalty)
|
||||
},
|
||||
misc: {
|
||||
fullDaysLeft: P.fullDaysLeft(cur, cfg),
|
||||
payDayAPassed: P.payDayAPassed(sc.year, sc.month, cfg, now) === true,
|
||||
isCurrentMonth: P.isCurrentMonth(sc.year, sc.month, now) === true,
|
||||
recordCount: Object.keys(records).length,
|
||||
boardInt: String(parts.int),
|
||||
boardFrac: String(parts.frac),
|
||||
boardSign: String(parts.sign),
|
||||
hourOptions: P.hourOptions.join(','),
|
||||
daysInMonth: days,
|
||||
firstDayOfWeek: P.firstDayOfWeek(sc.year, sc.month)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { probeApp, probePure, r4, STAT_KEYS };
|
||||
Reference in New Issue
Block a user