import { LessonRecordsComponent } from './lesson-records.component'; import { AdminSessionService } from '../admin-session.service'; import { DropdownDirective } from '../shared/dropdown.directive'; import { OperationsDateFilterComponent } from '../components/operations-date-filter.component'; import { MemberCreditAccountComponent } from '../components/member-credit-account.component'; import { MemberEnrollmentComponent } from '../components/member-enrollment.component'; import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, OnInit, computed, inject, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { ActivatedRoute, RouterLink } from '@angular/router'; import { ArrowLeft, BookOpen, CalendarDays, ClipboardList, Clock3, Eye, FileChartColumn, FileClock, GraduationCap, LoaderCircle, LucideAngularModule, MessageSquarePlus, RefreshCw, Save, ShieldAlert, UserRound, WalletCards } from 'lucide-angular'; import { MemberAgentOption, OperationsUserDetail } from '../admin.models'; import { OperationsListKind, OperationsService } from '../operations.service'; type MemberTab = 'overview' | 'courses' | 'appointments' | 'lessons' | 'reports' | 'balances' | 'audit'; @Component({ selector: 'app-operations-user-detail', standalone: true, imports: [LessonRecordsComponent, DropdownDirective, OperationsDateFilterComponent, MemberCreditAccountComponent, MemberEnrollmentComponent, CommonModule, FormsModule, RouterLink, LucideAngularModule], templateUrl: './operations-user-detail.component.html', styleUrl: './operations-user-detail.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class OperationsUserDetailComponent implements OnInit { readonly isStore=(()=>{const sessions=inject(AdminSessionService);return computed(()=>sessions.session()?.operationsRole==='store-admin');})(); readonly user = signal(null); readonly tab = signal('overview'); readonly related = signal[]>>({ overview: [], courses: [], appointments: [], lessons: [], reports: [], balances: [], audit: [] }); readonly notes = signal[]>([]); readonly loading = signal(true); readonly sectionLoading = signal(false); readonly saving = signal(false); readonly error = signal(''); readonly message = signal(''); readonly reason = signal(''); readonly editName = signal(''); readonly editNickname = signal(''); readonly editMobile = signal(''); readonly editEmail = signal(''); readonly editGender = signal(''); readonly editGrade = signal(''); readonly parentUserId = signal(0); readonly agentSearch = signal(''); readonly refreshVersion = signal(0); readonly recordPage = signal(1); readonly recordTotal = signal(0); readonly pageSize = 20; readonly reportFrom = signal(''); readonly reportTo = signal(''); private sectionSerial = 0; readonly pageCount = computed(() => Math.max(1, Math.ceil(this.recordTotal() / this.pageSize))); readonly resetPassword = signal('12345678'); readonly noteContent = signal(''); readonly revealMobile = signal(false); readonly icons = { ArrowLeft, BookOpen, CalendarDays, ClipboardList, Clock3, Eye, FileChartColumn, FileClock, GraduationCap, LoaderCircle, MessageSquarePlus, RefreshCw, Save, ShieldAlert, UserRound, WalletCards }; private readonly route = inject(ActivatedRoute); private readonly operations = inject(OperationsService); private readonly objectId = this.route.snapshot.paramMap.get('objectId') || ''; readonly filteredAgents = computed(() => { const keyword = this.agentSearch().trim().toLowerCase(); return (this.user()?.agents || []).filter(agent => !keyword || `${agent.displayName} ${agent.userId}`.toLowerCase().includes(keyword)).slice(0, 30); }); ngOnInit(): void { void this.load().then(() => { const tab=this.route.snapshot.queryParamMap.get('tab') as MemberTab; if(['courses','balances'].includes(tab))void this.selectTab(tab); }); } async refreshBusiness(): Promise{await this.load();this.refreshVersion.update(v=>v+1);await this.loadSection();} async load(): Promise { this.loading.set(true); this.error.set(''); try { const user = await this.operations.userDetail(this.objectId); this.user.set(user); this.editName.set(user.displayName); this.editNickname.set(user.nickname || ''); this.editMobile.set(user.mobile || user.mobileMasked); this.editEmail.set(user.email || ''); this.editGender.set(user.gender || ''); this.editGrade.set(user.grade || ''); this.parentUserId.set(user.parentUserId); this.agentSearch.set(user.parentDisplayName || ''); this.revealMobile.set(Boolean(user.mobile)); } catch (error) { this.error.set(error instanceof Error ? error.message : '会员详情加载失败'); } finally { this.loading.set(false); } } async selectTab(tab: MemberTab): Promise { ++this.sectionSerial; this.tab.set(tab); this.recordPage.set(1); this.recordTotal.set(0); this.sectionLoading.set(false); this.error.set(''); if (tab !== 'overview') await this.loadSection(tab); } async changePage(page: number): Promise { if(this.sectionLoading() || page<1 || page>this.pageCount()) return; this.recordPage.set(page); await this.loadSection(); } async filterReports(clear=false): Promise { if(clear){this.reportFrom.set('');this.reportTo.set('');} if(this.reportFrom()&&this.reportTo()&&this.reportFrom()>this.reportTo()){this.error.set('开始日期不能晚于结束日期');return;}this.recordPage.set(1);await this.loadSection('reports'); } private kindFor(tab: MemberTab): OperationsListKind | null { return ({ courses: 'course-bindings', appointments: 'appointments', lessons: 'lessons', balances: 'money-logs' } as Partial>)[tab] || null; } async loadSection(tab = this.tab()): Promise { if(tab==='lessons'||tab==='balances')return; const user = this.user(); if (!user || tab === 'overview') return; const serial=++this.sectionSerial; this.sectionLoading.set(true); this.error.set(''); try { let items: Record[] = [], total=0; if (tab === 'audit') { const [notes, audit] = await Promise.all([this.operations.memberRows('ops/users/notes', user.objectId), this.operations.memberRows('ops/users/audit', user.objectId)]); if(serial!==this.sectionSerial)return; this.notes.set(notes.items); items=audit.items; total=items.length; } else if (tab === 'reports') { const page=await this.operations.learningReports(this.recordPage(), this.pageSize, '', {studentObjectId:user.objectId,dateFrom:this.reportFrom(),dateTo:this.reportTo()}, false); items=page.items; total=page.total; } else { const kind=this.kindFor(tab); if(kind){const page=await this.operations.request<{items:Record[];total:number}>(`ops/${kind}/list`, {page:this.recordPage(),pageSize:this.pageSize,studentId:user.userId});items=page.items;total=page.total;} } if(serial===this.sectionSerial){this.related.update(value=>({...value,[tab]:items}));this.recordTotal.set(total);} } catch (error) { if(serial===this.sectionSerial)this.error.set(error instanceof Error ? error.message : '业务记录加载失败'); } finally { if(serial===this.sectionSerial)this.sectionLoading.set(false); } } async run(operation: string, payload: Record, success: string, targetId = this.user()?.objectId || '', expectedUpdatedAt = this.user()?.updatedAt || ''): Promise { if(this.saving())return false; if (this.reason().trim().length < 2) { this.error.set('请输入至少 2 个字的操作原因'); return false; } this.saving.set(true); this.error.set(''); this.message.set(''); try { await this.operations.mutate(operation, targetId, this.reason(), payload, expectedUpdatedAt); this.message.set(success); this.reason.set(''); return true; } catch (error) { this.error.set(error instanceof Error ? error.message : '操作失败'); return false; } finally { this.saving.set(false); } } selectAgent(agent: MemberAgentOption): void { this.parentUserId.set(agent.userId); this.agentSearch.set(agent.displayName); } clearAgent(): void { this.parentUserId.set(0); this.agentSearch.set(''); } async saveProfile(): Promise { if (await this.run('ops/users/update-profile', { displayName: this.editName(), nickname: this.editNickname(), mobile: this.revealMobile() ? this.editMobile() : '', email: this.editEmail(), gender: this.editGender(), grade: this.editGrade(), parentUserId: this.parentUserId() }, '会员资料已更新')) await this.load(); } async changeStatus(): Promise { const user = this.user(); if (user && await this.run('ops/users/change-status', { disabled: user.status !== 'disabled' }, user.status === 'disabled' ? '账号已恢复' : '账号已停用')) await this.load(); } async resetMemberPassword(): Promise { if (this.resetPassword().length < 8) { this.error.set('新密码至少 8 位'); return; } if (await this.run('ops/users/reset-password', { password: this.resetPassword() }, '密码已更新,旧会话已退出')) { this.resetPassword.set('12345678'); await this.load(); } } async saveNote(): Promise { if (!this.noteContent().trim()) { this.error.set('请输入跟进备注'); return; } if (await this.run('ops/users/save-note', { content: this.noteContent() }, '跟进备注已保存')) { this.noteContent.set(''); await this.loadSection('audit'); } } async toggleCourse(row: Record): Promise { const recycled = Number(row['status']) === -2; if (await this.run(recycled ? 'ops/course-bindings/restore' : 'ops/course-bindings/recycle', {}, recycled ? '课程绑定已恢复' : '课程绑定已移入回收站', String(row['targetId'] || ''), String(row['updatedAt'] || ''))) await this.refreshBusiness(); } async reveal(): Promise { const user = this.user(); if (!user) return; this.error.set(''); try { const result = await this.operations.mutate<{ mobile: string }>('ops/users/reveal-mobile', user.objectId, '运营核对会员联系方式'); this.user.set({ ...user, mobile: result.mobile }); this.editMobile.set(result.mobile); this.revealMobile.set(true); } catch (error) { this.error.set(error instanceof Error ? error.message : '手机号查看失败'); } } }