Add class-teacher slots on SchoolClass with owner-only assign/clear.

EOF

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-21 09:37:37 +03:00
co-authored by Cursor
parent 99eca80d6d
commit 4096d5306e
22 changed files with 992 additions and 22 deletions
+13 -13
View File
@@ -12,22 +12,22 @@
## Задачи
- [ ] У `SchoolClass` опциональный `classTeacherId` (или эквивалент); сейв в ростере
- [ ] Назначить / снять — HTTP через мейлбокс школы; только хозяин; гость `403`
- [ ] Кандидат — нанятый с должностью `Teacher`; иначе отказ с текстом, не молчание
- [ ] Один учитель может быть классным у нескольких классов
- [ ] UI в «Управлении»: список классов со слотом классного (выбор / снять)
- [ ] Имя классного видно там, где уже показывают класс (карточка ученика / управление)
- [ ] Строки через `t(...)`, обе локали
- [ ] Пустой слот — школа живёт как сейчас; собраний ещё нет
- [x] У `SchoolClass` опциональный `classTeacherId` (или эквивалент); сейв в ростере
- [x] Назначить / снять — HTTP через мейлбокс школы; только хозяин; гость `403`
- [x] Кандидат — нанятый с должностью `Teacher`; иначе отказ с текстом, не молчание
- [x] Один учитель может быть классным у нескольких классов
- [x] UI в «Управлении»: список классов со слотом классного (выбор / снять)
- [x] Имя классного видно там, где уже показывают класс (карточка ученика / управление)
- [x] Строки через `t(...)`, обе локали
- [x] Пустой слот — школа живёт как сейчас; собраний ещё нет
## Тесты, без которых фаза не закрыта
- [ ] Назначение учителя класса пишет id в ростер и переживает сейв / reload
- [ ] Не-учитель и чужой id отклоняются
- [ ] Гость не назначает (API)
- [ ] Снятие очищает слот; класс без классного валиден
- [ ] Два класса могут указать одного и того же учителя
- [x] Назначение учителя класса пишет id в ростер и переживает сейв / reload
- [x] Не-учитель и чужой id отклоняются
- [x] Гость не назначает (API)
- [x] Снятие очищает слот; класс без классного валиден
- [x] Два класса могут указать одного и того же учителя
## Критерий готовности
+39 -1
View File
@@ -333,7 +333,9 @@ tick with the clock. Unknown school is `404` `unknown-school`; unknown person is
Family links are other members of the same family: parents and siblings for a child, children
and partners for a parent. The client opens another card by id; this response does not nest
cards. `classId` is the homeroom class for a pupil and `null` otherwise — the personal timetable
grid fetches `GET .../timetable?classId=` with it.
grid fetches `GET .../timetable?classId=` with it. `classTeacherId` / `classTeacherName` come from
that class's slot when a class teacher is assigned; both are null when the slot is empty or the
person has no class.
`activity` is the ActionDef name currently in progress, or `null` when idle. `activityLabel`
is that def in the request locale. HTTP JSON is additive — no protocol version bump.
@@ -376,6 +378,8 @@ compute thresholds. There is no school-wide opinions endpoint.
"classYear": 5,
"classLetter": "А",
"classId": "class-classroom-105",
"classTeacherId": "f3.p1",
"classTeacherName": "Иванова Ольга Михайловна",
"body": [{ "id": "Height", "label": "Рост", "value": "164" }],
"skills": [{ "id": "Math", "label": "Математика", "value": "62" }],
"traits": [{ "defName": "Diligent", "label": "Усидчивый" }],
@@ -694,10 +698,23 @@ one.
"gradeMax": 11,
"hoursPerWeek": 5
}
],
"classes": [
{
"id": "c5A",
"year": 5,
"letter": "А",
"classTeacherId": "f3.p1",
"classTeacherName": "Иванова Ольга Михайловна"
}
]
}
```
`classes` lists every homeroom with an optional class-teacher slot. `classTeacherId` /
`classTeacherName` are null when the slot is empty. Assign and clear (below) return this same
payload so the Management panel can refresh the slots without a second GET.
Applicants here are the same people as in `saves/{id}.people.json`. A parent keeps the same
id on the roster; hiring them sets `isStaff` on that person and does not create a second
entity. Generated candidates (`aN.p0`) join the roster only when hired. `skills` on an
@@ -747,6 +764,27 @@ and the same `?lang=ru|en`.
Removes one assignment. Payroll drops when the subject was not the only one. Same success
payload and `?lang=ru|en` as the other two. Unknown assignment is `404` `unknown-assignment`.
### `PUT /api/schools/{id}/classes/{classId}/class-teacher`
Body: `{ "personId": "f3.p1" }`. Sets the class-teacher slot on that class. Only a hired
person with position `Teacher` is accepted — not a new `PositionDef`. One teacher may cover
several classes. Goes through the school's mailbox; only the owner may call it (`403`
`not-owner`). Success returns the same payload as `GET .../staffing` (including `classes`),
so it takes the same `?lang=ru|en`.
| Status | `code` | When |
| --- | --- | --- |
| `403` | `not-owner` | The session is not the school's owner. |
| `404` | `unknown-school` | No school with that id. |
| `404` | `unknown-class` | `classId` is not in the roster. |
| `404` | `unknown-person` | `personId` is not on the roster. |
| `400` | `not-teacher` | Person is not hired staff with position `Teacher`. |
### `DELETE /api/schools/{id}/classes/{classId}/class-teacher`
Clears the class-teacher slot. An empty slot is valid — the class lives as before; parent
meetings (later) simply do not start. Same success payload and owner gate as the PUT.
### `GET /api/schools/{id}/timetable`
The published lesson table and uncovered hours. Optional `?classId=` or `?personId=` filter