Selaa lähdekoodia

fix(payroll): rename review rewards to anti-forgetting wages

彭峰 1 kuukausi sitten
vanhempi
commit
548508f2b1

+ 1 - 1
docs/features/review-rewards.md

@@ -1,4 +1,4 @@
-# 复习奖励与工资结算
+# 抗遗忘工资与工资结算
 
 ## 业务口径
 

+ 3 - 3
projects/xiaoshu-admin/src/app/pages/operations-coach-detail.component.html

@@ -8,7 +8,7 @@
       @if(data();as detail){<div class="coach-header-meta"><span>ID {{detail.coach.userId}}</span><span><lucide-icon [img]="icons.Building2" [size]="13"/>{{detail.profile.storeName||'未绑定门店'}}</span><strong>{{detail.profile.statusLabel}}</strong></div>}
     </div>
   </div>
-  @if(data();as detail){<a class="ui-button secondary" routerLink="/admin/payroll" [queryParams]="{tab:'rewards',coachId:detail.coach.userId}">复习奖励明细</a>}
+  @if(data();as detail){<a class="ui-button secondary" routerLink="/admin/payroll" [queryParams]="{tab:'rewards',coachId:detail.coach.userId}">抗遗忘工资明细</a>}
   <button class="ui-button secondary refresh-action" type="button" (click)="load(true)" [disabled]="loading()"><lucide-icon [img]="icons.RefreshCw" [size]="17" [class.spin]="loading()"/>{{loading()?'正在刷新':'刷新实时数据'}}</button>
 </header>
 
@@ -40,7 +40,7 @@
     <article class="summary-card"><span>今日排课</span><strong>{{detail.coach.todayAppointments}}</strong><small>未来待完成 {{upcomingCount()}} 节</small></article>
     <article class="summary-card"><span>{{detail.payrollMonth}} 有效课次</span><strong>{{detail.currentPayroll.lessonCount}}</strong><small>30分钟 {{detail.currentPayroll.type30Count}} · 60分钟 {{detail.currentPayroll.type60Count}}</small></article>
     <article class="summary-card"><span>{{detail.payrollMonth}} 工时</span><strong>{{detail.currentPayroll.totalHours|number:'1.1-1'}}h</strong><small>体验课 {{detail.currentPayroll.trialCount}} 节</small></article>
-    <article class="summary-card payroll-card"><span>{{detail.payrollMonth}} 预估工资</span><strong>¥{{detail.currentPayroll.payableAmount|number:'1.2-2'}}</strong><small>含复习奖励 ¥{{detail.currentPayroll.reviewAmount||0|number:'1.2-2'}}</small></article>
+    <article class="summary-card payroll-card"><span>{{detail.payrollMonth}} 预估工资</span><strong>¥{{detail.currentPayroll.payableAmount|number:'1.2-2'}}</strong><small>含抗遗忘工资 ¥{{detail.currentPayroll.reviewAmount||0|number:'1.2-2'}}</small></article>
     <article class="summary-card"><span>平均评分</span><strong>{{detail.currentPayroll.averageScore?(detail.currentPayroll.averageScore|number:'1.1-1'):'—'}}</strong><small>{{detail.reviews.length}} 条有效评价</small></article>
     <article class="summary-card"><span>21天复习任务</span><strong>{{detail.reviewTaskCount}}</strong><small>原系统默认不计薪</small></article>
   </section>
@@ -78,7 +78,7 @@
       <div class="table-scroll"><table class="data-table detail-table"><thead><tr><th>规则</th><th>日期/星期</th><th>时段</th><th>状态</th><th>原因</th><th>操作</th></tr></thead><tbody>@for(rule of detail.availability;track rule.objectId){<tr><td>{{rule.ruleType==='weekly'?'每周':'指定日期'}}</td><td>{{rule.ruleType==='weekly'?('星期'+['日','一','二','三','四','五','六'][rule.weekday]):rule.date}}</td><td>{{rule.startTime}}–{{rule.endTime}}</td><td><span class="badge">{{rule.available?'可排课':'休息/请假'}}</span></td><td>{{rule.reason}}</td><td><button class="icon-action danger" type="button" aria-label="删除可用时间规则" (click)="removeAvailability(rule.objectId,rule.updatedAt)"><lucide-icon [img]="icons.Trash2" [size]="16"/></button></td></tr>}@empty{<tr><td colspan="6"><div class="empty">尚未配置;排课时允许选择,但会提示未配置可用时间</div></td></tr>}</tbody></table></div>
     }@else{
       <div class="section-heading"><div><strong>{{payrollMonth()}} 老师工资记录</strong><span>只筛选工资预估和已生成工资批次,不影响排课和上课明细</span></div><div class="section-month-filter"><span>工资月份</span><button class="month-picker-trigger" data-testid="payroll-month" type="button" aria-label="选择工资月份" aria-haspopup="dialog" [attr.aria-expanded]="activeMonthPicker()==='payroll'" [class.active]="activeMonthPicker()==='payroll'" (click)="toggleMonthPicker('payroll',$event)" [disabled]="loading()||isSectionLoading('payroll')"><lucide-icon [img]="icons.CalendarDays" [size]="16"/><strong>{{monthLabel(payrollMonth())}}</strong><lucide-icon class="picker-chevron" [img]="icons.ChevronDown" [size]="15"/></button><small>{{isSectionLoading('payroll')?'正在读取…':(detail.payrolls.length+' 条工资记录')}}</small>@if(activeMonthPicker()==='payroll'){<div class="month-calendar-popover" role="dialog" aria-label="选择工资月份" (click)="$event.stopPropagation()"><header><button type="button" aria-label="上一年" (click)="shiftMonthPickerYear(-1,$event)"><lucide-icon [img]="icons.ChevronLeft" [size]="17"/></button><strong>{{monthPickerYear()}}年</strong><button type="button" aria-label="下一年" (click)="shiftMonthPickerYear(1,$event)"><lucide-icon [img]="icons.ChevronRight" [size]="17"/></button></header><div class="month-grid">@for(name of monthNames;track $index){<button type="button" [class.selected]="payrollMonth()===monthOptionValue($index)" [attr.aria-pressed]="payrollMonth()===monthOptionValue($index)" (click)="selectSectionMonth('payroll',$index)">{{name}}</button>}</div><footer><button type="button" (click)="selectCurrentMonth('payroll')">回到本月</button></footer></div>}</div></div>
-      <div class="table-scroll"><table class="data-table detail-table"><thead><tr><th>月份</th><th>状态</th><th>有效课次</th><th>工时</th><th>基础工资</th><th>复习奖励</th><th>调整</th><th>应发金额</th></tr></thead><tbody>
+      <div class="table-scroll"><table class="data-table detail-table"><thead><tr><th>月份</th><th>状态</th><th>有效课次</th><th>工时</th><th>基础工资</th><th>抗遗忘工资</th><th>调整</th><th>应发金额</th></tr></thead><tbody>
         @for(row of detail.payrolls;track row.objectId){<tr><td><strong>{{row.month}}</strong></td><td><span class="badge" [class.warn]="row.objectId.startsWith('preview:')">{{row.statusLabel}}</span></td><td>{{row.lessonCount}}</td><td>{{row.totalHours|number:'1.1-1'}}h</td><td>¥{{row.baseAmount|number:'1.2-2'}}</td><td>¥{{row.reviewAmount||0|number:'1.2-2'}}</td><td>¥{{row.adjustmentAmount|number:'1.2-2'}}</td><td class="money">¥{{row.payableAmount|number:'1.2-2'}}</td></tr>}
         @empty{<tr><td colspan="8"><div class="empty">{{isSectionLoading('payroll')?'正在读取工资记录…':'该月暂无工资记录'}}</div></td></tr>}
       </tbody></table></div>

+ 6 - 6
projects/xiaoshu-admin/src/app/pages/operations-payroll.component.html

@@ -1,18 +1,18 @@
-<header class="page-header payroll-header"><div><p>PAYROLL CENTER</p><h1>工资结算</h1><span>按有效上课记录与复习奖励核算工资,完成月度审核与发放登记</span></div><div class="header-actions"><button class="ui-button refresh-action" type="button" (click)="refreshLive()" [disabled]="refreshing()"><lucide-icon [img]="icons.RefreshCw" [size]="17" [class.spin]="refreshing()" />{{refreshing()?'正在刷新':'刷新实时数据'}}</button>@if(tab()!=='rewards'){<button class="ui-button export-action" type="button" (click)="exportExcel()" [disabled]="saving()"><lucide-icon [img]="icons.Download" [size]="17" />导出 Excel</button>}</div></header>
+<header class="page-header payroll-header"><div><p>PAYROLL CENTER</p><h1>工资结算</h1><span>按有效上课记录与抗遗忘工资核算工资,完成月度审核与发放登记</span></div><div class="header-actions"><button class="ui-button refresh-action" type="button" (click)="refreshLive()" [disabled]="refreshing()"><lucide-icon [img]="icons.RefreshCw" [size]="17" [class.spin]="refreshing()" />{{refreshing()?'正在刷新':'刷新实时数据'}}</button>@if(tab()!=='rewards'){<button class="ui-button export-action" type="button" (click)="exportExcel()" [disabled]="saving()"><lucide-icon [img]="icons.Download" [size]="17" />导出 Excel</button>}</div></header>
 @if(message()){<p class="notice" role="status">{{message()}}</p>} @if(error()){<p class="notice error" role="alert">{{error()}}</p>}
-<nav class="tabs payroll-tabs" aria-label="工资结算功能"><button type="button" [class.active]="tab()==='overview'" (click)="tab.set('overview')"><lucide-icon [img]="icons.TrendingUp" [size]="17" />收益统计</button><button type="button" [class.active]="tab()==='rewards'" (click)="openRewards()">复习奖励</button><button type="button" [class.active]="tab()==='batches'" (click)="tab.set('batches')"><lucide-icon [img]="icons.Banknote" [size]="17" />工资批次</button><button type="button" [class.active]="tab()==='rates'" (click)="tab.set('rates')"><lucide-icon [img]="icons.Settings2" [size]="17" />排课与工资标准</button></nav>
+<nav class="tabs payroll-tabs" aria-label="工资结算功能"><button type="button" [class.active]="tab()==='overview'" (click)="tab.set('overview')"><lucide-icon [img]="icons.TrendingUp" [size]="17" />收益统计</button><button type="button" [class.active]="tab()==='rewards'" (click)="openRewards()">抗遗忘工资</button><button type="button" [class.active]="tab()==='batches'" (click)="tab.set('batches')"><lucide-icon [img]="icons.Banknote" [size]="17" />工资批次</button><button type="button" [class.active]="tab()==='rates'" (click)="tab.set('rates')"><lucide-icon [img]="icons.Settings2" [size]="17" />排课与工资标准</button></nav>
 @if(tab()!=='rates'){<section class="toolbar payroll-toolbar"><label><span>统计月份</span><input type="month" [ngModel]="month()" (ngModelChange)="month.set($event)" /></label><label><span>门店</span><select [ngModel]="storeId()" (ngModelChange)="storeId.set(+$event)"><option [ngValue]="0">全部门店</option>@for(store of overview()?.stores||[];track store.storeId){<option [ngValue]="store.storeId">{{store.storeName==='未绑定门店'?'未绑定门店(ID '+store.storeId+')':store.storeName}}</option>}</select></label><label class="search-field"><span>陪练搜索</span><input [ngModel]="search()" (ngModelChange)="search.set($event)" [placeholder]="tab()==='rewards'?'老师姓名或 ID':'姓名或手机号'" (keyup.enter)="applyFilters()" /></label><div class="filter-actions"><button class="ui-button primary query-action" type="button" (click)="applyFilters()"><lucide-icon [img]="icons.Search" [size]="17" />查询</button><button class="ui-button secondary reset-action" type="button" (click)="resetFilters()">重置</button></div><span class="source-chip" [class.cache]="overview()?.sourceStatus?.mode==='cache'" role="status"><i></i>{{overview()?.sourceStatus?.mode==='live'?'实时数据':'缓存数据'}} · {{overview()?.sourceStatus?.refreshedAt|date:'MM-dd HH:mm:ss'}}</span></section>}
 @if(tab()==='overview'){
  @if(overview();as data){
   @if(data.sourceStatus.mode==='cache'){<p class="notice error">{{data.sourceStatus.message}}。当前可以查看缓存,但不能生成或审核工资单。</p>}
-  <section class="payroll-metrics" aria-label="工资收益摘要"><article><span>参与陪练</span><strong>{{data.coachCount}}</strong><small>本月有课酬或复习奖励</small></article><article><span>有效课次</span><strong>{{data.lessonCount}}</strong><small>含课型4 {{data.type4Count||0}} 节</small></article><article><span>总工时</span><strong>{{data.totalHours|number:'1.1-1'}}h</strong><small>课型4不计工时</small></article><article><span>基础工资</span><strong>¥{{data.baseAmount|number:'1.2-2'}}</strong><small>按历史生效单价</small></article><article><span>复习奖励</span><strong>¥{{data.reviewAmount||0|number:'1.2-2'}}</strong><small>有效复习 {{data.reviewCount||0}} 次</small></article><article><span>应发工资</span><strong>¥{{data.payableAmount|number:'1.2-2'}}</strong><small>生成草稿后可调账</small></article><article class="exception-card"><span>异常记录</span><strong>{{data.exceptionCount}}</strong><small>生成工资前必须处理</small></article></section>
-  <section class="content-card payroll-table-card"><header><div><strong>{{data.month}} 老师收益汇总</strong><span>点击老师查看课酬与复习奖励明细</span></div>@if(isManager()){<button class="ui-button primary create-payroll-action" type="button" (click)="create()" [disabled]="saving()||!data.sourceStatus.available||data.exceptionCount>0"><lucide-icon [img]="icons.Plus" [size]="17" />生成全公司工资草稿</button>}</header><div class="table-scroll"><table class="data-table payroll-summary-table"><thead><tr><th>陪练老师</th><th>门店</th><th>30分钟</th><th>60分钟</th><th>体验课</th><th>课型4</th><th>总课次</th><th>工时</th><th>基础工资</th><th>复习次数</th><th>复习奖励</th><th>应发</th><th>明细</th></tr></thead><tbody>@for(row of data.coachItems;track row.coachId){<tr><td><strong>{{coachLabel(row.coachName,row.coachId)}}</strong><small>ID {{row.coachId}}</small></td><td>{{row.storeName}}</td><td>{{row.type30Count}}</td><td>{{row.type60Count}}</td><td>{{row.trialCount}}</td><td>{{row.type4Count}}</td><td>{{row.lessonCount}}</td><td>{{row.totalHours|number:'1.1-1'}}h</td><td>¥{{row.baseAmount|number:'1.2-2'}}</td><td>{{row.reviewCount||0}}</td><td>¥{{row.reviewAmount||0|number:'1.2-2'}}</td><td class="money">¥{{row.payableAmount|number:'1.2-2'}}</td><td><button class="icon-action view-action" type="button" (click)="openCoach(row)" aria-label="查看老师上课明细" title="查看明细"><lucide-icon [img]="icons.Eye" [size]="17" /></button></td></tr>}@empty{<tr><td colspan="13"><div class="empty">当前筛选范围没有有效计薪记录</div></td></tr>}</tbody></table></div>@if(data.type4Count){<p class="type4-note">课型4“陪练”按原系统口径只计入课次,不计算工资和工时。</p>}</section>
+  <section class="payroll-metrics" aria-label="工资收益摘要"><article><span>参与陪练</span><strong>{{data.coachCount}}</strong><small>本月有课酬或抗遗忘工资</small></article><article><span>有效课次</span><strong>{{data.lessonCount}}</strong><small>含课型4 {{data.type4Count||0}} 节</small></article><article><span>总工时</span><strong>{{data.totalHours|number:'1.1-1'}}h</strong><small>课型4不计工时</small></article><article><span>基础工资</span><strong>¥{{data.baseAmount|number:'1.2-2'}}</strong><small>按历史生效单价</small></article><article><span>抗遗忘工资</span><strong>¥{{data.reviewAmount||0|number:'1.2-2'}}</strong><small>有效复习 {{data.reviewCount||0}} 次</small></article><article><span>应发工资</span><strong>¥{{data.payableAmount|number:'1.2-2'}}</strong><small>生成草稿后可调账</small></article><article class="exception-card"><span>异常记录</span><strong>{{data.exceptionCount}}</strong><small>生成工资前必须处理</small></article></section>
+  <section class="content-card payroll-table-card"><header><div><strong>{{data.month}} 老师收益汇总</strong><span>点击老师查看课酬与抗遗忘工资明细</span></div>@if(isManager()){<button class="ui-button primary create-payroll-action" type="button" (click)="create()" [disabled]="saving()||!data.sourceStatus.available||data.exceptionCount>0"><lucide-icon [img]="icons.Plus" [size]="17" />生成全公司工资草稿</button>}</header><div class="table-scroll"><table class="data-table payroll-summary-table"><thead><tr><th>陪练老师</th><th>门店</th><th>30分钟</th><th>60分钟</th><th>体验课</th><th>课型4</th><th>总课次</th><th>工时</th><th>基础工资</th><th>复习次数</th><th>抗遗忘工资</th><th>应发</th><th>明细</th></tr></thead><tbody>@for(row of data.coachItems;track row.coachId){<tr><td><strong>{{coachLabel(row.coachName,row.coachId)}}</strong><small>ID {{row.coachId}}</small></td><td>{{row.storeName}}</td><td>{{row.type30Count}}</td><td>{{row.type60Count}}</td><td>{{row.trialCount}}</td><td>{{row.type4Count}}</td><td>{{row.lessonCount}}</td><td>{{row.totalHours|number:'1.1-1'}}h</td><td>¥{{row.baseAmount|number:'1.2-2'}}</td><td>{{row.reviewCount||0}}</td><td>¥{{row.reviewAmount||0|number:'1.2-2'}}</td><td class="money">¥{{row.payableAmount|number:'1.2-2'}}</td><td><button class="icon-action view-action" type="button" (click)="openCoach(row)" aria-label="查看老师上课明细" title="查看明细"><lucide-icon [img]="icons.Eye" [size]="17" /></button></td></tr>}@empty{<tr><td colspan="13"><div class="empty">当前筛选范围没有有效计薪记录</div></td></tr>}</tbody></table></div>@if(data.type4Count){<p class="type4-note">课型4“陪练”按原系统口径只计入课次,不计算工资和工时。</p>}</section>
  }
 } @else if(tab()==='rewards'){
  @if(rewardCoachId()){<p class="notice">当前查看老师 ID {{rewardCoachId()}} <button class="ui-button secondary compact" (click)="clearRewardCoach()">查看全部老师</button></p>}
  <app-review-rewards [filters]="rewardFilters()" />
 } @else if(tab()==='batches'){
- <section class="content-card"><header><div><strong>全公司月度工资批次</strong><span>每月仅一张;门店筛选不会拆分工资单</span></div></header><div class="table-scroll"><table class="data-table"><thead><tr><th>月份</th><th>状态</th><th>陪练</th><th>有效课次</th><th>工时</th><th>基础工资</th><th>复习次数</th><th>复习奖励</th><th>调整</th><th>应发</th><th>异常</th><th>操作</th></tr></thead><tbody>@for(row of page()?.items||[];track row.objectId){<tr><td><strong>{{row.month}}</strong></td><td><span class="badge" [class.warn]="row.status==='draft'">{{row.statusLabel}}</span></td><td>{{row.coachCount}}</td><td>{{row.lessonCount}}</td><td>{{row.totalHours|number:'1.1-1'}}h</td><td>¥{{row.baseAmount|number:'1.2-2'}}</td><td>{{row.reviewCount||0}}</td><td>¥{{row.reviewAmount||0|number:'1.2-2'}}</td><td>¥{{row.adjustmentAmount|number:'1.2-2'}}</td><td class="money">¥{{row.payableAmount|number:'1.2-2'}}</td><td [class.exception]="row.exceptionCount">{{row.exceptionCount}}</td><td><button type="button" aria-label="查看工资单" (click)="open(row)"><lucide-icon [img]="icons.Eye" [size]="17" /></button></td></tr>}@empty{<tr><td colspan="12"><div class="empty">尚未生成工资批次。请先在“收益统计”核对实时数据。</div></td></tr>}</tbody></table></div></section>
+ <section class="content-card"><header><div><strong>全公司月度工资批次</strong><span>每月仅一张;门店筛选不会拆分工资单</span></div></header><div class="table-scroll"><table class="data-table"><thead><tr><th>月份</th><th>状态</th><th>陪练</th><th>有效课次</th><th>工时</th><th>基础工资</th><th>复习次数</th><th>抗遗忘工资</th><th>调整</th><th>应发</th><th>异常</th><th>操作</th></tr></thead><tbody>@for(row of page()?.items||[];track row.objectId){<tr><td><strong>{{row.month}}</strong></td><td><span class="badge" [class.warn]="row.status==='draft'">{{row.statusLabel}}</span></td><td>{{row.coachCount}}</td><td>{{row.lessonCount}}</td><td>{{row.totalHours|number:'1.1-1'}}h</td><td>¥{{row.baseAmount|number:'1.2-2'}}</td><td>{{row.reviewCount||0}}</td><td>¥{{row.reviewAmount||0|number:'1.2-2'}}</td><td>¥{{row.adjustmentAmount|number:'1.2-2'}}</td><td class="money">¥{{row.payableAmount|number:'1.2-2'}}</td><td [class.exception]="row.exceptionCount">{{row.exceptionCount}}</td><td><button type="button" aria-label="查看工资单" (click)="open(row)"><lucide-icon [img]="icons.Eye" [size]="17" /></button></td></tr>}@empty{<tr><td colspan="12"><div class="empty">尚未生成工资批次。请先在“收益统计”核对实时数据。</div></td></tr>}</tbody></table></div></section>
 } @else {
  @if(standards();as config){
   <section class="standards-summary"><article><span>课程数</span><strong>{{config.summary.courseCount}}</strong></article><article><span>已确认映射</span><strong>{{config.summary.confirmedCount}}</strong></article><article [class.warn]="config.summary.unmappedCount"><span>待确认课程</span><strong>{{config.summary.unmappedCount}}</strong></article><article><span>生效规则</span><strong>{{config.summary.activeRuleCount}}</strong></article></section>
@@ -37,7 +37,7 @@
  }
 }
 @if(detail()||selectedCoachId()){
- <div class="backdrop" (click)="closeDrawer()"></div><aside class="drawer payroll-drawer" role="dialog" aria-modal="true" aria-label="工资明细"><header><div><p>{{detail()?.objectId?'PAYROLL BATCH':'COACH LESSONS'}}</p><h2>@if(detail();as batch){ {{batch.month}} · {{batch.statusLabel}} }@else{ {{selectedCoachName()||'老师上课明细'}} }</h2><span>{{coachLines().length}} 条来源记录</span></div><button class="icon-action" type="button" aria-label="关闭" (click)="closeDrawer()"><lucide-icon [img]="icons.X" [size]="20" /></button></header>@if(detail()?.exceptionCount){<p class="notice error">存在 {{detail()?.exceptionCount}} 条异常,处理完成前不能审核。</p>}<div class="table-scroll"><table class="data-table"><thead><tr><th>陪练</th><th>学员 / 课程</th><th>日期</th><th>课型</th><th>工时</th><th>单价</th><th>金额</th><th>来源ID</th><th>说明</th></tr></thead><tbody>@for(line of coachLines();track line.objectId){<tr [class.muted]="line.excluded"><td>{{line.coachName}}<small>{{line.storeName}}</small></td><td>{{line.studentName}}<small>{{line.courseName}}</small></td><td>{{line.lessonAt|date:'MM-dd HH:mm'}}</td><td>{{line.classTypeLabel}}</td><td>{{line.durationHours}}h</td><td>¥{{line.rateAmount|number:'1.2-2'}}</td><td class="money">¥{{line.amount|number:'1.2-2'}}</td><td>{{line.reviewTaskId||line.legacyGeneralId||'—'}}</td><td [class.exception]="line.exception&&!line.excluded">{{line.excluded?'已排除':line.exception||line.description||'—'}} @if(detail()?.status==='draft'&&isManager()&&line.exception&&!line.excluded){<button class="ui-button danger compact" type="button" (click)="excludeLine(line.objectId)">排除</button>}</td></tr>}@empty{<tr><td colspan="9"><div class="empty">暂无可查看记录</div></td></tr>}</tbody></table></div>
+ <div class="backdrop" (click)="closeDrawer()"></div><aside class="drawer payroll-drawer" role="dialog" aria-modal="true" aria-label="工资明细"><header><div><p>{{detail()?.objectId?'PAYROLL BATCH':'COACH LESSONS'}}</p><h2>@if(detail();as batch){ {{batch.month}} · {{batch.statusLabel}} }@else{ {{selectedCoachName()||'老师上课明细'}} }</h2><span>{{coachLines().length}} 条来源记录</span></div><button class="icon-action" type="button" aria-label="关闭" (click)="closeDrawer()"><lucide-icon [img]="icons.X" [size]="20" /></button></header>@if(detail()?.exceptionCount){<p class="notice error">存在 {{detail()?.exceptionCount}} 条异常,处理完成前不能审核。</p>}<div class="table-scroll"><table class="data-table"><thead><tr><th>陪练</th><th>学员 / 课程</th><th>日期</th><th>课型</th><th>工时</th><th>单价</th><th>金额</th><th>来源ID</th><th>说明</th></tr></thead><tbody>@for(line of coachLines();track line.objectId){<tr [class.muted]="line.excluded"><td>{{line.coachName}}<small>{{line.storeName}}</small></td><td>{{line.studentName}}<small>{{line.courseName}}</small></td><td>{{line.lessonAt|date:'MM-dd HH:mm'}}</td><td>{{line.kind==='review_reward'?'抗遗忘工资':line.classTypeLabel}}</td><td>{{line.durationHours}}h</td><td>¥{{line.rateAmount|number:'1.2-2'}}</td><td class="money">¥{{line.amount|number:'1.2-2'}}</td><td>{{line.reviewTaskId||line.legacyGeneralId||'—'}}</td><td [class.exception]="line.exception&&!line.excluded">{{line.excluded?'已排除':line.exception||line.description||'—'}} @if(detail()?.status==='draft'&&isManager()&&line.exception&&!line.excluded){<button class="ui-button danger compact" type="button" (click)="excludeLine(line.objectId)">排除</button>}</td></tr>}@empty{<tr><td colspan="9"><div class="empty">暂无可查看记录</div></td></tr>}</tbody></table></div>
  @if(detail();as batch){@if(batch.status==='draft'&&isManager()){<section class="panel"><h3>新增工资调整</h3><div class="form-grid"><label>陪练老师<select [ngModel]="adjustmentCoachId()" (ngModelChange)="adjustmentCoachId.set(+$event)"><option [ngValue]="0">请选择陪练</option>@for(coach of overview()?.coachItems||[];track coach.coachId){<option [ngValue]="coach.coachId">{{coachLabel(coach.coachName,coach.coachId)}} · {{coach.storeName}}</option>}</select></label><label>调整金额<input type="number" step="0.01" [ngModel]="adjustmentAmount()" (ngModelChange)="adjustmentAmount.set(+$event)" placeholder="负数表示扣减" /></label><label class="wide">调整 / 排除原因<input [ngModel]="adjustmentReason()" (ngModelChange)="adjustmentReason.set($event)" placeholder="必填,将写入审计日志" /></label><button class="ui-button secondary" type="button" (click)="addAdjustment()">添加调整</button></div></section>}@if(batch.status==='reviewed'&&isManager()){<section class="panel"><h3>登记线下发放</h3><div class="form-grid"><label>发放方式<input [ngModel]="paymentMethod()" (ngModelChange)="paymentMethod.set($event)" /></label><label>凭证编号<input [ngModel]="paymentReference()" (ngModelChange)="paymentReference.set($event)" placeholder="银行流水或内部凭证号" /></label></div></section>}}
  <footer><button class="ui-button secondary" type="button" (click)="exportExcel()"><lucide-icon [img]="icons.Download" [size]="16" />导出 Excel</button>@if(detail();as batch){@if(isManager()&&batch.status==='draft'){<button class="ui-button secondary" type="button" (click)="refreshDraft()" [disabled]="saving()">刷新并增量重算</button><button class="ui-button primary" type="button" (click)="review()" [disabled]="batch.exceptionCount>0||saving()">审核并永久锁定</button>}@else if(isManager()&&batch.status==='reviewed'){<button class="ui-button primary" type="button" (click)="markPaid()" [disabled]="saving()">登记已发放</button>}}</footer></aside>
 }

+ 2 - 2
projects/xiaoshu-admin/src/app/pages/operations-payroll.component.ts

@@ -52,6 +52,6 @@ export class OperationsPayrollComponent implements OnInit{
  async excludeLine(objectId:string):Promise<void>{const batch=this.detail(),reason=this.adjustmentReason().trim();if(!batch||batch.status!=='draft')return;if(!reason){this.error.set('请先在调整原因中填写排除该异常记录的原因');return;}this.saving.set(true);this.error.set('');try{await this.operations.mutate('ops/payroll/exclude-line',objectId,reason,{},'');await this.open(batch);this.message.set('异常记录已排除并保留审计原因');}catch(error){this.error.set(error instanceof Error?error.message:'异常记录排除失败');}finally{this.saving.set(false);}}
  async review():Promise<void>{const batch=this.detail();if(!batch)return;this.saving.set(true);this.error.set('');try{await this.operations.mutate('ops/payroll/review',batch.objectId,'实时刷新完成,复核工资明细并永久锁定',{},batch.updatedAt);await this.load();await this.open(batch);this.message.set('工资单已审核锁定');}catch(error){this.error.set(error instanceof Error?error.message:'工资审核失败');}finally{this.saving.set(false);}}
  async markPaid():Promise<void>{const batch=this.detail();if(!batch||!this.paymentMethod().trim()||!this.paymentReference().trim()){this.error.set('请填写发放方式和凭证编号');return;}this.saving.set(true);this.error.set('');try{await this.operations.mutate('ops/payroll/mark-paid',batch.objectId,'确认工资已线下发放',{paymentMethod:this.paymentMethod(),paymentReference:this.paymentReference()},batch.updatedAt);await this.load();await this.open(batch);this.message.set('工资发放信息已登记');}catch(error){this.error.set(error instanceof Error?error.message:'发放登记失败');}finally{this.saving.set(false);}}
- async exportExcel():Promise<void>{this.saving.set(true);this.error.set('');try{const batch=this.detail(),data=await this.operations.payrollExport(this.month(),batch?.objectId||'',this.storeId(),this.search());const summary=data.coachItems.map((row)=>({'陪练老师':this.coachLabel(row.coachName,row.coachId),'门店':row.storeName,'30分钟课':row.type30Count,'60分钟课':row.type60Count,'体验课':row.trialCount,'课型4':row.type4Count,'总课次':row.lessonCount,'工时':row.totalHours,'基础工资':row.baseAmount,'有效复习次数':row.reviewCount||0,'复习奖励':row.reviewAmount||0,'调整':row.adjustmentAmount,'应发金额':row.payableAmount}));const details=data.lines.map((line)=>this.exportLine(line)),exceptions=data.exceptions.map((line)=>({...this.exportLine(line),'异常原因':line.exception}));const book=utils.book_new();utils.book_append_sheet(book,utils.json_to_sheet(summary),'老师汇总');utils.book_append_sheet(book,utils.json_to_sheet(details),'计薪明细');utils.book_append_sheet(book,utils.json_to_sheet(exceptions),'异常记录');writeFile(book,data.fileName);}catch(error){this.error.set(error instanceof Error?error.message:'工资 Excel 导出失败');}finally{this.saving.set(false);}}
- private exportLine(line:PayrollLine):Record<string,unknown>{return{'明细类型':line.kind==='review_reward'?'复习奖励':line.kind==='adjustment'?'人工调整':'课酬','奖励凭据':line.reviewRewardId||'','复习任务':line.reviewTaskId||'','计划复习日期':line.plannedDate||'','来源记录ID':line.legacyGeneralId,'上课日期':line.lessonAt,'结算月份':line.settlementMonth,'原上课月份':line.originalMonth,'陪练老师':this.coachLabel(line.coachName,line.coachId),'门店':line.storeName,'学员':line.studentName,'课程':line.courseName,'课程分类':line.courseCategoryName||line.classTypeLabel,'授课方式':line.deliveryLabel||'未记录','时长':line.durationMinutes?`${line.durationMinutes}分钟`:line.classTypeLabel,'工时':line.durationHours,'单价':line.rateAmount,'金额':line.amount,'说明':line.description};}
+ async exportExcel():Promise<void>{this.saving.set(true);this.error.set('');try{const batch=this.detail(),data=await this.operations.payrollExport(this.month(),batch?.objectId||'',this.storeId(),this.search());const summary=data.coachItems.map((row)=>({'陪练老师':this.coachLabel(row.coachName,row.coachId),'门店':row.storeName,'30分钟课':row.type30Count,'60分钟课':row.type60Count,'体验课':row.trialCount,'课型4':row.type4Count,'总课次':row.lessonCount,'工时':row.totalHours,'基础工资':row.baseAmount,'有效复习次数':row.reviewCount||0,'抗遗忘工资':row.reviewAmount||0,'调整':row.adjustmentAmount,'应发金额':row.payableAmount}));const details=data.lines.map((line)=>this.exportLine(line)),exceptions=data.exceptions.map((line)=>({...this.exportLine(line),'异常原因':line.exception}));const book=utils.book_new();utils.book_append_sheet(book,utils.json_to_sheet(summary),'老师汇总');utils.book_append_sheet(book,utils.json_to_sheet(details),'计薪明细');utils.book_append_sheet(book,utils.json_to_sheet(exceptions),'异常记录');writeFile(book,data.fileName);}catch(error){this.error.set(error instanceof Error?error.message:'工资 Excel 导出失败');}finally{this.saving.set(false);}}
+ private exportLine(line:PayrollLine):Record<string,unknown>{return{'明细类型':line.kind==='review_reward'?'抗遗忘工资':line.kind==='adjustment'?'人工调整':'课酬','计薪凭据':line.reviewRewardId||'','复习任务':line.reviewTaskId||'','计划复习日期':line.plannedDate||'','来源记录ID':line.legacyGeneralId,'上课日期':line.lessonAt,'结算月份':line.settlementMonth,'原上课月份':line.originalMonth,'陪练老师':this.coachLabel(line.coachName,line.coachId),'门店':line.storeName,'学员':line.studentName,'课程':line.courseName,'课程分类':line.kind==='review_reward'?'抗遗忘工资':line.courseCategoryName||line.classTypeLabel,'授课方式':line.deliveryLabel||'未记录','时长':line.kind==='review_reward'?'—':line.durationMinutes?`${line.durationMinutes}分钟`:line.classTypeLabel,'工时':line.durationHours,'单价':line.rateAmount,'金额':line.amount,'说明':line.description};}
 }

+ 4 - 4
projects/xiaoshu-admin/src/app/pages/review-rewards.component.html

@@ -1,12 +1,12 @@
 @if(error()){<p class="notice error" role="alert">{{error()}} <button class="ui-button secondary compact" (click)="load()">重新加载</button></p>}
 <section class="content-card reward-card" [attr.aria-busy]="loading()">
-  <header><div><strong>复习奖励</strong><span>每条计划任务首次完成奖励 1 元,重复复习不重复计薪。历史待核对和异常记录不计入工资。</span></div><div class="reward-actions"><label>记录类型<select [ngModel]="status()" (ngModelChange)="status.set($event);load()"><option value="">全部记录</option><option value="earned">有效奖励</option><option value="historical">历史待核对</option><option value="exception">异常待核对</option></select></label><button class="ui-button secondary" (click)="export()" [disabled]="loading()||exporting()||!data()">{{exporting()?'正在导出':'导出奖励明细'}}</button></div></header>
+  <header><div><strong>抗遗忘工资</strong><span>每条抗遗忘复习计划任务首次完成计薪 1 元,重复复习不重复计薪。历史待核对和异常记录不计入工资。</span></div><div class="reward-actions"><label>记录类型<select [ngModel]="status()" (ngModelChange)="status.set($event);load()"><option value="">全部记录</option><option value="earned">有效计薪</option><option value="historical">历史待核对</option><option value="exception">异常待核对</option></select></label><button class="ui-button secondary" (click)="export()" [disabled]="loading()||exporting()||!data()">{{exporting()?'正在导出':'导出工资明细'}}</button></div></header>
   @if(data();as result){
-    <div class="reward-summary"><span>有效复习 <strong>{{result.summary.reviewCount}}</strong> 次</span><span>奖励合计 <strong>¥{{result.summary.reviewAmount|number:'1.2-2'}}</strong></span><span>历史待核对 {{result.summary.historicalCount}} 条</span><span>异常待核对 {{result.summary.exceptionCount}} 条</span></div>
+    <div class="reward-summary"><span>有效复习 <strong>{{result.summary.reviewCount}}</strong> 次</span><span>工资合计 <strong>¥{{result.summary.reviewAmount|number:'1.2-2'}}</strong></span><span>历史待核对 {{result.summary.historicalCount}} 条</span><span>异常待核对 {{result.summary.exceptionCount}} 条</span></div>
     <div class="table-scroll"><table class="data-table"><thead><tr><th>老师 / 门店</th><th>学生 / 课程</th><th>计划日期</th><th>首次完成时间</th><th>金额</th><th>结算状态</th><th>说明 / 任务</th></tr></thead><tbody>
       @for(row of result.items;track row.objectId){<tr><td><strong>{{row.coachName}}</strong><small>{{row.storeName}}</small></td><td>{{row.studentName}}<small>{{row.courseName||'未记录课程'}}</small></td><td>{{row.plannedDate||'未记录'}}</td><td>{{row.completedAt?(row.completedAt|date:'yyyy-MM-dd HH:mm':'+0800'):'待核对'}}</td><td class="money">¥{{row.amount|number:'1.2-2'}}</td><td><span class="badge" [class.warn]="row.status!=='earned'">{{row.statusLabel}}</span><small>{{row.settlementMonth}}</small></td><td>{{row.exception|| (row.status==='historical'?'需核对历史发放情况':'首次完成,仅计薪一次')}}<small>{{row.taskId}}</small></td></tr>}
-      @empty{<tr><td colspan="7"><div class="empty">{{loading()?'正在加载…':'当前筛选范围暂无复习奖励记录'}}</div></td></tr>}
+      @empty{<tr><td colspan="7"><div class="empty">{{loading()?'正在加载…':'当前筛选范围暂无抗遗忘工资记录'}}</div></td></tr>}
     </tbody></table></div>
     <footer class="reward-pagination"><span>共 {{result.total}} 条 · 第 {{result.page}} 页</span><button class="ui-button secondary compact" (click)="load(result.page-1)" [disabled]="loading()||result.page<=1">上一页</button><button class="ui-button secondary compact" (click)="load(result.page+1)" [disabled]="loading()||result.page*result.pageSize>=result.total">下一页</button></footer>
-  }@else if(loading()){<p class="empty" role="status">正在加载复习奖励…</p>}
+  }@else if(loading()){<p class="empty" role="status">正在加载抗遗忘工资…</p>}
 </section>

+ 3 - 3
projects/xiaoshu-admin/src/app/pages/review-rewards.component.ts

@@ -16,7 +16,7 @@ export class ReviewRewardsComponent implements OnChanges {
   async load(page=1):Promise<void> {
     const requestId=++this.requestId;this.loading.set(true);this.error.set('');
     try { const data=await this.operations.reviewRewards(this.filters,page,this.status());if(requestId===this.requestId)this.data.set(data); }
-    catch(error){if(requestId===this.requestId){this.data.set(null);this.error.set(error instanceof Error?error.message:'复习奖励加载失败');}}
+    catch(error){if(requestId===this.requestId){this.data.set(null);this.error.set(error instanceof Error?error.message:'抗遗忘工资加载失败');}}
     finally{if(requestId===this.requestId)this.loading.set(false);}
   }
   async export():Promise<void> {
@@ -25,10 +25,10 @@ export class ReviewRewardsComponent implements OnChanges {
     try {
       const rows:Record<string,unknown>[]=[];let page=1,total=0;
       do {const data=await this.operations.reviewRewards(filters,page,status,100);total=data.total;
-        rows.push(...data.items.map(row=>({'奖励凭据':row.objectId,'复习任务':row.taskId,'老师':row.coachName,'门店':row.storeName,'学生':row.studentName,'课程':row.courseName,'计划日期':row.plannedDate,'首次完成时间':row.completedAt,'收入月份':row.incomeMonth,'奖励金额':row.amount,'结算状态':row.statusLabel,'结算月份':row.settlementMonth||'','异常原因':row.exception})));
+        rows.push(...data.items.map(row=>({'计薪凭据':row.objectId,'复习任务':row.taskId,'老师':row.coachName,'门店':row.storeName,'学生':row.studentName,'课程':row.courseName,'计划日期':row.plannedDate,'首次完成时间':row.completedAt,'收入月份':row.incomeMonth,'工资金额':row.amount,'结算状态':row.statusLabel,'结算月份':row.settlementMonth||'','异常原因':row.exception})));
         if(!data.items.length)break;page++;
       }while(rows.length<total);
-      const book=utils.book_new();utils.book_append_sheet(book,utils.json_to_sheet(rows),'复习奖励');writeFile(book,`复习奖励_${filters.month}.xlsx`);
+      const book=utils.book_new();utils.book_append_sheet(book,utils.json_to_sheet(rows),'抗遗忘工资');writeFile(book,`抗遗忘工资_${filters.month}.xlsx`);
     }catch(error){this.error.set(error instanceof Error?error.message:'导出失败');}finally{this.exporting.set(false);}
   }
 }

+ 1 - 1
projects/xiaoshu-mobile/src/app/core/home-learning-modules.integration.spec.ts

@@ -641,7 +641,7 @@ describe('home learning modules', () => {
     harness.fixture.detectChanges();
     const teacherServices = Array.from(harness.routeNativeElement?.querySelectorAll('.service-grid > a') ?? []).map((link) => link.getAttribute('href'));
     expect(teacherServices).toEqual(['/pages/home/shop', '/pages/member/ke_xiao', '/pages/his/his', '/pages/member/feedback']);
-    expect(harness.routeNativeElement?.querySelector('.coach-wallet-strip')?.textContent).toContain('复习收入');
+    expect(harness.routeNativeElement?.querySelector('.coach-wallet-strip')?.textContent).toContain('抗遗忘工资');
 
     await harness.navigateByUrl('/pages/stu/stu_list?nid=801&bindingId=701');
     await harness.fixture.whenStable();

+ 1 - 1
projects/xiaoshu-mobile/src/app/core/page-catalog.ts

@@ -110,7 +110,7 @@ export const PAGE_CATALOG: PageMeta[] = [
   { path: 'pages/member/ghis', title: '抗遗忘历史', domain: 'member', kind: 'studyRecords', auth: true },
   { path: 'pages/member/myke', title: '我的练习进度', domain: 'member', kind: 'studyRecords', auth: true },
   { path: 'pages/member/ke_xiao', title: '销课记录', domain: 'member', kind: 'courseCenter', auth: true },
-  { path: 'pages/member/ke_fuxi', title: '复习收入', domain: 'member', kind: 'studyRecords', auth: true },
+  { path: 'pages/member/ke_fuxi', title: '抗遗忘工资', domain: 'member', kind: 'studyRecords', auth: true },
 ];
 
 export const PAGE_BY_PATH = new Map(PAGE_CATALOG.map((page) => [page.path, page]));

+ 2 - 2
projects/xiaoshu-mobile/src/app/features/home/account-page.component.html

@@ -36,12 +36,12 @@
     }
   </section>
   @if (session.groupId() === 3) {
-    <section class="wallet-strip coach-wallet-strip"><a routerLink="/pages/member/purse"><strong>¥{{ session.user()?.purse || 0 }}</strong><span>钱包余额</span></a><a routerLink="/pages/member/ke_fuxi"><strong>¥{{ coachPanel().reviewIncome | number:'1.0-2' }}</strong><span>复习收入</span></a><a routerLink="/pages/member/feedback"><strong>申请</strong><span>休息时间</span></a></section>
+    <section class="wallet-strip coach-wallet-strip"><a routerLink="/pages/member/purse"><strong>¥{{ session.user()?.purse || 0 }}</strong><span>钱包余额</span></a><a routerLink="/pages/member/ke_fuxi"><strong>¥{{ coachPanel().reviewIncome | number:'1.0-2' }}</strong><span>抗遗忘工资</span></a><a routerLink="/pages/member/feedback"><strong>申请</strong><span>休息时间</span></a></section>
   } @else {
     <section class="wallet-strip"><a routerLink="/pages/member/point"><strong>{{ session.user()?.point || 0 }}</strong><span>学习令</span></a><a routerLink="/pages/member/coin"><strong>{{ session.user()?.silverCoin || 0 }}</strong><span>学习豆</span></a><a routerLink="/pages/member/purse"><strong>{{ session.user()?.purse || 0 }}</strong><span>钱包余额</span></a></section>
   }
   @if (session.groupId() === 3) {
-    <section class="role-panel"><header><h2>陪练专用</h2><a routerLink="/pages/member/feedback">休息时间申请</a></header><div class="order-strip coach-strip"><a routerLink="/pages/home/shop"><strong>{{ coachPanel().todaySchedules }}</strong><span>今日排课</span></a><a routerLink="/pages/member/ke_xiao"><strong>{{ coachPanel().completedLessons }}</strong><span>销课记录</span></a><a routerLink="/pages/member/ke_fuxi"><strong>¥{{ coachPanel().reviewIncome | number:'1.0-2' }}</strong><span>复习收入</span></a></div></section>
+    <section class="role-panel"><header><h2>陪练专用</h2><a routerLink="/pages/member/feedback">休息时间申请</a></header><div class="order-strip coach-strip"><a routerLink="/pages/home/shop"><strong>{{ coachPanel().todaySchedules }}</strong><span>今日排课</span></a><a routerLink="/pages/member/ke_xiao"><strong>{{ coachPanel().completedLessons }}</strong><span>销课记录</span></a><a routerLink="/pages/member/ke_fuxi"><strong>¥{{ coachPanel().reviewIncome | number:'1.0-2' }}</strong><span>抗遗忘工资</span></a></div></section>
   } @else {
     <section class="role-panel course-memory">
       <header><h2>我的课程</h2><a routerLink="/pages/stu/stu_calendar">科学抗遗忘</a></header>

+ 1 - 1
projects/xiaoshu-mobile/src/app/features/home/home-page.component.html

@@ -105,7 +105,7 @@
       <a routerLink="/pages/home/shop"><span class="quick-icon learn"><lucide-icon [img]="icons.CalendarDays" [size]="25" /></span><span><strong>陪练排课</strong><small>查看今天与未来课程</small></span><lucide-icon class="quick-arrow" [img]="icons.ChevronRight" [size]="19" /></a>
       <a routerLink="/pages/member/ke_xiao"><span class="quick-icon review"><lucide-icon [img]="icons.Check" [size]="25" /></span><span><strong>销课记录</strong><small>核对已完成课程</small></span><lucide-icon class="quick-arrow" [img]="icons.ChevronRight" [size]="19" /></a>
       <a routerLink="/pages/his/his"><span class="quick-icon history"><lucide-icon [img]="icons.BookOpen" [size]="25" /></span><span><strong>学员记录</strong><small>按学员查看学习成果</small></span><lucide-icon class="quick-arrow" [img]="icons.ChevronRight" [size]="19" /></a>
-      <a routerLink="/pages/member/ke_fuxi"><span class="quick-icon exam"><lucide-icon [img]="icons.BarChart3" [size]="25" /></span><span><strong>复习收入</strong><small>查看复习任务收入</small></span><lucide-icon class="quick-arrow" [img]="icons.ChevronRight" [size]="19" /></a>
+      <a routerLink="/pages/member/ke_fuxi"><span class="quick-icon exam"><lucide-icon [img]="icons.BarChart3" [size]="25" /></span><span><strong>抗遗忘工资</strong><small>查看抗遗忘复习工资</small></span><lucide-icon class="quick-arrow" [img]="icons.ChevronRight" [size]="19" /></a>
     </section>
   } @else {
     <section class="quick-grid" aria-label="学习入口">

+ 1 - 1
projects/xiaoshu-mobile/src/app/features/study-records/study-records-page.component.html

@@ -91,7 +91,7 @@
         } @else if (meta.path === 'pages/member/myke') {
           <article class="progress-item" (click)="open(item)"><lucide-icon [img]="icons.RotateCcw" [size]="21" /><div><strong>{{ title(item) }}</strong><p>完成度:{{ item['yxx'] || 0 }}/{{ item['cksl2'] || item['cksl'] || 0 }}</p></div><span>复习</span></article>
         } @else if (meta.path === 'pages/member/ke_fuxi') {
-          <article class="income-item"><span [class.done]="item['fxzt']">{{ item['fxzt'] ? '已复习' : '未复习' }}</span><div><strong>{{ title(item) }}</strong><p>{{ incomeName(item) }} {{ item['kywsj'] }}</p>@if(item['rewardStatus']==='historical'){<small>历史待核对,未自动计薪</small>}@else if(item['rewardStatus']==='exception'){<small>奖励异常待核对</small>}</div><b>¥{{ item['money'] || 0 }}</b></article>
+          <article class="income-item"><span [class.done]="item['fxzt']">{{ item['fxzt'] ? '已复习' : '未复习' }}</span><div><strong>{{ title(item) }}</strong><p>{{ incomeName(item) }} {{ item['kywsj'] }}</p>@if(item['rewardStatus']==='historical'){<small>历史待核对,未自动计薪</small>}@else if(item['rewardStatus']==='exception'){<small>计薪异常待核对</small>}</div><b>¥{{ item['money'] || 0 }}</b></article>
         } @else {
           <article class="learning-item" [class.readonly]="!canStartPointReading" (click)="open(item)"><lucide-icon [img]="item['xxqs'] ? icons.BookOpen : icons.CalendarDays" [size]="21" /><div><time>{{ time(item) }}</time><strong>{{ title(item) }}</strong></div>@if (item['xxqs']) { <button type="button" class="reading-button" [disabled]="readingLoadingId() === recordId(item)" (click)="openReading(item, $event)">{{ readingLoadingId() === recordId(item) ? '加载中' : '阅读' }}</button> }<lucide-icon class="chevron" [img]="icons.ChevronRight" [size]="18" /></article>
         }

+ 1 - 1
projects/xiaoshu-mobile/src/app/features/study-records/study-records-page.component.ts

@@ -539,7 +539,7 @@ export class StudyRecordsPageComponent implements OnInit {
     }).pipe(finalize(() => this.loading.set(false))).subscribe({ next: ({ list, total }) => {
       this.items.set(this.asList(list.result));
       this.totalIncome.set(Number(this.asList(total.result)[0]?.['num'] ?? 0));
-    }, error: (error: ApiError) => this.error.set(error.message || '复习收入加载失败') });
+    }, error: (error: ApiError) => this.error.set(error.message || '抗遗忘工资加载失败') });
   }
 
   private asList(value: unknown): Item[] {

+ 3 - 3
scripts/cloud/review-rewards.js

@@ -9,12 +9,12 @@ function rewardPayrollLine(row, month) {
     reviewTaskId: row.taskId, plannedDate: row.plannedDate, coachId: Number(row.coachId), coachName: row.coachName,
     storeId: Number(row.storeId), storeName: row.storeName, studentName: row.studentName, courseName: row.courseName,
     legacyGeneralId: 0, lessonId: '', lessonAt: iso, originalMonth: row.incomeMonth, settlementMonth: month,
-    classType: 0, classTypeLabel: '复习奖励', courseCategoryName: '复习奖励', courseCategoryKey: '',
+    classType: 0, classTypeLabel: '抗遗忘工资', courseCategoryName: '抗遗忘工资', courseCategoryKey: '',
     durationMinutes: 0, durationHours: 0, deliveryMode: '', deliveryLabel: '', rateAmount: Number(row.amount),
     amount: Number(row.amount), score: 0, exception: '', description: '计划复习首次完成 · ' + row.plannedDate };
 }
 async function reviewRewardPayrollLines(companyId, month, includeUsed) {
-  if (!companyId) throw new Error('复习奖励必须指定帐套');
+  if (!companyId) throw new Error('抗遗忘工资必须指定帐套');
   if (!/^\d{4}-(0[1-9]|1[0-2])$/.test(month)) throw new Error('月份格式无效');
   const rows = await Psql.query('SELECT r.* FROM "ReviewReward" r WHERE r."company"=$1 AND r."status"=\'earned\' AND ' +
     (includeUsed ? 'r."incomeMonth"=$2' : 'r."incomeMonth"<=$2 AND NOT EXISTS (SELECT 1 FROM "PayrollLine" l WHERE l."company"=r."company" AND l."reviewRewardId"=r."objectId" AND l."kind"=\'review_reward\' AND COALESCE(l."excluded",false)=false)') +
@@ -22,7 +22,7 @@ async function reviewRewardPayrollLines(companyId, month, includeUsed) {
   return rows.map((row) => rewardPayrollLine(row, month));
 }
 async function reviewRewardPage(companyId, input) {
-  if (!companyId) throw new Error('复习奖励必须指定帐套');
+  if (!companyId) throw new Error('抗遗忘工资必须指定帐套');
   const page = Math.max(1, Math.floor(Number(input.page) || 1)), pageSize = Math.min(100, Math.max(1, Math.floor(Number(input.pageSize) || 20)));
   const values = [companyId], clauses = ['r."company"=$1'];
   const add = (condition, value) => { values.push(value); clauses.push(condition.replaceAll('?', '$' + values.length)); };

+ 4 - 4
scripts/sql/review-rewards.sql

@@ -172,7 +172,7 @@ BEGIN
  IF NOT FOUND THEN RAISE EXCEPTION '复习任务不存在'; END IF;
 END $$;
 CREATE OR REPLACE FUNCTION xiaoshu_reward_immutable() RETURNS trigger LANGUAGE plpgsql AS $$
-BEGIN RAISE EXCEPTION '复习奖励凭据不可改写或删除,请使用工资调整'; END $$;
+BEGIN RAISE EXCEPTION '抗遗忘工资凭据不可改写或删除,请使用工资调整'; END $$;
 DROP TRIGGER IF EXISTS review_reward_immutable ON "ReviewReward";
 CREATE TRIGGER review_reward_immutable BEFORE UPDATE OR DELETE ON "ReviewReward" FOR EACH ROW EXECUTE FUNCTION xiaoshu_reward_immutable();
 
@@ -187,15 +187,15 @@ BEGIN
  IF TG_OP='DELETE' THEN RETURN OLD; END IF;
  IF NEW."kind"='review_reward' THEN
    SELECT * INTO reward FROM "ReviewReward" WHERE "company"=tenant AND "objectId"=NEW."reviewRewardId";
-   IF reward."status" IS DISTINCT FROM 'earned' OR reward."incomeMonth">batch."month" THEN RAISE EXCEPTION '复习奖励不可结算'; END IF;
-   IF NEW."excluded" THEN RAISE EXCEPTION '有效复习奖励不能排除,请使用工资调整'; END IF;
+   IF reward."status" IS DISTINCT FROM 'earned' OR reward."incomeMonth">batch."month" THEN RAISE EXCEPTION '抗遗忘工资不可结算'; END IF;
+   IF NEW."excluded" THEN RAISE EXCEPTION '有效抗遗忘工资不能排除,请使用工资调整'; END IF;
    NEW."coachId" := reward."coachId"; NEW."coachName" := reward."coachName";
    NEW."studentName" := reward."studentName"; NEW."courseName" := reward."courseName";
    NEW."storeId" := reward."storeId"; NEW."storeName" := reward."storeName";
    NEW."reviewTaskId" := reward."taskId"; NEW."plannedDate" := reward."plannedDate";
    NEW."amount" := reward."amount"; NEW."rateAmount" := reward."amount"; NEW."durationHours" := 0;
    NEW."originalMonth" := reward."incomeMonth"; NEW."settlementMonth" := batch."month"; NEW."lessonAt" := reward."completedAt";
-   NEW."exception" := ''; NEW."classType" := 0; NEW."courseCategoryName" := '复习奖励';
+   NEW."exception" := ''; NEW."classType" := 0; NEW."courseCategoryName" := '抗遗忘工资';
  END IF;
  RETURN NEW;
 END $$;