| 12345678910111213141516171819202122232425 |
- import { CommonModule } from '@angular/common';
- import { Component, input, output, signal, inject } from '@angular/core';
- import { FormsModule } from '@angular/forms';
- import { OperationsService } from '../operations.service';
- interface Difference {id:string;dataset:string;legacyKey:string;student:string;course:string;date:string;fields:string[];current:unknown;fingerprint:string;cosmetic:boolean;resolved:boolean;}
- interface Cutoff {month:string;version:string;runId:string;cutoffAt:string;complete:boolean;pending:number;conflicts:Difference[];approved:boolean;monthClosed:boolean;datasets:{model:number;source:number;matched:number}[];}
- @Component({selector:'app-payroll-cutoff',standalone:true,imports:[CommonModule,FormsModule],template:`
- <section class="cutoff"><button type="button" (click)="load()" [disabled]="busy()">{{busy()?'核验中…':'核验 '+month()+' 结算来源'}}</button>
- @if(error()){<p role="alert">{{error()}}</p>}
- @if(data();as d){<h3>{{d.month}} · 工资来源核验</h3><p>旧接口同步截止:{{d.cutoffAt|date:'yyyy-MM-dd HH:mm':'+0800'}}。{{d.monthClosed?'已覆盖整月':'当前仅可生成截止时间内的草稿,整月同步后才能审核'}}</p>
- <p>业务编号:{{d.complete?'对账一致':'凭据缺失或记录缺失,暂不能确认'}} · 待核对 {{d.pending}} 条</p>
- <p>姓名或录入人文字差异单独标为“不影响计薪”;其余差异逐条核实后才能确认保留新系统。</p>
- @for(source of d.datasets;track source.model){<span class="source">{{source.model===54?'预约':source.model===59?'完课':'复习'}} {{source.matched}} / {{source.source}}</span>}
- <label>核验依据<textarea [(ngModel)]="reason" rows="2" placeholder="说明核对依据,至少 2 个字"></textarea></label>
- <div class="differences">@for(row of d.conflicts;track row.id){<details><summary>{{row.student||row.legacyKey}} · {{row.course}} · {{row.cosmetic?'不影响计薪':row.resolved?'已确认':'待核对'}}</summary><p>{{sourceName(row.dataset)}}编号 {{row.legacyKey}} · {{row.date||'日期待核对'}}</p><p>以下为新系统当前记录,请与旧系统同编号记录核对后填写依据。</p><dl>@for(field of row.fields;track field){<dt>{{fieldName(field)}}</dt><dd>{{fieldValue(row,field)}}</dd>}@empty{<dt>差异说明</dt><dd>来源没有提供具体字段,请完整核对该记录。</dd>}</dl>@if(!row.resolved&&canConfirm()){<button type="button" [disabled]="busy()||reason.trim().length<2" (click)="confirm(row)">确认此记录保留新系统</button>}</details>}</div>
- @if(canConfirm()){<button type="button" [disabled]="busy()||!d.complete||d.pending>0||reason.trim().length<2" (click)="confirm()">确认该月份截止来源</button>}@if(d.approved){<p role="status">来源已确认;数据变化后确认将自动失效。</p>}
- }</section>`,styles:[`.cutoff{padding:20px;border:1px solid #d8e5e1;border-radius:16px;background:#fff;margin:16px 0;color:#28443a}button{min-height:44px;padding:8px 16px;border:1px solid #197664;background:#eff8f5;border-radius:8px;color:#155c4e;cursor:pointer}button:disabled{opacity:.5;cursor:default}label{display:grid;gap:8px;margin:12px 0}textarea{padding:10px;border:1px solid #b8ccc4;border-radius:8px}dl{display:grid;grid-template-columns:minmax(100px,1fr) 2fr;gap:8px;margin:12px 0}dt{color:#567168}dd{margin:0;overflow-wrap:anywhere}details{padding:10px 0;border-bottom:1px solid #edf2f0}.differences{max-height:480px;overflow:auto;margin:12px 0}.source{display:inline-block;margin-right:20px}`]})
- export class PayrollCutoffComponent {
- readonly month=input.required<string>();readonly canConfirm=input(false);readonly changed=output<void>();readonly busy=signal(false);readonly error=signal('');readonly data=signal<Cutoff|null>(null);reason='';private readonly ops=inject(OperationsService);
- sourceName(value:string):string{return ({'54':'预约','59':'完课','60':'复习'} as Record<string,string>)[value]||'来源记录';}
- fieldName(value:string):string{return ({title:'学员姓名',inputer:'录入人',subtitle:'课程名称',teacherPaySnapshot:'课酬快照',dslx:'课时类型',dszt:'课程状态',yysj:'预约时间',szyh:'学员编号',pl:'陪练编号',fxpl:'复习老师编号',wcsj:'完成时间',kywrq:'复习日期',pldp:'陪练点评',plpjsj:'点评时间',yyds:'预约编号',lessonAt:'上课时间',durationMinutes:'上课分钟数',jffs:'交付方式'} as Record<string,string>)[value]||value;}
- fieldValue(row:Difference,field:string):string{const value=field==='title'?row.student:field==='subtitle'?row.course:(row.current as Record<string,unknown>|null)?.[field];return value==null||value===''?'未记录':typeof value==='object'?JSON.stringify(value):String(value);}
- async load():Promise<void>{this.busy.set(true);this.error.set('');try{this.data.set(await this.ops.request<Cutoff>('ops/payroll/cutoff',{month:this.month()}));}catch(e){this.error.set(e instanceof Error?e.message:'来源核验加载失败');}finally{this.busy.set(false);}}
- async confirm(row?:Difference):Promise<void>{const d=this.data();if(!d||this.busy()||d.month!==this.month())return;this.busy.set(true);this.error.set('');try{this.data.set(await this.ops.mutate<Cutoff>('ops/payroll/cutoff-confirm','',this.reason,{month:d.month,version:d.version,conflictId:row?.id||'',fingerprint:row?.fingerprint||''}));this.changed.emit();}catch(e){this.error.set(e instanceof Error?e.message:'确认失败,请重新核验');}finally{this.busy.set(false);}}
- }
|