operations-coach-detail.component.ts 8.1 KB

12345678910111213141516171819202122232425262728293031323334353637
  1. import { DropdownDirective } from '../shared/dropdown.directive';
  2. import { CommonModule } from '@angular/common';
  3. import { ChangeDetectionStrategy, Component, HostListener, OnDestroy, OnInit, computed, inject, signal } from '@angular/core';
  4. import { FormsModule } from '@angular/forms';
  5. import { ActivatedRoute, RouterLink } from '@angular/router';
  6. import { ArrowLeft, Building2, CalendarDays, ChevronDown, ChevronLeft, ChevronRight, Clock3, GraduationCap, LucideAngularModule, Phone, Plus, RefreshCw, Star, Trash2, UserRound, WalletCards } from 'lucide-angular';
  7. import { CoachDetailData, ScheduleAppointment } from '../admin.models';
  8. import { OperationsService } from '../operations.service';
  9. import { OperationsLiveRefreshService } from '../operations-live-refresh.service';
  10. import { ScheduleStatusTone, scheduleStatusLabel, scheduleStatusTone } from './operations-schedule-status';
  11. type MonthSection = 'schedule' | 'lessons' | 'payroll';
  12. const currentMonth = () => new Date().toLocaleDateString('sv-SE', { timeZone: 'Asia/Shanghai' }).slice(0, 7);
  13. @Component({selector:'app-operations-coach-detail',standalone:true,imports: [DropdownDirective, CommonModule,FormsModule,RouterLink,LucideAngularModule],templateUrl:'./operations-coach-detail.component.html',styleUrls:['./operations-professional.scss','./operations-coach-detail.component.scss'],changeDetection:ChangeDetectionStrategy.OnPush})
  14. export class OperationsCoachDetailComponent implements OnInit,OnDestroy{
  15. readonly data=signal<CoachDetailData|null>(null);readonly loading=signal(true);readonly sectionLoading=signal<MonthSection|null>(null);readonly saving=signal(false);readonly error=signal('');readonly message=signal('');readonly tab=signal<'schedule'|'lessons'|'reviews'|'availability'|'payroll'>('schedule');readonly currentMonth=currentMonth();readonly summaryMonth=signal(currentMonth());readonly scheduleMonth=signal(currentMonth());readonly lessonsMonth=signal(currentMonth());readonly payrollMonth=signal(currentMonth());readonly lessonsPayroll=signal<CoachDetailData['currentPayroll']|null>(null);readonly activeMonthPicker=signal<MonthSection|null>(null);readonly monthPickerYear=signal(new Date().getFullYear());readonly monthNames=['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'];readonly ruleType=signal<'weekly'|'date'>('weekly');readonly weekday=signal(1);readonly ruleDate=signal('');readonly startTime=signal('09:00');readonly endTime=signal('21:00');readonly available=signal(true);readonly ruleReason=signal('常规工作时间');readonly upcomingCount=computed(()=>this.data()?.appointments.filter((row)=>!row.cancelled&&row.statusCode<30&&new Date(row.endsAt).getTime()>=Date.now()).length||0);readonly icons={ArrowLeft,Building2,CalendarDays,ChevronDown,ChevronLeft,ChevronRight,Clock3,GraduationCap,Phone,Plus,RefreshCw,Star,Trash2,UserRound,WalletCards};private readonly operations=inject(OperationsService);private readonly liveRefresh=inject(OperationsLiveRefreshService);private readonly objectId=inject(ActivatedRoute).snapshot.paramMap.get('objectId')||'';private stopLiveRefresh?:()=>void;private loadRequest=0;
  16. ngOnInit():void{void this.load();this.stopLiveRefresh=this.liveRefresh.watch(['appointments','review','account'],()=>{if(this.loading()||this.sectionLoading()||this.saving())return false;void this.load(false,true);return true;});}
  17. ngOnDestroy():void{this.stopLiveRefresh?.();}
  18. async load(refresh=false,background=false):Promise<void>{const request=++this.loadRequest;if(!background)this.loading.set(true);this.error.set('');try{const detail=await this.operations.coachDetail(this.objectId,this.summaryMonth(),refresh);if(request!==this.loadRequest)return;const current=this.data();if(!current){this.data.set(detail);this.scheduleMonth.set(detail.payrollMonth);this.lessonsMonth.set(detail.payrollMonth);this.payrollMonth.set(detail.payrollMonth);this.lessonsPayroll.set(detail.currentPayroll);}else{this.data.set({...detail,appointments:this.scheduleMonth()===detail.payrollMonth?detail.appointments:current.appointments,lessons:this.lessonsMonth()===detail.payrollMonth?detail.lessons:current.lessons,payrolls:this.payrollMonth()===detail.payrollMonth?detail.payrolls:current.payrolls});if(this.lessonsMonth()===detail.payrollMonth)this.lessonsPayroll.set(detail.currentPayroll);}this.summaryMonth.set(detail.payrollMonth);}catch(error){if(request===this.loadRequest)this.error.set(error instanceof Error?error.message:'陪练详情加载失败');}finally{if(!background&&request===this.loadRequest)this.loading.set(false);}}
  19. async changeSectionMonth(section:MonthSection,value:string):Promise<void>{if(!/^\d{4}-\d{2}$/.test(value)||value===this.sectionMonth(section))return;this.loadRequest++;const previous=this.sectionMonth(section);this.setSectionMonth(section,value);this.sectionLoading.set(section);this.error.set('');try{const detail=await this.operations.coachDetail(this.objectId,value,false);this.data.update((current)=>{if(!current)return detail;if(section==='schedule')return{...current,appointments:detail.appointments};if(section==='lessons')return{...current,lessons:detail.lessons,reviews:detail.reviews};return{...current,payrolls:detail.payrolls};});if(section==='lessons')this.lessonsPayroll.set(detail.currentPayroll);}catch(error){this.setSectionMonth(section,previous);this.error.set(error instanceof Error?error.message:'月份数据加载失败');}finally{this.sectionLoading.set(null);}}
  20. @HostListener('document:click') closeMonthPicker():void{this.activeMonthPicker.set(null);}
  21. @HostListener('document:keydown.escape') closeMonthPickerWithKeyboard():void{this.closeMonthPicker();}
  22. toggleMonthPicker(section:MonthSection,event:Event):void{event.stopPropagation();if(this.loading()||this.isSectionLoading(section))return;if(this.activeMonthPicker()===section){this.activeMonthPicker.set(null);return;}const year=Number(this.sectionMonth(section).slice(0,4));this.monthPickerYear.set(year||new Date().getFullYear());this.activeMonthPicker.set(section);}
  23. shiftMonthPickerYear(delta:number,event:Event):void{event.stopPropagation();this.monthPickerYear.update((year)=>year+delta);}
  24. async selectSectionMonth(section:MonthSection,monthIndex:number):Promise<void>{const value=this.monthOptionValue(monthIndex);this.activeMonthPicker.set(null);await this.changeSectionMonth(section,value);}
  25. async selectCurrentMonth(section:MonthSection):Promise<void>{this.activeMonthPicker.set(null);await this.changeSectionMonth(section,this.currentMonth);}
  26. monthLabel(value:string):string{const [year,month]=value.split('-');return year&&month?`${year}年${Number(month)}月`:'选择月份';}
  27. monthOptionValue(monthIndex:number):string{return `${this.monthPickerYear()}-${String(monthIndex+1).padStart(2,'0')}`;}
  28. isSectionLoading(section:MonthSection):boolean{return this.sectionLoading()===section;}
  29. statusTone(row:ScheduleAppointment):ScheduleStatusTone{return scheduleStatusTone(row);}
  30. statusDisplayLabel(row:ScheduleAppointment):string{return scheduleStatusLabel(row);}
  31. async saveAvailability():Promise<void>{const coach=this.data()?.coach;if(!coach)return;this.saving.set(true);try{await this.operations.mutate('ops/coach-availability/save','',this.ruleReason(),{coachId:coach.userId,ruleType:this.ruleType(),weekday:this.weekday(),date:this.ruleDate(),startTime:this.startTime(),endTime:this.endTime(),available:this.available()});this.message.set('可用时间已保存');await this.load(true);}catch(error){this.error.set(error instanceof Error?error.message:'可用时间保存失败');}finally{this.saving.set(false);}}
  32. async removeAvailability(objectId:string,updatedAt:string):Promise<void>{this.saving.set(true);try{await this.operations.mutate('ops/coach-availability/delete',objectId,'删除错误的可用时间规则',{},updatedAt);await this.load(true);}catch(error){this.error.set(error instanceof Error?error.message:'删除失败');}finally{this.saving.set(false);}}
  33. sectionMonth(section:MonthSection):string{return section==='schedule'?this.scheduleMonth():section==='lessons'?this.lessonsMonth():this.payrollMonth();}
  34. private setSectionMonth(section:MonthSection,value:string):void{if(section==='schedule')this.scheduleMonth.set(value);else if(section==='lessons')this.lessonsMonth.set(value);else this.payrollMonth.set(value);}
  35. }