Просмотр исходного кода

feat(mobile): restore coach anti-forget workflow

彭峰 1 месяц назад
Родитель
Сommit
67e0d93c4f

+ 20 - 0
projects/xiaoshu-mobile/src/app/core/home-learning-modules.integration.spec.ts

@@ -15,6 +15,7 @@ class LearningModulesApiService {
   lastNodeParams: Record<string, unknown> = {};
   lastWordParams: Record<string, unknown> = {};
   lastLearningWordUpdate: Record<string, unknown> = {};
+  lastMemoryParams: Record<string, unknown> = {};
 
   get<T>(action: string, params: Record<string, unknown> = {}): Observable<ApiEnvelope<T>> {
     if (action === 'content_list' && Number(params['nid']) === 291) {
@@ -166,6 +167,7 @@ class LearningModulesApiService {
       });
     }
     if (action === 'e_get_21list') {
+      this.lastMemoryParams = params;
       return this.result<T>([
         { GeneralID: 100, Title: '张雅茜@历史任务', kc_title: '历史复习', kywsj: '2026-05-15 09:00', learned_at: '2026-05-01T01:15:00.000Z', fxzt: 1, yhid: 100, xxjlid: 900 },
         { GeneralID: 101, Title: '张雅茜@任务', kc_title: '初中英语复习', kywsj: '2026-08-01 09:00', learned_at: '2026-07-20T02:20:00.000Z', fxzt: 1, yhid: 100, xxjlid: 900 },
@@ -501,6 +503,24 @@ describe('home learning modules', () => {
     expect(coachLinks).toEqual(['/pages/home/shop', '/pages/member/ke_xiao', '/pages/his/his', '/pages/member/ke_fuxi']);
     expect(harness.routeNativeElement?.querySelector('.home-header .header-action')?.getAttribute('href')).toBe('/pages/home/ucenter');
     expect(harness.routeNativeElement?.querySelector('.content-section')).toBeNull();
+    const memoryApi = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
+    expect(memoryApi.lastMemoryParams).toEqual(jasmine.objectContaining({ uid: 300, shouru: 1 }));
+    expect(harness.routeNativeElement?.querySelector('.coach-memory-board')?.textContent).toContain('抗遗忘带练');
+    expect(harness.routeNativeElement?.querySelector('.coach-memory-board')?.textContent).toContain('张雅茜');
+    expect(harness.routeNativeElement?.querySelector('.coach-memory-list article.ready > button')?.textContent).toContain('带学生复习');
+    expect(harness.routeNativeElement?.querySelector('.coach-memory-footer')?.getAttribute('href')).toBe('/pages/stu/stu_calendar');
+    (harness.routeNativeElement?.querySelector('.coach-memory-list article.ready > button') as HTMLButtonElement).click();
+    await harness.fixture.whenStable();
+    expect(TestBed.inject(Router).url).toContain('/pages/stu/record?id=900&gid=102');
+    expect(session.student()?.userId).toBe(100);
+
+    await harness.navigateByUrl('/pages/stu/stu_calendar');
+    await harness.fixture.whenStable();
+    harness.fixture.detectChanges();
+    expect(memoryApi.lastMemoryParams).toEqual(jasmine.objectContaining({ uid: 300, shouru: 1, psize: 500 }));
+    expect(harness.routeNativeElement?.querySelector('.memory-copy')?.textContent).toContain('关联学员抗遗忘任务');
+    expect(harness.routeNativeElement?.querySelector('.memory-date-tasks')?.textContent).toContain('张雅茜');
+    expect(harness.routeNativeElement?.querySelector('.memory-date-tasks button')?.textContent).toContain('带学生复习');
 
     await harness.navigateByUrl('/pages/member/ke_xiao');
     await harness.fixture.whenStable();

+ 34 - 0
projects/xiaoshu-mobile/src/app/features/home/home-page.component.html

@@ -21,6 +21,40 @@
         <div class="coach-footer"><span>累计时长 {{ statsLoading() ? '—' : (stats()['t_shichang'] || 0) }}</span><span class="commission">佣金 ¥{{ commissionHidden() ? '***' : (stats()['t_yongji'] || 0) }}<button type="button" (click)="commissionHidden.set(!commissionHidden())" [attr.aria-label]="commissionHidden() ? '显示佣金' : '隐藏佣金'"><lucide-icon [img]="commissionHidden() ? icons.EyeOff : icons.Eye" [size]="16" /></button></span></div>
       }
     </section>
+
+    <section class="coach-memory-board" aria-labelledby="coach-memory-title">
+      <header>
+        <div class="coach-memory-heading">
+          <span class="coach-memory-icon"><lucide-icon [img]="icons.Clock3" [size]="22" /></span>
+          <div><span>关联学员复习情况</span><h2 id="coach-memory-title">抗遗忘带练</h2></div>
+        </div>
+        <button type="button" class="coach-memory-refresh" [disabled]="antiForgetLoading()" (click)="retryAntiForget()" aria-label="刷新抗遗忘任务"><lucide-icon [img]="icons.RefreshCw" [size]="18" [class.spin]="antiForgetLoading()" /></button>
+      </header>
+      @if (antiForgetLoading()) {
+        <div class="coach-memory-state" role="status"><span class="state-spinner dark"></span><span>正在同步学员复习任务</span></div>
+      } @else if (antiForgetError()) {
+        <button type="button" class="coach-memory-state error" (click)="retryAntiForget()"><lucide-icon [img]="icons.AlertCircle" [size]="21" /><span>{{ antiForgetError() }},点击重试</span></button>
+      } @else {
+        <div class="coach-memory-summary" aria-label="抗遗忘任务统计">
+          <span><strong>{{ coachReviewSummary().today }}</strong><small>今日任务</small></span>
+          <span><strong>{{ coachReviewSummary().due }}</strong><small>到期待复习</small></span>
+          <span><strong>{{ coachReviewSummary().completed }}</strong><small>已完成</small></span>
+        </div>
+        <div class="coach-memory-list">
+          @for (item of coachReviewItems().slice(0, 4); track $index) {
+            <article [class.ready]="coachReviewReady(item)" [class.completed]="coachReviewStatus(item) === '已完成'">
+              <div class="coach-student-avatar"><lucide-icon [img]="icons.UserRound" [size]="18" /></div>
+              <div class="coach-memory-copy"><strong>{{ coachReviewStudent(item) }}</strong><span>{{ coachReviewTitle(item) }} · {{ coachReviewDate(item) }}</span></div>
+              <em>{{ coachReviewStatus(item) }}</em>
+              <button type="button" (click)="openCoachReview(item)">{{ coachReviewAction(item) }}</button>
+            </article>
+          } @empty {
+            <div class="coach-memory-empty"><lucide-icon [img]="icons.Check" [size]="24" /><strong>暂无关联复习任务</strong><span>有学员进入复习周期后会显示在这里</span></div>
+          }
+        </div>
+      }
+      <a class="coach-memory-footer" routerLink="/pages/stu/stu_calendar"><span>查看全部学员复习计划</span><lucide-icon [img]="icons.ChevronRight" [size]="18" /></a>
+    </section>
   } @else {
     <section class="memory-hero" aria-labelledby="memory-title">
       <div class="memory-title-row">

+ 35 - 1
projects/xiaoshu-mobile/src/app/features/home/home-page.component.scss

@@ -68,6 +68,38 @@
 .coach-hero-error { display: flex; min-height: 142px; align-items: center; justify-content: center; padding: 18px; color: rgb(255 255 255 / 88%); text-align: center; flex-direction: column; gap: 9px; }
 .coach-hero-error button { min-height: 36px; padding: 0 15px; color: var(--brand-deep); background: white; border: 0; border-radius: 999px; font-size: 12px; font-weight: 750; }
 
+.coach-memory-board { overflow: hidden; margin-top: 14px; background: white; border: 1px solid var(--line); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); }
+.coach-memory-board > header { display: flex; min-height: 72px; align-items: center; justify-content: space-between; padding: 12px 16px; background: var(--brand-wash); border-bottom: 1px solid var(--line); gap: 12px; }
+.coach-memory-heading { display: flex; min-width: 0; align-items: center; gap: 11px; }
+.coach-memory-heading > div { display: flex; min-width: 0; flex-direction: column; gap: 2px; }
+.coach-memory-heading > div > span { color: var(--ink-soft); font-size: 10px; }
+.coach-memory-heading h2 { margin: 0; font-size: 19px; }
+.coach-memory-icon { display: grid; width: 42px; height: 42px; flex: 0 0 auto; color: white; background: var(--brand-strong); border-radius: 50%; place-items: center; }
+.coach-memory-refresh { display: grid; width: 36px; height: 36px; flex: 0 0 auto; padding: 0; color: var(--brand-strong); background: white; border: 1px solid var(--line); border-radius: 50%; place-items: center; }
+.coach-memory-refresh:disabled { opacity: .62; }
+.coach-memory-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); border-bottom: 1px solid var(--line); }
+.coach-memory-summary span { display: flex; min-height: 68px; align-items: center; justify-content: center; flex-direction: column; gap: 3px; }
+.coach-memory-summary span + span { border-left: 1px solid var(--line); }
+.coach-memory-summary strong { color: var(--brand-strong); font-size: 20px; }
+.coach-memory-summary small { color: var(--ink-soft); font-size: 10px; }
+.coach-memory-list article { display: grid; min-height: 72px; grid-template-columns: 36px minmax(0, 1fr) auto auto; align-items: center; padding: 10px 14px; border-bottom: 1px solid var(--line-soft); gap: 10px; }
+.coach-student-avatar { display: grid; width: 36px; height: 36px; color: var(--brand-strong); background: var(--brand-pale); border-radius: 50%; place-items: center; }
+.coach-memory-copy { display: flex; min-width: 0; flex-direction: column; gap: 3px; }
+.coach-memory-copy strong { overflow: hidden; font-size: 14px; text-overflow: ellipsis; white-space: nowrap; }
+.coach-memory-copy span { overflow: hidden; color: var(--ink-soft); font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
+.coach-memory-list article > em { padding: 4px 7px; color: var(--ink-soft); background: var(--surface-muted); border-radius: 999px; font-size: 10px; font-style: normal; white-space: nowrap; }
+.coach-memory-list article.ready > em { color: #9a6407; background: var(--accent-pale); }
+.coach-memory-list article.completed > em { color: var(--brand-strong); background: var(--brand-pale); }
+.coach-memory-list article > button { min-height: 34px; padding: 0 10px; color: var(--brand-strong); background: white; border: 1px solid var(--brand); border-radius: var(--radius-sm); font-size: 10px; font-weight: 700; white-space: nowrap; }
+.coach-memory-list article.ready > button { color: white; background: var(--brand-strong); border-color: var(--brand-strong); }
+.coach-memory-state { display: flex; width: 100%; min-height: 150px; align-items: center; justify-content: center; padding: 20px; color: var(--ink-soft); background: white; border: 0; text-align: center; flex-direction: column; gap: 9px; }
+.coach-memory-state.error { color: var(--danger); }
+.state-spinner.dark { border-color: var(--line); border-top-color: var(--brand-strong); }
+.coach-memory-empty { display: flex; min-height: 138px; align-items: center; justify-content: center; padding: 20px; color: var(--ink-soft); text-align: center; flex-direction: column; gap: 5px; }
+.coach-memory-empty strong { color: var(--ink); font-size: 13px; }
+.coach-memory-empty span { font-size: 10px; }
+.coach-memory-footer { display: flex; min-height: 48px; align-items: center; justify-content: space-between; padding: 0 16px; color: var(--brand-strong); background: var(--brand-wash); font-size: 12px; font-weight: 700; text-decoration: none; }
+
 .quick-grid { display: grid; overflow: hidden; grid-template-columns: repeat(2, minmax(0, 1fr)); margin-top: 14px; background: white; border: 1px solid var(--line); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); }
 .quick-grid a { display: grid; min-width: 0; min-height: 104px; grid-template-columns: 48px minmax(0, 1fr) 20px; align-items: center; padding: 14px 12px; color: var(--ink); text-decoration: none; gap: 10px; }
 .quick-grid a:nth-child(odd) { border-right: 1px solid var(--line); }
@@ -99,7 +131,7 @@
 
 @keyframes spin { to { transform: rotate(360deg); } }
 .spin { animation: spin .8s linear infinite; }
-@media (hover: hover) { .header-action:hover, .quick-grid a:hover { background: var(--surface-hover); } .continue-button:hover { background: var(--accent-strong); } }
+@media (hover: hover) { .header-action:hover, .quick-grid a:hover { background: var(--surface-hover); } .continue-button:hover { background: var(--accent-strong); } .coach-memory-list article > button:hover { filter: brightness(.97); } }
 @media (max-width: 430px) {
   .home-header { min-height: calc(70px + env(safe-area-inset-top)); }
   .brand { gap: 8px; }
@@ -130,6 +162,8 @@
   .quick-icon { width: 44px; height: 44px; }
   .quick-grid strong { font-size: 14px; }
   .quick-arrow { width: 16px; }
+  .coach-memory-list article { grid-template-columns: 34px minmax(0, 1fr) auto; padding-inline: 11px; gap: 8px; }
+  .coach-memory-list article > button { grid-column: 2 / -1; justify-self: stretch; }
 }
 @media (max-width: 350px) { .brand img { width: 116px; height: 32px; } .brand span { max-width: 86px; font-size: 11px; } .home-main { padding-inline: 10px; } .memory-content { grid-template-columns: 124px minmax(0, 1fr); padding-inline: 10px; } .day-ring { transform: scale(.82); } .today-panel { padding-left: 7px; } .task-row { font-size: 11px; } .quick-arrow { display: none; } .quick-grid a { grid-template-columns: 42px minmax(0, 1fr); } }
 @media (min-width: 620px) { .home-main { padding-inline: 22px; } .memory-content { grid-template-columns: 280px minmax(0, 1fr); } .day-ring { width: 190px; height: 190px; } .memory-dot { transform: rotate(var(--angle)) translateY(-84px); } .today-panel { padding: 8px 30px 0; } .product-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }

+ 72 - 3
projects/xiaoshu-mobile/src/app/features/home/home-page.component.ts

@@ -3,7 +3,7 @@ import { ChangeDetectionStrategy, Component, computed, inject, OnInit, signal }
 import { Router, RouterLink } from '@angular/router';
 import { AlertCircle, BarChart3, BookOpen, CalendarDays, Check, ChevronRight, Clock3, Eye, EyeOff, GraduationCap, Headphones, Info, LucideAngularModule, RefreshCw, Search, Sparkles, UserRound, Zap } from 'lucide-angular';
 import { finalize } from 'rxjs';
-import { buildAntiForgetView, findAntiForgetList } from '../../core/anti-forget';
+import { antiForgetCompleted, antiForgetDate, buildAntiForgetView, findAntiForgetList } from '../../core/anti-forget';
 import { ApiError, ApiService } from '../../core/api.service';
 import { PlatformService } from '../../core/platform.service';
 import { SessionService } from '../../core/session.service';
@@ -31,6 +31,22 @@ export class HomePageComponent implements OnInit {
   readonly antiForgetError = signal('');
   readonly antiForgetView = computed(() => buildAntiForgetView(this.antiForgetItems()));
   readonly isCoach = computed(() => Number(this.session.groupId()) === 3);
+  readonly coachReviewItems = computed(() => [...this.antiForgetItems()].sort((left, right) => {
+    const stateOrder = (item: Item) => antiForgetCompleted(item) ? 2 : this.coachReviewReady(item) ? 0 : 1;
+    const stateDifference = stateOrder(left) - stateOrder(right);
+    if (stateDifference) return stateDifference;
+    const dateDifference = antiForgetDate(left).localeCompare(antiForgetDate(right));
+    return stateOrder(left) === 1 ? dateDifference : -dateDifference;
+  }));
+  readonly coachReviewSummary = computed(() => {
+    const today = this.today();
+    const items = this.antiForgetItems().filter((item) => antiForgetDate(item));
+    return {
+      today: items.filter((item) => antiForgetDate(item) === today).length,
+      due: items.filter((item) => !antiForgetCompleted(item) && antiForgetDate(item) <= today).length,
+      completed: items.filter(antiForgetCompleted).length,
+    };
+  });
   readonly icons = { AlertCircle, BarChart3, BookOpen, CalendarDays, Check, ChevronRight, Clock3, Eye, EyeOff, GraduationCap, Headphones, Info, RefreshCw, Search, Sparkles, UserRound, Zap };
 
   private readonly api = inject(ApiService);
@@ -41,7 +57,7 @@ export class HomePageComponent implements OnInit {
     const uid = this.session.user()?.userId;
     if (uid) {
       this.loadStats(uid);
-      if (!this.isCoach()) this.loadAntiForget(uid);
+      this.loadAntiForget(uid);
     }
     if (this.isCoach()) return;
     this.loadRecommendations();
@@ -74,6 +90,53 @@ export class HomePageComponent implements OnInit {
     if (uid) this.loadAntiForget(uid);
   }
 
+  coachReviewStudent(item: Item): string {
+    return String(item['honeyName'] ?? item['HoneyName'] ?? item['Title'] ?? '关联学员').split('@')[0] || '关联学员';
+  }
+
+  coachReviewTitle(item: Item): string {
+    return String(item['kc_title'] ?? item['kctitle'] ?? item['kcmc'] ?? '单词复习').split('@')[0];
+  }
+
+  coachReviewDate(item: Item): string {
+    return antiForgetDate(item) || '日期待确认';
+  }
+
+  coachReviewReady(item: Item): boolean {
+    const date = antiForgetDate(item);
+    return Boolean(date && date <= this.today() && !antiForgetCompleted(item));
+  }
+
+  coachReviewStatus(item: Item): string {
+    if (antiForgetCompleted(item)) return '已完成';
+    return this.coachReviewReady(item) ? '待复习' : '未到期';
+  }
+
+  coachReviewAction(item: Item): string {
+    if (antiForgetCompleted(item)) return '查看';
+    return this.coachReviewReady(item) ? '带学生复习' : '查看计划';
+  }
+
+  openCoachReview(item: Item): void {
+    if (!this.coachReviewReady(item) && !antiForgetCompleted(item)) {
+      void this.router.navigateByUrl('/pages/stu/stu_calendar');
+      return;
+    }
+    const studentId = Number(item['yhid'] ?? 0);
+    if (!studentId) {
+      this.platform.toast('复习任务缺少关联学员', 'error');
+      return;
+    }
+    this.session.setStudent({
+      userId: studentId,
+      honeyName: this.coachReviewStudent(item),
+      groupId: 1,
+    });
+    void this.router.navigate(['/pages/stu/record'], {
+      queryParams: { id: item['xxjlid'], gid: item['GeneralID'], kcid: item['kcid'] },
+    });
+  }
+
   private loadStats(uid: string | number): void {
     this.statsLoading.set(true);
     this.coachStatsError.set('');
@@ -90,7 +153,7 @@ export class HomePageComponent implements OnInit {
   private loadAntiForget(uid: string | number): void {
     this.antiForgetLoading.set(true);
     this.antiForgetError.set('');
-    this.api.post<unknown>('e_get_21list', { uid, cpage: 1, psize: 100 }).pipe(
+    this.api.post<unknown>('e_get_21list', { uid, shouru: this.isCoach() ? 1 : undefined, cpage: 1, psize: this.isCoach() ? 500 : 100 }).pipe(
       finalize(() => this.antiForgetLoading.set(false)),
     ).subscribe({
       next: ({ result }) => this.antiForgetItems.set(findAntiForgetList(result)),
@@ -98,6 +161,12 @@ export class HomePageComponent implements OnInit {
     });
   }
 
+  private today(): string {
+    const parts = new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit' }).formatToParts(new Date());
+    const value = (type: Intl.DateTimeFormatPartTypes) => parts.find((part) => part.type === type)?.value ?? '';
+    return `${value('year')}-${value('month')}-${value('day')}`;
+  }
+
   private toList(value: unknown): Item[] {
     if (Array.isArray(value)) return value as Item[];
     if (value && typeof value === 'object') {

+ 5 - 5
projects/xiaoshu-mobile/src/app/features/learning/learning-page.component.html

@@ -303,9 +303,9 @@
       <lucide-icon class="memory-watermark" [img]="icons.BrainCircuit" [size]="156" aria-hidden="true" />
       <header>
         <div class="memory-copy">
-          <span class="memory-kicker"><lucide-icon [img]="icons.Activity" [size]="14" />21 天智能复习引擎</span>
-          <strong>记忆巩固进行中</strong>
-          <small>在遗忘临界点及时复习,让记忆一次次回升</small>
+          <span class="memory-kicker"><lucide-icon [img]="icons.Activity" [size]="14" />{{ canStartPointReading ? '关联学员抗遗忘任务' : '21 天智能复习引擎' }}</span>
+          <strong>{{ canStartPointReading ? '带学生及时复习' : '记忆巩固进行中' }}</strong>
+          <small>{{ canStartPointReading ? '查看学生姓名、复习日期和完成情况,并从任务中直接开始带练' : '在遗忘临界点及时复习,让记忆一次次回升' }}</small>
         </div>
         <div class="memory-score" role="progressbar" aria-label="总体完成度" aria-valuemin="0" aria-valuemax="100" [attr.aria-valuenow]="memoryProgress()"><b>{{ memoryProgress() }}%</b><small>总体完成</small></div>
       </header>
@@ -376,10 +376,10 @@
           <article [class.ready]="calendarReady(item)" [class.completed]="calendarCompleted(item)">
             <span class="task-state"><i></i>{{ memoryTaskStatus(item) }}</span>
             <div class="task-copy"><span class="task-heading"><em class="review-sequence">第 {{ calendarReviewNumber(item) }} 次复习</em><strong>{{ calendarTitle(item) }}</strong></span><small class="task-context">{{ calendarStudent(item) }}<em>复习日期 {{ calendarDate(item) }}</em></small><small class="first-learned-at">{{ calendarFirstLearnedAt(item) }}</small></div>
-            <button type="button" [disabled]="!calendarReady(item) && !calendarCompleted(item)" (click)="openCalendar(item)">{{ calendarCompleted(item) ? '查看' : calendarReady(item) ? '开始复习' : '未到期' }}</button>
+            <button type="button" [disabled]="!calendarReady(item) && !calendarCompleted(item)" (click)="openCalendar(item)">{{ calendarCompleted(item) ? '查看' : calendarReady(item) ? (canStartPointReading ? '带学生复习' : '开始复习') : '未到期' }}</button>
           </article>
         } @empty {
-          <div class="memory-empty"><lucide-icon [img]="icons.Clock3" [size]="26" /><strong>当天没有复习任务</strong><span>可以选择有颜色标记的日期查看计划</span></div>
+          <div class="memory-empty"><lucide-icon [img]="icons.Clock3" [size]="26" /><strong>当天没有复习任务</strong><span>{{ canStartPointReading ? '选择有颜色标记的日期查看关联学员任务' : '可以选择有颜色标记的日期查看计划' }}</span></div>
         }
       </div>
     </section>

+ 14 - 10
projects/xiaoshu-mobile/src/app/features/learning/learning-page.component.ts

@@ -926,15 +926,18 @@ export class LearningPageComponent implements OnInit, OnDestroy {
       this.platform.toast('还未到复习时间');
       return;
     }
-    this.api.post<AppUser>('user_get', { uid: item['yhid'] }).subscribe({
-      next: ({ result }) => {
-        const student = (Array.isArray(result) ? result[0] : result) as AppUser;
-        this.session.setStudent(student);
-        void this.router.navigate(['/pages/stu/record'], {
-          queryParams: { id: item['xxjlid'], gid: item['GeneralID'], kcid: item['kcid'] },
-        });
-      },
-      error: (error: ApiError) => this.platform.toast(error.message, 'error'),
+    const studentId = Number(item['yhid'] ?? 0);
+    if (!studentId) {
+      this.platform.toast('复习任务缺少关联学员', 'error');
+      return;
+    }
+    this.session.setStudent({
+      userId: studentId,
+      honeyName: this.calendarStudent(item) || '关联学员',
+      groupId: 1,
+    });
+    void this.router.navigate(['/pages/stu/record'], {
+      queryParams: { id: item['xxjlid'], gid: item['GeneralID'], kcid: item['kcid'] },
     });
   }
 
@@ -1446,7 +1449,8 @@ export class LearningPageComponent implements OnInit, OnDestroy {
 
   private loadCalendar(): void {
     const uid = this.session.user()?.userId;
-    this.api.post<unknown>('e_get_21list', { uid, cpage: 1, psize: 500 })
+    const coachMode = Number(this.session.groupId()) === 3;
+    this.api.post<unknown>('e_get_21list', { uid, shouru: coachMode ? 1 : undefined, cpage: 1, psize: 500 })
       .pipe(finalize(() => this.loading.set(false)))
       .subscribe({
       next: ({ result }) => {