operations-coaches.component.html 8.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344
  1. <header class="page-header">
  2. <div><p>COACH OPERATIONS</p><h1>陪练老师</h1><span>查看老师排课、完成课次、工时、评分与预估工资</span></div>
  3. <div class="header-actions"><button class="ui-button secondary" type="button" (click)="load(page()?.page||1,true,'refresh')" [disabled]="loading()"><lucide-icon [img]="icons.RefreshCw" [size]="17" [class.spin]="refreshing()"/>{{refreshing()?'刷新中':'刷新实时数据'}}</button>@if(page()?.canManage){<button class="ui-button primary" type="button" (click)="openCreate()"><lucide-icon [img]="icons.Plus" [size]="17"/>新增陪练</button>}</div>
  4. </header>
  5. <section class="toolbar coach-filters" aria-label="陪练老师筛选" [attr.aria-busy]="querying()">
  6. <label class="grow"><span>搜索老师</span><span class="search-control"><lucide-icon [img]="icons.Search" [size]="17"/><input type="search" [ngModel]="queryInput()" (ngModelChange)="queryInput.set($event)" (keyup.enter)="applyFilters()" placeholder="姓名、账号、手机号、业务 ID 或所属门店"/></span></label>
  7. <label class="status-control"><span>账号状态</span><select [ngModel]="statusInput()" (ngModelChange)="statusInput.set($event)"><option value="all">全部状态</option><option value="active">正常</option><option value="disabled">已停用</option></select></label>
  8. <div class="filter-actions"><button class="ui-button primary" type="button" (click)="applyFilters()" [disabled]="querying()"><lucide-icon [img]="querying()?icons.LoaderCircle:icons.Search" [size]="17" [class.spin]="querying()"/>{{querying()?'正在查询':'查询'}}</button><button class="ui-button secondary" type="button" (click)="reset()" [disabled]="querying()"><lucide-icon [img]="icons.RotateCcw" [size]="16"/>重置</button></div>
  9. </section>
  10. @if(message()){<p class="notice" role="status">{{message()}}</p>}@if(error()&&!drawerOpen()){<p class="notice error" role="alert">{{error()}}</p>}
  11. @if(page()&&!page()?.liveSourceAvailable&&page()?.liveSourceMessage){<p class="notice warning" role="status">{{page()?.liveSourceMessage}}</p>}
  12. @if(page()?.summary){<section class="coach-summary" aria-label="陪练老师统计">
  13. <article><span>陪练总数</span><strong>{{page()?.summary?.total||0}}</strong><small>按历史排课与授课证据识别</small></article>
  14. <article><span>正常账号</span><strong>{{page()?.summary?.active||0}}</strong><small>旧系统账号当前可用</small></article>
  15. <article><span>本月有课</span><strong>{{page()?.summary?.monthActive||0}}</strong><small>本月已产生上课记录</small></article>
  16. <article><span>已停用</span><strong>{{page()?.summary?.disabled||0}}</strong><small>保留历史排课和工资</small></article>
  17. </section>}
  18. <section class="content-card">
  19. <header><div><lucide-icon [img]="icons.GraduationCap" [size]="19"/><strong>陪练名单</strong></div><span>{{page()?.total||0}} 位 · 完整历史陪练口径 · {{ page()?.sync?.label || (page()?.liveSourceAvailable?'实时账号已读取':'迁移库备用数据') }} · {{page()?.payrollSourceStatus?.mode==='live'?'本月工资已实时同步':'本月工资显示缓存'}}</span></header>
  20. <div class="table-scroll"><table class="data-table"><thead><tr><th>老师</th><th>所属门店</th><th>今日排课</th><th>全部排课</th><th>最近/下次排课</th><th>待完成</th><th>本月课次</th><th>本月工时</th><th>预估工资</th><th>平均评分</th><th>最近上课</th><th>状态</th><th>操作</th></tr></thead><tbody>
  21. @for(row of page()?.items||[];track row.objectId){<tr><td><strong>{{row.displayName}}</strong><small>{{row.username?row.username+' · ':''}}ID {{row.userId}} · {{row.mobile||row.mobileMasked||'未留手机号'}}</small><small class="source-note">{{row.sourceLabel||'新系统账号'}}</small></td><td>{{row.storeName||'未绑定'}}</td><td>{{row.todayAppointments}}</td><td><strong>{{row.totalAppointments}}</strong></td><td>@if(row.nextAppointmentAt){<span class="schedule-date future">下次 {{row.nextAppointmentAt|date:'yyyy-MM-dd'}}</span>}@else if(row.lastAppointmentAt){<span class="schedule-date">最近 {{row.lastAppointmentAt|date:'yyyy-MM-dd'}}</span>}@else{暂无}</td><td>{{row.pendingAppointments}}</td><td>{{row.monthLessons}}</td><td>{{row.monthHours|number:'1.1-1'}}h</td><td class="money">¥{{row.estimatedPay|number:'1.2-2'}}</td><td>{{row.averageScore? (row.averageScore|number:'1.1-1'):'暂无'}}</td><td>{{row.lastLessonAt?(row.lastLessonAt|date:'MM-dd HH:mm'):'暂无'}}</td><td><span class="badge" [class.muted]="row.status==='disabled'">{{row.statusLabel}}</span>@if(!row.availabilityConfigured&&!row.readOnly){<small class="exception">未设置可用时间</small>}</td><td><div class="row-actions"><a class="icon-action" [routerLink]="['/admin/coaches',row.objectId]" aria-label="查看陪练详情" title="查看"><lucide-icon [img]="icons.Eye" [size]="16"/></a>@if(!row.readOnly&&page()?.canManage){<button class="icon-action" type="button" (click)="openEdit(row)" aria-label="编辑陪练" title="编辑"><lucide-icon [img]="icons.Pencil" [size]="16"/></button><button class="icon-action danger" type="button" (click)="openEdit(row,true)" aria-label="删除陪练" title="删除"><lucide-icon [img]="icons.Trash2" [size]="16"/></button>}@if(row.readOnly){<span class="sync-only">详情只读</span>}</div></td></tr>}
  22. @empty{<tr><td colspan="13"><div class="empty">没有符合条件的陪练老师</div></td></tr>}
  23. </tbody></table></div>
  24. <footer class="pagination"><span>第 {{page()?.page||1}} / {{pageCount()}} 页</span><div><button type="button" [disabled]="(page()?.page||1)<=1" (click)="load((page()?.page||1)-1)"><lucide-icon [img]="icons.ChevronLeft" [size]="17"/></button><button type="button" [disabled]="(page()?.page||1)>=pageCount()" (click)="load((page()?.page||1)+1)"><lucide-icon [img]="icons.ChevronRight" [size]="17"/></button></div></footer>
  25. </section>
  26. @if(drawerOpen()){
  27. <div class="backdrop" (click)="closeDrawer()"></div>
  28. <aside class="drawer coach-editor" role="dialog" aria-modal="true" [attr.aria-label]="editing()?'编辑陪练老师':'新增陪练老师'">
  29. <header><div><p>COACH ACCOUNT</p><h2>{{editing()?'编辑陪练老师':'新增陪练老师'}}</h2><span>{{editing()?'修改账号资料、门店归属和状态':'创建可用于排课的陪练账号'}}</span></div><button class="icon-action" type="button" aria-label="关闭" (click)="closeDrawer()"><lucide-icon [img]="icons.X" [size]="19"/></button></header>
  30. @if(error()){<p class="notice error" role="alert">{{error()}}</p>}
  31. <div class="form-grid coach-form">
  32. <label><span>登录账号 <em>*</em></span><input autocomplete="username" [ngModel]="username()" (ngModelChange)="username.set($event)" placeholder="至少 3 位,例如 coach_wang"/></label>
  33. <label><span>{{editing()?'重置密码':'初始密码'}} <em [class.optional]="editing()">{{editing()?'选填':'*'}}</em></span><input type="password" autocomplete="new-password" [ngModel]="password()" (ngModelChange)="password.set($event)" [placeholder]="editing()?'留空则不修改密码':'至少 8 位'"/></label>
  34. <label><span>老师姓名 <em>*</em></span><input [ngModel]="displayName()" (ngModelChange)="displayName.set($event)" placeholder="用于排课和工资单显示"/></label>
  35. <label><span>手机号码</span><input inputmode="numeric" maxlength="11" [ngModel]="mobile()" (ngModelChange)="mobile.set($event)" placeholder="11 位手机号码"/></label>
  36. <label><span>所属门店</span><select [ngModel]="parentUserId()" (ngModelChange)="parentUserId.set(+$event)"><option [ngValue]="0">暂不绑定门店</option>@for(store of page()?.stores||[];track store.userId){<option [ngValue]="store.userId">{{store.displayName}}</option>}</select></label>
  37. <label><span>账号状态</span><select [ngModel]="disabled()" (ngModelChange)="disabled.set($event)"><option [ngValue]="false">正常使用</option><option [ngValue]="true">停用账号</option></select></label>
  38. <label class="wide"><span>操作原因 <em>*</em></span><textarea rows="3" [ngModel]="reason()" (ngModelChange)="reason.set($event)" placeholder="用于安全审计,例如:新员工入职、资料更正或离职"></textarea></label>
  39. </div>
  40. @if(deleteArmed()){<p class="delete-warning">删除后老师将不能登录,历史排课和工资记录会保留。有未完成排课时系统会阻止删除。</p>}
  41. <footer>@if(editing()){<button class="ui-button danger push-left" type="button" (click)="remove()" [disabled]="saving()"><lucide-icon [img]="icons.Trash2" [size]="16"/>{{deleteArmed()?'确认删除老师':'删除老师'}}</button>}<button class="ui-button secondary" type="button" (click)="closeDrawer()">取消</button><button class="ui-button primary" type="button" (click)="save()" [disabled]="saving()">{{saving()?'保存中…':'保存资料'}}</button></footer>
  42. </aside>
  43. }