|
@@ -145,14 +145,16 @@ export class OperationsScheduleComponent implements OnInit, OnDestroy {
|
|
|
readonly studentCourses=computed(()=>scheduleCoursesForStudent(this.data()?.courses||[],this.studentId()));
|
|
readonly studentCourses=computed(()=>scheduleCoursesForStudent(this.data()?.courses||[],this.studentId()));
|
|
|
readonly courseStandards=SCHEDULE_COURSE_STANDARDS;
|
|
readonly courseStandards=SCHEDULE_COURSE_STANDARDS;
|
|
|
readonly categoryKey=signal('word');
|
|
readonly categoryKey=signal('word');
|
|
|
|
|
+ readonly enrollmentFreeTextbook=computed(()=>['trial','word_self_study'].includes(this.categoryKey()));
|
|
|
|
|
+ readonly textbookFree=computed(()=>['primary_writing','middle_writing','high_writing'].includes(this.categoryKey()));
|
|
|
readonly standardKey=computed(()=>`${this.categoryKey()}:${this.durationMinutes()}`);
|
|
readonly standardKey=computed(()=>`${this.categoryKey()}:${this.durationMinutes()}`);
|
|
|
readonly standardLabel=computed(()=>this.courseStandards.find(row=>row.key===this.standardKey())?.label||'当前课型');
|
|
readonly standardLabel=computed(()=>this.courseStandards.find(row=>row.key===this.standardKey())?.label||'当前课型');
|
|
|
readonly categoryCourses=computed(()=>this.studentCourses().filter(course=>course.categoryKey===this.categoryKey()));
|
|
readonly categoryCourses=computed(()=>this.studentCourses().filter(course=>course.categoryKey===this.categoryKey()));
|
|
|
readonly filteredCourses=computed(()=>this.categoryCourses().filter(course=>matchesScheduleSearch(this.courseId()?'':this.courseSearch(),[course.courseName,course.categoryName,course.courseId])));
|
|
readonly filteredCourses=computed(()=>this.categoryCourses().filter(course=>matchesScheduleSearch(this.courseId()?'':this.courseSearch(),[course.courseName,course.categoryName,course.courseId])));
|
|
|
readonly enrollmentOpen=signal(false);
|
|
readonly enrollmentOpen=signal(false);
|
|
|
@ViewChild(MemberEnrollmentComponent) enrollment?:MemberEnrollmentComponent;
|
|
@ViewChild(MemberEnrollmentComponent) enrollment?:MemberEnrollmentComponent;
|
|
|
- readonly selectedCourse=computed(()=>this.data()?.courses.find((course)=>course.courseId===this.courseId()&&(!this.bindingId()||course.bindingId===this.bindingId())&&scheduleCourseAvailable(course,this.studentId()))||null);
|
|
|
|
|
- readonly courseSelectionValid=computed(()=>{const course=this.selectedCourse();return Boolean(course?.mappingConfirmed&&course.categoryKey===this.categoryKey()&&course.supportedDurations?.includes(this.durationMinutes()));});
|
|
|
|
|
|
|
+ readonly selectedCourse=computed(()=>this.data()?.courses.find((course)=>course.courseId===this.courseId()&&course.categoryKey===this.categoryKey()&&(this.enrollmentFreeTextbook()||!this.bindingId()||course.bindingId===this.bindingId())&&scheduleCourseAvailable(course,this.studentId()))||null);
|
|
|
|
|
+ readonly courseSelectionValid=computed(()=>{if(this.textbookFree())return this.studentId()>0&&[30,60].includes(this.durationMinutes());const course=this.selectedCourse();return Boolean(course?.mappingConfirmed&&course.categoryKey===this.categoryKey()&&course.supportedDurations?.includes(this.durationMinutes()));});
|
|
|
readonly filteredCoaches=computed(()=>(this.data()?.coaches||[]).filter(coach=>matchesScheduleSearch(this.coachSearch(),[coach.displayName,coach.mobile,coach.mobileMasked,coach.userId])).sort((a,b)=>this.mode()==='time'?(Number(this.coachBusyAtSelectedTime(a.userId))-Number(this.coachBusyAtSelectedTime(b.userId)))||(Number(b.availabilityConfigured)-Number(a.availabilityConfigured))||a.displayName.localeCompare(b.displayName,'zh-CN'):a.displayName.localeCompare(b.displayName,'zh-CN')).slice(0,12));
|
|
readonly filteredCoaches=computed(()=>(this.data()?.coaches||[]).filter(coach=>matchesScheduleSearch(this.coachSearch(),[coach.displayName,coach.mobile,coach.mobileMasked,coach.userId])).sort((a,b)=>this.mode()==='time'?(Number(this.coachBusyAtSelectedTime(a.userId))-Number(this.coachBusyAtSelectedTime(b.userId)))||(Number(b.availabilityConfigured)-Number(a.availabilityConfigured))||a.displayName.localeCompare(b.displayName,'zh-CN'):a.displayName.localeCompare(b.displayName,'zh-CN')).slice(0,12));
|
|
|
readonly previewCandidates=computed(()=>schedulePreflightCandidates(this.preview()));
|
|
readonly previewCandidates=computed(()=>schedulePreflightCandidates(this.preview()));
|
|
|
private preflightRequest=0; private previewFingerprint=''; private initialRange=true; private saveKey=''; private saveFingerprint=''; private stopLiveRefresh?:()=>void; private loadRequest=0;
|
|
private preflightRequest=0; private previewFingerprint=''; private initialRange=true; private saveKey=''; private saveFingerprint=''; private stopLiveRefresh?:()=>void; private loadRequest=0;
|
|
@@ -242,7 +244,7 @@ export class OperationsScheduleComponent implements OnInit, OnDestroy {
|
|
|
updateMemberSearch(value:string):void{this.memberSearch.set(value);this.studentId.set(0);this.selectedMemberObjectId.set('');this.courseId.set(0);this.bindingId.set('');this.courseSearch.set('');this.preview.set(null);this.openSearch('member');}
|
|
updateMemberSearch(value:string):void{this.memberSearch.set(value);this.studentId.set(0);this.selectedMemberObjectId.set('');this.courseId.set(0);this.bindingId.set('');this.courseSearch.set('');this.preview.set(null);this.openSearch('member');}
|
|
|
updateCourseSearch(value:string):void{this.courseSearch.set(value);this.courseId.set(0);this.bindingId.set('');this.preview.set(null);this.openSearch('course');}
|
|
updateCourseSearch(value:string):void{this.courseSearch.set(value);this.courseId.set(0);this.bindingId.set('');this.preview.set(null);this.openSearch('course');}
|
|
|
updateCoachSearch(value:string):void{this.coachSearch.set(value);this.coachId.set(0);this.preview.set(null);this.openSearch('coach');}
|
|
updateCoachSearch(value:string):void{this.coachSearch.set(value);this.coachId.set(0);this.preview.set(null);this.openSearch('coach');}
|
|
|
- chooseMember(member:ScheduleCalendarData['members'][number]):void{if(member.readOnly){this.error.set(member.readOnlyReason||'该会员尚未完成持续同步,当前只能查看实时资料,暂不能从新后台排课。');return;}this.studentId.set(member.userId);this.selectedMemberObjectId.set(member.objectId);this.memberSearch.set(`${member.displayName} · ${member.mobileMasked}`);this.courseId.set(0);this.bindingId.set('');this.courseSearch.set('');const courses=scheduleCoursesForStudent(this.data()?.courses||[],member.userId);if(courses.length===1){this.chooseCourse(courses[0]);if(courses[0].mappingConfirmed)this.message.set(`已自动选择该学员唯一的已开课程:${courses[0].courseName}`);}else{this.selectStandard(this.standardKey());}this.preview.set(null);}
|
|
|
|
|
|
|
+ chooseMember(member:ScheduleCalendarData['members'][number]):void{if(member.readOnly){this.error.set(member.readOnlyReason||'该会员尚未完成持续同步,当前只能查看实时资料,暂不能从新后台排课。');return;}this.studentId.set(member.userId);this.selectedMemberObjectId.set(member.objectId);this.memberSearch.set(`${member.displayName} · ${member.mobileMasked}`);this.courseId.set(0);this.bindingId.set('');this.courseSearch.set('');const courses=scheduleCoursesForStudent(this.data()?.courses||[],member.userId);if(courses.length===1&&!this.textbookFree()&&!this.enrollmentFreeTextbook()){this.chooseCourse(courses[0]);if(courses[0].mappingConfirmed)this.message.set(`已自动选择该学员唯一的已开课程:${courses[0].courseName}`);}else{this.selectStandard(this.standardKey());}this.preview.set(null);}
|
|
|
chooseCourse(course:ScheduleCalendarData['courses'][number]):void{
|
|
chooseCourse(course:ScheduleCalendarData['courses'][number]):void{
|
|
|
if(!scheduleCourseAvailable(course,this.studentId()))return;
|
|
if(!scheduleCourseAvailable(course,this.studentId()))return;
|
|
|
const sameCategory=course.categoryKey===this.categoryKey();
|
|
const sameCategory=course.categoryKey===this.categoryKey();
|
|
@@ -252,6 +254,7 @@ export class OperationsScheduleComponent implements OnInit, OnDestroy {
|
|
|
}
|
|
}
|
|
|
standardAvailability(categoryKey:string):string{
|
|
standardAvailability(categoryKey:string):string{
|
|
|
if(!this.studentId())return '';
|
|
if(!this.studentId())return '';
|
|
|
|
|
+ if(['primary_writing','middle_writing','high_writing','trial','word_self_study'].includes(categoryKey))return '无需开课';
|
|
|
const count=this.studentCourses().filter(course=>course.categoryKey===categoryKey).length;
|
|
const count=this.studentCourses().filter(course=>course.categoryKey===categoryKey).length;
|
|
|
return count?`已开 ${count} 门教材`:'待开课';
|
|
return count?`已开 ${count} 门教材`:'待开课';
|
|
|
}
|
|
}
|
|
@@ -259,6 +262,7 @@ export class OperationsScheduleComponent implements OnInit, OnDestroy {
|
|
|
const standard=this.courseStandards.find(row=>row.key===key);if(!standard)return;
|
|
const standard=this.courseStandards.find(row=>row.key===key);if(!standard)return;
|
|
|
const previous=this.selectedCourse();
|
|
const previous=this.selectedCourse();
|
|
|
this.categoryKey.set(standard.categoryKey);this.durationMinutes.set(standard.duration);this.error.set('');this.preview.set(null);
|
|
this.categoryKey.set(standard.categoryKey);this.durationMinutes.set(standard.duration);this.error.set('');this.preview.set(null);
|
|
|
|
|
+ if(this.textbookFree()){this.courseId.set(0);this.bindingId.set('');this.courseSearch.set('');this.activeSearch.set(null);return;}
|
|
|
const courses=this.categoryCourses();
|
|
const courses=this.categoryCourses();
|
|
|
const course=courses.find(row=>previous&&row.bindingId===previous.bindingId&&row.courseId===previous.courseId)||(courses.length===1?courses[0]:null);
|
|
const course=courses.find(row=>previous&&row.bindingId===previous.bindingId&&row.courseId===previous.courseId)||(courses.length===1?courses[0]:null);
|
|
|
if(course)this.chooseCourse(course);
|
|
if(course)this.chooseCourse(course);
|
|
@@ -282,13 +286,13 @@ export class OperationsScheduleComponent implements OnInit, OnDestroy {
|
|
|
coachBusyAtSelectedTime(coachId:number):boolean{return scheduleCoachBusy(this.data()?.appointments||[],coachId,`${this.date()}T${this.startTime()}:00+08:00`,this.durationMinutes(),this.editing()?.targetId||'');}
|
|
coachBusyAtSelectedTime(coachId:number):boolean{return scheduleCoachBusy(this.data()?.appointments||[],coachId,`${this.date()}T${this.startTime()}:00+08:00`,this.durationMinutes(),this.editing()?.targetId||'');}
|
|
|
coachCandidateHint(coach:ScheduleCalendarData['coaches'][number]):string{return this.coachBusyAtSelectedTime(coach.userId)?'该时段已有排课':coach.availabilityConfigured?'该时段可继续预检':'未设置可用时间';}
|
|
coachCandidateHint(coach:ScheduleCalendarData['coaches'][number]):string{return this.coachBusyAtSelectedTime(coach.userId)?'该时段已有排课':coach.availabilityConfigured?'该时段可继续预检':'未设置可用时间';}
|
|
|
memberMobile(userId:number):string{return this.data()?.members.find(member=>member.userId===userId)?.mobileMasked||'';}
|
|
memberMobile(userId:number):string{return this.data()?.members.find(member=>member.userId===userId)?.mobileMasked||'';}
|
|
|
- payload():Record<string,unknown>{return{bindingId:this.bindingId(),studentId:this.studentId(),courseId:this.courseId(),coachId:this.coachId(),classType:this.classType(),durationMinutes:this.durationMinutes(),deliveryMethod:this.deliveryMethod(),deliveryMode:this.deliveryMethod()==='线下'?'offline':'online',date:this.date(),startTime:this.startTime(),recurrence:this.recurrence(),occurrences:this.occurrences()};}
|
|
|
|
|
|
|
+ payload():Record<string,unknown>{return{categoryKey:this.categoryKey(),bindingId:this.textbookFree()?'':this.bindingId(),studentId:this.studentId(),courseId:this.textbookFree()?0:this.courseId(),coachId:this.coachId(),classType:this.classType(),durationMinutes:this.durationMinutes(),deliveryMethod:this.deliveryMethod(),deliveryMode:this.deliveryMethod()==='线下'?'offline':'online',date:this.date(),startTime:this.startTime(),recurrence:this.recurrence(),occurrences:this.occurrences()};}
|
|
|
async preflight():Promise<void>{
|
|
async preflight():Promise<void>{
|
|
|
if(this.checking()||this.saving()||this.enrollmentOpen())return;
|
|
if(this.checking()||this.saving()||this.enrollmentOpen())return;
|
|
|
this.error.set('');this.preview.set(null);this.previewFingerprint='';
|
|
this.error.set('');this.preview.set(null);this.previewFingerprint='';
|
|
|
- if(!this.selectedMemberObjectId()||!this.courseId()){this.error.set('请先选择会员和课程');return;}
|
|
|
|
|
|
|
+ if(!this.selectedMemberObjectId()||(!this.textbookFree()&&!this.courseId())){this.error.set('请先选择会员和课程');return;}
|
|
|
if(!this.studentId()){this.error.set('该会员缺少排课编号,暂时不能排课');return;}
|
|
if(!this.studentId()){this.error.set('该会员缺少排课编号,暂时不能排课');return;}
|
|
|
- if(!this.courseSelectionValid()){this.error.set('请选择该课型下的有效已开教材,并确认所选时长有可用规则');return;}
|
|
|
|
|
|
|
+ if(!this.courseSelectionValid()){this.error.set('请选择该课型下的有效教材,并确认所选时长有可用规则');return;}
|
|
|
const request=++this.preflightRequest,targetId=this.editing()?.targetId||'',payload=this.payload(),fingerprint=JSON.stringify([targetId,payload]);
|
|
const request=++this.preflightRequest,targetId=this.editing()?.targetId||'',payload=this.payload(),fingerprint=JSON.stringify([targetId,payload]);
|
|
|
this.checking.set(true);
|
|
this.checking.set(true);
|
|
|
try{
|
|
try{
|
|
@@ -298,6 +302,6 @@ export class OperationsScheduleComponent implements OnInit, OnDestroy {
|
|
|
}catch(error){if(request===this.preflightRequest&&fingerprint===JSON.stringify([this.editing()?.targetId||'',this.payload()]))this.error.set(error instanceof Error?error.message:'排课预检失败');}
|
|
}catch(error){if(request===this.preflightRequest&&fingerprint===JSON.stringify([this.editing()?.targetId||'',this.payload()]))this.error.set(error instanceof Error?error.message:'排课预检失败');}
|
|
|
finally{if(request===this.preflightRequest)this.checking.set(false);}
|
|
finally{if(request===this.preflightRequest)this.checking.set(false);}
|
|
|
}
|
|
}
|
|
|
- async save():Promise<void>{if(this.saving())return;if(!this.selectedMemberObjectId()||!this.courseId()||!this.reason().trim()){this.error.set('请选择会员、课程并填写操作原因');return;}if(!this.studentId()){this.error.set('该会员缺少旧系统排课编号,暂时不能保存;请先补齐该会员的迁移标识');return;}if(!this.preview()){this.error.set('请先检查冲突并确认逐项预检结果');return;}if(!this.canSavePreview()){this.error.set('当前没有可以提交的排课,请先调整冲突项');return;}this.saving.set(true);this.error.set('');try{const editing=this.editing(),skipConflicts=!editing&&this.previewConflictCount()>0&&this.previewCandidates().length>1;const fingerprint=JSON.stringify([editing?.targetId||'',this.payload(),skipConflicts,this.reason()]);if(this.saveFingerprint!==fingerprint){this.saveFingerprint=fingerprint;this.saveKey=crypto.randomUUID();}const result=await this.operations.mutate<{created?:number;skipped?:number}>(editing?'ops/appointments/update':'ops/appointments/create',editing?.targetId||'',this.reason(),{...this.payload(),skipConflicts},editing?.updatedAt||'',this.saveKey);this.saveFingerprint='';this.saveKey='';this.message.set(editing?'排课已更新':result.skipped?`已创建 ${result.created||0} 节排课,跳过 ${result.skipped} 节冲突项`:`已创建 ${result.created||1} 节排课`);this.drawerOpen.set(false);await this.load();}catch(error){this.error.set(error instanceof Error?error.message:'排课保存失败');}finally{this.saving.set(false);}}
|
|
|
|
|
|
|
+ async save():Promise<void>{if(this.saving())return;if(!this.selectedMemberObjectId()||(!this.textbookFree()&&!this.courseId())||!this.reason().trim()){this.error.set('请选择会员、课程并填写操作原因');return;}if(!this.studentId()){this.error.set('该会员缺少旧系统排课编号,暂时不能保存;请先补齐该会员的迁移标识');return;}if(!this.preview()){this.error.set('请先检查冲突并确认逐项预检结果');return;}if(!this.canSavePreview()){this.error.set('当前没有可以提交的排课,请先调整冲突项');return;}this.saving.set(true);this.error.set('');try{const editing=this.editing(),skipConflicts=!editing&&this.previewConflictCount()>0&&this.previewCandidates().length>1;const fingerprint=JSON.stringify([editing?.targetId||'',this.payload(),skipConflicts,this.reason()]);if(this.saveFingerprint!==fingerprint){this.saveFingerprint=fingerprint;this.saveKey=crypto.randomUUID();}const result=await this.operations.mutate<{created?:number;skipped?:number}>(editing?'ops/appointments/update':'ops/appointments/create',editing?.targetId||'',this.reason(),{...this.payload(),skipConflicts},editing?.updatedAt||'',this.saveKey);this.saveFingerprint='';this.saveKey='';this.message.set(editing?'排课已更新':result.skipped?`已创建 ${result.created||0} 节排课,跳过 ${result.skipped} 节冲突项`:`已创建 ${result.created||1} 节排课`);this.drawerOpen.set(false);await this.load();}catch(error){this.error.set(error instanceof Error?error.message:'排课保存失败');}finally{this.saving.set(false);}}
|
|
|
async action(item:ScheduleAppointment,operation:string):Promise<void>{if(item.readOnly){this.message.set('实时业务数据暂不支持从新后台修改,请在原业务端完成操作。');return;}const reason=globalThis.prompt(operation.endsWith('restore')?'请输入恢复原因':operation.endsWith('complete')?'请输入确认完成原因':'请输入取消原因');if(!reason?.trim())return;this.saving.set(true);try{await this.operations.mutate(operation,item.targetId,reason,{},item.updatedAt);await this.load();}catch(error){this.error.set(error instanceof Error?error.message:'操作失败');}finally{this.saving.set(false);}}
|
|
async action(item:ScheduleAppointment,operation:string):Promise<void>{if(item.readOnly){this.message.set('实时业务数据暂不支持从新后台修改,请在原业务端完成操作。');return;}const reason=globalThis.prompt(operation.endsWith('restore')?'请输入恢复原因':operation.endsWith('complete')?'请输入确认完成原因':'请输入取消原因');if(!reason?.trim())return;this.saving.set(true);try{await this.operations.mutate(operation,item.targetId,reason,{},item.updatedAt);await this.load();}catch(error){this.error.set(error instanceof Error?error.message:'操作失败');}finally{this.saving.set(false);}}
|
|
|
}
|
|
}
|