FRE-4665: Implement Phase 3 AI training plans and premium features
- 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>
This commit is contained in:
312
Lendair/Models/TrainingPlan.swift
Normal file
312
Lendair/Models/TrainingPlan.swift
Normal file
@@ -0,0 +1,312 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user