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:
2026-05-03 15:21:01 -04:00
parent db23f533af
commit 57a460761a
26 changed files with 4457 additions and 62 deletions

View File

@@ -0,0 +1,201 @@
import Foundation
import SwiftUI
// MARK: - Beginner Configuration
struct BeginnerConfig: Codable {
var isEnabled: Bool
var currentLevel: BeginnerLevel
var completedOnboardingSteps: [OnboardingStep]
var milestones: [Milestone]
var shownTips: Set<String>
var preferredMetric: MetricType
enum CodingKeys: String, CodingKey {
case isEnabled, currentLevel, completedOnboardingSteps, milestones, shownTips, preferredMetric
}
}
// MARK: - Beginner Level
enum BeginnerLevel: String, CaseIterable, Codable {
case justStarted
case gettingComfortable
case buildingConsistency
case progressing
var displayName: String {
switch self {
case .justStarted: return "Just Started"
case .gettingComfortable: return "Getting Comfortable"
case .buildingConsistency: return "Building Consistency"
case .progressing: return "Progressing"
}
}
var requiredWorkouts: Int {
switch self {
case .justStarted: return 0
case .gettingComfortable: return 5
case .buildingConsistency: return 15
case .progressing: return 30
}
}
var icon: String {
switch self {
case .justStarted: return "sparkles"
case .gettingComfortable: return "leaf.fill"
case .buildingConsistency: return "chart.bar.fill"
case .progressing: return "bolt.fill"
}
}
}
// MARK: - Onboarding Step
enum OnboardingStep: String, CaseIterable, Codable {
case profileSetup
case goalSelection
case firstActivity
case inviteFriends
case enableNotifications
case choosePlan
var displayName: String {
switch self {
case .profileSetup: return "Profile Setup"
case .goalSelection: return "Set Goals"
case .firstActivity: return "First Activity"
case .inviteFriends: return "Invite Friends"
case .enableNotifications: return "Enable Notifications"
case .choosePlan: return "Choose a Plan"
}
}
var description: String {
switch self {
case .profileSetup: return "Complete your profile with your name and photo"
case .goalSelection: return "Tell us what you want to achieve"
case .firstActivity: return "Record your first workout"
case .inviteFriends: return "Invite friends to join Nessa"
case .enableNotifications: return "Get reminders and updates"
case .choosePlan: return "Pick a training plan to follow"
}
}
}
// MARK: - Milestone
struct Milestone: Identifiable, Codable {
let id: String
let title: String
let description: String
let icon: String
let requirement: MilestoneRequirement
var isCompleted: Bool
var completedAt: Date?
enum CodingKeys: String, CodingKey {
case id, title, description, icon, requirement, isCompleted, completedAt
}
init(
id: String,
title: String,
description: String,
icon: String,
requirement: MilestoneRequirement,
isCompleted: Bool,
completedAt: Date?
) {
self.id = id
self.title = title
self.description = description
self.icon = icon
self.requirement = requirement
self.isCompleted = isCompleted
self.completedAt = completedAt
}
}
// MARK: - Milestone Requirement
struct MilestoneRequirement: Codable {
let type: RequirementType
let targetValue: Double
}
enum RequirementType: String, CaseIterable, Codable {
case totalDistanceKm
case totalWorkouts
case consecutiveDays
case weeklyConsistency
case firstWorkout
var displayName: String {
switch self {
case .totalDistanceKm: return "Total Distance"
case .totalWorkouts: return "Total Workouts"
case .consecutiveDays: return "Streak"
case .weeklyConsistency: return "Weekly Consistency"
case .firstWorkout: return "First Workout"
}
}
}
// MARK: - Tip
struct BeginnerTip: Identifiable, Codable {
let id: String
let context: TipContext
let title: String
let message: String
var isShown: Bool
}
enum TipContext: String, CaseIterable, Codable {
case beforeWorkout
case afterWorkout
case dailyReminder
case progressUpdate
case restDay
}
// MARK: - Metric Type
enum MetricType: String, CaseIterable, Codable {
case distance
case duration
case pace
var displayName: String {
switch self {
case .distance: return "Distance"
case .duration: return "Duration"
case .pace: return "Pace"
}
}
}
// MARK: - API Response Types
struct BeginnerConfigResponse: Decodable {
let config: BeginnerConfig
}
struct UpdateBeginnerConfigRequest: Encodable {
var isEnabled: Bool?
var completedOnboardingSteps: [OnboardingStep]?
var preferredMetric: MetricType?
}
struct UpdateBeginnerConfigResponse: Decodable {
let success: Bool
let config: BeginnerConfig
}
struct MilestoneProgressResponse: Decodable {
let milestones: [Milestone]
let currentLevel: BeginnerLevel
}

View File

@@ -0,0 +1,243 @@
import Foundation
import SwiftUI
// MARK: - Community Event
struct CommunityEvent: Identifiable, Equatable, Codable {
let id: String
let title: String
let description: String
let eventType: EventType
let location: String
let latitude: Double
let longitude: Double
let startDate: Date
let endDate: Date
let distanceKm: Double?
let organizerId: String
let organizerName: String
let maxParticipants: Int?
let participantCount: Int
let imageUrl: String?
let difficulty: Difficulty?
var rsvpStatus: RSVPStatus
let createdAt: Date
enum CodingKeys: String, CodingKey {
case id, title, description, eventType, location, latitude, longitude, startDate, endDate, distanceKm, organizerId, organizerName, maxParticipants, participantCount, imageUrl, difficulty, rsvpStatus, createdAt
}
init(
id: String,
title: String,
description: String,
eventType: EventType,
location: String,
latitude: Double,
longitude: Double,
startDate: Date,
endDate: Date,
distanceKm: Double?,
organizerId: String,
organizerName: String,
maxParticipants: Int?,
participantCount: Int,
imageUrl: String?,
difficulty: Difficulty?,
rsvpStatus: RSVPStatus,
createdAt: Date
) {
self.id = id
self.title = title
self.description = description
self.eventType = eventType
self.location = location
self.latitude = latitude
self.longitude = longitude
self.startDate = startDate
self.endDate = endDate
self.distanceKm = distanceKm
self.organizerId = organizerId
self.organizerName = organizerName
self.maxParticipants = maxParticipants
self.participantCount = participantCount
self.imageUrl = imageUrl
self.difficulty = difficulty
self.rsvpStatus = rsvpStatus
self.createdAt = createdAt
}
static func == (lhs: CommunityEvent, rhs: CommunityEvent) -> Bool {
lhs.id == rhs.id && lhs.rsvpStatus == rhs.rsvpStatus
}
var isUpcoming: Bool {
startDate > Date()
}
var isOngoing: Bool {
Date() >= startDate && Date() <= endDate
}
var isPast: Bool {
endDate < Date()
}
var availableSpots: Int? {
guard let max = maxParticipants else { return nil }
return max - participantCount
}
}
// MARK: - Event Type
enum EventType: String, CaseIterable, Codable {
case groupRun
case race
case workshop
case socialGather
case charityEvent
case trainingCamp
var displayName: String {
switch self {
case .groupRun: return "Group Run"
case .race: return "Race"
case .workshop: return "Workshop"
case .socialGather: return "Social"
case .charityEvent: return "Charity"
case .trainingCamp: return "Training Camp"
}
}
var icon: String {
switch self {
case .groupRun: return "person.3.fill"
case .race: return "flag.fill"
case .workshop: return "lightbulb.fill"
case .socialGather: return "cup.and.saucer.fill"
case .charityEvent: return "heart.fill"
case .trainingCamp: return "figure.run"
}
}
var color: Color {
switch self {
case .groupRun: return .blue
case .race: return .orange
case .workshop: return .purple
case .socialGather: return .green
case .charityEvent: return .red
case .trainingCamp: return .indigo
}
}
}
// MARK: - RSVP Status
enum RSVPStatus: String, CaseIterable, Codable {
case going
case maybe
case notGoing
case pending
}
// MARK: - Event Participant
struct EventParticipant: Identifiable, Codable {
let id: String
let name: String
let avatarUrl: String?
let rsvpStatus: RSVPStatus
let joinedAt: Date
}
// MARK: - Create Event Request
struct CreateEventRequest: Encodable {
let title: String
let description: String
let eventType: EventType
let location: String
let latitude: Double
let longitude: Double
let startDate: Date
let endDate: Date
let distanceKm: Double?
let maxParticipants: Int?
let difficulty: Difficulty?
}
// MARK: - Update Event Request
struct UpdateEventRequest: Encodable {
var title: String?
var description: String?
var eventType: EventType?
var location: String?
var latitude: Double?
var longitude: Double?
var startDate: Date?
var endDate: Date?
var distanceKm: Double?
var maxParticipants: Int?
}
// MARK: - Event Filter
struct EventFilter: Encodable {
var eventType: EventType?
var startDate: Date?
var endDate: Date?
var location: String?
var radiusKm: Double?
var rsvpStatus: RSVPStatus?
var limit: Int
var offset: Int
init(
eventType: EventType? = nil,
startDate: Date? = nil,
endDate: Date? = nil,
location: String? = nil,
radiusKm: Double? = nil,
rsvpStatus: RSVPStatus? = nil,
limit: Int = 20,
offset: Int = 0
) {
self.eventType = eventType
self.startDate = startDate
self.endDate = endDate
self.location = location
self.radiusKm = radiusKm
self.rsvpStatus = rsvpStatus
self.limit = limit
self.offset = offset
}
}
// MARK: - API Response Types
struct EventListResponse: Decodable {
let events: [CommunityEvent]
let hasMore: Bool
}
struct EventDetailResponse: Decodable {
let event: CommunityEvent
let participants: [EventParticipant]
}
struct CreateEventResponse: Decodable {
let event: CommunityEvent
}
struct UpdateEventResponse: Decodable {
let event: CommunityEvent
}
struct RSVPResponse: Decodable {
let success: Bool
let eventId: String
let status: RSVPStatus
}

View File

@@ -0,0 +1,217 @@
import Foundation
import SwiftUI
// MARK: - Family Plan
struct FamilyPlan: Identifiable, Equatable, Codable {
let id: String
let ownerId: String
let ownerName: String
let members: [FamilyMember]
let maxMembers: Int
let subscriptionStatus: SubscriptionStatus
let renewalDate: Date?
let createdAt: Date
enum CodingKeys: String, CodingKey {
case id, ownerId, ownerName, members, maxMembers, subscriptionStatus, renewalDate, createdAt
}
init(
id: String,
ownerId: String,
ownerName: String,
members: [FamilyMember],
maxMembers: Int,
subscriptionStatus: SubscriptionStatus,
renewalDate: Date?,
createdAt: Date
) {
self.id = id
self.ownerId = ownerId
self.ownerName = ownerName
self.members = members
self.maxMembers = maxMembers
self.subscriptionStatus = subscriptionStatus
self.renewalDate = renewalDate
self.createdAt = createdAt
}
static func == (lhs: FamilyPlan, rhs: FamilyPlan) -> Bool {
lhs.id == rhs.id
}
var availableSlots: Int {
maxMembers - members.count
}
var isActive: Bool {
subscriptionStatus == .active
}
}
// MARK: - Family Member
struct FamilyMember: Identifiable, Equatable, Codable {
let id: String
let name: String
let email: String
let role: MemberRole
let joinedAt: Date
let avatarUrl: String?
var isPrimary: Bool
var totalDistanceKm: Double
var totalWorkouts: Int
var weeklyDistanceKm: Double
var weeklyWorkouts: Int
enum CodingKeys: String, CodingKey {
case id, name, email, role, joinedAt, avatarUrl, isPrimary, totalDistanceKm, totalWorkouts, weeklyDistanceKm, weeklyWorkouts
}
init(
id: String,
name: String,
email: String,
role: MemberRole,
joinedAt: Date,
avatarUrl: String?,
isPrimary: Bool,
totalDistanceKm: Double,
totalWorkouts: Int,
weeklyDistanceKm: Double,
weeklyWorkouts: Int
) {
self.id = id
self.name = name
self.email = email
self.role = role
self.joinedAt = joinedAt
self.avatarUrl = avatarUrl
self.isPrimary = isPrimary
self.totalDistanceKm = totalDistanceKm
self.totalWorkouts = totalWorkouts
self.weeklyDistanceKm = weeklyDistanceKm
self.weeklyWorkouts = weeklyWorkouts
}
static func == (lhs: FamilyMember, rhs: FamilyMember) -> Bool {
lhs.id == rhs.id
}
}
// MARK: - Member Role
enum MemberRole: String, CaseIterable, Codable {
case owner
case member
case pending
var displayName: String {
switch self {
case .owner: return "Owner"
case .member: return "Member"
case .pending: return "Pending"
}
}
var icon: String {
switch self {
case .owner: return "star.fill"
case .member: return "person.fill"
case .pending: return "person.crop.circle.badge.exclamationmark"
}
}
}
// MARK: - Subscription Status
enum SubscriptionStatus: String, CaseIterable, Codable {
case active
case expired
case cancelled
case pending
var displayName: String {
switch self {
case .active: return "Active"
case .expired: return "Expired"
case .cancelled: return "Cancelled"
case .pending: return "Pending"
}
}
var color: Color {
switch self {
case .active: return .green
case .expired: return .red
case .cancelled: return .orange
case .pending: return .yellow
}
}
}
// MARK: - Family Leaderboard Entry
struct FamilyLeaderboardEntry: Identifiable, Codable {
let id: String
let memberId: String
let memberName: String
let avatarUrl: String?
let metric: LeaderboardMetric
let value: Double
let rank: Int
}
// MARK: - Leaderboard Metric
enum LeaderboardMetric: String, CaseIterable, Codable {
case distance
case workouts
case streak
var displayName: String {
switch self {
case .distance: return "Distance"
case .workouts: return "Workouts"
case .streak: return "Streak"
}
}
var unit: String {
switch self {
case .distance: return "km"
case .workouts: return ""
case .streak: return "days"
}
}
}
// MARK: - Invite Member Request
struct InviteMemberRequest: Encodable {
let email: String
let name: String
}
// MARK: - API Response Types
struct FamilyPlanDetailResponse: Decodable {
let plan: FamilyPlan
}
struct InviteMemberResponse: Decodable {
let success: Bool
let invitationId: String
let memberEmail: String
}
struct RemoveMemberResponse: Decodable {
let success: Bool
let memberId: String
}
struct FamilyLeaderboardResponse: Decodable {
let entries: [FamilyLeaderboardEntry]
let metric: LeaderboardMetric
}

183
Lendair/Models/Race.swift Normal file
View File

@@ -0,0 +1,183 @@
import Foundation
import SwiftUI
// MARK: - Race
struct Race: Identifiable, Equatable, Codable {
let id: String
let name: String
let description: String
let location: String
let latitude: Double
let longitude: Double
let raceDate: Date
let distanceKm: Double
let raceType: RaceType
let organizerName: String
let registrationUrl: String?
let imageUrl: String?
let participantCount: Int?
let isRegistered: Bool
let isSaved: Bool
let elevationGain: Double
let terrainType: TerrainType
enum CodingKeys: String, CodingKey {
case id, name, description, location, latitude, longitude, raceDate, distanceKm, raceType, organizerName, registrationUrl, imageUrl, participantCount, isRegistered, isSaved, elevationGain, terrainType
}
init(
id: String,
name: String,
description: String,
location: String,
latitude: Double,
longitude: Double,
raceDate: Date,
distanceKm: Double,
raceType: RaceType,
organizerName: String,
registrationUrl: String?,
imageUrl: String?,
participantCount: Int?,
isRegistered: Bool,
isSaved: Bool,
elevationGain: Double,
terrainType: TerrainType
) {
self.id = id
self.name = name
self.description = description
self.location = location
self.latitude = latitude
self.longitude = longitude
self.raceDate = raceDate
self.distanceKm = distanceKm
self.raceType = raceType
self.organizerName = organizerName
self.registrationUrl = registrationUrl
self.imageUrl = imageUrl
self.participantCount = participantCount
self.isRegistered = isRegistered
self.isSaved = isSaved
self.elevationGain = elevationGain
self.terrainType = terrainType
}
static func == (lhs: Race, rhs: Race) -> Bool {
lhs.id == rhs.id && lhs.isRegistered == rhs.isRegistered && lhs.isSaved == rhs.isSaved
}
var daysUntilRace: Int {
let calendar = Calendar.current
return calendar.dateComponents([.day], from: Date(), to: raceDate).day ?? 0
}
var isUpcoming: Bool {
raceDate > Date()
}
}
// MARK: - Race Type
enum RaceType: String, CaseIterable, Codable {
case road
case trail
case track
case virtual
var displayName: String {
switch self {
case .road: return "Road"
case .trail: return "Trail"
case .track: return "Track"
case .virtual: return "Virtual"
}
}
var icon: String {
switch self {
case .road: return "car.fill"
case .trail: return "mountain.2.fill"
case .track: return "circle.fill"
case .virtual: return "globe"
}
}
}
// MARK: - Terrain Type
enum TerrainType: String, CaseIterable, Codable {
case flat
case rolling
case hilly
case mountainous
var displayName: String {
switch self {
case .flat: return "Flat"
case .rolling: return "Rolling"
case .hilly: return "Hilly"
case .mountainous: return "Mountainous"
}
}
}
// MARK: - Race Filter
struct RaceFilter: Encodable {
var distanceKm: Double?
var raceType: RaceType?
var terrainType: TerrainType?
var startDate: Date?
var endDate: Date?
var location: String?
var radiusKm: Double?
var limit: Int
var offset: Int
init(
distanceKm: Double? = nil,
raceType: RaceType? = nil,
terrainType: TerrainType? = nil,
startDate: Date? = nil,
endDate: Date? = nil,
location: String? = nil,
radiusKm: Double? = nil,
limit: Int = 20,
offset: Int = 0
) {
self.distanceKm = distanceKm
self.raceType = raceType
self.terrainType = terrainType
self.startDate = startDate
self.endDate = endDate
self.location = location
self.radiusKm = radiusKm
self.limit = limit
self.offset = offset
}
}
// MARK: - API Response Types
struct RaceListResponse: Decodable {
let races: [Race]
let hasMore: Bool
}
struct RaceDetailResponse: Decodable {
let race: Race
}
struct SaveRaceResponse: Decodable {
let success: Bool
let raceId: String
let isSaved: Bool
}
struct RegisterRaceResponse: Decodable {
let success: Bool
let raceId: String
let registrationUrl: String?
}

View 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
}