feat(polls): implement group trip polling MVP

Add complete group trip polling feature allowing users to share trips
with friends for voting using Borda count scoring.

New components:
- TripPoll and PollVote domain models with share codes and rankings
- LocalTripPoll and LocalPollVote SwiftData models for persistence
- CKTripPoll and CKPollVote CloudKit record wrappers
- PollService actor for CloudKit CRUD operations and subscriptions
- PollCreation/Detail/Voting views and view models
- Deep link handling for sportstime://poll/{code} URLs
- Debug Pro status override toggle in Settings

Integration:
- HomeView shows polls section in My Trips
- SportsTimeApp registers SwiftData models and handles deep links

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-13 21:54:42 -06:00
parent 8e78828bde
commit 13385b6562
16 changed files with 2416 additions and 19 deletions

View File

@@ -0,0 +1,134 @@
//
// LocalPoll.swift
// SportsTime
//
// SwiftData models for local poll persistence
//
import Foundation
import SwiftData
// MARK: - Local Trip Poll
@Model
final class LocalTripPoll {
@Attribute(.unique) var id: UUID
var title: String
var ownerId: String
var shareCode: String
var tripSnapshotsData: Data // Encoded [Trip]
var tripVersions: [String]
var createdAt: Date
var modifiedAt: Date
var lastSyncedAt: Date?
@Relationship(deleteRule: .cascade)
var votes: [LocalPollVote]?
init(
id: UUID = UUID(),
title: String,
ownerId: String,
shareCode: String,
tripSnapshotsData: Data,
tripVersions: [String],
createdAt: Date = Date(),
modifiedAt: Date = Date(),
lastSyncedAt: Date? = nil
) {
self.id = id
self.title = title
self.ownerId = ownerId
self.shareCode = shareCode
self.tripSnapshotsData = tripSnapshotsData
self.tripVersions = tripVersions
self.createdAt = createdAt
self.modifiedAt = modifiedAt
self.lastSyncedAt = lastSyncedAt
}
var tripSnapshots: [Trip] {
(try? JSONDecoder().decode([Trip].self, from: tripSnapshotsData)) ?? []
}
func toPoll() -> TripPoll {
var poll = TripPoll(
id: id,
title: title,
ownerId: ownerId,
shareCode: shareCode,
tripSnapshots: tripSnapshots,
createdAt: createdAt,
modifiedAt: modifiedAt
)
poll.tripVersions = tripVersions
return poll
}
static func from(_ poll: TripPoll) -> LocalTripPoll? {
guard let tripsData = try? JSONEncoder().encode(poll.tripSnapshots) else { return nil }
return LocalTripPoll(
id: poll.id,
title: poll.title,
ownerId: poll.ownerId,
shareCode: poll.shareCode,
tripSnapshotsData: tripsData,
tripVersions: poll.tripVersions,
createdAt: poll.createdAt,
modifiedAt: poll.modifiedAt
)
}
}
// MARK: - Local Poll Vote
@Model
final class LocalPollVote {
@Attribute(.unique) var id: UUID
var pollId: UUID
var voterId: String
var rankings: [Int]
var votedAt: Date
var modifiedAt: Date
var lastSyncedAt: Date?
init(
id: UUID = UUID(),
pollId: UUID,
voterId: String,
rankings: [Int],
votedAt: Date = Date(),
modifiedAt: Date = Date(),
lastSyncedAt: Date? = nil
) {
self.id = id
self.pollId = pollId
self.voterId = voterId
self.rankings = rankings
self.votedAt = votedAt
self.modifiedAt = modifiedAt
self.lastSyncedAt = lastSyncedAt
}
func toVote() -> PollVote {
PollVote(
id: id,
pollId: pollId,
odg: voterId,
rankings: rankings,
votedAt: votedAt,
modifiedAt: modifiedAt
)
}
static func from(_ vote: PollVote) -> LocalPollVote {
LocalPollVote(
id: vote.id,
pollId: vote.pollId,
voterId: vote.odg,
rankings: vote.rankings,
votedAt: vote.votedAt,
modifiedAt: vote.modifiedAt
)
}
}