operations-schedule.component.html 7.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
  1. <header class="page-header">
  2. <div><p>SCHEDULING CENTER</p><h1>排课中心</h1><span>按陪练或时间快速排课,并在提交前发现冲突</span></div>
  3. <div class="header-actions">
  4. <button type="button" (click)="load()" [disabled]="loading()"><lucide-icon [img]="icons.RefreshCw" [size]="17" [class.spin]="loading()" />刷新</button>
  5. <button class="primary" type="button" (click)="openCreate()"><lucide-icon [img]="icons.Plus" [size]="17" />新增排课</button>
  6. </div>
  7. </header>
  8. <section class="toolbar">
  9. <div class="segmented">
  10. <button type="button" [class.active]="view()==='day'" (click)="view.set('day');load()">日</button>
  11. <button type="button" [class.active]="view()==='week'" (click)="view.set('week');load()">周</button>
  12. <button type="button" [class.active]="view()==='list'" (click)="view.set('list');load()">列表</button>
  13. </div>
  14. <button type="button" aria-label="上一时间段" (click)="move(view()==='week'?-7:-1)"><lucide-icon [img]="icons.ChevronLeft" [size]="17" /></button>
  15. <label><span>日期</span><input type="date" [ngModel]="date()" (ngModelChange)="date.set($event);load()" /></label>
  16. <button type="button" aria-label="下一时间段" (click)="move(view()==='week'?7:1)"><lucide-icon [img]="icons.ChevronRight" [size]="17" /></button>
  17. <label class="grow"><span>陪练筛选</span><select [ngModel]="coachFilter()" (ngModelChange)="coachFilter.set(+$event);load()"><option [ngValue]="0">全部陪练</option>@for(coach of data()?.coaches||[];track coach.userId){<option [ngValue]="coach.userId">{{coach.displayName}}</option>}</select></label>
  18. </section>
  19. @if(message()){<p class="notice" role="status">{{message()}}</p>}
  20. @if(error()){<p class="notice error" role="alert">{{error()}}</p>}
  21. <section class="summary-grid">
  22. <article class="summary-card"><span>当前范围课程</span><strong>{{activeAppointments()}}</strong><small>{{range().from}} 至 {{range().to}}</small></article>
  23. <article class="summary-card"><span>待分配</span><strong>{{unassignedCount()}}</strong><small>未分配老师时不能开始</small></article>
  24. <article class="summary-card"><span>在岗陪练</span><strong>{{data()?.coaches?.length||0}}</strong><small>未配置可用时间会提示</small></article>
  25. <article class="summary-card"><span>最后刷新</span><strong>{{data()?.refreshedAt|date:'HH:mm'}}</strong><small>服务端实时数据</small></article>
  26. </section>
  27. <section class="content-card">
  28. <header><strong>{{view()==='list'?'预约明细':'陪练排课泳道'}}</strong><span>{{loading()?'加载中…':'点击课程可改期或换老师'}}</span></header>
  29. @if(view()==='list'){
  30. <div class="table-scroll"><table class="data-table"><thead><tr><th>时间</th><th>会员</th><th>陪练</th><th>课程</th><th>课型</th><th>状态</th><th>操作</th></tr></thead><tbody>
  31. @for(item of data()?.appointments||[];track item.targetId){<tr><td>{{item.startsAt|date:'MM-dd HH:mm'}}–{{item.endsAt|date:'HH:mm'}}</td><td>{{item.studentName}}</td><td>{{item.coachName}}</td><td>{{item.courseName}}</td><td>{{item.classTypeLabel}}</td><td><span class="badge" [class.muted]="item.cancelled">{{item.statusLabel}}</span></td><td><div class="row-actions"><button type="button" (click)="openEdit(item)" [disabled]="item.statusCode>0||item.cancelled">修改</button>@if(item.cancelled){<button type="button" (click)="action(item,'ops/appointments/restore')">恢复</button>}@else{<button type="button" (click)="action(item,'ops/appointments/cancel')">取消</button><button type="button" (click)="action(item,'ops/appointments/complete')">完成</button>}</div></td></tr>}
  32. @empty{<tr><td colspan="7"><div class="empty">当前范围暂无排课</div></td></tr>}
  33. </tbody></table></div>
  34. } @else {
  35. <div class="table-scroll"><div class="calendar">
  36. @for(coach of data()?.coaches||[];track coach.userId){<div class="calendar-row"><div class="coach-label"><strong>{{coach.displayName}}</strong><span>今日 {{coach.todayAppointments}} 节</span>@if(!coach.availabilityConfigured){<span class="exception">未设置可用时间</span>}</div><div class="appointment-strip">@for(item of data()?.appointments||[];track item.targetId){@if(item.coachId===coach.userId){<button type="button" class="appointment" [class.cancelled]="item.cancelled" (click)="openEdit(item)"><strong>{{item.startsAt|date:'MM-dd HH:mm'}} {{item.studentName}}</strong><span>{{item.courseName}}</span><small>{{item.classTypeLabel}} · {{item.statusLabel}}</small></button>}}</div></div>}
  37. @empty{<div class="empty">暂无陪练资料</div>}
  38. </div></div>
  39. }
  40. </section>
  41. @if(drawerOpen()){
  42. <div class="backdrop" (click)="drawerOpen.set(false)"></div>
  43. <aside class="drawer" role="dialog" aria-modal="true" aria-label="排课编辑">
  44. <header><div><p>SCHEDULE FORM</p><h2>{{editing()?'修改排课':'新增排课'}}</h2></div><button type="button" aria-label="关闭" (click)="drawerOpen.set(false)">×</button></header>
  45. <div class="segmented"><button type="button" [class.active]="mode()==='coach'" (click)="mode.set('coach')">陪练优先</button><button type="button" [class.active]="mode()==='time'" (click)="mode.set('time')">时间优先</button></div>
  46. <div class="form-grid">
  47. <label>会员<select [ngModel]="studentId()" (ngModelChange)="studentId.set(+$event)"><option [ngValue]="0">请选择会员</option>@for(member of data()?.members||[];track member.objectId){<option [ngValue]="member.userId">{{member.displayName}} · {{member.mobileMasked}} · {{member.learningStatusLabel}}</option>}</select></label>
  48. <label>课程<select [ngModel]="courseId()" (ngModelChange)="courseId.set(+$event)"><option [ngValue]="0">请选择课程</option>@for(course of data()?.courses||[];track course.courseId){<option [ngValue]="course.courseId">{{course.courseName}}</option>}</select></label>
  49. <label>课程类型<select [ngModel]="classType()" (ngModelChange)="classType.set(+$event)"><option [ngValue]="1">30分钟</option><option [ngValue]="2">60分钟</option><option [ngValue]="3">体验课</option><option [ngValue]="4">陪练</option></select></label>
  50. <label>交付方式<select [ngModel]="deliveryMethod()" (ngModelChange)="deliveryMethod.set($event)"><option>线上</option><option>线下</option></select></label>
  51. <label>日期<input type="date" [ngModel]="date()" (ngModelChange)="date.set($event)" /></label>
  52. <label>开始时间<input type="time" step="1800" [ngModel]="startTime()" (ngModelChange)="startTime.set($event)" /></label>
  53. <label>陪练<select [ngModel]="coachId()" (ngModelChange)="coachId.set(+$event)"><option [ngValue]="0">暂不分配</option>@for(coach of data()?.coaches||[];track coach.userId){<option [ngValue]="coach.userId">{{coach.displayName}}</option>}</select></label>
  54. @if(!editing()){
  55. <label>重复方式<select [ngModel]="recurrence()" (ngModelChange)="recurrence.set($event)"><option value="once">单次</option><option value="daily">每天</option><option value="alternate">隔天</option><option value="weekly">每周</option><option value="same_day_30">同日间隔30分钟</option><option value="same_day_60">同日间隔60分钟</option></select></label>
  56. <label>排课次数<input type="number" min="1" max="60" [ngModel]="occurrences()" (ngModelChange)="occurrences.set(+$event)" /></label>
  57. }
  58. <label class="wide">操作原因<textarea rows="2" [ngModel]="reason()" (ngModelChange)="reason.set($event)"></textarea></label>
  59. </div>
  60. @if(preview();as result){<p class="notice" [class.error]="previewConflictCount()>0">预检完成:可排 {{result['availableCount']||0}} 节,冲突 {{result['conflictCount']||0}} 节。{{result['warnings']||''}}</p>}
  61. <footer><button type="button" (click)="preflight()">检查冲突</button><button class="primary" type="button" (click)="save()" [disabled]="saving()">{{saving()?'保存中…':'确认保存'}}</button></footer>
  62. </aside>
  63. }