- Models: TrainingPlan, Race, FamilyPlan, BeginnerMode, CommunityEvent - Services: 5 service layers with protocol-based architecture - ViewModels: 5 view models with @MainActor ObservableObject pattern - Views: 10 SwiftUI views for all Phase 3 features - Updated README with full Phase 3 documentation Co-Authored-By: Paperclip <noreply@paperclip.ing>
313 lines
7.7 KiB
Swift
313 lines
7.7 KiB
Swift
import Foundation
|
|
import SwiftUI
|
|
|
|
// MARK: - Training Plan
|
|
|
|
struct TrainingPlan: Identifiable, Equatable, Codable {
|
|
let id: String
|
|
let title: String
|
|
let description: String
|
|
let planType: PlanType
|
|
let durationWeeks: Int
|
|
let difficulty: Difficulty
|
|
let startDate: Date
|
|
let endDate: Date
|
|
let weeklyWorkouts: [WeeklyWorkout]
|
|
var progress: PlanProgress
|
|
var isFollowing: Bool
|
|
let createdAt: Date
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case id, title, description, planType, durationWeeks, difficulty, startDate, endDate, weeklyWorkouts, progress, isFollowing, createdAt
|
|
}
|
|
|
|
init(
|
|
id: String,
|
|
title: String,
|
|
description: String,
|
|
planType: PlanType,
|
|
durationWeeks: Int,
|
|
difficulty: Difficulty,
|
|
startDate: Date,
|
|
endDate: Date,
|
|
weeklyWorkouts: [WeeklyWorkout],
|
|
progress: PlanProgress,
|
|
isFollowing: Bool,
|
|
createdAt: Date
|
|
) {
|
|
self.id = id
|
|
self.title = title
|
|
self.description = description
|
|
self.planType = planType
|
|
self.durationWeeks = durationWeeks
|
|
self.difficulty = difficulty
|
|
self.startDate = startDate
|
|
self.endDate = endDate
|
|
self.weeklyWorkouts = weeklyWorkouts
|
|
self.progress = progress
|
|
self.isFollowing = isFollowing
|
|
self.createdAt = createdAt
|
|
}
|
|
|
|
static func == (lhs: TrainingPlan, rhs: TrainingPlan) -> Bool {
|
|
lhs.id == rhs.id
|
|
}
|
|
}
|
|
|
|
// MARK: - Plan Type
|
|
|
|
enum PlanType: String, CaseIterable, Codable {
|
|
case fiveK = "5K"
|
|
case tenK = "10K"
|
|
case halfMarathon = "HALF_MARATHON"
|
|
case fullMarathon = "FULL_MARATHON"
|
|
case custom = "CUSTOM"
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .fiveK: return "5K"
|
|
case .tenK: return "10K"
|
|
case .halfMarathon: return "Half Marathon"
|
|
case .fullMarathon: return "Full Marathon"
|
|
case .custom: return "Custom"
|
|
}
|
|
}
|
|
|
|
var distanceKm: Double {
|
|
switch self {
|
|
case .fiveK: return 5.0
|
|
case .tenK: return 10.0
|
|
case .halfMarathon: return 21.1
|
|
case .fullMarathon: return 42.2
|
|
case .custom: return 0.0
|
|
}
|
|
}
|
|
|
|
var icon: String {
|
|
switch self {
|
|
case .fiveK: return "figure.run"
|
|
case .tenK: return "figure.run"
|
|
case .halfMarathon: return "flag.fill"
|
|
case .fullMarathon: return "flag.fill"
|
|
case .custom: return "wrench.and.screwdriver"
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Difficulty
|
|
|
|
enum Difficulty: String, CaseIterable, Codable {
|
|
case beginner
|
|
case intermediate
|
|
case advanced
|
|
case elite
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .beginner: return "Beginner"
|
|
case .intermediate: return "Intermediate"
|
|
case .advanced: return "Advanced"
|
|
case .elite: return "Elite"
|
|
}
|
|
}
|
|
|
|
var color: Color {
|
|
switch self {
|
|
case .beginner: return .green
|
|
case .intermediate: return .blue
|
|
case .advanced: return .orange
|
|
case .elite: return .red
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Weekly Workout
|
|
|
|
struct WeeklyWorkout: Identifiable, Codable {
|
|
let id: String
|
|
let weekNumber: Int
|
|
let dailySessions: [DailySession]
|
|
|
|
var completedSessions: Int {
|
|
dailySessions.filter { $0.status == .completed }.count
|
|
}
|
|
|
|
var totalSessions: Int {
|
|
dailySessions.count
|
|
}
|
|
|
|
var progressPercentage: Double {
|
|
totalSessions == 0 ? 0 : Double(completedSessions) / Double(totalSessions) * 100
|
|
}
|
|
}
|
|
|
|
// MARK: - Daily Session
|
|
|
|
struct DailySession: Identifiable, Codable {
|
|
let id: String
|
|
let dayOfWeek: DayOfWeek
|
|
let workoutType: WorkoutType
|
|
let title: String
|
|
let description: String
|
|
let targetDistanceKm: Double?
|
|
let targetDurationMinutes: Int?
|
|
let targetPaceMinPerKm: Double?
|
|
let intensity: Intensity
|
|
var status: SessionStatus
|
|
var completedDistanceKm: Double?
|
|
var completedDurationMinutes: Int?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case id, dayOfWeek, workoutType, title, description, targetDistanceKm, targetDurationMinutes, targetPaceMinPerKm, intensity, status, completedDistanceKm, completedDurationMinutes
|
|
}
|
|
}
|
|
|
|
// MARK: - Day of Week
|
|
|
|
enum DayOfWeek: String, CaseIterable, Codable {
|
|
case monday, tuesday, wednesday, thursday, friday, saturday, sunday
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .monday: return "Mon"
|
|
case .tuesday: return "Tue"
|
|
case .wednesday: return "Wed"
|
|
case .thursday: return "Thu"
|
|
case .friday: return "Fri"
|
|
case .saturday: return "Sat"
|
|
case .sunday: return "Sun"
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Workout Type
|
|
|
|
enum WorkoutType: String, CaseIterable, Codable {
|
|
case easyRun
|
|
case tempoRun
|
|
case intervalTraining
|
|
case longRun
|
|
case speedWork
|
|
case recoveryRun
|
|
case crossTraining
|
|
case rest
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .easyRun: return "Easy Run"
|
|
case .tempoRun: return "Tempo Run"
|
|
case .intervalTraining: return "Intervals"
|
|
case .longRun: return "Long Run"
|
|
case .speedWork: return "Speed Work"
|
|
case .recoveryRun: return "Recovery Run"
|
|
case .crossTraining: return "Cross Train"
|
|
case .rest: return "Rest"
|
|
}
|
|
}
|
|
|
|
var icon: String {
|
|
switch self {
|
|
case .easyRun: return "figure.walk"
|
|
case .tempoRun: return "figure.run"
|
|
case .intervalTraining: return "bolt.fill"
|
|
case .longRun: return "figure.run"
|
|
case .speedWork: return "speedometer"
|
|
case .recoveryRun: return "leaf.fill"
|
|
case .crossTraining: return "dumbbell.fill"
|
|
case .rest: return "moon.fill"
|
|
}
|
|
}
|
|
|
|
var color: Color {
|
|
switch self {
|
|
case .easyRun: return .green
|
|
case .tempoRun: return .blue
|
|
case .intervalTraining: return .purple
|
|
case .longRun: return .orange
|
|
case .speedWork: return .red
|
|
case .recoveryRun: return .mint
|
|
case .crossTraining: return .gray
|
|
case .rest: return .secondary
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Intensity
|
|
|
|
enum Intensity: String, CaseIterable, Codable {
|
|
case veryEasy
|
|
case easy
|
|
case moderate
|
|
case hard
|
|
case veryHard
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .veryEasy: return "Very Easy"
|
|
case .easy: return "Easy"
|
|
case .moderate: return "Moderate"
|
|
case .hard: return "Hard"
|
|
case .veryHard: return "Very Hard"
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Session Status
|
|
|
|
enum SessionStatus: String, CaseIterable, Codable {
|
|
case pending
|
|
case inProgress
|
|
case completed
|
|
case skipped
|
|
}
|
|
|
|
// MARK: - Plan Progress
|
|
|
|
struct PlanProgress: Codable {
|
|
let completedWeeks: Int
|
|
let totalWeeks: Int
|
|
let completedSessions: Int
|
|
let totalSessions: Int
|
|
let currentWeekNumber: Int
|
|
|
|
var percentage: Double {
|
|
totalWeeks == 0 ? 0 : Double(completedWeeks) / Double(totalWeeks) * 100
|
|
}
|
|
|
|
var sessionPercentage: Double {
|
|
totalSessions == 0 ? 0 : Double(completedSessions) / Double(totalSessions) * 100
|
|
}
|
|
}
|
|
|
|
// MARK: - AI Plan Generation Request
|
|
|
|
struct GeneratePlanRequest: Encodable {
|
|
let planType: PlanType
|
|
let difficulty: Difficulty
|
|
let startDate: Date
|
|
let currentWeeklyMileageKm: Double?
|
|
let goalTimeMinutes: Int?
|
|
let availableDays: [DayOfWeek]
|
|
}
|
|
|
|
// MARK: - API Response Types
|
|
|
|
struct TrainingPlanListResponse: Decodable {
|
|
let plans: [TrainingPlan]
|
|
let hasMore: Bool
|
|
}
|
|
|
|
struct TrainingPlanDetailResponse: Decodable {
|
|
let plan: TrainingPlan
|
|
}
|
|
|
|
struct GeneratePlanResponse: Decodable {
|
|
let plan: TrainingPlan
|
|
}
|
|
|
|
struct UpdateSessionStatusResponse: Decodable {
|
|
let success: Bool
|
|
let sessionId: String
|
|
let status: SessionStatus
|
|
}
|