|
@@ -6,7 +6,8 @@ import { ChangeDetectionStrategy, Component, computed, effect, ElementRef, injec
|
|
|
import { FormsModule } from '@angular/forms';
|
|
import { FormsModule } from '@angular/forms';
|
|
|
import { ActivatedRoute, Router } from '@angular/router';
|
|
import { ActivatedRoute, Router } from '@angular/router';
|
|
|
import { BookCheck, Check, ChevronRight, CircleHelp, Clock3, Flag, GraduationCap, LucideAngularModule, Play, RefreshCw, Save, Star, X } from 'lucide-angular';
|
|
import { BookCheck, Check, ChevronRight, CircleHelp, Clock3, Flag, GraduationCap, LucideAngularModule, Play, RefreshCw, Save, Star, X } from 'lucide-angular';
|
|
|
-import { finalize, Subscription } from 'rxjs';
|
|
|
|
|
|
|
+import { finalize, firstValueFrom, Subscription } from 'rxjs';
|
|
|
|
|
+import { LessonAttachment, lessonAttachments, lessonAttachmentContentType, LESSON_ATTACHMENT_ACCEPT, LESSON_ATTACHMENT_MAX_COUNT, LESSON_ATTACHMENT_MAX_SIZE } from '../../core/lesson-attachments';
|
|
|
import { LiveDataRefreshService } from '../../core/live-data-refresh.service';
|
|
import { LiveDataRefreshService } from '../../core/live-data-refresh.service';
|
|
|
import { ApiError, ApiService } from '../../core/api.service';
|
|
import { ApiError, ApiService } from '../../core/api.service';
|
|
|
import { PageMeta } from '../../core/models';
|
|
import { PageMeta } from '../../core/models';
|
|
@@ -50,6 +51,10 @@ export class AppointmentPageComponent implements OnInit, OnDestroy {
|
|
|
readonly classStudyLoading = signal(false);
|
|
readonly classStudyLoading = signal(false);
|
|
|
readonly editingFeedback = signal(false);
|
|
readonly editingFeedback = signal(false);
|
|
|
readonly feedbackSaved = signal(false);
|
|
readonly feedbackSaved = signal(false);
|
|
|
|
|
+ readonly feedbackAttachments = signal<LessonAttachment[]>([]);
|
|
|
|
|
+ readonly uploadingAttachments = signal(false);
|
|
|
|
|
+ readonly attachmentProgress = signal('');
|
|
|
|
|
+ readonly attachmentAccept = LESSON_ATTACHMENT_ACCEPT;
|
|
|
private feedbackRevision = '';
|
|
private feedbackRevision = '';
|
|
|
private autoEditOpened = false;
|
|
private autoEditOpened = false;
|
|
|
readonly dateLabel = lessonDateLabel;
|
|
readonly dateLabel = lessonDateLabel;
|
|
@@ -106,7 +111,8 @@ export class AppointmentPageComponent implements OnInit, OnDestroy {
|
|
|
get isStudentView(): boolean { return this.meta.path === 'pages/shop/stu_get_order'; }
|
|
get isStudentView(): boolean { return this.meta.path === 'pages/shop/stu_get_order'; }
|
|
|
get orderId(): string { return this.route.snapshot.queryParamMap.get('id') ?? this.route.snapshot.queryParamMap.get('cart') ?? ''; }
|
|
get orderId(): string { return this.route.snapshot.queryParamMap.get('id') ?? this.route.snapshot.queryParamMap.get('cart') ?? ''; }
|
|
|
get selectedCourseItem(): SourceItem | undefined { return this.courses().find(row => String(row['bindingId']) === this.selectedBindingId); }
|
|
get selectedCourseItem(): SourceItem | undefined { return this.courses().find(row => String(row['bindingId']) === this.selectedBindingId); }
|
|
|
- get isWriting(): boolean { return ['primary_writing','middle_writing','high_writing'].includes(String(this.selectedCourseItem?.['categoryKey'] || this.order()['courseCategoryKey'] || '')); }
|
|
|
|
|
|
|
+ get isWritingAppointment(): boolean { return ['primary_writing','middle_writing','high_writing'].includes(String(this.order()['courseCategoryKey'] || '')); }
|
|
|
|
|
+ get isWriting(): boolean { return this.isWritingAppointment || ['primary_writing','middle_writing','high_writing'].includes(String(this.selectedCourseItem?.['categoryKey'] || '')); }
|
|
|
|
|
|
|
|
load(background = false): void {
|
|
load(background = false): void {
|
|
|
this.loadRequest?.unsubscribe();
|
|
this.loadRequest?.unsubscribe();
|
|
@@ -127,7 +133,7 @@ export class AppointmentPageComponent implements OnInit, OnDestroy {
|
|
|
// Older servers already support starting lessons. Missing new workflow metadata
|
|
// Older servers already support starting lessons. Missing new workflow metadata
|
|
|
// must not disable that endpoint; never derive write access from the cached row.
|
|
// must not disable that endpoint; never derive write access from the cached row.
|
|
|
if (!Array.isArray(detail['allowedActions'])) Object.assign(detail, this.legacyStartActions(detail));
|
|
if (!Array.isArray(detail['allowedActions'])) Object.assign(detail, this.legacyStartActions(detail));
|
|
|
- if (background) { const changed = this.order()['bindingId'] !== detail['bindingId']; this.order.set({...this.order(),...detail}); if (changed && !this.isStudentView) this.loadCourses(); return; }
|
|
|
|
|
|
|
+ if (background) { const changed = this.order()['bindingId'] !== detail['bindingId']; this.order.set({...this.order(),...detail}); if (changed && !this.isStudentView && !this.isWritingAppointment) this.loadCourses(); return; }
|
|
|
this.applyOrder({ ...fallback, ...detail });
|
|
this.applyOrder({ ...fallback, ...detail });
|
|
|
this.loadClassStudy();
|
|
this.loadClassStudy();
|
|
|
},
|
|
},
|
|
@@ -147,6 +153,7 @@ export class AppointmentPageComponent implements OnInit, OnDestroy {
|
|
|
|
|
|
|
|
loadClassStudy(): void {
|
|
loadClassStudy(): void {
|
|
|
this.classStudyError.set('');
|
|
this.classStudyError.set('');
|
|
|
|
|
+ if (this.isWritingAppointment) { this.classStudy.set(null); this.classStudyLoading.set(false); return; }
|
|
|
this.classStudyLoading.set(true);
|
|
this.classStudyLoading.set(true);
|
|
|
this.api.get<any>('app_lesson_study', {orderId: this.orderId}).pipe(finalize(() => this.classStudyLoading.set(false))).subscribe({next: ({result}) => this.classStudy.set(result), error: (e: ApiError) => this.classStudyError.set(e.message)});
|
|
this.api.get<any>('app_lesson_study', {orderId: this.orderId}).pipe(finalize(() => this.classStudyLoading.set(false))).subscribe({next: ({result}) => this.classStudy.set(result), error: (e: ApiError) => this.classStudyError.set(e.message)});
|
|
|
}
|
|
}
|
|
@@ -210,7 +217,7 @@ export class AppointmentPageComponent implements OnInit, OnDestroy {
|
|
|
if (!this.coachComment) this.coachComment = String(order['pldp'] ?? '');
|
|
if (!this.coachComment) this.coachComment = String(order['pldp'] ?? '');
|
|
|
this.loadPracticeRecord();
|
|
this.loadPracticeRecord();
|
|
|
if (!this.isStudentView) {
|
|
if (!this.isStudentView) {
|
|
|
- if (this.status() <= 10) this.loadCourses();
|
|
|
|
|
|
|
+ if (this.status() <= 10 && !this.isWritingAppointment) this.loadCourses();
|
|
|
this.loadLearningRecord();
|
|
this.loadLearningRecord();
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -289,7 +296,7 @@ export class AppointmentPageComponent implements OnInit, OnDestroy {
|
|
|
this.actionError.set('');
|
|
this.actionError.set('');
|
|
|
if (!this.can('start') && !this.can('continue')) { this.actionError.set(String(this.order()['blockedReason'] || '详情未核实,请刷新后重试')); return; }
|
|
if (!this.can('start') && !this.can('continue')) { this.actionError.set(String(this.order()['blockedReason'] || '详情未核实,请刷新后重试')); return; }
|
|
|
if(this.order()['cancelled']===true){this.platform.toast('此预约已取消,不能开始课程','error');return;}
|
|
if(this.order()['cancelled']===true){this.platform.toast('此预约已取消,不能开始课程','error');return;}
|
|
|
- if (this.coursesLoading() || this.unitsLoading() || !this.selectedCourseItem || this.coursesError()) { this.actionError.set('请先加载并选择该学员已开通的课程'); return; }
|
|
|
|
|
|
|
+ if (!this.isWritingAppointment && (this.coursesLoading() || this.unitsLoading() || !this.selectedCourseItem || this.coursesError())) { this.actionError.set('请先加载并选择该学员已开通的课程'); return; }
|
|
|
if (!this.isWriting && !this.selectedUnits().size) {
|
|
if (!this.isWriting && !this.selectedUnits().size) {
|
|
|
this.actionError.set(this.units().length ? '请选择本次学习单元' : '暂无可用学习单元,请刷新重试或联系管理员核对课程内容');
|
|
this.actionError.set(this.units().length ? '请选择本次学习单元' : '暂无可用学习单元,请刷新重试或联系管理员核对课程内容');
|
|
|
return;
|
|
return;
|
|
@@ -318,6 +325,7 @@ export class AppointmentPageComponent implements OnInit, OnDestroy {
|
|
|
openFeedbackEditor(): void {
|
|
openFeedbackEditor(): void {
|
|
|
if (this.isStudentView || !this.can('editFeedback') || this.submitting()) return;
|
|
if (this.isStudentView || !this.can('editFeedback') || this.submitting()) return;
|
|
|
const feedback = this.feedback() || {};
|
|
const feedback = this.feedback() || {};
|
|
|
|
|
+ this.feedbackAttachments.set(lessonAttachments(feedback['attachments']));
|
|
|
this.contentSummary = String(feedback['contentSummary'] || '');
|
|
this.contentSummary = String(feedback['contentSummary'] || '');
|
|
|
this.reviewStatus = String(feedback['reviewStatus'] || '');
|
|
this.reviewStatus = String(feedback['reviewStatus'] || '');
|
|
|
this.reviewSummary = String(feedback['reviewSummary'] || '');
|
|
this.reviewSummary = String(feedback['reviewSummary'] || '');
|
|
@@ -336,12 +344,39 @@ export class AppointmentPageComponent implements OnInit, OnDestroy {
|
|
|
|
|
|
|
|
closeCompletionFeedback(event?: Event): void {
|
|
closeCompletionFeedback(event?: Event): void {
|
|
|
event?.preventDefault();
|
|
event?.preventDefault();
|
|
|
- if (!this.submitting()) this.completionDialog()?.nativeElement.close();
|
|
|
|
|
|
|
+ if (!this.submitting() && !this.uploadingAttachments()) this.completionDialog()?.nativeElement.close();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async uploadFeedbackAttachments(event: Event): Promise<void> {
|
|
|
|
|
+ const input = event.target as HTMLInputElement, files = Array.from(input.files || []);
|
|
|
|
|
+ input.value = '';
|
|
|
|
|
+ if (!files.length || this.uploadingAttachments() || this.submitting() || !this.isWritingAppointment
|
|
|
|
|
+ || this.isStudentView || !this.can(this.editingFeedback() ? 'editFeedback' : 'complete') || !this.completionDialog()?.nativeElement.open) return;
|
|
|
|
|
+ this.feedbackError.set('');
|
|
|
|
|
+ if (this.feedbackAttachments().length + files.length > LESSON_ATTACHMENT_MAX_COUNT) { this.feedbackError.set('每节课最多保存10个附件'); return; }
|
|
|
|
|
+ const invalid = files.find(file => !lessonAttachmentContentType(file) || file.size < 1 || file.size > LESSON_ATTACHMENT_MAX_SIZE);
|
|
|
|
|
+ if (invalid) { this.feedbackError.set(`“${invalid.name}”不符合要求,请选择5MB以内的照片或常用文档`); return; }
|
|
|
|
|
+ this.uploadingAttachments.set(true);
|
|
|
|
|
+ try {
|
|
|
|
|
+ for (const [index, file] of files.entries()) {
|
|
|
|
|
+ this.attachmentProgress.set(`正在上传 ${index + 1}/${files.length}:${file.name}`);
|
|
|
|
|
+ const { result } = await firstValueFrom(this.api.uploadLessonAttachment(this.orderId, file, lessonAttachmentContentType(file)));
|
|
|
|
|
+ const attachment = lessonAttachments([result])[0];
|
|
|
|
|
+ if (!attachment) throw new Error('上传未返回有效附件,请重试');
|
|
|
|
|
+ this.feedbackAttachments.update(items => [...items, attachment]);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) { this.feedbackError.set((error as Error).message || '附件上传失败,请重试;已上传的附件和填写内容已保留'); }
|
|
|
|
|
+ finally { this.uploadingAttachments.set(false); this.attachmentProgress.set(''); }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ removeFeedbackAttachment(id: string): void {
|
|
|
|
|
+ if (this.uploadingAttachments() || this.submitting() || this.isStudentView || !this.can(this.editingFeedback() ? 'editFeedback' : 'complete')) return;
|
|
|
|
|
+ this.feedbackAttachments.update(items => items.filter(item => item.id !== id));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async endLesson(): Promise<void> {
|
|
async endLesson(): Promise<void> {
|
|
|
if (this.submitting() || !this.can('complete')) return;
|
|
if (this.submitting() || !this.can('complete')) return;
|
|
|
- if (this.classStudyLoading() || this.classStudyError() || !this.classStudy()) { this.actionError.set('请先加载本节学习成果,再结束课程'); return; }
|
|
|
|
|
|
|
+ if (!this.isWritingAppointment && (this.classStudyLoading() || this.classStudyError() || !this.classStudy())) { this.actionError.set('请先加载本节学习成果,再结束课程'); return; }
|
|
|
if (this.classStudy()?.pending) { this.actionError.set('还有未确认的学习批次,请先处理'); return; }
|
|
if (this.classStudy()?.pending) { this.actionError.set('还有未确认的学习批次,请先处理'); return; }
|
|
|
if (!await this.platform.confirm('确认结束本次课程?系统将记录课时,课堂反馈可以稍后补充。')) return;
|
|
if (!await this.platform.confirm('确认结束本次课程?系统将记录课时,课堂反馈可以稍后补充。')) return;
|
|
|
if (this.submitting() || !this.can('complete')) return;
|
|
if (this.submitting() || !this.can('complete')) return;
|
|
@@ -354,9 +389,9 @@ export class AppointmentPageComponent implements OnInit, OnDestroy {
|
|
|
readonly completionNotice=signal('');
|
|
readonly completionNotice=signal('');
|
|
|
|
|
|
|
|
finishCoachLesson(): void {
|
|
finishCoachLesson(): void {
|
|
|
- if (this.submitting() || !this.can(this.editingFeedback() ? 'editFeedback' : 'complete') || !this.completionDialog()?.nativeElement.open) return;
|
|
|
|
|
|
|
+ if (this.submitting() || this.uploadingAttachments() || !this.can(this.editingFeedback() ? 'editFeedback' : 'complete') || !this.completionDialog()?.nativeElement.open) return;
|
|
|
this.feedbackError.set('');
|
|
this.feedbackError.set('');
|
|
|
- if (!this.editingFeedback() && (this.classStudyLoading() || this.classStudyError() || !this.classStudy())) { this.feedbackError.set('请先加载本节学习成果,再提交结课'); return; }
|
|
|
|
|
|
|
+ if (!this.editingFeedback() && !this.isWritingAppointment && (this.classStudyLoading() || this.classStudyError() || !this.classStudy())) { this.feedbackError.set('请先加载本节学习成果,再提交结课'); return; }
|
|
|
if (!this.editingFeedback() && this.classStudy()?.pending) { this.feedbackError.set('还有未确认的学习批次,请返回课程先处理'); return; }
|
|
if (!this.editingFeedback() && this.classStudy()?.pending) { this.feedbackError.set('还有未确认的学习批次,请返回课程先处理'); return; }
|
|
|
if (!this.contentSummary.trim()) { this.feedbackError.set('请填写本次学习内容'); return; }
|
|
if (!this.contentSummary.trim()) { this.feedbackError.set('请填写本次学习内容'); return; }
|
|
|
if (!this.trainingSummary.trim()) { this.feedbackError.set('请填写进度说明'); return; }
|
|
if (!this.trainingSummary.trim()) { this.feedbackError.set('请填写进度说明'); return; }
|
|
@@ -377,6 +412,7 @@ export class AppointmentPageComponent implements OnInit, OnDestroy {
|
|
|
noHomework: this.noHomework,
|
|
noHomework: this.noHomework,
|
|
|
notes: this.notes.trim(),
|
|
notes: this.notes.trim(),
|
|
|
coachComment: this.coachComment.trim(),
|
|
coachComment: this.coachComment.trim(),
|
|
|
|
|
+ ...(this.isWritingAppointment ? { attachments: this.feedbackAttachments() } : {}),
|
|
|
}),
|
|
}),
|
|
|
}).pipe(finalize(() => this.submitting.set(false))).subscribe({
|
|
}).pipe(finalize(() => this.submitting.set(false))).subscribe({
|
|
|
next: () => { this.completionDialog()?.nativeElement.close(); this.feedbackSaved.set(editing); this.platform.toast(editing ? (supplement ? '课堂反馈已提交' : '报告修改已保存') : '课程已结课,学习报告已生成'); this.load(); },
|
|
next: () => { this.completionDialog()?.nativeElement.close(); this.feedbackSaved.set(editing); this.platform.toast(editing ? (supplement ? '课堂反馈已提交' : '报告修改已保存') : '课程已结课,学习报告已生成'); this.load(); },
|
|
@@ -395,7 +431,7 @@ export class AppointmentPageComponent implements OnInit, OnDestroy {
|
|
|
}
|
|
}
|
|
|
private updateOrderStatus(status:number,next:()=>void):void{
|
|
private updateOrderStatus(status:number,next:()=>void):void{
|
|
|
if(this.submitting())return;this.submitting.set(true);
|
|
if(this.submitting())return;this.submitting.set(true);
|
|
|
- this.api.post('e_order_update_v2',{status,uid:this.session.user()?.userId,learningBindingId:this.selectedBindingId,expectedUpdatedAt:this.order()['updatedAt'],content:JSON.stringify({GeneralID:this.orderId})}).pipe(finalize(()=>this.submitting.set(false))).subscribe({next:()=>next(),error:(error:ApiError)=>this.actionError.set(error.message)});
|
|
|
|
|
|
|
+ this.api.post('e_order_update_v2',{status,uid:this.session.user()?.userId,learningBindingId:this.isWritingAppointment?undefined:this.selectedBindingId,expectedUpdatedAt:this.order()['updatedAt'],content:JSON.stringify({GeneralID:this.orderId})}).pipe(finalize(()=>this.submitting.set(false))).subscribe({next:()=>next(),error:(error:ApiError)=>this.actionError.set(error.message)});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private loadPracticeRecord(): void {
|
|
private loadPracticeRecord(): void {
|