- 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>
218 lines
4.9 KiB
Swift
218 lines
4.9 KiB
Swift
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
|
|
}
|