feat: read "Занятия в Полоцке" as a day's town, show it in all clients

- Parser 14 (server/places.js): "Занятия в Полоцке/Новополоцке/г. Новополоцк" over a day's first row is no longer a lesson but a note of the day for its column (`places`); lessons of that day get `place`. A lesson below the note in the same cell keeps its own subject.
- A town on its own line ("Новополоцк", "г.Новополоцк") or after a room ("246(Новополоцк)") becomes the lesson's `place` and leaves the subject and room. "История Полоцка" is not a town.
- API 14: `place` on lessons and `places` on days of /api/timetable, `place` in the directory. The day notes are shared by the server and the PWA (shared/places.js); the same note in every subgroup reads as the whole group's.
- Android, PWA and iOS: a "Занятия в Полоцке" card above the day's lessons (day pages, calendar, overview, week view, desk stand), "Полоцк" under the date in the week grid, "249Ж · Полоцк" on lesson cards, a "Город" row in details, the town in widgets, tile, reminders and change lists.
- Snapshot lines show the town and the day notes; tests for notes, towns in cells and the API.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 01:26:54 +03:00
parent 3d08d0cb70
commit 10c85e3bb9
50 changed files with 808 additions and 80 deletions

View File

@@ -165,7 +165,7 @@ private struct DirectoryLessonCard: View {
Text(lesson.start + (lesson.end.map { " → \($0)" } ?? "") + (pair.map { " · \($0) пара" } ?? "")).font(.body.monospacedDigit())
FlowLayout(spacing: 14) {
if case .teacher = target {
Meta(symbol: "mappin.and.ellipse", text: lesson.distance ? "Дистанционно" : lesson.rooms.joined(separator: ", ") + (lesson.moved ? " (перенос)" : ""))
Meta(symbol: "mappin.and.ellipse", text: lesson.distance ? "Дистанционно" : withPlace(lesson.rooms.joined(separator: ", ") + (lesson.moved ? " (перенос)" : ""), lesson.place) ?? "")
} else if !lesson.teachers.isEmpty {
Meta(symbol: "person", text: lesson.teachers.joined(separator: ", "))
}