Эх сурвалжийг харах

fix: show classroom feedback for writing learning reports

彭峰 12 цаг өмнө
parent
commit
a053da663a

+ 11 - 3
projects/xiaoshu-admin/src/app/admin.models.ts

@@ -347,7 +347,7 @@ export interface OperationsUserDetail extends OperationsUser {
   agents: MemberAgentOption[];
 }
 
-export type LearningReportStatus = 'available' | 'incomplete' | 'missing' | 'historical';
+export type LearningReportStatus = 'available' | 'incomplete' | 'missing' | 'historical' | 'writing';
 
 export interface LearningReportSummary extends Record<string, unknown> {
   reportCount: number;
@@ -355,7 +355,7 @@ export interface LearningReportSummary extends Record<string, unknown> {
   learnedCount: number;
   masteredCount: number;
   reviewDueCount: number;
-  masteryRate: number;
+  masteryRate: number | null;
 }
 
 export interface LearningReportSourceStatus extends Record<string, unknown> {
@@ -386,7 +386,7 @@ export interface LearningReportListItem extends Record<string, unknown> {
   masteredCount: number;
   reviewDueCount: number;
   unmarkedCount: number;
-  masteryRate: number;
+  masteryRate: number | null;
   status: LearningReportStatus;
   statusLabel: string;
   canExport: boolean;
@@ -413,6 +413,14 @@ export interface LearningReportDetail extends LearningReportListItem {
   generatedAt: string;
   sourceStatus: LearningReportSourceStatus;
   countMismatch: boolean;
+  writingFeedback?: {
+    contentSummary: string;
+    reviewSummary: string;
+    trainingSummary: string;
+    homework: string;
+    coachComment: string;
+    notes: string;
+  } | null;
 }
 
 export interface LearningReportPage extends OperationsPage<LearningReportListItem> {

+ 30 - 9
projects/xiaoshu-admin/src/app/pages/operations-learning-reports.component.html

@@ -1,4 +1,4 @@
-<header class="page-header report-header"><div><p>LEARNING REPORTS</p><h1>单词学习报表</h1><span>集中查看学生每次学习结果,生成可下载的专业 PDF 报表</span></div><button class="ui-button refresh-action" type="button" (click)="load(true)" [disabled]="refreshing()"><lucide-icon [img]="icons.RefreshCw" [size]="17" [class.spin]="refreshing()" />{{ refreshing() ? '正在刷新' : '刷新最新数据' }}</button></header>
+<header class="page-header report-header"><div><p>LEARNING REPORTS</p><h1>学习报表</h1><span>查看课堂学习结果与反馈,下载单词学习报表</span></div><button class="ui-button refresh-action" type="button" (click)="load(true)" [disabled]="refreshing()"><lucide-icon [img]="icons.RefreshCw" [size]="17" [class.spin]="refreshing()" />{{ refreshing() ? '正在刷新' : '刷新最新数据' }}</button></header>
 
 @if (message()) { <p class="notice" role="status">{{ message() }}</p> }
 @if (error()) { <p class="notice error" role="alert">{{ error() }}</p> }
@@ -18,7 +18,7 @@
   <label class="search-field"><span>学员</span><input [ngModel]="search()" (ngModelChange)="search.set($event)" placeholder="姓名、手机号或ID" (keyup.enter)="applyFilters()" /></label>
   <label><span>陪练</span><input [ngModel]="coachSearch()" (ngModelChange)="coachSearch.set($event)" placeholder="姓名或ID" /></label>
   <label><span>课程</span><input [ngModel]="courseSearch()" (ngModelChange)="courseSearch.set($event)" placeholder="课程名称" /></label>
-  <label><span>报表状态</span><select [ngModel]="status()" (ngModelChange)="status.set($event)"><option value="">全部状态</option><option value="available">可生成</option><option value="historical">历史记录</option><option value="incomplete">学习未完成</option><option value="missing">明细缺失</option></select></label>
+  <label><span>报表状态</span><select [ngModel]="status()" (ngModelChange)="status.set($event)"><option value="">全部状态</option><option value="available">可生成</option><option value="historical">历史记录</option><option value="incomplete">学习未完成</option><option value="missing">明细缺失</option><option value="writing">写作课</option></select></label>
   <div class="filter-actions">
     <button class="ui-button secondary reset-action" type="button" (click)="resetFilters()" [disabled]="searching()"><lucide-icon [img]="icons.RotateCcw" [size]="17" />重置</button>
     <button class="ui-button primary query-action" type="button" (click)="applyFilters()" [disabled]="searching()"><lucide-icon [img]="searching() ? icons.LoaderCircle : icons.Search" [size]="17" [class.spin]="searching()" />{{ searching() ? '正在查找' : '查找' }}</button>
@@ -34,15 +34,21 @@
     <article><span>学习单词</span><strong>{{ pageData.summary.learnedCount }}</strong><small>全部学习词次</small></article>
     <article><span>已掌握</span><strong>{{ pageData.summary.masteredCount }}</strong><small>学习结果为已掌握</small></article>
     <article><span>待复习</span><strong>{{ pageData.summary.reviewDueCount }}</strong><small>进入复习计划</small></article>
-    <article><span>平均掌握率</span><strong>{{ pageData.summary.masteryRate | number:'1.0-1' }}%</strong><small>已掌握 / 学习单词</small></article>
+    <article><span>平均掌握率</span><strong>@if (pageData.summary.masteryRate === null) { 不适用 } @else { {{ pageData.summary.masteryRate | number:'1.0-1' }}% }</strong><small>已掌握 / 学习单词</small></article>
   </section>
 
   <section class="content-card report-list-card">
-    <header><div><strong>学习报表明细</strong><span>共 {{ pageData.total }} 份,点击查看完整单词与复习计划</span></div></header>
+    <header><div><strong>学习报表明细</strong><span>共 {{ pageData.total }} 份,点击查看报表详情</span></div></header>
     <div class="table-scroll"><table class="data-table report-table"><thead><tr><th>学习日期</th><th>学员</th><th>陪练老师</th><th>课程</th><th>学习结果</th><th>掌握率</th><th>状态</th><th>操作</th></tr></thead><tbody>
       @for (row of pageData.items; track trackReport($index, row)) {
-        <tr><td><strong>{{ row.studyDate || '日期缺失' }}</strong><small>@if(row.appointmentAt){ {{ row.appointmentAt | date:'HH:mm' }} } @else {记录 {{ row.generalId }}}</small></td><td><strong>{{ row.studentName }}</strong><small>ID {{ row.studentId }} · {{ row.studentMobileMasked || '无手机号' }}</small></td><td>{{ row.coachName }}<small>@if(row.coachId){ ID {{ row.coachId }} }</small></td><td>{{ row.courseName }}<small>{{ row.classTypeLabel }}</small></td><td><strong>{{ row.learnedCount }} 个</strong><small>掌握 {{ row.masteredCount }} · 复习 {{ row.reviewDueCount }} · 未标记 {{ row.unmarkedCount }}</small></td><td><div class="rate"><span [style.width.%]="row.masteryRate"></span></div><strong>{{ row.masteryRate | number:'1.0-1' }}%</strong></td><td><span class="report-status" [attr.data-status]="row.status">{{ row.statusLabel }}</span></td><td><div class="row-actions"><button type="button" title="查看报表" aria-label="查看报表" (click)="open(row.targetId)"><lucide-icon [img]="icons.Eye" [size]="16" /></button></div></td></tr>
-      } @empty { <tr><td colspan="8"><div class="empty">{{month()?monthLabel()+'暂无单词学习报表,可选择“全部月份”查看历史记录':'当前筛选范围暂无单词学习报表'}}</div></td></tr> }
+        <tr><td><strong>{{ row.studyDate || '日期缺失' }}</strong><small>@if(row.appointmentAt){ {{ row.appointmentAt | date:'HH:mm' }} } @else {记录 {{ row.generalId }}}</small></td><td><strong>{{ row.studentName }}</strong><small>ID {{ row.studentId }} · {{ row.studentMobileMasked || '无手机号' }}</small></td><td>{{ row.coachName }}<small>@if(row.coachId){ ID {{ row.coachId }} }</small></td><td>{{ row.courseName }}<small>{{ row.classTypeLabel }}</small></td><td>
+          @if (row.status === 'writing') { 课堂反馈 }
+          @else { <strong>{{ row.learnedCount }} 个</strong><small>掌握 {{ row.masteredCount }} · 复习 {{ row.reviewDueCount }} · 未标记 {{ row.unmarkedCount }}</small> }
+        </td><td>
+          @if (row.masteryRate === null) { 不适用 }
+          @else { <div class="rate"><span [style.width.%]="row.masteryRate"></span></div><strong>{{ row.masteryRate | number:'1.0-1' }}%</strong> }
+        </td><td><span class="report-status" [attr.data-status]="row.status">{{ row.statusLabel }}</span></td><td><div class="row-actions"><button type="button" title="查看报表" aria-label="查看报表" (click)="open(row.targetId)"><lucide-icon [img]="icons.Eye" [size]="16" /></button></div></td></tr>
+      } @empty { <tr><td colspan="8"><div class="empty">{{month()?monthLabel()+'暂无学习报表,可选择“全部月份”查看历史记录':'当前筛选范围暂无学习报表'}}</div></td></tr> }
     </tbody></table></div>
     <footer class="pagination"><span>第 {{ pageData.page }} / {{ totalPages() }} 页</span><div><button type="button" aria-label="上一页" [disabled]="pageData.page<=1" (click)="changePage(pageData.page-1)"><lucide-icon [img]="icons.ChevronLeft" [size]="17" /></button><button type="button" aria-label="下一页" [disabled]="pageData.page>=totalPages()" (click)="changePage(pageData.page+1)"><lucide-icon [img]="icons.ChevronRight" [size]="17" /></button></div></footer>
   </section>
@@ -50,18 +56,30 @@
 
 @if (detailLoading() && !detail()) { <div class="detail-loading"><lucide-icon class="spin" [img]="icons.LoaderCircle" [size]="28" />正在整理学习报表…</div> }
 @if (detail(); as report) {
-  <div class="backdrop" (click)="close()"></div><aside class="drawer report-drawer" role="dialog" aria-modal="true" aria-label="单词学习报表预览"><header><div><p>REPORT PREVIEW</p><h2>{{ report.studentName }} · {{ report.studyDate }}</h2><span>学习记录 {{ report.generalId }}</span></div><button class="icon-action" type="button" aria-label="关闭" [disabled]="!!exportingId()" (click)="close()"><lucide-icon [img]="icons.X" [size]="20" /></button></header>
+  <div class="backdrop" (click)="close()"></div><aside class="drawer report-drawer" role="dialog" aria-modal="true" [attr.aria-label]="report.status === 'writing' ? '语法写作学习报表' : '单词学习报表预览'"><header><div><p>REPORT PREVIEW</p><h2>{{ report.studentName }} · {{ report.studyDate }}</h2><span>学习记录 {{ report.generalId }}</span></div><button class="icon-action" type="button" aria-label="关闭" [disabled]="!!exportingId()" (click)="close()"><lucide-icon [img]="icons.X" [size]="20" /></button></header>
   @if (report.countMismatch) { <p class="notice error">历史汇总数量与单词明细不一致,报表已按实际单词明细重新计算。</p> }
+  @if (report.status !== 'writing') {
   <div class="report-variants" role="group" aria-label="报表类型">
     <button type="button" [class.selected]="reportVariant()==='full'" [attr.aria-pressed]="reportVariant()==='full'" [disabled]="!!exportingId()" (click)="selectVariant('full')">完整学习报表</button>
     <button type="button" [class.selected]="reportVariant()==='english'" [attr.aria-pressed]="reportVariant()==='english'" [disabled]="!!exportingId()" (click)="selectVariant('english')">英文填中文</button>
     <button type="button" [class.selected]="reportVariant()==='chinese'" [attr.aria-pressed]="reportVariant()==='chinese'" [disabled]="!!exportingId()" (click)="selectVariant('chinese')">中文填英文</button>
   </div>
   @if(reportVariant()!=='full'){<p class="worksheet-hint">每次打开复习版都会打乱顺序,下载 PDF 与当前预览一致。</p>}
+  }
   @if (error()) { <p class="notice error" role="alert">{{ error() }}</p> }
   <div class="preview-scroll"><article #pdfContent class="pdf-sheet" [class.worksheet-sheet]="reportVariant()!=='full'">
-    <section class="pdf-title"><div class="brand-mark"><lucide-icon [img]="icons.FileChartColumn" [size]="28" /></div><div><p>小树陪练</p><h1>{{ reportTitle() }}</h1></div><span>NO. {{ report.generalId }}</span></section>
+    <section class="pdf-title"><div class="brand-mark"><lucide-icon [img]="icons.FileChartColumn" [size]="28" /></div><div><p>小树陪练</p><h1>{{ report.status === 'writing' ? '语法写作学习报表' : reportTitle() }}</h1></div><span>NO. {{ report.generalId }}</span></section>
     <dl class="report-meta"><div><dt>学员</dt><dd>{{ report.studentName }}</dd></div><div><dt>陪练老师</dt><dd>{{ report.coachName }}</dd></div><div><dt>课程</dt><dd>{{ report.courseName }}</dd></div><div><dt>学习日期</dt><dd>{{ report.studyDate }} @if(report.appointmentAt){ {{ report.appointmentAt | date:'HH:mm' }} }</dd></div><div><dt>课型</dt><dd>{{ report.classTypeLabel }}</dd></div><div><dt>预约编号</dt><dd>{{ report.appointmentId || '历史记录' }}</dd></div></dl>
+    @if (report.status === 'writing') {
+      <section class="pdf-section">
+        <h2>课堂反馈</h2>
+        <div class="comments">
+          @for (section of writingSections(); track section.label) {
+            <article class="comment-card"><strong>{{ section.label }}</strong><p>{{ section.value }}</p></article>
+          } @empty { <p>暂无课堂反馈</p> }
+        </div>
+      </section>
+    } @else {
     @if(reportVariant()==='full'){<section class="report-summary"><article><strong>{{ report.learnedCount }}</strong><span>学习单词</span></article><article><strong>{{ report.masteredCount }}</strong><span>已掌握</span></article><article><strong>{{ report.reviewDueCount }}</strong><span>待复习</span></article><article><strong>{{ report.unmarkedCount }}</strong><span>未标记</span></article><article><strong>{{ report.masteryRate | number:'1.0-1' }}%</strong><span>掌握率</span></article></section>}
     @if (reportVariant()==='full') {
     <section class="pdf-section"><h2>单词学习明细</h2><table class="word-table"><thead><tr><th>#</th><th>单词 / 音标</th><th>中文释义</th><th>学习结果</th></tr></thead><tbody>@for (word of report.words; track word.index) { <tr class="word-row" [class.word-row-with-example]="word.example"><td>{{ word.index }}</td><td><div class="word-heading"><strong>{{ word.word }}</strong><small>{{ word.phonetic || '—' }}</small></div>@if(word.example){<em>{{ word.example }}</em>}</td><td>{{ word.meaning || '暂无释义' }}</td><td><span [attr.data-result]="word.result">{{ word.resultLabel }}</span></td></tr> } @empty { <tr><td colspan="4">没有可展示的单词明细</td></tr> }</tbody></table></section>
@@ -83,7 +101,10 @@
       <p class="worksheet-hint">每次复习后填写遗忘的单词数量,未复习的格子留空。@if(hasInferredReviewDates()){ 未设置的复习日期按学习日期推算;日期缺失时请手写补充。 }</p>
     </section>
     @if (reportVariant()==='full' && (report.score || report.studentComment || report.coachComment)) { <section class="pdf-section"><h2>课程评价</h2><div class="comments"><article class="comment-card"><strong>学员评分</strong><p>{{ report.score ? report.score + ' 分' : '未评分' }}</p></article><article class="comment-card"><strong>学员评价</strong><p>{{ report.studentComment || '暂无评价' }}</p></article><article class="comment-card"><strong>陪练点评</strong><p>{{ report.coachComment || '暂无点评' }}</p></article></div></section> }
+    }
     @if(reportVariant()==='full'){<footer class="pdf-footer"><span>数据来源:学习记录 {{ report.generalId }}</span><span>生成时间:{{ report.generatedAt | date:'yyyy-MM-dd HH:mm:ss' }}</span></footer>}
   </article></div>
-  <footer class="drawer-actions"><span class="report-status" [attr.data-status]="report.status">{{ report.statusLabel }}</span><button class="ui-button primary" type="button" [disabled]="!report.canExport || !!exportingId()" (click)="download(report, reportVariant())"><lucide-icon [img]="exportingId() ? icons.LoaderCircle : icons.Download" [size]="17" [class.spin]="!!exportingId()" />{{ exportingId() ? '正在生成' : '下载 PDF' }}</button></footer></aside>
+  <footer class="drawer-actions"><span class="report-status" [attr.data-status]="report.status">{{ report.statusLabel }}</span>
+  @if (report.status !== 'writing') { <button class="ui-button primary" type="button" [disabled]="!report.canExport || !!exportingId()" (click)="download(report, reportVariant())"><lucide-icon [img]="exportingId() ? icons.LoaderCircle : icons.Download" [size]="17" [class.spin]="!!exportingId()" />{{ exportingId() ? '正在生成' : '下载 PDF' }}</button> }
+  </footer></aside>
 }

+ 29 - 0
projects/xiaoshu-admin/src/app/pages/operations-learning-reports.component.spec.ts

@@ -80,6 +80,35 @@ describe('OperationsLearningReportsComponent', () => {
     expect((fixture.nativeElement as HTMLElement).textContent).toContain('21天复习计划');
   });
 
+  it('shows writing feedback with no word metrics or PDF worksheets', async () => {
+    const component = fixture.componentInstance;
+    const page = component.data()!;
+    const writing = {
+      ...report, courseName: '高中语法写作课', status: 'writing' as const, statusLabel: '写作课',
+      learnedCount: 0, masteredCount: 0, reviewDueCount: 0, masteryRate: null, canExport: false,
+    };
+    component.data.set({
+      ...page, items: [writing],
+      summary: { ...page.summary, learnedCount: 0, masteredCount: 0, reviewDueCount: 0, masteryRate: null },
+    });
+    operations.learningReportDetail.and.resolveTo({
+      ...writing, words: [], reviewSchedule: [], score: 0, studentComment: '', coachComment: '',
+      generatedAt: report.updatedAt, sourceStatus, countMismatch: false,
+      writingFeedback: { contentSummary: '练习续写', reviewSummary: '', trainingSummary: '', homework: '', coachComment: '', notes: '' },
+    });
+
+    await component.open(writing.targetId);
+    fixture.detectChanges();
+
+    const element: HTMLElement = fixture.nativeElement;
+    expect(element.querySelector('.report-table tbody tr')?.textContent).toContain('不适用');
+    expect(element.querySelector('.report-metrics')?.textContent).not.toContain('%');
+    const drawer = element.querySelector('.report-drawer')!;
+    expect(drawer.textContent).toContain('练习续写');
+    expect(drawer.textContent).not.toContain('单词学习明细');
+    expect(drawer.querySelector('.report-variants, .drawer-actions button')).toBeNull();
+  });
+
   it('offers download only after opening the preview and choosing a report version', async () => {
     const element: HTMLElement = fixture.nativeElement;
     expect(element.querySelector('.report-table button[aria-label="下载PDF"]')).toBeNull();

+ 12 - 0
projects/xiaoshu-admin/src/app/pages/operations-learning-reports.component.ts

@@ -65,6 +65,18 @@ export class OperationsLearningReportsComponent implements OnInit, OnDestroy {
     return rows;
   });
   readonly reportTitle = computed(() => reportTitles[this.reportVariant()]);
+  readonly writingSections = computed(() => {
+    const feedback = this.detail()?.writingFeedback;
+    if (!feedback) return [];
+    return [
+      { label: '课堂内容', value: feedback.contentSummary },
+      { label: '训练情况', value: feedback.trainingSummary },
+      { label: '复习情况', value: feedback.reviewSummary },
+      { label: '作业', value: feedback.homework },
+      { label: '陪练点评', value: feedback.coachComment },
+      { label: '备注', value: feedback.notes },
+    ].filter(section => section.value);
+  });
   readonly reviewSchedule = computed(() => this.detail() ? printableReviewSchedule(this.detail()!) : []);
   readonly hasInferredReviewDates = computed(() => this.reviewSchedule().some((item) => item.inferred));
   readonly loading = signal(true);

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 19 - 5
scripts/deploy-admin-functions.mjs


+ 35 - 7
scripts/tests/learning-report-query.test.mjs

@@ -15,7 +15,7 @@ before(async () => {
     CREATE TABLE "DailyStudyRecord"("objectId" text,"company" text,"id" int,"userId" int,"pl" text,"dsid" text,"dqrq" text,"xxqs" text,"createdAt" timestamptz);
     CREATE TABLE "CommonModel"("objectId" text,"company" text,"modelId" int,"itemId" int,"generalId" int,"createTime" timestamptz,"createdAt" timestamptz);
     CREATE TABLE "_User"("objectId" text,"company" text,"legacyUserId" int,"legacyUserData" jsonb,"realName" text,"nickname" text,"username" text,"mobile" text,"identityType" text,"isDeleted" boolean);
-    CREATE TABLE "CourseAppointment"("objectId" text,"company" text,"id" int,"kcid" text,"dszt" text,"courseCategoryKey" text,"courseCategoryName" text);
+    CREATE TABLE "CourseAppointment"("objectId" text,"company" text,"id" int,"kcid" text,"dszt" text,"courseCategoryKey" text,"courseCategoryName" text,szyh text);
     CREATE TABLE "Node"("company" text,"nodeId" int,"nodeName" text);
     INSERT INTO "DailyStudyRecord" VALUES
       ('compact','co',1,20,'30','','20261004','[]','2026-10-04T00:00:00Z'),
@@ -59,21 +59,49 @@ test('门店报表按当前会员归属过滤后再计数,无法通过过滤
 
 test('无教材的语法写作课按预约分类显示并支持课程名称搜索',async()=>{
  await db.exec(`INSERT INTO "CourseAppointment" VALUES
-   ('writing','co',201,'0','30','high_writing','高中语法写作课'),
-   ('word','co',202,'13','30','word','单词课'),
-   ('legacy-writing','co',203,'398','30','high_writing','高中语法写作课');
+   ('writing','co',201,'0','30','high_writing','高中语法写作课','59'),
+   ('word','co',202,'13','30','word','单词课','59'),
+   ('legacy-writing','co',203,'398','30','high_writing','高中语法写作课','59'),
+   ('legacy-unclassified','co',204,'398','30',NULL,NULL,'59'),
+   ('unrelated-appointment','co',205,'398','30',NULL,NULL,'60');
   INSERT INTO "CommonModel" VALUES
    ('writing-main','co',54,201,201,NULL,NULL),
    ('word-main','co',54,202,202,NULL,NULL),
-   ('legacy-writing-main','co',54,203,203,NULL,NULL);
+   ('legacy-writing-main','co',54,203,203,NULL,NULL),
+   ('legacy-unclassified-main','co',54,204,204,NULL,NULL),
+   ('unrelated-main','co',54,205,205,NULL,NULL);
   INSERT INTO "Node" VALUES ('co',13,'人教版词表'),('co',398,'阅读写作');
   INSERT INTO "DailyStudyRecord" VALUES
    ('writing-report','co',21,59,'88','201','20261111','[]',NULL),
    ('word-report','co',22,59,'88','202','20261111','[]',NULL),
-   ('legacy-writing-report','co',23,59,'88','203','20261111','[]',NULL);`);
+   ('legacy-writing-report','co',23,59,'88','203','20261111','[]',NULL),
+   ('legacy-unclassified-report','co',24,59,'88','204','20261111','[]',NULL),
+   ('unrelated-report','co',25,59,'88','205','20261111','[]',NULL);`);
  const q=gateway.learningReportQueryParts({}, {filters:{dateFrom:'2026-11-11',dateTo:'2026-11-11',studentId:59}});
  const sql='SELECT d."objectId",'+gateway.learningReportCourseTitleSql()+' AS title'+q.from+q.where+' ORDER BY d.id';
- assert.deepEqual((await db.query(sql,q.values)).rows.map(row=>row.title),['高中语法写作课','人教版词表','阅读写作']);
+ assert.deepEqual((await db.query(sql,q.values)).rows.map(row=>row.title),['高中语法写作课','人教版词表','阅读写作','阅读写作','阅读写作']);
  assert.deepEqual(await rows({courseSearch:'高中语法写作课'}),['writing-report']);
  assert.deepEqual(await rows({courseSearch:'人教版'}),['word-report']);
 });
+
+test('写作课不进入单词明细缺失或单词报表筛选', async () => {
+  const dates = { dateFrom: '2026-11-11', dateTo: '2026-11-11' };
+  assert.deepEqual(await rows({ ...dates, status: 'writing' }), [
+    'legacy-unclassified-report', 'legacy-writing-report', 'writing-report',
+  ]);
+  assert.deepEqual(await rows({ ...dates, status: 'missing' }), ['unrelated-report', 'word-report']);
+  assert.deepEqual(await rows({ ...dates, status: 'available' }), []);
+});
+
+test('写作报表的词汇掌握率不适用,普通单词报表仍计算掌握率', () => {
+  const wordData = { xxqs: '[{"Title":"apple","check":1}]', appointmentObjectId: 'word', appointmentStatus: '30' };
+  const writing = gateway.learningReportProjection({ ...wordData, isWriting: true });
+  assert.equal(writing.status, 'writing');
+  assert.equal(writing.masteryRate, null);
+  assert.equal(writing.canExport, false);
+  assert.equal(writing.learnedCount, 0);
+  const word = gateway.learningReportProjection(wordData);
+  assert.equal(word.status, 'available');
+  assert.equal(word.masteryRate, 100);
+  assert.equal(word.canExport, true);
+});

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно