# Classroom Field Simplification Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Remove the unused classroom `courseType` and `supervisor` fields from the admin UI, API contract, persistence model, and Excel import flow.

**Architecture:** Perform a clean cutover across every classroom caller. Add an idempotent bootstrap migration that drops the legacy columns on SQLite and MySQL, while leaving scheduling unchanged because schedule `subject` and class `classType` already own those concepts.

**Tech Stack:** React 19, Ant Design 6, NestJS 11, TypeORM 0.3, Jest, Vitest, SQLite, MySQL 8

## Global Constraints

- Do not add a replacement abstraction or compatibility alias.
- Preserve classroom name, building, floor, capacity, room type, status, and notes behavior.
- Do not change schedule form fields or schedule conflict behavior.
- Use test-first changes and run focused tests before package builds.

---

### Task 1: Drop Legacy Classroom Columns

**Files:**
- Modify: `apps/server/src/database/database-migrations.spec.ts`
- Modify: `apps/server/src/database/database-migrations.service.ts:11`
- Modify: `apps/server/src/entities/classroom.entity.ts:23`

**Interfaces:**
- Consumes: TypeORM `DataSource`, `QueryRunner`, and the existing bootstrap migration sequence.
- Produces: private `removeUnusedClassroomColumns(): Promise<void>` called during application bootstrap.

- [ ] **Step 1: Write failing migration tests**

Extend `MigrationsPrivate` with `removeUnusedClassroomColumns(): Promise<void>`. Add focused tests proving that the method:

```typescript
it('drops legacy classroom fields when present', async () => {
  const runner = mockRunner({
    getTable: {
      name: 'classrooms',
      columns: [{ name: 'id' }, { name: 'course_type' }, { name: 'supervisor' }],
    },
  });
  await bootstrapClassroomCleanup(runner);

  await service.removeUnusedClassroomColumns();

  expect(runner.query).toHaveBeenCalledWith('ALTER TABLE classrooms DROP COLUMN course_type');
  expect(runner.query).toHaveBeenCalledWith('ALTER TABLE classrooms DROP COLUMN supervisor');
  expect(runner.release).toHaveBeenCalled();
});

it('does nothing when classrooms or legacy fields are absent', async () => {
  const runner = mockRunner({ getTables: [], getTable: { name: 'classrooms', columns: [] } });
  await bootstrapClassroomCleanup(runner);

  await service.removeUnusedClassroomColumns();

  expect(runner.query).not.toHaveBeenCalled();
  expect(runner.release).toHaveBeenCalled();
});
```

Use a local `bootstrapClassroomCleanup` helper matching the existing Nest testing-module setup.

- [ ] **Step 2: Run tests and verify RED**

Run: `npm test -w apps/server -- database/database-migrations.spec.ts --runInBand`

Expected: FAIL because `removeUnusedClassroomColumns` does not exist.

- [ ] **Step 3: Implement the idempotent migration**

Call `await this.removeUnusedClassroomColumns();` from `onApplicationBootstrap()`. Implement the method with one query runner: inspect `classrooms`, return when absent, and issue one `ALTER TABLE classrooms DROP COLUMN <name>` per present legacy column. Always release the runner in `finally`.

Remove these entity properties and decorators:

```typescript
courseType: string;
supervisor: string;
```

- [ ] **Step 4: Run migration tests and verify GREEN**

Run: `npm test -w apps/server -- database/database-migrations.spec.ts --runInBand`

Expected: PASS.

### Task 2: Remove Fields From API and Excel Import

**Files:**
- Modify: `apps/server/src/classrooms/dto/classroom.dto.ts:3`
- Modify: `apps/server/src/classrooms/classrooms.controller.ts:48`
- Modify: `apps/server/src/classrooms/classrooms.service.ts:126`
- Create: `apps/server/src/classrooms/classrooms.controller.spec.ts`

**Interfaces:**
- Consumes: `CreateClassroomDto`, `UpdateClassroomDto`, classroom Excel template/import endpoints.
- Produces: classroom requests and spreadsheets containing only `name`, `building`, `floor`, `roomType`, `capacity`, and optional `notes` where applicable.

- [ ] **Step 1: Write a failing Excel contract test**

Instantiate `ClassroomsController` with mocked service/log dependencies, invoke `downloadTemplate`, capture the workbook response buffer, load it with ExcelJS, and assert:

```typescript
expect(worksheet.getRow(1).values).toEqual([
  undefined,
  '教室名',
  '楼栋',
  '楼层',
  '类型',
  '容量',
]);
```

Also assert the usage notes no longer mention course type or supervisor.

- [ ] **Step 2: Run test and verify RED**

Run: `npm test -w apps/server -- classrooms/classrooms.controller.spec.ts --runInBand`

Expected: FAIL because the generated template still includes `课程类型` and `负责人`.

- [ ] **Step 3: Remove fields from server contracts**

Delete `courseType` and `supervisor` from both DTO classes. Remove them from:

- template columns, example rows, and usage notes;
- parsed import rows;
- `batchImport` row type.

Keep the existing field order `name`, `building`, `floor`, `roomType`, `capacity` consistent between export and import.

- [ ] **Step 4: Run focused server tests**

Run: `npm test -w apps/server -- classrooms/classrooms.controller.spec.ts database/database-migrations.spec.ts --runInBand`

Expected: PASS.

### Task 3: Simplify Classroom Admin UI

**Files:**
- Modify: `apps/admin/src/pages/Classrooms/index.tsx:143`
- Create: `apps/admin/src/pages/Classrooms/classroom-fields.ts`
- Create: `apps/admin/src/pages/Classrooms/classroom-fields.integration.test.ts`

**Interfaces:**
- Consumes: classroom records returned by `/classrooms`.
- Produces: exported `CLASSROOM_VISIBLE_FIELDS` used to document/test the UI contract; list and form without `courseType` or `supervisor`.

- [ ] **Step 1: Write a failing UI contract test**

Create a small contract test:

```typescript
import { describe, expect, it } from 'vitest';
import { CLASSROOM_VISIBLE_FIELDS } from './classroom-fields';

describe('classroom visible fields', () => {
  it('excludes obsolete course and supervisor metadata', () => {
    expect(CLASSROOM_VISIBLE_FIELDS).toEqual([
      'name',
      'building',
      'floor',
      'roomType',
      'capacity',
      'status',
      'notes',
    ]);
  });
});
```

- [ ] **Step 2: Run test and verify RED**

Run: `npm test -w apps/admin -- src/pages/Classrooms/classroom-fields.integration.test.ts`

Expected: FAIL because `classroom-fields.ts` does not exist.

- [ ] **Step 3: Implement minimal UI cleanup**

Create the constant exactly as asserted. Remove the `课程类型` and `负责人` table columns and the corresponding two `Form.Item` blocks. Do not alter scheduling pages.

- [ ] **Step 4: Run focused admin test**

Run: `npm test -w apps/admin -- src/pages/Classrooms/classroom-fields.integration.test.ts`

Expected: PASS.

### Task 4: Verify Clean Cutover

**Files:**
- Verify: `apps/server/src/entities/classroom.entity.ts`
- Verify: `apps/server/src/classrooms/`
- Verify: `apps/admin/src/pages/Classrooms/`

**Interfaces:**
- Consumes: completed Tasks 1–3.
- Produces: buildable admin/server packages with no classroom `courseType` or `supervisor` path.

- [ ] **Step 1: Run focused tests together**

Run: `npm test -w apps/server -- classrooms/classrooms.controller.spec.ts database/database-migrations.spec.ts --runInBand`

Run: `npm test -w apps/admin -- src/pages/Classrooms/classroom-fields.integration.test.ts`

Expected: all PASS.

- [ ] **Step 2: Run package typechecks/builds**

Run: `npm run typecheck -w apps/server`

Run: `npm run build -w apps/admin`

Expected: both exit successfully.

- [ ] **Step 3: Smoke-test observable contracts**

Start the existing app and verify:

1. Adding/editing a classroom shows no course type or supervisor field.
2. Classroom list shows neither obsolete column.
3. Downloaded import template has five headers: 教室名、楼栋、楼层、类型、容量.
4. Creating a schedule still selects and saves a classroom normally.

Expected: all four scenarios succeed without console or API errors.
