Просмотр исходного кода

fix(admin): scope payroll notices to selected month

彭峰 4 недель назад
Родитель
Сommit
40b5807bef

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

@@ -64,7 +64,7 @@ describe('payroll interactions', () => {
   component.month.set('2026-08');component.storeId.set(10);component.search.set('老师');component.rewardCoachId.set(30);component.openRewards();await component.resetFilters();expect(component.rewardFilters()).toEqual(jasmine.objectContaining({month:'2026-08',storeId:0,search:'',coachId:0,resetRevision:1}));
  });
  it('selects payroll months through the shared calendar-style picker',()=>{
-  component.month.set('2026-09');const event=new Event('click');spyOn(event,'stopPropagation');component.toggleMonthPicker(event);expect(component.monthPickerOpen()).toBeTrue();expect(component.monthPickerYear()).toBe(2026);component.shiftMonthPickerYear(1,event);component.selectMonth(0);expect(component.month()).toBe('2027-01');expect(component.monthLabel()).toBe('2027年1月');expect(component.monthPickerOpen()).toBeFalse();
+  component.month.set('2026-09');component.message.set('2026年9月全公司工资草稿已生成');component.detail.set({objectId:'old-month'} as never);const event=new Event('click');spyOn(event,'stopPropagation');component.toggleMonthPicker(event);expect(component.monthPickerOpen()).toBeTrue();expect(component.monthPickerYear()).toBe(2026);component.shiftMonthPickerYear(1,event);component.selectMonth(0);expect(component.month()).toBe('2027-01');expect(component.monthLabel()).toBe('2027年1月');expect(component.monthPickerOpen()).toBeFalse();expect(component.message()).toBe('');expect(component.detail()).toBeNull();
  });
  it('retains adjustment request identity after a lost response and sends batch version',async()=>{
   component.detail.set({objectId:'batch',version:3,status:'draft'} as never);component.adjustmentCoachId.set(30);component.adjustmentAmount.set(10);component.adjustmentReason.set('补计工资');service.mutate.and.rejectWith(new Error('网络中断'));

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

@@ -43,9 +43,11 @@ export class OperationsPayrollComponent implements OnInit{
  @HostListener('document:keydown.escape') closeMonthPickerWithKeyboard():void{this.closeMonthPicker();}
  toggleMonthPicker(event:Event):void{event.stopPropagation();if(this.monthPickerOpen()){this.closeMonthPicker();return;}this.monthPickerYear.set(Number(this.month().slice(0,4))||new Date().getFullYear());this.monthPickerOpen.set(true);}
  shiftMonthPickerYear(delta:number,event:Event):void{event.stopPropagation();this.monthPickerYear.update(year=>year+delta);}
- selectMonth(monthIndex:number):void{this.month.set(this.monthOptionValue(monthIndex));this.closeMonthPicker();}
- selectCurrentMonth():void{this.month.set(localDateKey(new Date()).slice(0,7));this.closeMonthPicker();}
- monthLabel():string{const [year,month]=this.month().split('-');return year&&month?`${year}年${Number(month)}月`:'选择月份';}
+ selectMonth(monthIndex:number):void{this.month.set(this.monthOptionValue(monthIndex));this.clearMonthContext();this.closeMonthPicker();}
+ selectCurrentMonth():void{this.month.set(localDateKey(new Date()).slice(0,7));this.clearMonthContext();this.closeMonthPicker();}
+ monthLabel():string{return this.monthLabelOf(this.month());}
+ private monthLabelOf(value:string):string{const [year,month]=value.split('-');return year&&month?`${year}年${Number(month)}月`:'选择月份';}
+ private clearMonthContext():void{this.message.set('');this.error.set('');this.closeDrawer();}
  monthOptionValue(monthIndex:number):string{return `${this.monthPickerYear()}-${String(monthIndex+1).padStart(2,'0')}`;}
  clearRewardCoach():void{this.rewardCoachId.set(0);this.openRewards();}isManager():boolean{return this.sessions.session()?.operationsRole==='ops-manager'||this.sessions.session()?.isSuperAdmin===true;}
  async load(refresh=false):Promise<void>{
@@ -66,11 +68,11 @@ export class OperationsPayrollComponent implements OnInit{
 
  async refreshLive():Promise<void>{if(this.tab()==='rewards'){this.openRewards();return;}await this.load(true);}
  async resetFilters():Promise<void>{this.resetRevision++;this.closeDrawer();this.search.set('');this.storeId.set(0);this.rewardCoachId.set(0);await this.applyFilters();}
- async applyFilters():Promise<void>{if(this.tab()==='batches')this.allBatchMonths.set(false);if(this.tab()==='rewards'){this.openRewards();return;}await this.load(true);}
+ async applyFilters():Promise<void>{this.message.set('');if(this.tab()==='batches')this.allBatchMonths.set(false);if(this.tab()==='rewards'){this.openRewards();return;}await this.load(true);}
  async open(row:PayrollSummary):Promise<void>{const request=++this.openRequest;this.loading.set(true);this.error.set('');try{this.selectedCoachId.set(0);const detail=await this.operations.payrollDetail(row.objectId);if(request===this.openRequest)this.detail.set(detail);}catch(error){if(request===this.openRequest)this.error.set(error instanceof Error?error.message:'工资单加载失败');}finally{if(request===this.openRequest)this.loading.set(false);}}
  coachLabel(name:string,coachId:number):string{const value=String(name||'').trim();return !value||value===String(coachId)?`陪练 ${coachId}`:value;}
  openCoach(row:PayrollCoachSummary):void{this.selectedCoachId.set(row.coachId);this.selectedCoachName.set(this.coachLabel(row.coachName,row.coachId));this.detail.set(null);}closeDrawer():void{this.openRequest++;this.detail.set(null);this.selectedCoachId.set(0);this.selectedCoachName.set('');}
- async create():Promise<void>{if(this.saving())return;if(!this.overview()?.sourceStatus.available){this.error.set('实时上课记录不可用,不能生成工资草稿');return;}this.saving.set(true);this.error.set('');try{const result=await this.payrollCommand<PayrollSummary>('ops/payroll/create','',`生成 ${this.month()} 月全公司工资草稿`,{month:this.month()});await this.load();await this.open(result);this.message.set('全公司工资草稿已生成,尚未审核或发放');this.tab.set('batches');}catch(error){this.error.set(error instanceof Error?error.message:'生成工资草稿失败');}finally{this.saving.set(false);}}
+ async create():Promise<void>{if(this.saving())return;if(!this.overview()?.sourceStatus.available){this.error.set('实时上课记录不可用,不能生成工资草稿');return;}const generatedMonth=this.month();this.saving.set(true);this.error.set('');try{const result=await this.payrollCommand<PayrollSummary>('ops/payroll/create','',`生成 ${generatedMonth} 月全公司工资草稿`,{month:generatedMonth});await this.load();await this.open(result);this.message.set(`${this.monthLabelOf(generatedMonth)}全公司工资草稿已生成,尚未审核或发放`);this.tab.set('batches');}catch(error){this.error.set(error instanceof Error?error.message:'生成工资草稿失败');}finally{this.saving.set(false);}}
  async saveRate():Promise<void>{this.saving.set(true);this.error.set('');try{await this.operations.mutate('ops/pay-rates/save','',`调整课型 ${this.rateType()} 的计价规则`,{classType:this.rateType(),amount:this.rateAmount(),durationHours:this.rateHours(),effectiveFrom:this.effectiveFrom(),active:true});this.message.set('计价规则已保存');await this.load();}catch(error){this.error.set(error instanceof Error?error.message:'计价规则保存失败');}finally{this.saving.set(false);}}
  creditCosts(categoryKey=this.ruleCategoryKey(),duration=this.ruleDuration()):Array<{account:TeachingCreditAccount;amount:number}>{if(categoryKey==='trial')return[{account:'UserExp',amount:1}];if(categoryKey==='review')return[{account:'UserPoint',amount:duration===30?.5:1}];return duration===30?[{account:'Purse',amount:1},{account:'UserPoint',amount:.5}]:[{account:'SilverCoin',amount:1},{account:'UserPoint',amount:1}];}
  creditSummary(rule:TeachingRule):string{return rule.creditCosts.map((item)=>`${item.label} -${item.amount}`).join('+');}