feat: Enhance session handling in timetable and UI

- Introduced session types (exams, credits, consultations, coursework defenses) with distinct icons and colors for better visibility in the UI.
- Updated PlansScreen, ScheduleScreen, and WeekGrid to display session information appropriately.
- Modified CalendarView and PlansView to include session indicators in the calendar and plans.
- Enhanced LessonCard and CompactLesson to show session details and adjust styling based on session presence.
- Added new SessionMark component for consistent session representation across Android and iOS.
- Updated server logic to handle session entries from external documents, ensuring accurate timetable representation.
- Improved date parsing to accommodate session-related dates in various formats.
- Added tests to verify session handling and date parsing functionality.
This commit is contained in:
2026-10-19 20:49:13 +03:00
parent db9a3f8243
commit 15851b26cd
21 changed files with 421 additions and 42 deletions

View File

@@ -0,0 +1,90 @@
import SwiftUI
import UIKit
/// The session's events: exams, credits, consultations and coursework defences. They wear an
/// amber of their own — red would read as an error — and each kind its own symbol, so they
/// stand out from ordinary lessons in the schedule, the calendar and the plans.
enum SessionKind {
case exam, credit, defence, consultation
init?(_ type: String) {
switch type {
case "Экзамен": self = .exam
case "Зачёт": self = .credit
case "Защита курсовой работы": self = .defence
case "Консультация": self = .consultation
default: return nil
}
}
var label: String {
switch self {
case .exam: "Экзамен"
case .credit: "Зачёт"
case .defence: "Защита курсовой"
case .consultation: "Консультация"
}
}
var symbol: String {
switch self {
case .exam: "pencil.and.list.clipboard"
case .credit: "checkmark.seal.fill"
case .defence: "graduationcap.fill"
case .consultation: "bubble.left.and.bubble.right.fill"
}
}
/// Exams, credits and defences are filled; consultations are tonal.
var strong: Bool { self != .consultation }
var fill: Color { strong ? .sessionStrong : .sessionSoft }
var content: Color { strong ? .onSessionStrong : .onSessionSoft }
}
extension Lesson {
var session: SessionKind? { SessionKind(type) }
}
private func dynamic(_ light: UInt32, _ dark: UInt32) -> Color {
func ui(_ hex: UInt32) -> UIColor {
UIColor(red: CGFloat((hex >> 16) & 0xFF) / 255, green: CGFloat((hex >> 8) & 0xFF) / 255, blue: CGFloat(hex & 0xFF) / 255, alpha: 1)
}
return Color(UIColor { $0.userInterfaceStyle == .dark ? ui(dark) : ui(light) })
}
extension Color {
/// The session amber (an M3 tonal palette from #8A5100): text, marks and outlines.
static let sessionAccent = dynamic(0x8A5100, 0xFFB95C)
static let sessionStrong = dynamic(0x8A5100, 0xFFB95C)
static let onSessionStrong = dynamic(0xFFFFFF, 0x482900)
static let sessionSoft = dynamic(0xFFDDB8, 0x693C00)
static let onSessionSoft = dynamic(0x2C1600, 0xFFDDB8)
}
/// "Зачёт" with its symbol, as a capsule above the subject.
struct SessionBadge: View {
var kind: SessionKind
var body: some View {
Label(kind.label, systemImage: kind.symbol)
.font(.footnote.weight(.semibold))
.padding(.horizontal, 10)
.padding(.vertical, 4)
.foregroundStyle(kind.content)
.background(kind.fill, in: .capsule)
}
}
/// A round marker with the kind's symbol, in place of a subject's dot.
struct SessionDot: View {
var kind: SessionKind
var size: CGFloat = 20
var body: some View {
Image(systemName: kind.symbol)
.font(.system(size: size * 0.5, weight: .bold))
.foregroundStyle(kind.content)
.frame(width: size, height: size)
.background(kind.fill, in: .circle)
.accessibilityHidden(true)
}
}