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:
217
Lendair/Models/FamilyPlan.swift
Normal file
217
Lendair/Models/FamilyPlan.swift
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user