Compare commits

..

No commits in common. "4003625fb1c0011b4670f73cbea2a55960e3cf82" and "be55bd44fe0cf5701e01aceb8798bc7d3ecb2671" have entirely different histories.

11 changed files with 33 additions and 4368 deletions

122
app.wxss
View File

@ -709,32 +709,11 @@ scroll-view.page {
pointer-events: none; pointer-events: none;
} }
.over-limit-watermark {
position: absolute;
right: 28rpx;
top: 18rpx;
z-index: 4;
padding: 8rpx 18rpx;
border: 2rpx solid rgba(197, 34, 31, 0.72);
border-radius: 12rpx;
color: rgba(148, 27, 23, 0.92);
background: rgba(255, 238, 238, 0.94);
font-size: 30rpx;
font-weight: 900;
line-height: 1.2;
transform: rotate(10deg);
pointer-events: none;
}
.lower-watermark, .lower-watermark,
.misc-watermark.lower-watermark, .misc-watermark.lower-watermark,
.cleaning-watermark.lower-watermark, .cleaning-watermark.lower-watermark,
.multi-person-watermark.lower-watermark, .multi-person-watermark.lower-watermark,
.continuous-die-watermark.lower-watermark, .continuous-die-watermark.lower-watermark {
.timeout-watermark.lower-watermark,
.voided-watermark.lower-watermark,
.modified-watermark.lower-watermark,
.over-limit-watermark.lower-watermark {
top: 78rpx; top: 78rpx;
} }
@ -742,50 +721,10 @@ scroll-view.page {
.misc-watermark.third-watermark, .misc-watermark.third-watermark,
.cleaning-watermark.third-watermark, .cleaning-watermark.third-watermark,
.multi-person-watermark.third-watermark, .multi-person-watermark.third-watermark,
.continuous-die-watermark.third-watermark, .continuous-die-watermark.third-watermark {
.timeout-watermark.third-watermark,
.voided-watermark.third-watermark,
.modified-watermark.third-watermark,
.over-limit-watermark.third-watermark {
top: 138rpx; top: 138rpx;
} }
.fourth-watermark,
.misc-watermark.fourth-watermark,
.cleaning-watermark.fourth-watermark,
.multi-person-watermark.fourth-watermark,
.continuous-die-watermark.fourth-watermark,
.timeout-watermark.fourth-watermark,
.voided-watermark.fourth-watermark,
.modified-watermark.fourth-watermark,
.over-limit-watermark.fourth-watermark {
top: 198rpx;
}
.fifth-watermark,
.misc-watermark.fifth-watermark,
.cleaning-watermark.fifth-watermark,
.multi-person-watermark.fifth-watermark,
.continuous-die-watermark.fifth-watermark,
.timeout-watermark.fifth-watermark,
.voided-watermark.fifth-watermark,
.modified-watermark.fifth-watermark,
.over-limit-watermark.fifth-watermark {
top: 258rpx;
}
.sixth-watermark,
.misc-watermark.sixth-watermark,
.cleaning-watermark.sixth-watermark,
.multi-person-watermark.sixth-watermark,
.continuous-die-watermark.sixth-watermark,
.timeout-watermark.sixth-watermark,
.voided-watermark.sixth-watermark,
.modified-watermark.sixth-watermark,
.over-limit-watermark.sixth-watermark {
top: 318rpx;
}
.timeout-watermark { .timeout-watermark {
position: absolute; position: absolute;
right: 24rpx; right: 24rpx;
@ -893,63 +832,6 @@ scroll-view.page {
line-height: 1.35; line-height: 1.35;
} }
.over-limit-panel {
margin-top: 16rpx;
padding: 16rpx;
border-radius: 14rpx;
display: flex;
flex-direction: column;
gap: 8rpx;
box-sizing: border-box;
}
.over-limit-panel.danger {
border: 2rpx solid rgba(197, 34, 31, 0.28);
background: rgba(255, 239, 239, 0.78);
}
.over-limit-panel.warn {
border: 2rpx solid rgba(217, 119, 6, 0.28);
background: rgba(255, 247, 232, 0.78);
}
.over-limit-title {
color: #9f1c19;
font-size: 24rpx;
font-weight: 900;
line-height: 1.35;
}
.over-limit-panel.warn .over-limit-title {
color: #9a4a05;
}
.over-limit-text {
color: #7a271a;
font-size: 23rpx;
font-weight: 700;
line-height: 1.45;
word-break: break-all;
}
.over-limit-panel.warn .over-limit-text {
color: #7a4a12;
}
.over-limit-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 6rpx 14rpx;
color: #7a271a;
font-size: 22rpx;
font-weight: 700;
line-height: 1.4;
}
.over-limit-panel.warn .over-limit-grid {
color: #7a4a12;
}
.filter-bar { .filter-bar {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,350 +0,0 @@
# 报工跨日期工时归属拆分设计
## 背景
现有报工记录使用 `report_date` 作为单一报工日期。这个字段被报工记录、管理员审核、经理看板、导出、对账小账本、设备/模具使用统计等模块复用。
当员工夜里或凌晨跨日期工作时,单一 `report_date` 无法准确表达工时归属。例如:
- `00:10` 上班,`11:00` 下班
- `00:10-06:00` 是前一天夜班尾段
- `06:00-08:00` 是作息空白区间,但按新规则应算加班
- `08:00-11:00` 是当天白班
如果整条报工强行归前一天或当天,都会造成一部分工时、数量、工资统计不合理。
## 目标
实现一条原始报工记录可以按工时归属拆分到多个日期。
核心原则:
- 原始报工记录仍然只保留一条,不拆成多条审核记录。
- 管理员审核、作废、更改、水印、员工记录仍围绕原始报工记录。
- 经理看板、导出、对账小账本、设备/模具统计、概览等统计类模块按拆分后的归属明细汇总。
- 跨日期时,成品数量、不良数量、报废数量、换料次数、参考工资等涉及统计的字段按有效工时比例拆分。
- 作息空白区间统一算加班,但不直接改变原始 `report_date`
## 非目标
- 不把一条原始报工拆成多条 `ProductionReport`
- 不开放管理员手工编辑 `report_date`
- 不批量回改历史记录的原始 `report_date`
- 不改变员工扫码上班、换模具、下班提交的主流程。
## 术语
- 原始报工:员工提交或系统自动提交后形成的一条 `ProductionReport`
- 工序明细:一条报工下的产品工序明细。
- 归属明细:按日期和班种拆出来的统计明细。
- 有效工时:扣除符合规则的吃饭时间后,用于统计、工资和数量分摊的工时。
- 空白区间:不属于白班、夜班、原加班区间的作息时间,例如默认 `06:00-08:00`、`17:20-18:00`。
## 推荐方案
采用“一条原始报工 + 多条归属明细”的方案。
### 原始记录
`ProductionReport` 继续保存员工真实提交的完整记录:
- 考勤点
- 员工
- 上班时间
- 下班时间
- 报工明细
- 审核状态
- 作废状态
- 更改状态
- 原始 `report_date`
原始 `report_date` 仍用于报工记录卡片的基础展示,但统计类模块不再只依赖它。
### 归属明细
新增一张后端表,例如 `production_report_allocations`
建议字段:
- `id`
- `report_id`
- `report_item_id`
- `attendance_point_name`
- `employee_phone`
- `allocation_date`
- `day_minutes`
- `overtime_minutes`
- `night_minutes`
- `effective_minutes`
- `good_qty`
- `defect_qty`
- `scrap_qty`
- `changeover_count`
- `reference_wage`
- `created_at`
- `updated_at`
如果后续设备/模具统计需要直接按设备或模具拆分,可以再补:
- `device_no`
- `mold_name`
- `product_name`
- `process_name`
- `stamping_method`
第一版建议保留到 `report_item_id` 级别,设备/模具统计可以根据现有工序明细和设备段关联计算,避免一次性改表过重。
## 拆分规则
### 班种区间
按报工所属考勤点的作息配置计算:
- 白班:`day_start` 到 `day_end`
- 原加班:`overtime_start` 到 `overtime_end`
- 夜班:`night_start` 到 `night_end`
- 午饭、晚饭:继续使用当前吃饭扣除规则
夜班跨天时,例如 `20:00-06:00`,按当前系统已有跨天区间逻辑处理。
### 空白区间
一天中不属于白班、原加班、夜班的时间,统一计入加班。
默认作息下:
- `06:00-08:00` 算加班
- `17:20-18:00` 算加班
空白区间只影响工时分布,不单独改变原始 `report_date`
### 日期归属
归属明细按时间片所在的自然日期和夜班归属拆分。
默认作息 `20:00-06:00` 下:
- `20:00-24:00` 夜班归属当天
- `00:00-06:00` 夜班归属前一天
- `06:00` 之后的空白加班、白班、加班归属当天
例子:`2026-07-25 00:10` 到 `2026-07-25 11:00`
- `2026-07-24`:夜班 `350` 分钟
- `2026-07-25`:加班 `120` 分钟,白班 `180` 分钟
### 数量和金额分摊
每个工序明细先计算总有效分钟,再按日期归属比例拆分。
公式:
```text
归属比例 = 某归属日有效分钟 / 当前工序明细总有效分钟
归属数量 = 原始数量 * 归属比例
归属工资 = 原始参考工资 * 归属比例
```
适用字段:
- 成品数量
- 不良数量
- 报废数量
- 换料次数
- 参考工资
### 小数处理
后端内部保留两位小数。
导出和页面展示可以使用两位小数,避免比例拆分后出现不合理的整数强行取整。
如果未来现场要求成品必须整数,可以另开一个“整数分摊余数归属规则”,本次不引入。
## 数据流
### 员工提交报工
1. 员工扫码、填写报工并提交。
2. 后端创建一条 `ProductionReport` 和对应 `ProductionReportItem`
3. 后端计算原始记录指标。
4. 后端生成或刷新归属明细。
5. 返回原始报工卡片。
### 系统自动提交
1. 自动提交服务创建原始报工。
2. 原始结束时间为系统自动提交时间。
3. 后端计算原始记录指标。
4. 后端生成或刷新归属明细。
### 管理员审核或更改
管理员调整以下内容时,需要重算归属明细:
- 上班时间
- 下班时间
- 换模具时间
- 产品工序
- 设备
- 成品数量
- 不良数量
- 报废数量
- 换料次数
- 工序单价
- 作废/撤销作废
保存流程:
1. 校验管理员权限和编辑窗口。
2. 保存原始记录修改。
3. 重算原始记录指标。
4. 删除旧归属明细。
5. 生成新归属明细。
6. 设置更改水印。
作废记录不参与统计。作废时可以保留归属明细,但统计查询必须过滤;也可以删除归属明细。第一版建议保留并在统计查询中过滤,方便撤销作废后立即恢复统计。
## 模块影响
### 员工报工记录
仍显示一条原始记录。
可在明细里补充展示工时归属,例如:
```text
工时归属2026-07-24 夜班 5小时50分2026-07-25 加班 2小时、白班 3小时
```
第一版只在管理员和经理侧展示归属摘要,员工侧报工记录保留现状。
### 管理员审核页
仍按原始记录审核。
管理员改时间或数量后,页面返回重算后的原始指标和归属摘要。
审核页可以展示归属摘要,帮助管理员知道这条记录会被拆到哪些日期。
### 经理报工看板
看板按归属明细汇总,不再只按 `ProductionReport.report_date` 汇总。
筛选日期查的是 `allocation_date`
同人、同考勤点、同项目、同产品、同工序、同冲压方式的聚合,应基于归属明细汇总。
### 经理看板导出
第一张明细 sheet 按归属明细导出。
建议保留两个日期字段:
- 原始报工日期
- 统计归属日期
这样经理既能追溯原始记录,也能看到工资和工时实际归属。
参考工资、工序单价、工种、备注等现有导出字段继续保留。
工资汇总 sheet 按归属明细汇总。
### 对账小账本
已报工成品数量使用归属明细统计。
最后一道工序判断仍按产品清单数字工序大小确定。
月份归属改为 `allocation_date` 所在月份。
### 设备/模具使用统计
默认统计审核通过且未作废记录。
时间筛选按归属明细日期。
普通设备/模具:
- 时长按归属明细有效分钟汇总
- 报工次数按原始报工 ID 去重
建议第一版:
- 时长按归属明细汇总
- 报工次数按原始报工 ID 去重
清洗设备:
- 清洗数量按归属明细数量汇总
- 报工次数按原始报工 ID 去重
### 概览
涉及今日、历史待审核、已通过等指标时:
- 审核状态类数量仍按原始报工记录。
- 工时、数量、工资类指标按归属明细。
## 错误处理
- 如果归属明细生成失败,原始报工保存应回滚,避免原始记录和统计数据不一致。
- 如果旧数据没有归属明细,统计查询应提供兼容兜底:用原始记录实时计算或用迁移脚本补齐。
- 如果有效分钟为 `0`,数量和工资不做比例拆分,统一归到原始 `report_date`,同时记录日志,避免除零。
- 如果作息配置缺失,使用当前默认作息。
## 历史数据处理
历史原始 `report_date` 不批量回改。
但为了让统计类模块切换到新口径,建议提供一次性迁移脚本:
1. 扫描审核通过且未作废的历史报工。
2. 根据记录当时的上下班时间、工序明细、考勤点作息生成归属明细。
3. 不修改原始 `report_date`
4. 迁移后输出处理数量、跳过数量、异常记录 ID。
如果担心历史作息被后续修改导致还原不准,第一版接受使用当前考勤点作息生成历史归属明细。后续如要严格追溯,需要在报工时快照作息配置。
## 测试用例
核心用例:
- `2026-07-25 00:10``2026-07-25 11:00`
- `2026-07-24` 夜班 `350` 分钟
- `2026-07-25` 加班 `120` 分钟,白班 `180` 分钟
- `2026-07-25 05:59``2026-07-25 11:00`
- `2026-07-24` 夜班 `1` 分钟
- `2026-07-25` 加班 `120` 分钟,白班 `180` 分钟
- `2026-07-24 20:00``2026-07-25 06:30`
- `2026-07-24` 夜班 `600` 分钟
- `2026-07-25` 加班 `30` 分钟
- `2026-07-25 06:00``2026-07-25 08:00`
- `2026-07-25` 加班 `120` 分钟
- `2026-07-25 17:20``2026-07-25 18:00`
- `2026-07-25` 加班 `40` 分钟
- 覆盖吃饭扣除和 5 分钟缓冲规则。
- 覆盖管理员修改上下班时间后归属明细重算。
- 覆盖管理员修改换模具时间后归属明细重算。
- 覆盖作废记录从统计中排除。
- 覆盖撤销作废后统计恢复。
## 实施顺序建议
1. 后端新增归属计算服务和单元测试。
2. 新增归属明细表和迁移脚本。
3. 报工提交、自动提交、管理员审核保存后写入归属明细。
4. 经理看板和导出切换到归属明细。
5. 对账小账本、设备/模具统计、概览按风险顺序切换。
6. 前端增加归属摘要展示。
7. 跑迁移脚本补齐历史归属明细。
## 已固定技术口径
当前方案已按以下业务口径固定:
- 涉及拆分的字段都按工时比例拆。
- 空白作息区间算加班。
- 空白作息区间不影响原始 `report_date`
- 原始报工记录不拆。
- 报工次数统计按原始报工 ID 去重,不因一条报工拆到多个日期而重复计数。

View File

@ -37,18 +37,16 @@
<view wx:if="{{!rows.length}}" class="list-empty">暂无已审核报工</view> <view wx:if="{{!rows.length}}" class="list-empty">暂无已审核报工</view>
<view wx:for="{{rows}}" wx:key="id" class="card dashboard-card {{item.isSystemAutoSubmitted ? 'system-auto' : ''}} {{item.isVoided ? 'voided' : ''}}"> <view wx:for="{{rows}}" wx:key="id" class="card dashboard-card {{item.isSystemAutoSubmitted ? 'system-auto' : ''}} {{item.isVoided ? 'voided' : ''}}">
<text wx:if="{{item.hasOverLimit}}" class="over-limit-watermark {{item.watermarkClasses.overLimit}}">超额</text> <text wx:if="{{item.isVoided}}" class="voided-watermark">作废</text>
<text wx:if="{{item.isVoided}}" class="voided-watermark {{item.watermarkClasses.state}}">作废</text> <text wx:elif="{{item.correctionPairs.length}}" class="modified-watermark">更改</text>
<text wx:elif="{{item.correctionPairs.length}}" class="modified-watermark {{item.watermarkClasses.state}}">更改</text> <text wx:if="{{item.isSystemAutoSubmitted}}" class="timeout-watermark">超时-系统自动提交</text>
<text wx:if="{{item.isSystemAutoSubmitted}}" class="timeout-watermark {{item.watermarkClasses.timeout}}">超时-系统自动提交</text> <text wx:if="{{item.isCleaning}}" class="cleaning-watermark {{item.isSystemAutoSubmitted || item.isVoided || item.correctionPairs.length ? 'lower-watermark' : ''}}">清洗</text>
<text wx:if="{{item.isCleaning}}" class="cleaning-watermark {{item.watermarkClasses.cleaning}}">清洗</text> <text wx:if="{{item.isMisc}}" class="misc-watermark {{item.isSystemAutoSubmitted || item.isVoided || item.correctionPairs.length ? 'lower-watermark' : ''}}">包含杂活</text>
<text wx:if="{{item.isMisc}}" class="misc-watermark {{item.watermarkClasses.misc}}">包含杂活</text> <text wx:if="{{item.isMultiPerson}}" class="multi-person-watermark {{item.isSystemAutoSubmitted || item.isVoided || item.correctionPairs.length || item.isMisc || item.isCleaning ? 'lower-watermark' : ''}}">多人协作</text>
<text wx:if="{{item.isMultiPerson}}" class="multi-person-watermark {{item.watermarkClasses.multiPerson}}">多人协作</text> <text wx:if="{{item.isContinuousDie}}" class="continuous-die-watermark {{item.isSystemAutoSubmitted || item.isVoided || item.correctionPairs.length || item.isMisc || item.isCleaning || item.isMultiPerson ? 'lower-watermark' : ''}} {{item.isMultiPerson && (item.isSystemAutoSubmitted || item.isVoided || item.correctionPairs.length || item.isMisc || item.isCleaning) ? 'third-watermark' : ''}}">连续模</text>
<text wx:if="{{item.isContinuousDie}}" class="continuous-die-watermark {{item.watermarkClasses.continuousDie}}">连续模</text>
<view class="row"> <view class="row">
<view class="row-left"> <view class="row-left">
<text class="value">{{item.employeeName}} · {{item.reportDate}}</text> <text class="value">{{item.employeeName}} · {{item.reportDate}}</text>
<text wx:if="{{item.sourceReportDatesText}}" class="allocation-summary">原始报工日期 {{item.sourceReportDatesText}}</text>
<text class="subtitle">考勤点 {{item.attendancePointName || '-'}}</text> <text class="subtitle">考勤点 {{item.attendancePointName || '-'}}</text>
<text class="subtitle">{{item.employeePhone}} · {{item.projectNo}} / {{item.productName}}</text> <text class="subtitle">{{item.employeePhone}} · {{item.projectNo}} / {{item.productName}}</text>
<text class="subtitle">{{item.processDisplayName || item.productName || '-'}}</text> <text class="subtitle">{{item.processDisplayName || item.productName || '-'}}</text>
@ -119,10 +117,6 @@
<text class="label">备注</text> <text class="label">备注</text>
<text class="value">{{item.reviewRemark}}</text> <text class="value">{{item.reviewRemark}}</text>
</view> </view>
<view wx:if="{{item.hasOverLimitNotice}}" class="over-limit-panel {{item.hasOverLimit ? 'danger' : 'warn'}}">
<text class="over-limit-title">{{item.overLimitSummary || (item.hasOverLimit ? '超额' : '无法校验')}}</text>
<text wx:if="{{item.overLimitReasons}}" class="over-limit-text">{{item.overLimitReasons}}</text>
</view>
<view class="salary-divider"></view> <view class="salary-divider"></view>
<view class="salary-row"> <view class="salary-row">
<text class="label">参考工资</text> <text class="label">参考工资</text>

View File

@ -21,14 +21,6 @@
overflow: hidden; overflow: hidden;
} }
.allocation-summary {
display: block;
margin-top: 6rpx;
color: #4b5563;
font-size: 24rpx;
line-height: 1.45;
}
.cleaning-watermark { .cleaning-watermark {
position: absolute; position: absolute;
right: 28rpx; right: 28rpx;

View File

@ -63,14 +63,13 @@
data-id="{{item.id}}" data-id="{{item.id}}"
bindtap="toggleRecordDetail" bindtap="toggleRecordDetail"
> >
<text wx:if="{{item.hasOverLimit}}" class="over-limit-watermark {{item.watermarkClasses.overLimit}}">超额</text> <text wx:if="{{item.isVoided}}" class="voided-watermark">作废</text>
<text wx:if="{{item.isVoided}}" class="voided-watermark {{item.watermarkClasses.state}}">作废</text> <text wx:elif="{{item.isModified}}" class="modified-watermark">更改</text>
<text wx:elif="{{item.isModified}}" class="modified-watermark {{item.watermarkClasses.state}}">更改</text> <text wx:if="{{item.isSystemAutoSubmitted}}" class="timeout-watermark">{{item.autoSubmitReason || '超时-系统自动提交'}}</text>
<text wx:if="{{item.isSystemAutoSubmitted}}" class="timeout-watermark {{item.watermarkClasses.timeout}}">{{item.autoSubmitReason || '超时-系统自动提交'}}</text> <text wx:if="{{item.isCleaning}}" class="cleaning-watermark {{item.isSystemAutoSubmitted || item.isVoided || item.isModified ? 'lower-watermark' : ''}}">清洗</text>
<text wx:if="{{item.isCleaning}}" class="cleaning-watermark {{item.watermarkClasses.cleaning}}">清洗</text> <text wx:if="{{item.hasMisc}}" class="misc-watermark {{item.isSystemAutoSubmitted || item.isVoided || item.isModified ? 'lower-watermark' : ''}}">包含杂活</text>
<text wx:if="{{item.hasMisc}}" class="misc-watermark {{item.watermarkClasses.misc}}">包含杂活</text> <text wx:if="{{item.hasMultiPerson}}" class="multi-person-watermark {{item.isSystemAutoSubmitted || item.isVoided || item.isModified || item.hasMisc || item.isCleaning ? 'lower-watermark' : ''}}">多人协作</text>
<text wx:if="{{item.hasMultiPerson}}" class="multi-person-watermark {{item.watermarkClasses.multiPerson}}">多人协作</text> <text wx:if="{{item.hasContinuousDie}}" class="continuous-die-watermark {{item.isSystemAutoSubmitted || item.isVoided || item.isModified || item.hasMisc || item.isCleaning || item.hasMultiPerson ? 'lower-watermark' : ''}} {{item.hasMultiPerson && (item.isSystemAutoSubmitted || item.isVoided || item.isModified || item.hasMisc || item.isCleaning) ? 'third-watermark' : ''}}">连续模</text>
<text wx:if="{{item.hasContinuousDie}}" class="continuous-die-watermark {{item.watermarkClasses.continuousDie}}">连续模</text>
<view class="row"> <view class="row">
<view class="row-left"> <view class="row-left">
<text class="value">{{item.employeeName}} · {{item.reportDate}}</text> <text class="value">{{item.employeeName}} · {{item.reportDate}}</text>
@ -150,16 +149,6 @@
<text class="value">{{detail.changeoverCount || 0}}</text> <text class="value">{{detail.changeoverCount || 0}}</text>
</view> </view>
</view> </view>
<view wx:if="{{detail.hasOverLimitNotice}}" class="over-limit-panel {{detail.hasOverLimit ? 'danger' : 'warn'}}">
<text class="over-limit-title">{{detail.hasOverLimit ? '超额' : '无法校验'}}</text>
<text class="over-limit-text">{{detail.overLimitReason || '-'}}</text>
<view class="over-limit-grid">
<text>本次消耗 {{detail.overLimitCurrentReportQtyText}}</text>
<text>累计消耗 {{detail.overLimitCurrentCumulativeQtyText}}</text>
<text wx:if="{{detail.hasOverLimitMaxReportableQty}}">最大可报 {{detail.overLimitMaxReportableQtyText}}</text>
<text wx:if="{{detail.hasOverLimitPreviousGoodQty}}">前序可用 {{detail.overLimitPreviousGoodQtyText}}</text>
</view>
</view>
<view wx:if="{{detail.correctionPairs.length}}" class="correction-list detail-corrections"> <view wx:if="{{detail.correctionPairs.length}}" class="correction-list detail-corrections">
<view wx:for="{{detail.correctionPairs}}" wx:key="key" wx:for-item="pair" class="correction-pair"> <view wx:for="{{detail.correctionPairs}}" wx:key="key" wx:for-item="pair" class="correction-pair">
<text>{{pair.label}} </text> <text>{{pair.label}} </text>

View File

@ -326,26 +326,6 @@ Page({
changeoverCount: detail.changeoverCount || 0, changeoverCount: detail.changeoverCount || 0,
changeoverCountError: false, changeoverCountError: false,
remark: detail.remark || '', remark: detail.remark || '',
overLimitStatus: detail.overLimitStatus || 'not_checked',
overLimitType: detail.overLimitType || '',
overLimitReason: detail.overLimitReason || '',
overLimitCheckedAt: detail.overLimitCheckedAt || '',
overLimitCheckSource: detail.overLimitCheckSource || '',
overLimitBatchNo: detail.overLimitBatchNo || '',
overLimitProductGrossWeightKg: detail.overLimitProductGrossWeightKg ?? '',
overLimitIssuedWeightKg: detail.overLimitIssuedWeightKg ?? '',
overLimitMaxReportableQty: detail.overLimitMaxReportableQty ?? '',
overLimitPreviousGoodQty: detail.overLimitPreviousGoodQty ?? '',
overLimitCurrentCumulativeQty: detail.overLimitCurrentCumulativeQty ?? '',
overLimitCurrentReportQty: detail.overLimitCurrentReportQty ?? '',
overLimitMaxReportableQtyText: detail.overLimitMaxReportableQtyText || '-',
overLimitPreviousGoodQtyText: detail.overLimitPreviousGoodQtyText || '-',
overLimitCurrentCumulativeQtyText: detail.overLimitCurrentCumulativeQtyText || '-',
overLimitCurrentReportQtyText: detail.overLimitCurrentReportQtyText || '-',
hasOverLimitMaxReportableQty: !!detail.hasOverLimitMaxReportableQty,
hasOverLimitPreviousGoodQty: !!detail.hasOverLimitPreviousGoodQty,
hasOverLimit: !!detail.hasOverLimit,
hasOverLimitNotice: !!detail.hasOverLimitNotice,
deviceError: false, deviceError: false,
deviceNo: detail.deviceNo, deviceNo: detail.deviceNo,
deviceIndex, deviceIndex,

View File

@ -38,21 +38,19 @@
data-id="{{item.id}}" data-id="{{item.id}}"
bindtap="toggleReportDetail" bindtap="toggleReportDetail"
> >
<text wx:if="{{item.hasOverLimit}}" class="over-limit-watermark {{item.watermarkClasses.overLimit}}">超额</text> <text wx:if="{{item.isVoided}}" class="voided-watermark">作废</text>
<text wx:if="{{item.isVoided}}" class="voided-watermark {{item.watermarkClasses.state}}">作废</text> <text wx:elif="{{item.isModified}}" class="modified-watermark">更改</text>
<text wx:elif="{{item.isModified}}" class="modified-watermark {{item.watermarkClasses.state}}">更改</text> <text wx:if="{{item.isSystemAutoSubmitted}}" class="timeout-watermark">{{item.autoSubmitReason || '超时-系统自动提交'}}</text>
<text wx:if="{{item.isSystemAutoSubmitted}}" class="timeout-watermark {{item.watermarkClasses.timeout}}">{{item.autoSubmitReason || '超时-系统自动提交'}}</text> <text wx:if="{{item.isCleaning}}" class="cleaning-watermark {{item.isSystemAutoSubmitted || item.isVoided || item.isModified ? 'lower-watermark' : ''}}">清洗</text>
<text wx:if="{{item.isCleaning}}" class="cleaning-watermark {{item.watermarkClasses.cleaning}}">清洗</text> <text wx:if="{{item.hasMisc}}" class="misc-watermark {{item.isSystemAutoSubmitted || item.isVoided || item.isModified ? 'lower-watermark' : ''}}">包含杂活</text>
<text wx:if="{{item.hasMisc}}" class="misc-watermark {{item.watermarkClasses.misc}}">包含杂活</text> <text wx:if="{{item.hasMultiPerson}}" class="multi-person-watermark {{item.isSystemAutoSubmitted || item.isVoided || item.isModified || item.hasMisc || item.isCleaning ? 'lower-watermark' : ''}}">多人协作</text>
<text wx:if="{{item.hasMultiPerson}}" class="multi-person-watermark {{item.watermarkClasses.multiPerson}}">多人协作</text> <text wx:if="{{item.hasContinuousDie}}" class="continuous-die-watermark {{item.isSystemAutoSubmitted || item.isVoided || item.isModified || item.hasMisc || item.isCleaning || item.hasMultiPerson ? 'lower-watermark' : ''}} {{item.hasMultiPerson && (item.isSystemAutoSubmitted || item.isVoided || item.isModified || item.hasMisc || item.isCleaning) ? 'third-watermark' : ''}}">连续模</text>
<text wx:if="{{item.hasContinuousDie}}" class="continuous-die-watermark {{item.watermarkClasses.continuousDie}}">连续模</text>
<view class="row"> <view class="row">
<view class="row-left"> <view class="row-left">
<text class="value">{{item.employeeName}} · {{item.reportDate}}</text> <text class="value">{{item.employeeName}} · {{item.reportDate}}</text>
<text class="subtitle">考勤点 {{item.attendancePointName || '-'}}</text> <text class="subtitle">考勤点 {{item.attendancePointName || '-'}}</text>
<text class="subtitle">{{item.timeRangeText}}</text> <text class="subtitle">{{item.timeRangeText}}</text>
<text class="subtitle">{{item.metrics.shiftDistributionText || ''}}</text> <text class="subtitle">{{item.metrics.shiftDistributionText || ''}}</text>
<text wx:if="{{item.allocationSummaryText}}" class="allocation-summary">归属拆分:{{item.allocationSummaryText}}</text>
</view> </view>
<text class="pill {{item.status === 'approved' ? 'success' : 'warn'}}">{{item.statusName}}</text> <text class="pill {{item.status === 'approved' ? 'success' : 'warn'}}">{{item.statusName}}</text>
</view> </view>
@ -159,17 +157,6 @@
</view> </view>
</view> </view>
<view wx:if="{{detail.hasOverLimitNotice}}" class="over-limit-panel {{detail.hasOverLimit ? 'danger' : 'warn'}}">
<text class="over-limit-title">{{detail.hasOverLimit ? '超额' : '无法校验'}}</text>
<text class="over-limit-text">{{detail.overLimitReason || '-'}}</text>
<view class="over-limit-grid">
<text>本次消耗 {{detail.overLimitCurrentReportQtyText}}</text>
<text>累计消耗 {{detail.overLimitCurrentCumulativeQtyText}}</text>
<text wx:if="{{detail.hasOverLimitMaxReportableQty}}">最大可报 {{detail.overLimitMaxReportableQtyText}}</text>
<text wx:if="{{detail.hasOverLimitPreviousGoodQty}}">前序可用 {{detail.overLimitPreviousGoodQtyText}}</text>
</view>
</view>
<view wx:if="{{detail.isMisc && !item.isVoided && item.canEdit}}" class="review-edit-panel misc-remark-panel"> <view wx:if="{{detail.isMisc && !item.isVoided && item.canEdit}}" class="review-edit-panel misc-remark-panel">
<text class="panel-title">杂活事项备注</text> <text class="panel-title">杂活事项备注</text>
<textarea <textarea

View File

@ -53,14 +53,6 @@
border-color: #aebbd0; border-color: #aebbd0;
} }
.allocation-summary {
display: block;
margin-top: 6rpx;
color: #4b5563;
font-size: 24rpx;
line-height: 1.45;
}
.cleaning-watermark { .cleaning-watermark {
position: absolute; position: absolute;
right: 28rpx; right: 28rpx;

View File

@ -544,47 +544,6 @@ const formatWorkTime = minutes => {
return `${Math.round(safeMinutes * 100) / 100}分钟` return `${Math.round(safeMinutes * 100) / 100}分钟`
} }
const snapshotValueText = value => (
value === null || value === undefined || value === '' ? '-' : String(value)
)
const hasSnapshotValue = value => (
value !== null && value !== undefined && value !== ''
)
const watermarkClassAt = index => (
['', 'lower-watermark', 'third-watermark', 'fourth-watermark', 'fifth-watermark', 'sixth-watermark'][index] || 'sixth-watermark'
)
const buildWatermarkClasses = flags => {
const classes = {}
let index = 0
const assign = (key, enabled) => {
classes[key] = enabled ? watermarkClassAt(index++) : ''
}
assign('overLimit', flags.hasOverLimit)
assign('state', flags.hasState)
assign('timeout', flags.isSystemAutoSubmitted)
assign('cleaning', flags.isCleaning)
assign('misc', flags.hasMisc)
assign('multiPerson', flags.hasMultiPerson)
assign('continuousDie', flags.hasContinuousDie)
return classes
}
const toCamelAllocation = row => ({
allocationDate: row.allocation_date || '',
dayMinutes: Number(row.day_minutes || 0),
overtimeMinutes: Number(row.overtime_minutes || 0),
nightMinutes: Number(row.night_minutes || 0),
effectiveMinutes: Number(row.effective_minutes || 0),
goodQty: Number(row.good_qty || 0),
defectQty: Number(row.defect_qty || 0),
scrapQty: Number(row.scrap_qty || 0),
changeoverCount: Number(row.changeover_count || 0),
referenceWage: Number(row.reference_wage || 0),
})
const toCamelReportItem = row => { const toCamelReportItem = row => {
const defectQty = Number(row.defect_qty || 0) const defectQty = Number(row.defect_qty || 0)
const scrapQty = Number(row.scrap_qty || 0) const scrapQty = Number(row.scrap_qty || 0)
@ -605,7 +564,6 @@ const toCamelReportItem = row => {
const beatComparison = noOutputMetric ? classifyBeat(0, 0) : classifyBeat(actualBeat, standardBeat) const beatComparison = noOutputMetric ? classifyBeat(0, 0) : classifyBeat(actualBeat, standardBeat)
const workloadComparison = noOutputMetric ? classifyWorkload(0, 0) : classifyWorkload(goodQty, standardWorkload) const workloadComparison = noOutputMetric ? classifyWorkload(0, 0) : classifyWorkload(goodQty, standardWorkload)
const corrections = row.corrections || {} const corrections = row.corrections || {}
const overLimitStatus = row.over_limit_status || 'not_checked'
const correctionPairs = [ const correctionPairs = [
correctionPair(corrections, 'device_no', '设备号', row.device_no), correctionPair(corrections, 'device_no', '设备号', row.device_no),
correctionPair(corrections, 'project_no', '项目号', row.project_no), correctionPair(corrections, 'project_no', '项目号', row.project_no),
@ -659,30 +617,9 @@ const toCamelReportItem = row => {
workloadCompareClass: workloadComparison.className, workloadCompareClass: workloadComparison.className,
workloadReason: workloadComparison.reason, workloadReason: workloadComparison.reason,
remark: row.remark || '', remark: row.remark || '',
overLimitStatus,
overLimitType: row.over_limit_type || '',
overLimitReason: row.over_limit_reason || '',
overLimitCheckedAt: row.over_limit_checked_at || '',
overLimitCheckSource: row.over_limit_check_source || '',
overLimitBatchNo: row.over_limit_batch_no || '',
overLimitProductGrossWeightKg: row.over_limit_product_gross_weight_kg ?? '',
overLimitIssuedWeightKg: row.over_limit_issued_weight_kg ?? '',
overLimitMaxReportableQty: row.over_limit_max_reportable_qty ?? '',
overLimitPreviousGoodQty: row.over_limit_previous_good_qty ?? '',
overLimitCurrentCumulativeQty: row.over_limit_current_cumulative_qty ?? '',
overLimitCurrentReportQty: row.over_limit_current_report_qty ?? '',
overLimitMaxReportableQtyText: snapshotValueText(row.over_limit_max_reportable_qty),
overLimitPreviousGoodQtyText: snapshotValueText(row.over_limit_previous_good_qty),
overLimitCurrentCumulativeQtyText: snapshotValueText(row.over_limit_current_cumulative_qty),
overLimitCurrentReportQtyText: snapshotValueText(row.over_limit_current_report_qty),
hasOverLimitMaxReportableQty: hasSnapshotValue(row.over_limit_max_reportable_qty),
hasOverLimitPreviousGoodQty: hasSnapshotValue(row.over_limit_previous_good_qty),
hasOverLimit: overLimitStatus === 'exceeded',
hasOverLimitNotice: overLimitStatus === 'exceeded' || overLimitStatus === 'uncheckable',
corrections, corrections,
correctionPairs, correctionPairs,
hasCorrections: correctionPairs.length > 0, hasCorrections: correctionPairs.length > 0,
allocations: (row.allocations || []).map(toCamelAllocation),
} }
} }
@ -722,7 +659,6 @@ const toCamelReport = row => {
const hasMisc = items.some(item => item.isMisc) const hasMisc = items.some(item => item.isMisc)
const hasContinuousDie = items.some(item => item.isContinuousDie) const hasContinuousDie = items.some(item => item.isContinuousDie)
const hasMultiPerson = items.some(item => item.isMultiPerson) const hasMultiPerson = items.some(item => item.isMultiPerson)
const hasOverLimit = !!row.has_over_limit || items.some(item => item.hasOverLimit)
const totalDefect = Math.round(items.reduce((sum, item) => sum + Number(item.defectQty || 0), 0) * 100) / 100 const totalDefect = Math.round(items.reduce((sum, item) => sum + Number(item.defectQty || 0), 0) * 100) / 100
const corrections = row.corrections || {} const corrections = row.corrections || {}
const reportCorrectionPairs = [ const reportCorrectionPairs = [
@ -740,15 +676,6 @@ const toCamelReport = row => {
label: `${segment.displayName} ${pair.label}`, label: `${segment.displayName} ${pair.label}`,
})) }))
))) )))
const watermarkClasses = buildWatermarkClasses({
hasOverLimit,
hasState: !!row.is_voided || !!row.is_modified,
isSystemAutoSubmitted: !!row.is_system_auto_submitted,
isCleaning,
hasMisc,
hasMultiPerson,
hasContinuousDie,
})
return { return {
id: row.id, id: row.id,
attendancePointName: row.attendance_point_name || '', attendancePointName: row.attendance_point_name || '',
@ -774,8 +701,6 @@ const toCamelReport = row => {
submittedAtText: util.formatTime(row.submitted_at), submittedAtText: util.formatTime(row.submitted_at),
isSystemAutoSubmitted: !!row.is_system_auto_submitted, isSystemAutoSubmitted: !!row.is_system_auto_submitted,
autoSubmitReason: row.auto_submit_reason || '超时-系统自动提交', autoSubmitReason: row.auto_submit_reason || '超时-系统自动提交',
hasOverLimit,
overLimitSummary: row.over_limit_summary || '',
isMultiPersonAssistant: !!row.is_multi_person_assistant, isMultiPersonAssistant: !!row.is_multi_person_assistant,
multiPersonSourceReportId: row.multi_person_source_report_id || null, multiPersonSourceReportId: row.multi_person_source_report_id || null,
isVoided: !!row.is_voided, isVoided: !!row.is_voided,
@ -789,14 +714,12 @@ const toCamelReport = row => {
canUnvoid: !!row.can_unvoid, canUnvoid: !!row.can_unvoid,
canEdit: !!row.can_edit, canEdit: !!row.can_edit,
isModified: !!row.is_modified, isModified: !!row.is_modified,
allocationSummaryText: row.allocation_summary_text || '',
deviceSegments, deviceSegments,
items, items,
isCleaning, isCleaning,
hasMisc, hasMisc,
hasContinuousDie, hasContinuousDie,
hasMultiPerson, hasMultiPerson,
watermarkClasses,
metrics: { metrics: {
...toCamelMetrics(row.metrics || {}), ...toCamelMetrics(row.metrics || {}),
totalDefect, totalDefect,
@ -1605,28 +1528,9 @@ const listDashboard = async (filters = {}) => {
}) })
return toCamelPage(data, row => { return toCamelPage(data, row => {
const isMisc = !!row.is_misc || isMiscRow(row) const isMisc = !!row.is_misc || isMiscRow(row)
const isCleaning = isCleaningRow(row) const noOutputMetric = isMisc || isCleaningRow(row)
const isContinuousDie = isContinuousDieRow(row)
const isMultiPerson = isMultiPersonRow(row)
const hasOverLimit = !!row.has_over_limit
const correctionPairs = (row.correction_pairs || []).map(pair => ({
key: pair.key,
label: pair.label,
oldValue: correctionDisplayValue(pair.old_value),
newValue: correctionDisplayValue(pair.new_value),
}))
const noOutputMetric = isMisc || isCleaning
const beatComparison = noOutputMetric ? classifyBeat(0, 0) : classifyBeat(row.actual_beat, row.standard_beat) const beatComparison = noOutputMetric ? classifyBeat(0, 0) : classifyBeat(row.actual_beat, row.standard_beat)
const workloadComparison = noOutputMetric ? classifyWorkload(0, 0) : classifyWorkload(row.total_good_qty, row.expected_workload) const workloadComparison = noOutputMetric ? classifyWorkload(0, 0) : classifyWorkload(row.total_good_qty, row.expected_workload)
const watermarkClasses = buildWatermarkClasses({
hasOverLimit,
hasState: !!row.is_voided || correctionPairs.length > 0,
isSystemAutoSubmitted: !!row.is_system_auto_submitted,
isCleaning,
hasMisc: isMisc,
hasMultiPerson: isMultiPerson,
hasContinuousDie: isContinuousDie,
})
return { return {
id: row.id, id: row.id,
attendancePointName: row.attendance_point_name || '', attendancePointName: row.attendance_point_name || '',
@ -1639,21 +1543,18 @@ const listDashboard = async (filters = {}) => {
processDisplayName: moldDisplayName(row.product_name, row.process_name, row.stamping_method), processDisplayName: moldDisplayName(row.product_name, row.process_name, row.stamping_method),
rawMaterialBatchNo: row.raw_material_batch_no || '', rawMaterialBatchNo: row.raw_material_batch_no || '',
stampingMethod: row.stamping_method || '', stampingMethod: row.stamping_method || '',
isCleaning, isCleaning: isCleaningRow(row),
isContinuousDie, isContinuousDie: isContinuousDieRow(row),
changeoverCount: row.changeover_count || 0, changeoverCount: row.changeover_count || 0,
isMisc, isMisc,
isSystemAutoSubmitted: !!row.is_system_auto_submitted, isSystemAutoSubmitted: !!row.is_system_auto_submitted,
isVoided: !!row.is_voided, isVoided: !!row.is_voided,
isMultiPerson, isMultiPerson: isMultiPersonRow(row),
isMultiPersonAssistant: !!row.is_multi_person_assistant, isMultiPersonAssistant: !!row.is_multi_person_assistant,
workerType: row.worker_type || '正式工', workerType: row.worker_type || '正式工',
reportCount: row.report_count, reportCount: row.report_count,
effectiveMinutes: row.effective_minutes, effectiveMinutes: row.effective_minutes,
shiftDistributionText: row.shift_distribution_text || '', shiftDistributionText: row.shift_distribution_text || '',
sourceReportDate: row.source_report_date || '',
sourceReportDates: row.source_report_dates || [],
sourceReportDatesText: (row.source_report_dates || []).join('、'),
shiftDayMinutes: row.shift_day_minutes || 0, shiftDayMinutes: row.shift_day_minutes || 0,
shiftOvertimeMinutes: row.shift_overtime_minutes || 0, shiftOvertimeMinutes: row.shift_overtime_minutes || 0,
shiftNightMinutes: row.shift_night_minutes || 0, shiftNightMinutes: row.shift_night_minutes || 0,
@ -1665,11 +1566,6 @@ const listDashboard = async (filters = {}) => {
standardBeat: row.standard_beat, standardBeat: row.standard_beat,
processUnitPriceYuan: row.process_unit_price_yuan || 0, processUnitPriceYuan: row.process_unit_price_yuan || 0,
referenceWage: round2(row.reference_wage || 0), referenceWage: round2(row.reference_wage || 0),
hasOverLimit,
overLimitSummary: row.over_limit_summary || '',
overLimitReasons: row.over_limit_reasons || '',
hasOverLimitNotice: !!(row.over_limit_summary || row.over_limit_reasons),
watermarkClasses,
expectedWorkload: row.expected_workload, expectedWorkload: row.expected_workload,
paceRate: row.pace_rate, paceRate: row.pace_rate,
workloadRate: row.workload_rate, workloadRate: row.workload_rate,
@ -1680,7 +1576,12 @@ const listDashboard = async (filters = {}) => {
workloadCompareClass: workloadComparison.className, workloadCompareClass: workloadComparison.className,
workloadReason: workloadComparison.reason, workloadReason: workloadComparison.reason,
reviewRemark: row.review_remark || '', reviewRemark: row.review_remark || '',
correctionPairs, correctionPairs: (row.correction_pairs || []).map(pair => ({
key: pair.key,
label: pair.label,
oldValue: correctionDisplayValue(pair.old_value),
newValue: correctionDisplayValue(pair.new_value),
})),
} }
}) })
} }