From 671edd34128baf9ad9698d279aabd020994a088e Mon Sep 17 00:00:00 2001 From: Laurent Date: Fri, 18 Oct 2024 10:22:57 +0200 Subject: [PATCH] first commit --- PadelClub.xcodeproj/project.pbxproj | 20 +- PadelClub/Data/Club.swift | 28 +- PadelClub/Data/Court.swift | 21 +- .../Data/{User.swift => CustomUser.swift} | 17 +- PadelClub/Data/DataStore.swift | 62 ++- PadelClub/Data/DateInterval.swift | 10 +- PadelClub/Data/Event.swift | 16 +- PadelClub/Data/GroupStage.swift | 62 +-- PadelClub/Data/Match.swift | 90 +--- PadelClub/Data/MonthData.swift | 4 +- PadelClub/Data/PlayerRegistration.swift | 22 +- PadelClub/Data/README.md | 1 - PadelClub/Data/Round.swift | 63 +-- PadelClub/Data/TeamRegistration.swift | 58 +-- PadelClub/Data/TeamScore.swift | 13 +- PadelClub/Data/Tournament.swift | 132 ++--- PadelClub/Data/TournamentStore.swift | 17 +- PadelClub/Utils/SourceFileManager.swift | 4 +- PadelClub/Views/Club/ClubDetailView.swift | 6 +- .../LoserBracketFromGroupStageView.swift | 26 +- .../Agenda/TournamentSubscriptionView.swift | 4 +- PadelClub/Views/Round/RoundSettingsView.swift | 53 +- .../Views/Tournament/Screen/AddTeamView.swift | 476 +++++++++++------- .../Views/Tournament/Subscription/Guard.swift | 1 - .../Tournament/Subscription/Purchase.swift | 10 +- PadelClub/Views/User/AccountView.swift | 2 +- PadelClub/Views/User/LoginView.swift | 4 +- PadelClub/Views/User/UserCreationView.swift | 2 +- PadelClubTests/ServerDataTests.swift | 32 +- PadelClubTests/SynchronizationTests.swift | 33 ++ PadelClubTests/UserDataTests.swift | 8 +- 31 files changed, 657 insertions(+), 640 deletions(-) rename PadelClub/Data/{User.swift => CustomUser.swift} (93%) create mode 100644 PadelClubTests/SynchronizationTests.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index b8a1649..8de2d45 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -42,12 +42,13 @@ C4A47D9F2B7D0BCE00ADC637 /* StepperView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D9E2B7D0BCE00ADC637 /* StepperView.swift */; }; C4A47DA62B83948E00ADC637 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47DA52B83948E00ADC637 /* LoginView.swift */; }; C4A47DA92B85F82100ADC637 /* ChangePasswordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47DA82B85F82100ADC637 /* ChangePasswordView.swift */; }; - C4A47DAD2B85FCCD00ADC637 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47DAC2B85FCCD00ADC637 /* User.swift */; }; + C4A47DAD2B85FCCD00ADC637 /* CustomUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47DAC2B85FCCD00ADC637 /* CustomUser.swift */; }; C4A47DB32B86387500ADC637 /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47DB22B86387500ADC637 /* AccountView.swift */; }; C4B3A1552C2581DA0078EAA8 /* Patcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B3A1542C2581DA0078EAA8 /* Patcher.swift */; }; C4C01D982C481C0C0059087C /* CapsuleViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C01D972C481C0C0059087C /* CapsuleViewModifier.swift */; }; C4C33F762C9B1ED4006316DE /* CodingContainer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C33F752C9B1EC5006316DE /* CodingContainer+Extensions.swift */; }; C4C33F772C9B1ED4006316DE /* CodingContainer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C33F752C9B1EC5006316DE /* CodingContainer+Extensions.swift */; }; + C4D477992CB6704C0077713D /* SynchronizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D477982CB6704C0077713D /* SynchronizationTests.swift */; }; C4EC6F572BE92CAC000CEAB4 /* local.plist in Resources */ = {isa = PBXBuildFile; fileRef = C4EC6F562BE92CAC000CEAB4 /* local.plist */; }; C4EC6F592BE92D88000CEAB4 /* PListReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC6F582BE92D88000CEAB4 /* PListReader.swift */; }; C4FC2E272C2AABC90021F3BF /* PasswordField.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FC2E262C2AABC90021F3BF /* PasswordField.swift */; }; @@ -308,7 +309,7 @@ FF4CBFFA2C996C0600151637 /* TournamentScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF0E0B6C2BC254C6005F00A9 /* TournamentScheduleView.swift */; }; FF4CBFFB2C996C0600151637 /* MatchFormatStorageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AF02BD1AEBD00A86CF8 /* MatchFormatStorageView.swift */; }; FF4CBFFC2C996C0600151637 /* UmpireView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3F74F52B919E45004CFE0E /* UmpireView.swift */; }; - FF4CBFFD2C996C0600151637 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47DAC2B85FCCD00ADC637 /* User.swift */; }; + FF4CBFFD2C996C0600151637 /* CustomUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47DAC2B85FCCD00ADC637 /* CustomUser.swift */; }; FF4CBFFE2C996C0600151637 /* MatchSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967D002BAEF0B400A9A3BD /* MatchSummaryView.swift */; }; FF4CBFFF2C996C0600151637 /* TournamentDurationManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26442BAE0A3400650388 /* TournamentDurationManagerView.swift */; }; FF4CC0002C996C0600151637 /* MockData.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1DC5522BAB354A00FD8220 /* MockData.swift */; }; @@ -611,7 +612,7 @@ FF70FB792C90584900129CC2 /* TournamentScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF0E0B6C2BC254C6005F00A9 /* TournamentScheduleView.swift */; }; FF70FB7A2C90584900129CC2 /* MatchFormatStorageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AF02BD1AEBD00A86CF8 /* MatchFormatStorageView.swift */; }; FF70FB7B2C90584900129CC2 /* UmpireView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3F74F52B919E45004CFE0E /* UmpireView.swift */; }; - FF70FB7C2C90584900129CC2 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47DAC2B85FCCD00ADC637 /* User.swift */; }; + FF70FB7C2C90584900129CC2 /* CustomUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47DAC2B85FCCD00ADC637 /* CustomUser.swift */; }; FF70FB7D2C90584900129CC2 /* MatchSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967D002BAEF0B400A9A3BD /* MatchSummaryView.swift */; }; FF70FB7E2C90584900129CC2 /* TournamentDurationManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26442BAE0A3400650388 /* TournamentDurationManagerView.swift */; }; FF70FB7F2C90584900129CC2 /* MockData.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1DC5522BAB354A00FD8220 /* MockData.swift */; }; @@ -897,11 +898,12 @@ C4A47D9E2B7D0BCE00ADC637 /* StepperView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepperView.swift; sourceTree = ""; }; C4A47DA52B83948E00ADC637 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; C4A47DA82B85F82100ADC637 /* ChangePasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangePasswordView.swift; sourceTree = ""; }; - C4A47DAC2B85FCCD00ADC637 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; + C4A47DAC2B85FCCD00ADC637 /* CustomUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomUser.swift; sourceTree = ""; }; C4A47DB22B86387500ADC637 /* AccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountView.swift; sourceTree = ""; }; C4B3A1542C2581DA0078EAA8 /* Patcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Patcher.swift; sourceTree = ""; }; C4C01D972C481C0C0059087C /* CapsuleViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapsuleViewModifier.swift; sourceTree = ""; }; C4C33F752C9B1EC5006316DE /* CodingContainer+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CodingContainer+Extensions.swift"; sourceTree = ""; }; + C4D477982CB6704C0077713D /* SynchronizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronizationTests.swift; sourceTree = ""; }; C4EC6F562BE92CAC000CEAB4 /* local.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = local.plist; sourceTree = ""; }; C4EC6F582BE92D88000CEAB4 /* PListReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PListReader.swift; sourceTree = ""; }; C4FC2E262C2AABC90021F3BF /* PasswordField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordField.swift; sourceTree = ""; }; @@ -1293,6 +1295,7 @@ C411C9C22BEBA453003017AD /* ServerDataTests.swift */, C411C9C82BF219CB003017AD /* UserDataTests.swift */, C411C9CF2BF38F41003017AD /* TokenExemptionTests.swift */, + C4D477982CB6704C0077713D /* SynchronizationTests.swift */, ); path = PadelClubTests; sourceTree = ""; @@ -1320,7 +1323,7 @@ C411C9CC2BF21DAF003017AD /* README.md */, C4A47D5D2B6D38EC00ADC637 /* DataStore.swift */, C4FC2E2A2C2C0E4D0021F3BF /* TournamentStore.swift */, - C4A47DAC2B85FCCD00ADC637 /* User.swift */, + C4A47DAC2B85FCCD00ADC637 /* CustomUser.swift */, C4A47D592B6D383C00ADC637 /* Tournament.swift */, FF967CE72BAEC70100A9A3BD /* GroupStage.swift */, FF967CED2BAECBD700A9A3BD /* Round.swift */, @@ -2405,7 +2408,7 @@ FF0E0B6D2BC254C6005F00A9 /* TournamentScheduleView.swift in Sources */, FF025AF12BD1AEBD00A86CF8 /* MatchFormatStorageView.swift in Sources */, FF3F74F62B919E45004CFE0E /* UmpireView.swift in Sources */, - C4A47DAD2B85FCCD00ADC637 /* User.swift in Sources */, + C4A47DAD2B85FCCD00ADC637 /* CustomUser.swift in Sources */, C4C33F762C9B1ED4006316DE /* CodingContainer+Extensions.swift in Sources */, FF967D012BAEF0B400A9A3BD /* MatchSummaryView.swift in Sources */, FF8F26452BAE0A3400650388 /* TournamentDurationManagerView.swift in Sources */, @@ -2470,6 +2473,7 @@ files = ( C411C9D02BF38F41003017AD /* TokenExemptionTests.swift in Sources */, C49EF0422BE23BF50077B5AA /* PaymentTests.swift in Sources */, + C4D477992CB6704C0077713D /* SynchronizationTests.swift in Sources */, C425D4122B6D249E002A7B48 /* PadelClubTests.swift in Sources */, C411C9C92BF219CB003017AD /* UserDataTests.swift in Sources */, C411C9C32BEBA453003017AD /* ServerDataTests.swift in Sources */, @@ -2675,7 +2679,7 @@ FFBA2D2D2CA2CE9E00D5BBDD /* CodingContainer+Extensions.swift in Sources */, FF4CBFFB2C996C0600151637 /* MatchFormatStorageView.swift in Sources */, FF4CBFFC2C996C0600151637 /* UmpireView.swift in Sources */, - FF4CBFFD2C996C0600151637 /* User.swift in Sources */, + FF4CBFFD2C996C0600151637 /* CustomUser.swift in Sources */, FF4CBFFE2C996C0600151637 /* MatchSummaryView.swift in Sources */, FF4CBFFF2C996C0600151637 /* TournamentDurationManagerView.swift in Sources */, FF4CC0002C996C0600151637 /* MockData.swift in Sources */, @@ -2922,7 +2926,7 @@ FF70FB792C90584900129CC2 /* TournamentScheduleView.swift in Sources */, FF70FB7A2C90584900129CC2 /* MatchFormatStorageView.swift in Sources */, FF70FB7B2C90584900129CC2 /* UmpireView.swift in Sources */, - FF70FB7C2C90584900129CC2 /* User.swift in Sources */, + FF70FB7C2C90584900129CC2 /* CustomUser.swift in Sources */, C4C33F772C9B1ED4006316DE /* CodingContainer+Extensions.swift in Sources */, FF70FB7D2C90584900129CC2 /* MatchSummaryView.swift in Sources */, FF70FB7E2C90584900129CC2 /* TournamentDurationManagerView.swift in Sources */, diff --git a/PadelClub/Data/Club.swift b/PadelClub/Data/Club.swift index 58ef226..b7dc5ad 100644 --- a/PadelClub/Data/Club.swift +++ b/PadelClub/Data/Club.swift @@ -10,22 +10,15 @@ import SwiftUI import LeStorage @Observable -final class Club : ModelObject, Storable, Hashable { +final class Club: ModelObject, SyncedStorable { static func resourceName() -> String { return "clubs" } static func tokenExemptedMethods() -> [HTTPMethod] { return [.get] } static func filterByStoreIdentifier() -> Bool { return false } static var relationshipNames: [String] = [] - static func == (lhs: Club, rhs: Club) -> Bool { - lhs.id == rhs.id - } - - func hash(into hasher: inout Hasher) { - return hasher.combine(id) - } - var id: String = Store.randomId() + var lastUpdate: Date var creator: String? var name: String var acronym: String @@ -41,7 +34,11 @@ final class Club : ModelObject, Storable, Hashable { var broadcastCode: String? // var alphabeticalName: Bool = false + var storeId: String? { return nil } + internal init(creator: String? = nil, name: String, acronym: String? = nil, phone: String? = nil, code: String? = nil, address: String? = nil, city: String? = nil, zipCode: String? = nil, latitude: Double? = nil, longitude: Double? = nil, courtCount: Int = 2, broadcastCode: String? = nil) { + self.lastUpdate = Date() + self.name = name self.creator = creator self.acronym = acronym ?? name.acronym() @@ -54,8 +51,14 @@ final class Club : ModelObject, Storable, Hashable { self.longitude = longitude self.courtCount = courtCount self.broadcastCode = broadcastCode + + super.init() } +// required init(from decoder: any Decoder) throws { +// fatalError("init(from:) has not been implemented") +// } + override func copyFromServerInstance(_ instance: any Storable) -> Bool { guard let copy = instance as? Club else { return false } self.broadcastCode = copy.broadcastCode @@ -80,16 +83,17 @@ final class Club : ModelObject, Storable, Hashable { DataStore.shared.courts.filter { $0.club == self.id }.sorted(by: \.index) } - override func deleteDependencies() throws { + override func deleteDependencies() { let customizedCourts = self.customizedCourts for customizedCourt in customizedCourts { - try customizedCourt.deleteDependencies() + customizedCourt.deleteDependencies() } DataStore.shared.courts.deleteDependencies(customizedCourts) } enum CodingKeys: String, CodingKey { case _id = "id" + case _lastUpdate = "lastUpdate" case _creator = "creator" case _name = "name" case _acronym = "acronym" @@ -106,9 +110,11 @@ final class Club : ModelObject, Storable, Hashable { } func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: ._id) + try container.encode(lastUpdate, forKey: ._lastUpdate) try container.encode(creator, forKey: ._creator) try container.encode(name, forKey: ._name) try container.encode(acronym, forKey: ._acronym) diff --git a/PadelClub/Data/Court.swift b/PadelClub/Data/Court.swift index e7751bc..3853c3a 100644 --- a/PadelClub/Data/Court.swift +++ b/PadelClub/Data/Court.swift @@ -10,7 +10,8 @@ import SwiftUI import LeStorage @Observable -final class Court : ModelObject, Storable, Hashable { +final class Court : ModelObject, SyncedStorable { + static func resourceName() -> String { return "courts" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func filterByStoreIdentifier() -> Bool { return false } @@ -20,31 +21,25 @@ final class Court : ModelObject, Storable, Hashable { lhs.id == rhs.id } - func hash(into hasher: inout Hasher) { - return hasher.combine(id) - } - var id: String = Store.randomId() + var lastUpdate: Date var index: Int var club: String var name: String? var exitAllowed: Bool = false var indoor: Bool = false + var storeId: String? { return nil } + init(index: Int, club: String, name: String? = nil, exitAllowed: Bool = false, indoor: Bool = false) { self.index = index + self.lastUpdate = Date() self.club = club self.name = name self.exitAllowed = exitAllowed self.indoor = indoor } -// internal init(club: String, name: String? = nil, index: Int) { -// self.club = club -// self.name = name -// self.index = index -// } - func courtTitle() -> String { self.name ?? courtIndexTitle() } @@ -61,11 +56,12 @@ final class Court : ModelObject, Storable, Hashable { Store.main.findById(club) } - override func deleteDependencies() throws { + override func deleteDependencies() { } enum CodingKeys: String, CodingKey { case _id = "id" + case _lastUpdate = "lastUpdate" case _index = "index" case _club = "club" case _name = "name" @@ -77,6 +73,7 @@ final class Court : ModelObject, Storable, Hashable { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: ._id) + try container.encode(lastUpdate, forKey: ._lastUpdate) try container.encode(index, forKey: ._index) try container.encode(club, forKey: ._club) try container.encode(name, forKey: ._name) diff --git a/PadelClub/Data/User.swift b/PadelClub/Data/CustomUser.swift similarity index 93% rename from PadelClub/Data/User.swift rename to PadelClub/Data/CustomUser.swift index 06ff5bd..3d0b71d 100644 --- a/PadelClub/Data/User.swift +++ b/PadelClub/Data/CustomUser.swift @@ -15,7 +15,7 @@ enum UserRight: Int, Codable { } @Observable -class User: ModelObject, UserBase, Storable { +class CustomUser: ModelObject, UserBase, SyncedStorable { static func resourceName() -> String { "users" } static func tokenExemptedMethods() -> [HTTPMethod] { return [.post] } @@ -23,6 +23,7 @@ class User: ModelObject, UserBase, Storable { static var relationshipNames: [String] = [] public var id: String = Store.randomId() + var lastUpdate: Date public var username: String public var email: String var clubs: [String] = [] @@ -46,8 +47,11 @@ class User: ModelObject, UserBase, Storable { var loserBracketMode: LoserBracketMode = .automatic var deviceId: String? + + var storeId: String? { return nil } init(username: String, email: String, firstName: String, lastName: String, phone: String?, country: String?, loserBracketMode: LoserBracketMode = .automatic) { + self.lastUpdate = Date() self.username = username self.firstName = firstName self.lastName = lastName @@ -121,6 +125,7 @@ class User: ModelObject, UserBase, Storable { enum CodingKeys: String, CodingKey { case _id = "id" + case _lastUpdate = "lastUpdate" case _username = "username" case _email = "email" case _clubs = "clubs" @@ -149,6 +154,7 @@ class User: ModelObject, UserBase, Storable { // Required properties id = try container.decodeIfPresent(String.self, forKey: ._id) ?? Store.randomId() + lastUpdate = try container.decodeIfPresent(Date.self, forKey: ._lastUpdate) ?? Date() username = try container.decode(String.self, forKey: ._username) email = try container.decode(String.self, forKey: ._email) firstName = try container.decode(String.self, forKey: ._firstName) @@ -181,6 +187,7 @@ class User: ModelObject, UserBase, Storable { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: ._id) + try container.encode(lastUpdate, forKey: ._lastUpdate) try container.encode(username, forKey: ._username) try container.encode(email, forKey: ._email) try container.encode(clubs, forKey: ._clubs) @@ -207,15 +214,15 @@ class User: ModelObject, UserBase, Storable { try container.encode(loserBracketMode, forKey: ._loserBracketMode) } - static func placeHolder() -> User { - return User(username: "", email: "", firstName: "", lastName: "", phone: nil, country: nil) + static func placeHolder() -> CustomUser { + return CustomUser(username: "", email: "", firstName: "", lastName: "", phone: nil, country: nil) } } -class UserCreationForm: User, UserPasswordBase { +class UserCreationForm: CustomUser, UserPasswordBase { - init(user: User, username: String, password: String, firstName: String, lastName: String, email: String, phone: String?, country: String?) { + init(user: CustomUser, username: String, password: String, firstName: String, lastName: String, email: String, phone: String?, country: String?) { self.password = password super.init(username: username, email: email, firstName: firstName, lastName: lastName, phone: phone, country: country) diff --git a/PadelClub/Data/DataStore.swift b/PadelClub/Data/DataStore.swift index 2a2052e..8991440 100644 --- a/PadelClub/Data/DataStore.swift +++ b/PadelClub/Data/DataStore.swift @@ -13,7 +13,7 @@ class DataStore: ObservableObject { static let shared = DataStore() - @Published var user: User = User.placeHolder() { + @Published var user: CustomUser = CustomUser.placeHolder() { didSet { let loggedUser = StoreCenter.main.userId != nil StoreCenter.main.collectionsCanSynchronize = loggedUser @@ -22,7 +22,7 @@ class DataStore: ObservableObject { if self.user.id != self.userStorage.item()?.id { self.userStorage.setItemNoSync(self.user) if StoreCenter.main.collectionsCanSynchronize { - Store.main.loadCollectionsFromServer() + StoreCenter.main.initialSynchronization() self._fixMissingClubCreatorIfNecessary(self.clubs) self._fixMissingEventCreatorIfNecessary(self.events) } @@ -41,9 +41,9 @@ class DataStore: ObservableObject { fileprivate(set) var dateIntervals: StoredCollection fileprivate(set) var purchases: StoredCollection - fileprivate var userStorage: StoredSingleton + fileprivate var userStorage: StoredSingleton - fileprivate var _temporaryLocalUser: OptionalStorage = OptionalStorage(fileName: "tmp_local_user.json") + fileprivate var _temporaryLocalUser: OptionalStorage = OptionalStorage(fileName: "tmp_local_user.json") fileprivate(set) var appSettingsStorage: MicroStorage = MicroStorage(fileName: "appsettings.json") var appSettings: AppSettings { @@ -75,18 +75,20 @@ class DataStore: ObservableObject { } #endif + StoreCenter.main.forceNoSynchronization = !synchronized + Logger.log("Sync URL: \(StoreCenter.main.synchronizationApiURL ?? "none"), sync: \(synchronized) ") let indexed: Bool = true - self.clubs = store.registerCollection(synchronized: synchronized, indexed: indexed) - self.courts = store.registerCollection(synchronized: synchronized, indexed: indexed) - self.tournaments = store.registerCollection(synchronized: synchronized, indexed: indexed) - self.events = store.registerCollection(synchronized: synchronized, indexed: indexed) - self.monthData = store.registerCollection(synchronized: false, indexed: indexed) - self.dateIntervals = store.registerCollection(synchronized: synchronized, indexed: indexed) - + self.clubs = store.registerSynchronizedCollection(indexed: indexed) + self.courts = store.registerSynchronizedCollection(indexed: indexed) + self.tournaments = store.registerSynchronizedCollection(indexed: indexed) + self.events = store.registerSynchronizedCollection(indexed: indexed) + self.dateIntervals = store.registerSynchronizedCollection(indexed: indexed) self.userStorage = store.registerObject(synchronized: synchronized) - self.purchases = Store.main.registerCollection(synchronized: true, inMemory: true) + self.purchases = Store.main.registerSynchronizedCollection(inMemory: true) + + self.monthData = store.registerCollection(indexed: indexed) // Load ApiCallCollection, making them restart at launch and deletable on disconnect StoreCenter.main.loadApiCallCollection(type: GroupStage.self) @@ -102,14 +104,10 @@ class DataStore: ObservableObject { } func saveUser() { - do { - if user.username.count > 0 { - try self.userStorage.update() - } else { - self._temporaryLocalUser.item = self.user - } - } catch { - Logger.error(error) + if user.username.count > 0 { + self.userStorage.update() + } else { + self._temporaryLocalUser.item = self.user } } @@ -119,8 +117,8 @@ class DataStore: ObservableObject { self.objectWillChange.send() } - if let userSingleton: StoredSingleton = notification.object as? StoredSingleton { - self.user = userSingleton.item() ?? self._temporaryLocalUser.item ?? User.placeHolder() + if let userSingleton: StoredSingleton = notification.object as? StoredSingleton { + self.user = userSingleton.item() ?? self._temporaryLocalUser.item ?? CustomUser.placeHolder() } else if let clubsCollection: StoredCollection = notification.object as? StoredCollection { self._fixMissingClubCreatorIfNecessary(clubsCollection) } else if let eventsCollection: StoredCollection = notification.object as? StoredCollection { @@ -134,17 +132,13 @@ class DataStore: ObservableObject { } fileprivate func _fixMissingClubCreatorIfNecessary(_ clubsCollection: StoredCollection) { - do { - for club in clubsCollection { - if let userId = StoreCenter.main.userId, club.creator == nil { - club.creator = userId - self.userStorage.item()?.addClub(club) - try self.userStorage.update() - clubsCollection.writeChangeAndInsertOnServer(instance: club) - } + for club in clubsCollection { + if let userId = StoreCenter.main.userId, club.creator == nil { + club.creator = userId + self.userStorage.item()?.addClub(club) + self.userStorage.update() + clubsCollection.writeChangeAndInsertOnServer(instance: club) } - } catch { - Logger.error(error) } } @@ -221,7 +215,7 @@ class DataStore: ObservableObject { Guard.main.disconnect() StoreCenter.main.disconnect() - self.user = self._temporaryLocalUser.item ?? User.placeHolder() + self.user = self._temporaryLocalUser.item ?? CustomUser.placeHolder() self.user.clubs.removeAll() // done after because otherwise folders remain @@ -240,7 +234,7 @@ class DataStore: ObservableObject { let login = PListReader.readString(plist: "local", key: "username"), let pass = PListReader.readString(plist: "local", key: "password") { let service = Services(url: url) - let _: User = try await service.login(username: login, password: pass) + let _: CustomUser = try await service.login(username: login, password: pass) tournament.event = nil _ = try await service.post(tournament) diff --git a/PadelClub/Data/DateInterval.swift b/PadelClub/Data/DateInterval.swift index 23438bd..c6a7a9f 100644 --- a/PadelClub/Data/DateInterval.swift +++ b/PadelClub/Data/DateInterval.swift @@ -10,19 +10,22 @@ import SwiftUI import LeStorage @Observable -final class DateInterval: ModelObject, Storable { +final class DateInterval: ModelObject, SyncedStorable { + static func resourceName() -> String { return "date-intervals" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func filterByStoreIdentifier() -> Bool { return false } static var relationshipNames: [String] = [] var id: String = Store.randomId() + var lastUpdate: Date var event: String var courtIndex: Int var startDate: Date var endDate: Date internal init(event: String, courtIndex: Int, startDate: Date, endDate: Date) { + self.lastUpdate = Date() self.event = event self.courtIndex = courtIndex self.startDate = startDate @@ -45,11 +48,12 @@ final class DateInterval: ModelObject, Storable { date <= startDate && date <= endDate && date >= startDate && date >= endDate } - override func deleteDependencies() throws { + override func deleteDependencies() { } enum CodingKeys: String, CodingKey { case _id = "id" + case _lastUpdate = "lastUpdate" case _event = "event" case _courtIndex = "courtIndex" case _startDate = "startDate" @@ -57,7 +61,7 @@ final class DateInterval: ModelObject, Storable { } func insertOnServer() throws { - try DataStore.shared.dateIntervals.writeChangeAndInsertOnServer(instance: self) + DataStore.shared.dateIntervals.writeChangeAndInsertOnServer(instance: self) } } diff --git a/PadelClub/Data/Event.swift b/PadelClub/Data/Event.swift index 0517c72..4b6a59f 100644 --- a/PadelClub/Data/Event.swift +++ b/PadelClub/Data/Event.swift @@ -10,7 +10,7 @@ import LeStorage import SwiftUI @Observable -final class Event: ModelObject, Storable { +final class Event: ModelObject, SyncedStorable { static func resourceName() -> String { return "events" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } @@ -18,30 +18,34 @@ final class Event: ModelObject, Storable { static var relationshipNames: [String] = [] var id: String = Store.randomId() + var lastUpdate: Date var creator: String? var club: String? var creationDate: Date = Date() var name: String? var tenupId: String? + var storeId: String? { return nil } + internal init(creator: String? = nil, club: String? = nil, name: String? = nil, tenupId: String? = nil) { + self.lastUpdate = Date() self.creator = creator self.club = club self.name = name self.tenupId = tenupId } - override func deleteDependencies() throws { + override func deleteDependencies() { let tournaments = self.tournaments for tournament in tournaments { - try tournament.deleteDependencies() + tournament.deleteDependencies() } DataStore.shared.tournaments.deleteDependencies(tournaments) let courtsUnavailabilities = self.courtsUnavailability for courtsUnavailability in courtsUnavailabilities { - try courtsUnavailability.deleteDependencies() + courtsUnavailability.deleteDependencies() } DataStore.shared.dateIntervals.deleteDependencies(courtsUnavailabilities) } @@ -94,7 +98,7 @@ final class Event: ModelObject, Storable { } func insertOnServer() throws { - try DataStore.shared.events.writeChangeAndInsertOnServer(instance: self) + DataStore.shared.events.writeChangeAndInsertOnServer(instance: self) for tournament in self.tournaments { try tournament.insertOnServer() } @@ -109,6 +113,7 @@ final class Event: ModelObject, Storable { extension Event { enum CodingKeys: String, CodingKey { case _id = "id" + case _lastUpdate = "lastUpdate" case _creator = "creator" case _club = "club" case _creationDate = "creationDate" @@ -120,6 +125,7 @@ extension Event { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: ._id) + try container.encode(lastUpdate, forKey: ._lastUpdate) try container.encode(creator, forKey: ._creator) try container.encode(club, forKey: ._club) try container.encode(creationDate, forKey: ._creationDate) diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index f51fe5e..740a730 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -11,13 +11,15 @@ import Algorithms import SwiftUI @Observable -final class GroupStage: ModelObject, Storable { +final class GroupStage: ModelObject, SyncedStorable, SideStorable { + static func resourceName() -> String { "group-stages" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func filterByStoreIdentifier() -> Bool { return true } static var relationshipNames: [String] = [] var id: String = Store.randomId() + var lastUpdate: Date var tournament: String var index: Int var size: Int @@ -35,7 +37,10 @@ final class GroupStage: ModelObject, Storable { } } + var storeId: String? = nil + internal init(tournament: String, index: Int, size: Int, matchFormat: MatchFormat? = nil, startDate: Date? = nil, name: String? = nil, step: Int = 0) { + self.lastUpdate = Date() self.tournament = tournament self.index = index self.size = size @@ -107,7 +112,7 @@ final class GroupStage: ModelObject, Storable { } fileprivate func _createMatch(index: Int) -> Match { - let match: Match = Match(groupStage: self.id, + let match: Match = Match(groupStage: self.id, index: index, matchFormat: self.matchFormat, name: self.localizedMatchUpLabel(for: index)) @@ -128,12 +133,8 @@ final class GroupStage: ModelObject, Storable { matches.append(newMatch) } - do { - try self.tournamentStore.matches.addOrUpdate(contentOfs: matches) - try self.tournamentStore.teamScores.addOrUpdate(contentOfs: teamScores) - } catch { - Logger.error(error) - } + self.tournamentStore.matches.addOrUpdate(contentOfs: matches) + self.tournamentStore.teamScores.addOrUpdate(contentOfs: teamScores) } func playedMatches() -> [Match] { @@ -151,19 +152,15 @@ final class GroupStage: ModelObject, Storable { clearScoreCache() if hasEnded(), let tournament = tournamentObject() { - do { - let teams = teams(true) - for (index, team) in teams.enumerated() { - team.qualified = index < tournament.qualifiedPerGroupStage - if team.bracketPosition != nil && team.qualified == false { - tournamentObject()?.resetTeamScores(in: team.bracketPosition) - team.bracketPosition = nil - } + let teams = teams(true) + for (index, team) in teams.enumerated() { + team.qualified = index < tournament.qualifiedPerGroupStage + if team.bracketPosition != nil && team.qualified == false { + tournamentObject()?.resetTeamScores(in: team.bracketPosition) + team.bracketPosition = nil } - try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams) - } catch { - Logger.error(error) } + self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams) let groupStagesAreOverAtFirstStep = tournament.groupStagesAreOver(atStep: 0) let nextStepGroupStages = tournament.groupStages(atStep: 1) @@ -171,11 +168,7 @@ final class GroupStage: ModelObject, Storable { if groupStagesAreOverAtFirstStep, nextStepGroupStages.isEmpty || groupStagesAreOverAtSecondStep == true, tournament.groupStageLoserBracketAreOver(), tournament.rounds().isEmpty { tournament.endDate = Date() - do { - try DataStore.shared.tournaments.addOrUpdate(instance: tournament) - } catch { - Logger.error(error) - } + DataStore.shared.tournaments.addOrUpdate(instance: tournament) } } } @@ -332,11 +325,7 @@ final class GroupStage: ModelObject, Storable { } private func _removeMatches() { - do { - try self.tournamentStore.matches.delete(contentOfs: _matches()) - } catch { - Logger.error(error) - } + self.tournamentStore.matches.delete(contentOfs: _matches()) } private func _numberOfMatchesToBuild() -> Int { @@ -485,11 +474,7 @@ final class GroupStage: ModelObject, Storable { playedMatches.forEach { match in match.matchFormat = matchFormat } - do { - try self.tournamentStore.matches.addOrUpdate(contentOfs: playedMatches) - } catch { - Logger.error(error) - } + self.tournamentStore.matches.addOrUpdate(contentOfs: playedMatches) } func pasteData() -> String { @@ -507,10 +492,10 @@ final class GroupStage: ModelObject, Storable { return teams(true).firstIndex(of: team) } - override func deleteDependencies() throws { + override func deleteDependencies() { let matches = self._matches() for match in matches { - try match.deleteDependencies() + match.deleteDependencies() } self.tournamentStore.matches.deleteDependencies(matches) } @@ -519,6 +504,7 @@ final class GroupStage: ModelObject, Storable { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(String.self, forKey: ._id) + lastUpdate = try container.decode(Date.self, forKey: ._lastUpdate) tournament = try container.decode(String.self, forKey: ._tournament) index = try container.decode(Int.self, forKey: ._index) size = try container.decode(Int.self, forKey: ._size) @@ -532,6 +518,8 @@ final class GroupStage: ModelObject, Storable { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: ._id) + try container.encode(storeId, forKey: ._storeId) + try container.encode(lastUpdate, forKey: ._lastUpdate) try container.encode(tournament, forKey: ._tournament) try container.encode(index, forKey: ._index) try container.encode(size, forKey: ._size) @@ -553,6 +541,8 @@ final class GroupStage: ModelObject, Storable { extension GroupStage { enum CodingKeys: String, CodingKey { case _id = "id" + case _storeId = "storeId" + case _lastUpdate = "lastUpdate" case _tournament = "tournament" case _index = "index" case _size = "size" diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index 7fee09c..f44eb8e 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -9,7 +9,7 @@ import Foundation import LeStorage @Observable -final class Match: ModelObject, Storable { +final class Match: ModelObject, SyncedStorable, SideStorable { static func resourceName() -> String { "matches" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func filterByStoreIdentifier() -> Bool { return true } @@ -23,6 +23,7 @@ final class Match: ModelObject, Storable { var byeState: Bool = false var id: String = Store.randomId() + var lastUpdate: Date var round: String? var groupStage: String? var startDate: Date? @@ -40,7 +41,10 @@ final class Match: ModelObject, Storable { private(set) var courtIndex: Int? var confirmed: Bool = false + var storeId: String? = nil + init(round: String? = nil, groupStage: String? = nil, startDate: Date? = nil, endDate: Date? = nil, index: Int, matchFormat: MatchFormat? = nil, servingTeamId: String? = nil, winningTeamId: String? = nil, losingTeamId: String? = nil, name: String? = nil, disabled: Bool = false, courtIndex: Int? = nil, confirmed: Bool = false) { + self.lastUpdate = Date() self.round = round self.groupStage = groupStage self.startDate = startDate @@ -78,13 +82,13 @@ final class Match: ModelObject, Storable { // MARK: - - override func deleteDependencies() throws { + override func deleteDependencies() { guard let tournament = self.currentTournament() else { return } let teamScores = self.teamScores for teamScore in teamScores { - try teamScore.deleteDependencies() + teamScore.deleteDependencies() } tournament.tournamentStore.teamScores.deleteDependencies(teamScores) } @@ -207,11 +211,7 @@ defer { endDate = nil followingMatch()?.cleanScheduleAndSave(nil) _loserMatch()?.cleanScheduleAndSave(nil) - do { - try self.tournamentStore.matches.addOrUpdate(instance: self) - } catch { - Logger.error(error) - } + self.tournamentStore.matches.addOrUpdate(instance: self) } func resetMatch() { @@ -227,22 +227,14 @@ defer { func resetScores() { teamScores.forEach({ $0.score = nil }) - do { - try self.tournamentStore.teamScores.addOrUpdate(contentOfs: teamScores) - } catch { - Logger.error(error) - } + self.tournamentStore.teamScores.addOrUpdate(contentOfs: teamScores) } func teamWillBeWalkOut(_ team: TeamRegistration) { resetMatch() let existingTeamScore = teamScore(ofTeam: team) ?? TeamScore(match: id, team: team) existingTeamScore.walkOut = 1 - do { - try self.tournamentStore.teamScores.addOrUpdate(instance: existingTeamScore) - } catch { - Logger.error(error) - } + self.tournamentStore.teamScores.addOrUpdate(instance: existingTeamScore) } func luckyLosers() -> [TeamRegistration] { @@ -260,19 +252,11 @@ defer { let position = matchIndex * 2 + teamPosition.rawValue let previousScores = teamScores.filter({ $0.luckyLoser == position }) - do { - try self.tournamentStore.teamScores.delete(contentOfs: previousScores) - } catch { - Logger.error(error) - } + self.tournamentStore.teamScores.delete(contentOfs: previousScores) let teamScoreLuckyLoser = teamScore(ofTeam: team) ?? TeamScore(match: id, team: team) teamScoreLuckyLoser.luckyLoser = position - do { - try self.tournamentStore.teamScores.addOrUpdate(instance: teamScoreLuckyLoser) - } catch { - Logger.error(error) - } + self.tournamentStore.teamScores.addOrUpdate(instance: teamScoreLuckyLoser) } func disableMatch() { @@ -373,32 +357,19 @@ defer { disabled = state if disabled { - do { - try self.tournamentStore.teamScores.delete(contentOfs: teamScores) - } catch { - Logger.error(error) - } + self.tournamentStore.teamScores.delete(contentOfs: teamScores) } if state == true { let teams = teams() for team in teams { if isSeededBy(team: team) { team.bracketPosition = nil - do { - try self.tournamentStore.teamRegistrations.addOrUpdate(instance: team) - } catch { - Logger.error(error) - } + self.tournamentStore.teamRegistrations.addOrUpdate(instance: team) } } } //byeState = false - do { - try self.tournamentStore.matches.addOrUpdate(instance: self) - } catch { - Logger.error(error) - } - + self.tournamentStore.matches.addOrUpdate(instance: self) if single == false { _toggleLoserMatchDisableState(state) if forward { @@ -506,11 +477,7 @@ defer { teamScoreWalkout.walkOut = 0 let teamScoreWinning = teamScore(teamPosition.otherTeam) ?? TeamScore(match: id, team: team(teamPosition.otherTeam)) teamScoreWinning.walkOut = nil - do { - try self.tournamentStore.teamScores.addOrUpdate(contentOfs: [teamScoreWalkout, teamScoreWinning]) - } catch { - Logger.error(error) - } + self.tournamentStore.teamScores.addOrUpdate(contentOfs: [teamScoreWalkout, teamScoreWinning]) if endDate == nil { endDate = Date() @@ -555,11 +522,7 @@ defer { teamScoreOne.score = matchDescriptor.teamOneScores.joined(separator: ",") let teamScoreTwo = teamScore(.two) ?? TeamScore(match: id, team: team(.two)) teamScoreTwo.score = matchDescriptor.teamTwoScores.joined(separator: ",") - do { - try self.tournamentStore.teamScores.addOrUpdate(contentOfs: [teamScoreOne, teamScoreTwo]) - } catch { - Logger.error(error) - } + self.tournamentStore.teamScores.addOrUpdate(contentOfs: [teamScoreOne, teamScoreTwo]) matchFormat = matchDescriptor.matchFormat } @@ -572,11 +535,7 @@ defer { let ids = newTeamScores.map { $0.id } let teamScores = teamScores.filter({ ids.contains($0.id) == false }) if teamScores.isEmpty == false { - do { - try self.tournamentStore.teamScores.delete(contentOfs: teamScores) - } catch { - Logger.error(error) - } + self.tournamentStore.teamScores.delete(contentOfs: teamScores) followingMatch()?.resetTeamScores(outsideOf: []) _loserMatch()?.resetTeamScores(outsideOf: []) } @@ -598,11 +557,8 @@ defer { func updateTeamScores() { let teams = getOrCreateTeamScores() - do { - try self.tournamentStore.teamScores.addOrUpdate(contentOfs: teams) - } catch { - Logger.error(error) - } + + self.tournamentStore.teamScores.addOrUpdate(contentOfs: teams) resetTeamScores(outsideOf: teams) if teams.isEmpty == false { updateFollowingMatchTeamScore() @@ -883,6 +839,8 @@ defer { enum CodingKeys: String, CodingKey { case _id = "id" + case _storeId = "storeId" + case _lastUpdate = "lastUpdate" case _round = "round" case _groupStage = "groupStage" case _startDate = "startDate" @@ -905,6 +863,8 @@ defer { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: ._id) + try container.encode(storeId, forKey: ._storeId) + try container.encode(lastUpdate, forKey: ._lastUpdate) try container.encode(round, forKey: ._round) try container.encode(groupStage, forKey: ._groupStage) try container.encode(startDate, forKey: ._startDate) @@ -923,7 +883,7 @@ defer { func insertOnServer() { self.tournamentStore.matches.writeChangeAndInsertOnServer(instance: self) for teamScore in self.teamScores { - try teamScore.insertOnServer() + teamScore.insertOnServer() } } diff --git a/PadelClub/Data/MonthData.swift b/PadelClub/Data/MonthData.swift index 676ccd6..a7a102b 100644 --- a/PadelClub/Data/MonthData.swift +++ b/PadelClub/Data/MonthData.swift @@ -10,7 +10,7 @@ import SwiftUI import LeStorage @Observable -final class MonthData : ModelObject, Storable { +final class MonthData: ModelObject, Storable { static func resourceName() -> String { return "month-data" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } @@ -85,7 +85,7 @@ final class MonthData : ModelObject, Storable { } } - override func deleteDependencies() throws { + override func deleteDependencies() { } enum CodingKeys: String, CodingKey { diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index 92d29cf..ca1afa4 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -9,13 +9,14 @@ import Foundation import LeStorage @Observable -final class PlayerRegistration: ModelObject, Storable { +final class PlayerRegistration: ModelObject, SyncedStorable, SideStorable { static func resourceName() -> String { "player-registrations" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func filterByStoreIdentifier() -> Bool { return true } static var relationshipNames: [String] = ["teamRegistration"] var id: String = Store.randomId() + var lastUpdate: Date var teamRegistration: String? var firstName: String var lastName: String @@ -39,7 +40,10 @@ final class PlayerRegistration: ModelObject, Storable { var hasArrived: Bool = false + var storeId: String? = nil + init(teamRegistration: String? = nil, firstName: String, lastName: String, licenceId: String? = nil, rank: Int? = nil, paymentType: PlayerPaymentType? = nil, sex: PlayerSexType? = nil, tournamentPlayed: Int? = nil, points: Double? = nil, clubName: String? = nil, ligueName: String? = nil, assimilation: String? = nil, phoneNumber: String? = nil, email: String? = nil, birthdate: String? = nil, computedRank: Int = 0, source: PlayerDataSource? = nil, hasArrived: Bool = false) { + self.lastUpdate = Date() self.teamRegistration = teamRegistration self.firstName = firstName self.lastName = lastName @@ -61,6 +65,7 @@ final class PlayerRegistration: ModelObject, Storable { } internal init(importedPlayer: ImportedPlayer) { + self.lastUpdate = Date() self.teamRegistration = "" self.firstName = (importedPlayer.firstName ?? "").trimmed.capitalized self.lastName = (importedPlayer.lastName ?? "").trimmed.uppercased() @@ -77,6 +82,7 @@ final class PlayerRegistration: ModelObject, Storable { } internal init?(federalData: [String], sex: Int, sexUnknown: Bool) { + self.lastUpdate = Date() let _lastName = federalData[0].trimmed.uppercased() let _firstName = federalData[1].trimmed.capitalized if _lastName.isEmpty && _firstName.isEmpty { return nil } @@ -323,6 +329,8 @@ final class PlayerRegistration: ModelObject, Storable { enum CodingKeys: String, CodingKey { case _id = "id" + case _storeId = "storeId" + case _lastUpdate = "lastUpdate" case _teamRegistration = "teamRegistration" case _firstName = "firstName" case _lastName = "lastName" @@ -348,6 +356,8 @@ final class PlayerRegistration: ModelObject, Storable { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: ._id) + try container.encode(storeId, forKey: ._storeId) + try container.encode(lastUpdate, forKey: ._lastUpdate) try container.encode(teamRegistration, forKey: ._teamRegistration) try container.encode(firstName, forKey: ._firstName) @@ -454,16 +464,6 @@ final class PlayerRegistration: ModelObject, Storable { } -extension PlayerRegistration: Hashable { - static func == (lhs: PlayerRegistration, rhs: PlayerRegistration) -> Bool { - lhs.id == rhs.id - } - - func hash(into hasher: inout Hasher) { - hasher.combine(id) - } -} - extension PlayerRegistration: PlayerHolder { func getAssimilatedAsMaleRank() -> Int? { nil diff --git a/PadelClub/Data/README.md b/PadelClub/Data/README.md index 334dd01..48056ab 100644 --- a/PadelClub/Data/README.md +++ b/PadelClub/Data/README.md @@ -18,4 +18,3 @@ Dans Django: Enfin, revenir dans Xcode, ouvrir ServerDataTests et lancer le test mis à jour - diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index c618811..e1e4ed5 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -10,13 +10,15 @@ import LeStorage import SwiftUI @Observable -final class Round: ModelObject, Storable { +final class Round: ModelObject, SyncedStorable, SideStorable { + static func resourceName() -> String { "rounds" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func filterByStoreIdentifier() -> Bool { return true } static var relationshipNames: [String] = [] var id: String = Store.randomId() + var lastUpdate: Date var tournament: String var index: Int var parent: String? @@ -25,7 +27,10 @@ final class Round: ModelObject, Storable { var groupStageLoserBracket: Bool = false var loserBracketMode: LoserBracketMode = .automatic + var storeId: String? = nil + internal init(tournament: String, index: Int, parent: String? = nil, matchFormat: MatchFormat? = nil, startDate: Date? = nil, groupStageLoserBracket: Bool = false, loserBracketMode: LoserBracketMode = .automatic) { + self.lastUpdate = Date() self.tournament = tournament self.index = index self.parent = parent @@ -397,11 +402,7 @@ defer { // Logger.error(error) // } } - do { - try self.tournamentStore.matches.addOrUpdate(contentOfs: _matches) - } catch { - Logger.error(error) - } + self.tournamentStore.matches.addOrUpdate(contentOfs: _matches) } var cumulativeMatchCount: Int { @@ -550,11 +551,7 @@ defer { func updateTournamentState() { if let tournamentObject = tournamentObject(), index == 0, isUpperBracket(), hasEnded() { tournamentObject.endDate = Date() - do { - try DataStore.shared.tournaments.addOrUpdate(instance: tournamentObject) - } catch { - Logger.error(error) - } + DataStore.shared.tournaments.addOrUpdate(instance: tournamentObject) } } @@ -596,12 +593,8 @@ defer { } func deleteLoserBracket() { - do { - let loserRounds = loserRounds() - try self.tournamentStore.rounds.delete(contentOfs: loserRounds) - } catch { - Logger.error(error) - } + let loserRounds = loserRounds() + self.tournamentStore.rounds.delete(contentOfs: loserRounds) } func buildLoserBracket() { @@ -620,12 +613,7 @@ defer { round.parent = id //parent return round } - - do { - try self.tournamentStore.rounds.addOrUpdate(contentOfs: rounds) - } catch { - Logger.error(error) - } + self.tournamentStore.rounds.addOrUpdate(contentOfs: rounds) let matchCount = RoundRule.numberOfMatches(forTeams: currentRoundMatchCount) let matches = (0.. Bool { - lhs.id == rhs.id - } - +extension Round: Selectable { func selectionLabel(index: Int) -> String { if let parentRound { diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index aa4f940..0173702 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -10,13 +10,14 @@ import LeStorage import SwiftUI @Observable -final class TeamRegistration: ModelObject, Storable { +final class TeamRegistration: ModelObject, SyncedStorable, SideStorable { static func resourceName() -> String { "team-registrations" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func filterByStoreIdentifier() -> Bool { return true } static var relationshipNames: [String] = [] var id: String = Store.randomId() + var lastUpdate: Date var tournament: String var groupStage: String? var registrationDate: Date? @@ -39,7 +40,12 @@ final class TeamRegistration: ModelObject, Storable { var finalRanking: Int? var pointsEarned: Int? + var storeId: String? = nil + init(tournament: String, groupStage: String? = nil, registrationDate: Date? = nil, callDate: Date? = nil, bracketPosition: Int? = nil, groupStagePosition: Int? = nil, comment: String? = nil, source: String? = nil, sourceValue: String? = nil, logo: String? = nil, name: String? = nil, walkOut: Bool = false, wildCardBracket: Bool = false, wildCardGroupStage: Bool = false, weight: Int = 0, lockedWeight: Int? = nil, confirmationDate: Date? = nil, qualified: Bool = false) { + + self.storeId = tournament + self.lastUpdate = Date() self.tournament = tournament self.groupStage = groupStage self.registrationDate = registrationDate @@ -74,23 +80,19 @@ final class TeamRegistration: ModelObject, Storable { func deleteTeamScores() { let ts = self.tournamentStore.teamScores.filter({ $0.teamRegistration == id }) - do { - try self.tournamentStore.teamScores.delete(contentOfs: ts) - } catch { - Logger.error(error) - } + self.tournamentStore.teamScores.delete(contentOfs: ts) } - override func deleteDependencies() throws { + override func deleteDependencies() { let unsortedPlayers = unsortedPlayers() for player in unsortedPlayers { - try player.deleteDependencies() + player.deleteDependencies() } self.tournamentStore.playerRegistrations.deleteDependencies(unsortedPlayers) let teamScores = teamScores() for teamScore in teamScores { - try teamScore.deleteDependencies() + teamScore.deleteDependencies() } self.tournamentStore.teamScores.deleteDependencies(teamScores) } @@ -98,11 +100,7 @@ final class TeamRegistration: ModelObject, Storable { func hasArrived(isHere: Bool = false) { let unsortedPlayers = unsortedPlayers() unsortedPlayers.forEach({ $0.hasArrived = !isHere }) - do { - try self.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: unsortedPlayers) - } catch { - Logger.error(error) - } + self.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: unsortedPlayers) } func isHere() -> Bool { @@ -301,11 +299,7 @@ final class TeamRegistration: ModelObject, Storable { if let groupStage { let matches = self.tournamentStore.matches.filter({ $0.groupStage == groupStage }).map { $0.id } let teamScores = self.tournamentStore.teamScores.filter({ $0.teamRegistration == id && matches.contains($0.match) }) - do { - try tournamentStore.teamScores.delete(contentOfs: teamScores) - } catch { - Logger.error(error) - } + self.tournamentStore.teamScores.delete(contentOfs: teamScores) } //groupStageObject()?._matches().forEach({ $0.updateTeamScores() }) groupStage = nil @@ -315,11 +309,7 @@ final class TeamRegistration: ModelObject, Storable { func resetBracketPosition() { let matches = self.tournamentStore.matches.filter({ $0.groupStage == nil }).map { $0.id } let teamScores = self.tournamentStore.teamScores.filter({ $0.teamRegistration == id && matches.contains($0.match) }) - do { - try tournamentStore.teamScores.delete(contentOfs: teamScores) - } catch { - Logger.error(error) - } + self.tournamentStore.teamScores.delete(contentOfs: teamScores) self.bracketPosition = nil } @@ -389,11 +379,7 @@ final class TeamRegistration: ModelObject, Storable { func updatePlayers(_ players: Set, inTournamentCategory tournamentCategory: TournamentCategory) { let previousPlayers = Set(unsortedPlayers()) let playersToRemove = previousPlayers.subtracting(players) - do { - try self.tournamentStore.playerRegistrations.delete(contentOfs: playersToRemove) - } catch { - Logger.error(error) - } + self.tournamentStore.playerRegistrations.delete(contentOfs: playersToRemove) setWeight(from: Array(players), inTournamentCategory: tournamentCategory) players.forEach { player in @@ -525,6 +511,8 @@ final class TeamRegistration: ModelObject, Storable { enum CodingKeys: String, CodingKey { case _id = "id" + case _lastUpdate = "lastUpdate" + case _storeId = "storeId" case _tournament = "tournament" case _groupStage = "groupStage" case _registrationDate = "registrationDate" @@ -551,6 +539,8 @@ final class TeamRegistration: ModelObject, Storable { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: ._id) + try container.encode(storeId, forKey: ._storeId) + try container.encode(lastUpdate, forKey: ._lastUpdate) try container.encode(tournament, forKey: ._tournament) try container.encode(groupStage, forKey: ._groupStage) try container.encode(registrationDate, forKey: ._registrationDate) @@ -582,16 +572,6 @@ final class TeamRegistration: ModelObject, Storable { } -extension TeamRegistration: Hashable { - static func == (lhs: TeamRegistration, rhs: TeamRegistration) -> Bool { - lhs.id == rhs.id - } - - func hash(into hasher: inout Hasher) { - hasher.combine(id) - } -} - enum TeamDataSource: Int, Codable { case beachPadel } diff --git a/PadelClub/Data/TeamScore.swift b/PadelClub/Data/TeamScore.swift index 9a7c94b..b996fca 100644 --- a/PadelClub/Data/TeamScore.swift +++ b/PadelClub/Data/TeamScore.swift @@ -9,22 +9,26 @@ import Foundation import LeStorage @Observable -final class TeamScore: ModelObject, Storable { - +final class TeamScore: ModelObject, SyncedStorable, SideStorable { + static func resourceName() -> String { "team-scores" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func filterByStoreIdentifier() -> Bool { return true } static var relationshipNames: [String] = ["match"] var id: String = Store.randomId() + var lastUpdate: Date var match: String var teamRegistration: String? //var playerRegistrations: [String] = [] var score: String? var walkOut: Int? var luckyLoser: Int? + + var storeId: String? = nil init(match: String, teamRegistration: String? = nil, score: String? = nil, walkOut: Int? = nil, luckyLoser: Int? = nil) { + self.lastUpdate = Date() self.match = match self.teamRegistration = teamRegistration // self.playerRegistrations = playerRegistrations @@ -34,6 +38,7 @@ final class TeamScore: ModelObject, Storable { } init(match: String, team: TeamRegistration?) { + self.lastUpdate = Date() self.match = match if let team { self.teamRegistration = team.id @@ -72,6 +77,8 @@ final class TeamScore: ModelObject, Storable { enum CodingKeys: String, CodingKey { case _id = "id" + case _storeId = "storeId" + case _lastUpdate = "lastUpdate" case _match = "match" case _teamRegistration = "teamRegistration" //case _playerRegistrations = "playerRegistrations" @@ -84,6 +91,8 @@ final class TeamScore: ModelObject, Storable { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: ._id) + try container.encode(storeId, forKey: ._storeId) + try container.encode(lastUpdate, forKey: ._lastUpdate) try container.encode(match, forKey: ._match) try container.encode(teamRegistration, forKey: ._teamRegistration) try container.encode(score, forKey: ._score) diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 31f6bbb..bc5f7e0 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -10,13 +10,15 @@ import LeStorage import SwiftUI @Observable -final class Tournament : ModelObject, Storable { +final class Tournament: ModelObject, SyncedStorable { + static func resourceName() -> String { "tournaments" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func filterByStoreIdentifier() -> Bool { return false } static var relationshipNames: [String] = [] var id: String = Store.randomId() + var lastUpdate: Date var event: String? var name: String? var startDate: Date @@ -58,12 +60,15 @@ final class Tournament : ModelObject, Storable { var hidePointsEarned: Bool = false var publishRankings: Bool = false var loserBracketMode: LoserBracketMode = .automatic + + var storeId: String? { return nil } @ObservationIgnored var navigationPath: [Screen] = [] enum CodingKeys: String, CodingKey { case _id = "id" + case _lastUpdate = "lastUpdate" case _event = "event" case _creator = "creator" case _name = "name" @@ -110,6 +115,7 @@ final class Tournament : ModelObject, Storable { } internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = false, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false, shouldVerifyBracket: Bool = false, shouldVerifyGroupStage: Bool = false, hideTeamsWeight: Bool = false, publishTournament: Bool = false, hidePointsEarned: Bool = false, publishRankings: Bool = false, loserBracketMode: LoserBracketMode = .automatic) { + self.lastUpdate = Date() self.event = event self.name = name self.startDate = startDate @@ -153,6 +159,7 @@ final class Tournament : ModelObject, Storable { required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(String.self, forKey: ._id) + lastUpdate = try container.decode(Date.self, forKey: ._lastUpdate) event = try container.decodeIfPresent(String.self, forKey: ._event) name = try container.decodeIfPresent(String.self, forKey: ._name) startDate = try container.decode(Date.self, forKey: ._startDate) @@ -230,6 +237,7 @@ final class Tournament : ModelObject, Storable { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: ._id) + try container.encode(lastUpdate, forKey: ._lastUpdate) try container.encode(event, forKey: ._event) try container.encode(name, forKey: ._name) @@ -321,23 +329,23 @@ final class Tournament : ModelObject, Storable { return TournamentStore.instance(tournamentId: self.id) } - override func deleteDependencies() throws { + override func deleteDependencies() { let store = self.tournamentStore let teams = self.tournamentStore.teamRegistrations - for team in teams { - try team.deleteDependencies() + for team in Array(teams) { + team.deleteDependencies() } store.teamRegistrations.deleteDependencies(teams) let groups = self.tournamentStore.groupStages for group in groups { - try group.deleteDependencies() + group.deleteDependencies() } store.groupStages.deleteDependencies(groups) let rounds = self.tournamentStore.rounds for round in rounds { - try round.deleteDependencies() + round.deleteDependencies() } store.rounds.deleteDependencies(rounds) @@ -1056,17 +1064,8 @@ defer { } } - do { - try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teamsToImport) - } catch { - Logger.error(error) - } - do { - try self.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: teams.flatMap { $0.players }) - } catch { - Logger.error(error) - } - + self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teamsToImport) + self.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: teams.flatMap { $0.players }) if state() == .build && groupStageCount > 0 && groupStageTeams().isEmpty { setGroupStage(randomize: groupStageSortMode == .random) @@ -1316,12 +1315,7 @@ defer { } } - do { - try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams()) - } catch { - Logger.error(error) - } - + self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams()) return rankings } @@ -1336,11 +1330,7 @@ defer { teams.forEach { team in team.lockedWeight = team.weight } - do { - try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams) - } catch { - Logger.error(error) - } + self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams) } func unlockRegistration() { @@ -1349,13 +1339,8 @@ defer { teams.forEach { team in team.lockedWeight = nil } - do { - try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams) - } catch { - Logger.error(error) - } + self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams) } - func updateWeights() { let teams = self.unsortedTeams() @@ -1363,17 +1348,9 @@ defer { let players = team.unsortedPlayers() players.forEach { $0.setComputedRank(in: self) } team.setWeight(from: players, inTournamentCategory: tournamentCategory) - do { - try self.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) - } catch { - Logger.error(error) - } - } - do { - try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams) - } catch { - Logger.error(error) + self.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) } + self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams) } func updateRank(to newDate: Date?) async throws { @@ -1388,11 +1365,7 @@ defer { let monthData: MonthData = MonthData(monthKey: formatted) monthData.maleUnrankedValue = lastRankMan monthData.femaleUnrankedValue = lastRankWoman - do { - try DataStore.shared.monthData.addOrUpdate(instance: monthData) - } catch { - Logger.error(error) - } + DataStore.shared.monthData.addOrUpdate(instance: monthData) } } @@ -1685,12 +1658,7 @@ defer { _groupStages.append(groupStage) } - do { - try self.tournamentStore.groupStages.addOrUpdate(contentOfs: _groupStages) - } catch { - Logger.error(error) - } - + self.tournamentStore.groupStages.addOrUpdate(contentOfs: _groupStages) refreshGroupStages() } @@ -1707,11 +1675,7 @@ defer { return Round(tournament: id, index: $0, matchFormat: roundSmartMatchFormat($0), loserBracketMode: loserBracketMode) } - do { - try self.tournamentStore.rounds.addOrUpdate(contentOfs: rounds) - } catch { - Logger.error(error) - } + self.tournamentStore.rounds.addOrUpdate(contentOfs: rounds) let matchCount = RoundRule.numberOfMatches(forTeams: bracketTeamCount()) let matches = (0.. Bool { @@ -2097,11 +2045,7 @@ defer { let lastStep = lastStep() + 1 for i in 0.. Bool { - lhs.id == rhs.id - } - - func hash(into hasher: inout Hasher) { - hasher.combine(id) - } -} - extension Tournament: FederalTournamentHolder { func tournamentTitle(_ displayStyle: DisplayStyle, forBuild build: any TournamentBuildHolder) -> String { diff --git a/PadelClub/Data/TournamentStore.swift b/PadelClub/Data/TournamentStore.swift index 6cb2fc7..3413068 100644 --- a/PadelClub/Data/TournamentStore.swift +++ b/PadelClub/Data/TournamentStore.swift @@ -12,9 +12,6 @@ import SwiftUI class TournamentStore: Store, ObservableObject { static func instance(tournamentId: String) -> TournamentStore { -// if StoreCenter.main.userId == nil { -// fatalError("cant request store without id") -// } return StoreCenter.main.store(identifier: tournamentId, parameter: "tournament") } @@ -44,13 +41,13 @@ class TournamentStore: Store, ObservableObject { } #endif - self.groupStages = self.registerCollection(synchronized: synchronized, indexed: indexed) - self.rounds = self.registerCollection(synchronized: synchronized, indexed: indexed) - self.teamRegistrations = self.registerCollection(synchronized: synchronized, indexed: indexed) - self.playerRegistrations = self.registerCollection(synchronized: synchronized, indexed: indexed) - self.matches = self.registerCollection(synchronized: synchronized, indexed: indexed) - self.teamScores = self.registerCollection(synchronized: synchronized, indexed: indexed) - self.matchSchedulers = self.registerCollection(synchronized: false, indexed: indexed) + self.groupStages = self.registerSynchronizedCollection(indexed: indexed) + self.rounds = self.registerSynchronizedCollection(indexed: indexed) + self.teamRegistrations = self.registerSynchronizedCollection(indexed: indexed) + self.playerRegistrations = self.registerSynchronizedCollection(indexed: indexed) + self.matches = self.registerSynchronizedCollection(indexed: indexed) + self.teamScores = self.registerSynchronizedCollection(indexed: indexed) + self.matchSchedulers = self.registerCollection(indexed: indexed) self.loadCollectionsFromServerIfNoFile() diff --git a/PadelClub/Utils/SourceFileManager.swift b/PadelClub/Utils/SourceFileManager.swift index ac06446..5313241 100644 --- a/PadelClub/Utils/SourceFileManager.swift +++ b/PadelClub/Utils/SourceFileManager.swift @@ -26,9 +26,9 @@ class SourceFileManager { if !fileManager.fileExists(atPath: directoryURL.path) { // Directory does not exist, create it try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) - print("Directory created at: \(directoryURL)") +// print("Directory created at: \(directoryURL)") } else { - print("Directory already exists at: \(directoryURL)") +// print("Directory already exists at: \(directoryURL)") } } catch { print("Error: \(error)") diff --git a/PadelClub/Views/Club/ClubDetailView.swift b/PadelClub/Views/Club/ClubDetailView.swift index b04b4f0..5ad196a 100644 --- a/PadelClub/Views/Club/ClubDetailView.swift +++ b/PadelClub/Views/Club/ClubDetailView.swift @@ -242,11 +242,7 @@ struct ClubDetailView: View { } .onDisappear { if displayContext == .edition && clubDeleted == false { - do { - try dataStore.clubs.addOrUpdate(instance: club) - } catch { - Logger.error(error) - } + dataStore.clubs.addOrUpdate(instance: club) } } .onAppear { diff --git a/PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift b/PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift index 15f41b1..adf8694 100644 --- a/PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift +++ b/PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift @@ -108,21 +108,13 @@ struct LoserBracketFromGroupStageView: View { let placeCount = displayableMatches.isEmpty ? currentGroupStageLoserBracketsInitialPlace : max(currentGroupStageLoserBracketsInitialPlace, displayableMatches.map({ $0.index }).max()! + 2) let match = Match(round: loserBracket.id, index: placeCount, matchFormat: loserBracket.matchFormat) match.name = "\(placeCount)\(placeCount.ordinalFormattedSuffix()) place" - do { - try tournamentStore.matches.addOrUpdate(instance: match) - } catch { - Logger.error(error) - } + tournamentStore.matches.addOrUpdate(instance: match) } private func _deleteAllMatches() { let displayableMatches = loserBracket.playedMatches().sorted(by: \.index) - do { - try tournamentStore.matches.delete(contentOfs: displayableMatches) - } catch { - Logger.error(error) - } + tournamentStore.matches.delete(contentOfs: displayableMatches) } @@ -205,15 +197,7 @@ struct GroupStageLoserBracketMatchFooterView: View { match.name = "\(newIndexValidated)\(newIndexValidated.ordinalFormattedSuffix()) place" - do { - try match.tournamentStore.teamScores.addOrUpdate(contentOfs: teamScores) - } catch { - Logger.error(error) - } - do { - try match.tournamentStore.matches.addOrUpdate(instance: match) - } catch { - Logger.error(error) - } - } + match.tournamentStore.teamScores.addOrUpdate(contentOfs: teamScores) + match.tournamentStore.matches.addOrUpdate(instance: match) + } } diff --git a/PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift b/PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift index 91dedc7..c69ffb9 100644 --- a/PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift +++ b/PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift @@ -13,7 +13,7 @@ struct TournamentSubscriptionView: View { let federalTournament: FederalTournament let build: any TournamentBuildHolder - let user: User + let user: CustomUser @State private var selectedPlayers: [ImportedPlayer] @State private var contactType: ContactType? = nil @@ -21,7 +21,7 @@ struct TournamentSubscriptionView: View { @State private var didSendMessage: Bool = false @State private var didSaveInCalendar: Bool = false - init(federalTournament: FederalTournament, build: any TournamentBuildHolder, user: User) { + init(federalTournament: FederalTournament, build: any TournamentBuildHolder, user: CustomUser) { self.federalTournament = federalTournament self.build = build self.user = user diff --git a/PadelClub/Views/Round/RoundSettingsView.swift b/PadelClub/Views/Round/RoundSettingsView.swift index 6efa24b..f095bc2 100644 --- a/PadelClub/Views/Round/RoundSettingsView.swift +++ b/PadelClub/Views/Round/RoundSettingsView.swift @@ -42,11 +42,7 @@ struct RoundSettingsView: View { Menu { Button("Retirer du tableau") { team.resetBracketPosition() - do { - try self.tournamentStore.teamRegistrations.addOrUpdate(instance: team) - } catch { - Logger.error(error) - } + self.tournamentStore.teamRegistrations.addOrUpdate(instance: team) } } label: { TeamRowView(team: team) @@ -63,11 +59,7 @@ struct RoundSettingsView: View { RowButtonView("Valider l'état du tableau", role: .destructive) { tournament.shouldVerifyBracket = false - do { - try dataStore.tournaments.addOrUpdate(instance: tournament) - } catch { - Logger.error(error) - } + dataStore.tournaments.addOrUpdate(instance: tournament) } } footer: { Text("Suite à un changement dans votre liste d'inscrits, veuillez vérifier l'intégrité de votre tableau et valider que tout est ok.") @@ -121,16 +113,9 @@ struct RoundSettingsView: View { return match } - do { - try tournamentStore.rounds.addOrUpdate(instance: round) - } catch { - Logger.error(error) - } - do { - try tournamentStore.matches.addOrUpdate(contentOfs: matches) - } catch { - Logger.error(error) - } + tournamentStore.rounds.addOrUpdate(instance: round) + tournamentStore.matches.addOrUpdate(contentOfs: matches) + round.buildLoserBracket() matches.filter { $0.disabled }.forEach { $0._toggleLoserMatchDisableState(true) @@ -141,12 +126,12 @@ struct RoundSettingsView: View { Section { if let lastRound = tournament.rounds().first { // first is final, last round RowButtonView("Supprimer " + lastRound.roundTitle(), role: .destructive) { + let teams = lastRound.seeds() + teams.forEach { team in + team.resetBracketPosition() + } + tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams) do { - let teams = lastRound.seeds() - teams.forEach { team in - team.resetBracketPosition() - } - try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams) try tournamentStore.rounds.delete(instance: lastRound) } catch { Logger.error(error) @@ -159,11 +144,7 @@ struct RoundSettingsView: View { RowButtonView("Synchroniser les noms des matchs") { let allRoundMatches = tournament.allRoundMatches() allRoundMatches.forEach({ $0.name = $0.roundTitle() }) - do { - try self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: allRoundMatches) - } catch { - Logger.error(error) - } + self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: allRoundMatches) } } } @@ -182,16 +163,8 @@ struct RoundSettingsView: View { match.teamScores } - do { - try tournamentStore.teamScores.delete(contentOfs: ts) - } catch { - Logger.error(error) - } - do { - try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams()) - } catch { - Logger.error(error) - } + tournamentStore.teamScores.delete(contentOfs: ts) + tournamentStore.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams()) tournament.allRounds().forEach({ round in round.enableRound() }) diff --git a/PadelClub/Views/Tournament/Screen/AddTeamView.swift b/PadelClub/Views/Tournament/Screen/AddTeamView.swift index a86f18a..466ddff 100644 --- a/PadelClub/Views/Tournament/Screen/AddTeamView.swift +++ b/PadelClub/Views/Tournament/Screen/AddTeamView.swift @@ -10,7 +10,7 @@ import LeStorage import CoreData struct AddTeamView: View { - + @Environment(\.dismiss) var dismiss private var fetchRequest: FetchRequest @@ -45,7 +45,7 @@ struct AddTeamView: View { @State private var displayWarningNotEnoughCharacter: Bool = false @State private var testMessageIndex: Int = 0 @State private var presentLocalMultiplayerSearch: Bool = false - + var tournamentStore: TournamentStore { return self.tournament.tournamentStore } @@ -61,11 +61,11 @@ struct AddTeamView: View { createdPlayers.insert(player) createdPlayerIds.insert(player.id) } - + _createdPlayers = .init(wrappedValue: createdPlayers) _createdPlayerIds = .init(wrappedValue: createdPlayerIds) } - + let request: NSFetchRequest = ImportedPlayer.fetchRequest() request.sortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)] request.fetchLimit = 1000 @@ -77,10 +77,10 @@ struct AddTeamView: View { _textHeight = .init(wrappedValue: Self._calculateHeight(text: pasteString)) cancelShouldDismiss = true } - + fetchRequest = FetchRequest(fetchRequest: request, animation: .default) } - + var body: some View { if let pasteString, pasteString.isEmpty == false, fetchPlayers.isEmpty == false { computedBody @@ -89,7 +89,7 @@ struct AddTeamView: View { computedBody } } - + var computedBody: some View { List(selection: $createdPlayerIds) { _buildingTeamView() @@ -119,7 +119,7 @@ struct AddTeamView: View { Button("Créer l'équipe quand même") { _createTeam(checkDuplicates: false, checkHomonym: false) } - + Button("Annuler", role: .cancel) { confirmHomonym = false } @@ -132,7 +132,7 @@ struct AddTeamView: View { Button("Créer l'équipe quand même") { _createTeam(checkDuplicates: false, checkHomonym: true) } - + Button("Annuler", role: .cancel) { confirmDuplicate = false } @@ -213,11 +213,11 @@ struct AddTeamView: View { .buttonBorderShape(.capsule) } } - + ToolbarItem(placement: .topBarTrailing) { Button { let generalString = UIPasteboard.general.string ?? "" - + #if targetEnvironment(simulator) let s = testMessages[testMessageIndex % testMessages.count] handlePasteString(s) @@ -241,15 +241,15 @@ struct AddTeamView: View { .navigationTitle(editedTeam == nil ? "Ajouter une équipe" : "Modifier l'équipe") .environment(\.editMode, Binding.constant(EditMode.active)) } - + private func _isEditingTeam() -> Bool { createdPlayerIds.isEmpty == false || editedTeam != nil || pasteString != nil } - + var unsortedPlayers: [PlayerRegistration] { tournament.unsortedPlayers() } - + @ViewBuilder private func _managementView() -> some View { Section { @@ -261,7 +261,7 @@ struct AddTeamView: View { Text("Cherchez dans la base fédérale de \(rankSourceDate.monthYearFormatted), vous y trouverez tous les joueurs ayant participé à au moins un tournoi dans les 12 derniers mois.") } } - + if tournament.isAnimation(), createdPlayers.isEmpty == true { Section { RowButtonView("Ajouter plusieurs joueurs du club") { @@ -271,7 +271,7 @@ struct AddTeamView: View { Text("Crée une équipe par joueur sélectionné") } } - + Section { RowButtonView("Créer un non classé / non licencié") { if let pasteString, pasteString.isEmpty == false { @@ -284,7 +284,7 @@ struct AddTeamView: View { Text("Si le joueur n'a pas encore de licence ou n'a pas encore participé à une compétition, vous pouvez le créer vous-même.") } } - + private func _addPlayerSex() -> Int { switch tournament.tournamentCategory { case .men, .unlisted: @@ -296,11 +296,11 @@ struct AddTeamView: View { } } - + private func _filterOption() -> PlayerFilterOption { return tournament.tournamentCategory.playerFilterOption } - + private func _currentSelection() -> Set { var currentSelection = Set() createdPlayerIds.compactMap { id in @@ -310,7 +310,7 @@ struct AddTeamView: View { player.setComputedRank(in: tournament) currentSelection.insert(player) } - + createdPlayerIds.compactMap { id in createdPlayers.first(where: { id == $0.id }) }.forEach { @@ -326,7 +326,7 @@ struct AddTeamView: View { }.forEach { player in currentSelection.append(player.license) } - + createdPlayerIds.compactMap { id in createdPlayers.first(where: { id == $0.id }) }.forEach { @@ -334,7 +334,7 @@ struct AddTeamView: View { } return currentSelection } - + private func _isDuplicate() -> Bool { if tournament.isAnimation() { return false } let ids : [String?] = _currentSelectionIds() @@ -343,15 +343,15 @@ struct AddTeamView: View { } return false } - - private func _createTeam(checkDuplicates: Bool, checkHomonym: Bool) { + + private func _createTeam(checkDuplicates: Bool, checkHomonym: Bool) { if checkDuplicates && _isDuplicate() { confirmDuplicate = true return } let players = _currentSelection() - + if checkHomonym { homonyms = players.filter({ $0.hasHomonym() }) if homonyms.isEmpty == false { @@ -359,31 +359,23 @@ struct AddTeamView: View { return } } - + let team = tournament.addTeam(players) - do { - try self.tournamentStore.teamRegistrations.addOrUpdate(instance: team) - } catch { - Logger.error(error) - } - do { - try self.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) - } catch { - Logger.error(error) - } - - pasteString = nil - editableTextField = "" - - if team.players().count > 1 { - createdPlayers.removeAll() - createdPlayerIds.removeAll() - dismiss() - } else { - editedTeam = team - } + self.tournamentStore.teamRegistrations.addOrUpdate(instance: team) + self.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) + + pasteString = nil + editableTextField = "" + + if team.players().count > 1 { + createdPlayers.removeAll() + createdPlayerIds.removeAll() + dismiss() + } else { + editedTeam = team + } } - + private func _updateTeam(checkDuplicates: Bool) { guard let editedTeam else { return } if checkDuplicates && _isDuplicate() { @@ -393,17 +385,9 @@ struct AddTeamView: View { let players = _currentSelection() editedTeam.updatePlayers(players, inTournamentCategory: tournament.tournamentCategory) - do { - try self.tournamentStore.teamRegistrations.addOrUpdate(instance: editedTeam) - } catch { - Logger.error(error) - } - do { - try self.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) - } catch { - Logger.error(error) - } - + self.tournamentStore.teamRegistrations.addOrUpdate(instance: editedTeam) + self.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) + pasteString = nil editableTextField = "" @@ -411,7 +395,7 @@ struct AddTeamView: View { dismiss() } } - + // Calculating the height based on the content of the TextEditor static private func _calculateHeight(text: String) -> CGFloat { let size = CGSize(width: UIScreen.main.bounds.width - 32, height: .infinity) @@ -424,15 +408,23 @@ struct AddTeamView: View { ) return max(boundingRect.height + 20, 40) // Add some padding and set a minimum height } - - @ViewBuilder - private func _buildingTeamView() -> some View { + + struct PasteStringSection: View { + + let pasteString: String? + @Binding var editableTextField: String + @Binding var textHeight: CGFloat + @FocusState var focusedField: AddTeamView.FocusField? + var handlePasteString: (String) -> Void + @Binding var displayWarningNotEnoughCharacter: Bool + + var body: some View { if let pasteString { Section { TextEditor(text: $editableTextField) .frame(height: textHeight) .onChange(of: editableTextField) { - textHeight = Self._calculateHeight(text: pasteString) + textHeight = AddTeamView._calculateHeight(text: pasteString) } .focused($focusedField, equals: .pasteField) .toolbar { @@ -464,128 +456,111 @@ struct AddTeamView: View { FooterButtonView("effacer le texte") { self.focusedField = nil self.editableTextField = "" - self.pasteString = nil + self.handlePasteString("") } } } } - - Section { - ForEach(createdPlayerIds.sorted(), id: \.self) { id in - if let p = createdPlayers.first(where: { $0.id == id }) { - VStack(alignment: .leading, spacing: 0) { - if let player = unsortedPlayers.first(where: { ($0.licenceId == p.licenceId && $0.licenceId != nil) }), editedTeam?.includes(player: player) == false { - Text("Déjà inscrit !").foregroundStyle(.logoRed).bold() - } - if tournament.isPlayerAgeInadequate(player: p) { - Text("Âge invalide !").foregroundStyle(.logoRed).bold() - } - if tournament.isPlayerRankInadequate(player: p) { - Text("Trop bien classé !").foregroundStyle(.logoRed).bold() - } - PlayerView(player: p).tag(p.id) - .environment(tournament) - } - } - if let p = fetchPlayers.first(where: { $0.license == id }) { - VStack(alignment: .leading, spacing: 0) { - if let pasteString, pasteString.isEmpty == false, unsortedPlayers.first(where: { $0.licenceId == p.license }) != nil { - Text("Déjà inscrit !").foregroundStyle(.logoRed).bold() - } - if tournament.isPlayerAgeInadequate(player: p) { - Text("Âge invalide !").foregroundStyle(.logoRed).bold() - } - if tournament.isPlayerRankInadequate(player: p) { - Text("Trop bien classé !").foregroundStyle(.logoRed).bold() - } - ImportedPlayerView(player: p).tag(p.license!) - } - } - } - if editedTeam == nil { - if createdPlayerIds.isEmpty { - RowButtonView("Bloquer une place") { - _createTeam(checkDuplicates: false, checkHomonym: false) - } - } else { - RowButtonView("Ajouter l'équipe") { - _createTeam(checkDuplicates: true, checkHomonym: true) - } - } - } else { - RowButtonView("Confirmer") { - _updateTeam(checkDuplicates: false) - dismiss() - } - } - } header: { - let _currentSelection = _currentSelection() - let selectedSortedTeams = tournament.selectedSortedTeams() - let rank = _currentSelection.map { - $0.computedRank - }.reduce(0, +) - let teamIndex = selectedSortedTeams.firstIndex(where: { $0.weight >= rank }) ?? selectedSortedTeams.count - if _currentSelection.isEmpty == false, tournament.hideWeight() == false, rank > 0 { - HStack(spacing: 16.0) { - VStack(alignment: .leading, spacing: 0) { - Text("Rang").font(.caption) - Text("#" + (teamIndex + 1).formatted()) - } + } + } - VStack(alignment: .leading, spacing: 0) { - Text("Poids").font(.caption) - Text(rank.formatted()) - } - Spacer() - VStack(alignment: .trailing, spacing: 0) { - Text("").font(.caption) - Text(tournament.cutLabel(index: teamIndex, teamCount: selectedSortedTeams.count)) - } - } -// } else { -// Text("Préparation de l'équipe") - } - } - + @ViewBuilder + private func _buildingTeamView() -> some View { + + PasteStringSection( + pasteString: pasteString, + editableTextField: $editableTextField, + textHeight: $textHeight, + focusedField: _focusedField, + handlePasteString: handlePasteString, + displayWarningNotEnoughCharacter: $displayWarningNotEnoughCharacter + ) + + TeamSelectionSection( + createdPlayerIds: createdPlayerIds, + createdPlayers: createdPlayers, + unsortedPlayers: unsortedPlayers, + fetchPlayers: fetchPlayers, + editedTeam: editedTeam, + pasteString: pasteString, + tournament: tournament, + _createTeam: _createTeam, + _updateTeam: _updateTeam, + dismiss: dismiss, + _currentSelection: _currentSelection + ) + if let pasteString, pasteString.isEmpty == false { - let sortedPlayers = _searchFilteredPlayers() - - if sortedPlayers.isEmpty { - ContentUnavailableView { - Label("Aucun résultat", systemImage: "person.2.slash") - } description: { - Text("Aucun joueur classé n'a été trouvé dans ce message. Attention, si un joueur n'a pas joué de tournoi dans les 12 derniers, Padel Club ne pourra pas le trouver.") - } actions: { - RowButtonView("Créer un joueur non classé") { - selectionSearchField = pasteString - } - - RowButtonView("Chercher dans la base") { - presentPlayerSearch = true - } + let sortedPlayers = _searchFilteredPlayers() + + if sortedPlayers.isEmpty { + ContentUnavailableView { + Label("Aucun résultat", systemImage: "person.2.slash") + } description: { + Text("Aucun joueur classé n'a été trouvé dans ce message. Attention, si un joueur n'a pas joué de tournoi dans les 12 derniers, Padel Club ne pourra pas le trouver.") + } actions: { + RowButtonView("Créer un joueur non classé") { + selectionSearchField = pasteString + } - RowButtonView("Effacer cette recherche") { - self.pasteString = nil - self.editableTextField = "" - } + RowButtonView("Chercher dans la base") { + presentPlayerSearch = true } - } else { - _listOfPlayers(searchFilteredPlayers: sortedPlayers, pasteString: pasteString) + RowButtonView("Effacer cette recherche") { + self.pasteString = nil + self.editableTextField = "" + } } + } else { - _managementView() + _listOfPlayers(searchFilteredPlayers: sortedPlayers, pasteString: pasteString) } + } else { + _managementView() + } } - + +// +// if let pasteString, pasteString.isEmpty == false { +// let sortedPlayers = _searchFilteredPlayers() +// +// if sortedPlayers.isEmpty { +// ContentUnavailableView { +// Label("Aucun résultat", systemImage: "person.2.slash") +// } description: { +// Text("Aucun joueur classé n'a été trouvé dans ce message. Attention, si un joueur n'a pas joué de tournoi dans les 12 derniers, Padel Club ne pourra pas le trouver.") +// } actions: { +// RowButtonView("Créer un joueur non classé") { +// selectionSearchField = pasteString +// } +// +// RowButtonView("Chercher dans la base") { +// presentPlayerSearch = true +// } +// +// RowButtonView("Effacer cette recherche") { +// self.pasteString = nil +// self.editableTextField = "" +// } +// } +// +// } else { +// _listOfPlayers(searchFilteredPlayers: sortedPlayers, pasteString: pasteString) +// } +// } else { +// _managementView() +// } +// } + @MainActor func hitForSearch(_ ip: ImportedPlayer, _ pasteString: String?) -> Int { guard let pasteString else { return 0 } let _searchForHit = pasteString.hashValue if searchForHit != _searchForHit { - DispatchQueue.main.async { + DispatchQueue.main.async { searchForHit = _searchForHit hitsForSearch = [:] } @@ -615,7 +590,7 @@ struct AddTeamView: View { } return 1 } - + @MainActor private func handlePasteString(_ first: String) { if first.isEmpty == false { @@ -633,7 +608,7 @@ struct AddTeamView: View { @ViewBuilder private func _listOfPlayers(searchFilteredPlayers: [ImportedPlayer], pasteString: String) -> some View { let sortedPlayers = _sortedPlayers(searchFilteredPlayers: searchFilteredPlayers, pasteString: pasteString) - + Section { ForEach(sortedPlayers) { player in ImportedPlayerView(player: player).tag(player.license!) @@ -644,7 +619,7 @@ struct AddTeamView: View { } } - + private func _searchFilteredPlayers() -> [ImportedPlayer] { if searchField.isEmpty { return Array(fetchPlayers) @@ -652,12 +627,171 @@ struct AddTeamView: View { return fetchPlayers.filter({ $0.contains(searchField) }) } } - + private func _sortedPlayers(searchFilteredPlayers: [ImportedPlayer], pasteString: String) -> [ImportedPlayer] { return searchFilteredPlayers.sorted(by: { hitForSearch($0, pasteString) > hitForSearch($1, pasteString) }) } } +struct TeamSelectionSection: View { + let createdPlayerIds: Set + let createdPlayers: Set + let unsortedPlayers: [PlayerRegistration] + let fetchPlayers: FetchedResults + let editedTeam: TeamRegistration? + let pasteString: String? + let tournament: Tournament + let _createTeam: (Bool, Bool) -> Void + let _updateTeam: (Bool) -> Void + let dismiss: DismissAction + let _currentSelection: () -> Set + + var body: some View { + Section { + PlayerList(createdPlayerIds: createdPlayerIds, + createdPlayers: createdPlayers, + unsortedPlayers: unsortedPlayers, + fetchPlayers: fetchPlayers, + editedTeam: editedTeam, + pasteString: pasteString, + tournament: tournament) + + ActionButton(editedTeam: editedTeam, + createdPlayerIds: createdPlayerIds, + _createTeam: _createTeam, + _updateTeam: _updateTeam, + dismiss: dismiss) + } header: { + TeamHeader(tournament: tournament, + _currentSelection: _currentSelection) + } + } +} + +struct PlayerList: View { + let createdPlayerIds: Set + let createdPlayers: Set + let unsortedPlayers: [PlayerRegistration] + let fetchPlayers: FetchedResults + let editedTeam: TeamRegistration? + let pasteString: String? + let tournament: Tournament + + var body: some View { + ForEach(createdPlayerIds.sorted(), id: \.self) { id in + if let p = createdPlayers.first(where: { $0.id == id }) { + CreatedPlayerView(player: p, unsortedPlayers: unsortedPlayers, editedTeam: editedTeam, tournament: tournament) + } + if let p = fetchPlayers.first(where: { $0.license == id }) { + FetchedPlayerView(player: p, unsortedPlayers: unsortedPlayers, pasteString: pasteString, tournament: tournament) + } + } + } +} + +struct CreatedPlayerView: View { + let player: PlayerRegistration + let unsortedPlayers: [PlayerRegistration] + let editedTeam: TeamRegistration? + let tournament: Tournament + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + if let existingPlayer = unsortedPlayers.first(where: { ($0.licenceId == player.licenceId && $0.licenceId != nil) }), editedTeam?.includes(player: existingPlayer) == false { + Text("Déjà inscrit !").foregroundStyle(.logoRed).bold() + } + if tournament.isPlayerAgeInadequate(player: player) { + Text("Âge invalide !").foregroundStyle(.logoRed).bold() + } + if tournament.isPlayerRankInadequate(player: player) { + Text("Trop bien classé !").foregroundStyle(.logoRed).bold() + } + PlayerView(player: player).tag(player.id) + .environment(tournament) + } + } +} + +struct FetchedPlayerView: View { + let player: ImportedPlayer + let unsortedPlayers: [PlayerRegistration] + let pasteString: String? + let tournament: Tournament + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + if let pasteString, pasteString.isEmpty == false, unsortedPlayers.first(where: { $0.licenceId == player.license }) != nil { + Text("Déjà inscrit !").foregroundStyle(.logoRed).bold() + } + if tournament.isPlayerAgeInadequate(player: player) { + Text("Âge invalide !").foregroundStyle(.logoRed).bold() + } + if tournament.isPlayerRankInadequate(player: player) { + Text("Trop bien classé !").foregroundStyle(.logoRed).bold() + } + ImportedPlayerView(player: player).tag(player.license!) + } + } +} + +struct ActionButton: View { + let editedTeam: TeamRegistration? + let createdPlayerIds: Set + let _createTeam: (Bool, Bool) -> Void + let _updateTeam: (Bool) -> Void + let dismiss: DismissAction + + var body: some View { + if editedTeam == nil { + if createdPlayerIds.isEmpty { + RowButtonView("Bloquer une place") { + _createTeam(false, false) + } + } else { + RowButtonView("Ajouter l'équipe") { + _createTeam(true, true) + } + } + } else { + RowButtonView("Confirmer") { + _updateTeam(false) + dismiss() + } + } + } +} + +struct TeamHeader: View { + let tournament: Tournament + let _currentSelection: () -> Set + + var body: some View { + let currentSelection = _currentSelection() + let selectedSortedTeams = tournament.selectedSortedTeams() + let rank = currentSelection.map { $0.computedRank }.reduce(0, +) + let teamIndex = selectedSortedTeams.firstIndex(where: { $0.weight >= rank }) ?? selectedSortedTeams.count + + if !currentSelection.isEmpty, !tournament.hideWeight(), rank > 0 { + HStack(spacing: 16.0) { + VStack(alignment: .leading, spacing: 0) { + Text("Rang").font(.caption) + Text("#" + (teamIndex + 1).formatted()) + } + + VStack(alignment: .leading, spacing: 0) { + Text("Poids").font(.caption) + Text(rank.formatted()) + } + Spacer() + VStack(alignment: .trailing, spacing: 0) { + Text("").font(.caption) + Text(tournament.cutLabel(index: teamIndex, teamCount: selectedSortedTeams.count)) + } + } + } + } +} + let testMessages = [ "Anthony dovetta ( 3620578 K )et christophe capeau ( 4666443v)", """ @@ -684,6 +818,6 @@ Tullou Benjamin 8990867f """, """ Sms Julien La Croix +33622886688 -Salut Raz, c'est ! Ju Lacroix J'espère que tu vas bien depuis le temps! Est-ce que tu peux nous inscrire au 1000 de Bandol avec Derek Gerson stp? +Salut Raz, c'est ! Ju Lacroix J'espère que tu vas bien depuis le temps! Est-ce que tu peux nous inscrire au 1000 de Bandol avec Derek Gerson stp? """ ] diff --git a/PadelClub/Views/Tournament/Subscription/Guard.swift b/PadelClub/Views/Tournament/Subscription/Guard.swift index 64cbdac..bee8592 100644 --- a/PadelClub/Views/Tournament/Subscription/Guard.swift +++ b/PadelClub/Views/Tournament/Subscription/Guard.swift @@ -20,7 +20,6 @@ import LeStorage var updateListenerTask: Task? = nil - override init() { super.init() diff --git a/PadelClub/Views/Tournament/Subscription/Purchase.swift b/PadelClub/Views/Tournament/Subscription/Purchase.swift index 103b5e4..06e1304 100644 --- a/PadelClub/Views/Tournament/Subscription/Purchase.swift +++ b/PadelClub/Views/Tournament/Subscription/Purchase.swift @@ -8,13 +8,15 @@ import Foundation import LeStorage -class Purchase: ModelObject, Storable { +class Purchase: ModelObject, SyncedStorable { + static func resourceName() -> String { return "purchases" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func filterByStoreIdentifier() -> Bool { return false } static var relationshipNames: [String] = [] var id: UInt64 + var lastUpdate: Date var user: String var purchaseDate: Date var productId: String @@ -22,8 +24,11 @@ class Purchase: ModelObject, Storable { var revocationDate: Date? = nil var expirationDate: Date? = nil + var storeId: String? { return nil } + init(user: String, transactionId: UInt64, purchaseDate: Date, productId: String, quantity: Int? = nil, revocationDate: Date? = nil, expirationDate: Date? = nil) { self.id = transactionId + self.lastUpdate = Date() self.user = user self.purchaseDate = purchaseDate self.productId = productId @@ -34,6 +39,7 @@ class Purchase: ModelObject, Storable { enum CodingKeys: String, CodingKey, CaseIterable { case id + case lastUpdate case user case purchaseDate case productId @@ -56,6 +62,7 @@ class Purchase: ModelObject, Storable { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.id, forKey: .id) + try container.encode(self.lastUpdate, forKey: .lastUpdate) try container.encodeAndEncryptIfPresent(self.user.data(using: .utf8), forKey: .user) try container.encode(self.purchaseDate, forKey: .purchaseDate) try container.encode(self.productId, forKey: .productId) @@ -68,6 +75,7 @@ class Purchase: ModelObject, Storable { let container = try decoder.container(keyedBy: CodingKeys.self) self.id = try container.decode(UInt64.self, forKey: .id) + self.lastUpdate = try container.decodeIfPresent(Date.self, forKey: .lastUpdate) ?? Date() self.user = try container.decodeEncrypted(key: .user) self.purchaseDate = try container.decode(Date.self, forKey: .purchaseDate) self.productId = try container.decode(String.self, forKey: .productId) diff --git a/PadelClub/Views/User/AccountView.swift b/PadelClub/Views/User/AccountView.swift index a45da3e..07771af 100644 --- a/PadelClub/Views/User/AccountView.swift +++ b/PadelClub/Views/User/AccountView.swift @@ -10,7 +10,7 @@ import LeStorage struct AccountView: View { - var user: User + var user: CustomUser var handler: () -> () var body: some View { diff --git a/PadelClub/Views/User/LoginView.swift b/PadelClub/Views/User/LoginView.swift index d66f8d7..57e36d4 100644 --- a/PadelClub/Views/User/LoginView.swift +++ b/PadelClub/Views/User/LoginView.swift @@ -74,7 +74,7 @@ struct LoginView: View { } } - var handler: (User) -> () + var handler: (CustomUser) -> () var body: some View { @@ -195,7 +195,7 @@ struct LoginView: View { self.isLoading = true do { let service = try StoreCenter.main.service() - let user: User = try await service.login( + let user: CustomUser = try await service.login( username: self.username, password: self.password) self.dataStore.user = user diff --git a/PadelClub/Views/User/UserCreationView.swift b/PadelClub/Views/User/UserCreationView.swift index a20ebb4..60a5a31 100644 --- a/PadelClub/Views/User/UserCreationView.swift +++ b/PadelClub/Views/User/UserCreationView.swift @@ -232,7 +232,7 @@ struct UserCreationFormView: View { country: country) let service: Services = try StoreCenter.main.service() - let _: User = try await service.createAccount(user: userCreationForm) + let _: CustomUser = try await service.createAccount(user: userCreationForm) DispatchQueue.main.async { self.isLoading = false diff --git a/PadelClubTests/ServerDataTests.swift b/PadelClubTests/ServerDataTests.swift index 3947937..8d44e9c 100644 --- a/PadelClubTests/ServerDataTests.swift +++ b/PadelClubTests/ServerDataTests.swift @@ -31,7 +31,7 @@ final class ServerDataTests: XCTestCase { func login() async throws { // print("LOGIN!") - let _: User = try await StoreCenter.main.service().login(username: self.username, password: self.password) + let _: CustomUser = try await StoreCenter.main.service().login(username: self.username, password: self.password) } func testClub() async throws { @@ -48,6 +48,7 @@ final class ServerDataTests: XCTestCase { club.courtCount = 3 let inserted_club: Club = try await StoreCenter.main.service().post(club) + assert(inserted_club.lastUpdate == club.lastUpdate) assert(inserted_club.name == club.name) assert(inserted_club.acronym == club.acronym) assert(inserted_club.zipCode == club.zipCode) @@ -59,6 +60,7 @@ final class ServerDataTests: XCTestCase { assert(inserted_club.broadcastCode != nil) inserted_club.phone = "123456" + inserted_club.lastUpdate = Date() let updated_club: Club = try await StoreCenter.main.service().put(inserted_club) assert(updated_club.phone == inserted_club.phone) @@ -66,7 +68,7 @@ final class ServerDataTests: XCTestCase { } func testLogin() async throws { - let user: User = try await StoreCenter.main.service().login(username: self.username, password: self.password) + let user: CustomUser = try await StoreCenter.main.service().login(username: self.username, password: self.password) assert(user.username == "test") } @@ -87,6 +89,7 @@ final class ServerDataTests: XCTestCase { let e = try await StoreCenter.main.service().post(event) assert(e.name == event.name) + assert(e.lastUpdate == event.lastUpdate) assert(e.tenupId == event.tenupId) } @@ -102,6 +105,7 @@ final class ServerDataTests: XCTestCase { let tournament = Tournament(event: eventId, name: "RG Homme", startDate: Date(), endDate: nil, creationDate: Date(), isPrivate: false, groupStageFormat: MatchFormat.megaTie, roundFormat: MatchFormat.nineGames, loserRoundFormat: MatchFormat.nineGamesDecisivePoint, groupStageSortMode: GroupStageOrderingMode.snake, groupStageCount: 2, rankSourceDate: Date(), dayDuration: 5, teamCount: 3, teamSorting: TeamSortingType.rank, federalCategory: TournamentCategory.mix, federalLevelCategory: TournamentLevel.p1000, federalAgeCategory: FederalTournamentAge.a45, closedRegistrationDate: Date(), groupStageAdditionalQualified: 4, courtCount: 9, prioritizeClubMembers: true, qualifiedPerGroupStage: 1, teamsPerGroupStage: 2, entryFee: 30.0, additionalEstimationDuration: 5, isDeleted: true, publishTeams: true, publishSummons: true, publishGroupStages: true, publishBrackets: true, shouldVerifyBracket: true, shouldVerifyGroupStage: true, hideTeamsWeight: true, publishTournament: true, hidePointsEarned: true, publishRankings: true, loserBracketMode: .manual) let t = try await StoreCenter.main.service().post(tournament) + assert(t.lastUpdate.formatted() == tournament.lastUpdate.formatted()) assert(t.event == tournament.event) assert(t.name == tournament.name) assert(t.startDate.formatted() == tournament.startDate.formatted()) @@ -151,9 +155,12 @@ final class ServerDataTests: XCTestCase { } let groupStage = GroupStage(tournament: tournamentId, index: 2, size: 3, matchFormat: MatchFormat.nineGames, startDate: Date(), name: "Yeah!", step: 1) + groupStage.storeId = "123" let gs: GroupStage = try await StoreCenter.main.service().post(groupStage) assert(gs.tournament == groupStage.tournament) + assert(gs.storeId == groupStage.storeId) + assert(gs.lastUpdate == groupStage.lastUpdate) assert(gs.name == groupStage.name) assert(gs.index == groupStage.index) assert(gs.size == groupStage.size) @@ -175,9 +182,12 @@ final class ServerDataTests: XCTestCase { let parentRoundId = rounds.first?.id let round = Round(tournament: tournamentId, index: 1, parent: parentRoundId, matchFormat: MatchFormat.nineGames, startDate: Date(), groupStageLoserBracket: false, loserBracketMode: .manual) + round.storeId = "abc" let r: Round = try await StoreCenter.main.service().post(round) + assert(r.storeId == round.storeId) assert(r.tournament == round.tournament) + assert(r.lastUpdate == round.lastUpdate) assert(r.index == round.index) assert(r.parent == round.parent) assert(r.matchFormat == round.matchFormat) @@ -201,10 +211,13 @@ final class ServerDataTests: XCTestCase { } let teamRegistration = TeamRegistration(tournament: tournamentId, groupStage: groupStageId, registrationDate: Date(), callDate: Date(), bracketPosition: 1, groupStagePosition: 2, comment: "comment", source: "source", sourceValue: "source V", logo: "logo", name: "Stax", walkOut: true, wildCardBracket: true, wildCardGroupStage: true, weight: 1, lockedWeight: 11, confirmationDate: Date(), qualified: true) + teamRegistration.storeId = "123" let tr: TeamRegistration = try await StoreCenter.main.service().post(teamRegistration) + assert(tr.storeId == teamRegistration.storeId) assert(tr.tournament == teamRegistration.tournament) + assert(tr.lastUpdate == teamRegistration.lastUpdate) assert(tr.groupStage == teamRegistration.groupStage) assert(tr.registrationDate != nil) assert(tr.callDate != nil) @@ -234,8 +247,11 @@ final class ServerDataTests: XCTestCase { } let playerRegistration = PlayerRegistration(teamRegistration: teamRegistrationId, firstName: "juan", lastName: "lebron", licenceId: "123", rank: 11, paymentType: PlayerRegistration.PlayerPaymentType.cash, sex: PlayerRegistration.PlayerSexType.male, tournamentPlayed: 2, points: 33, clubName: "le club", ligueName: "la league", assimilation: "ass", phoneNumber: "123123", email: "email@email.com", birthdate: nil, computedRank: 222, source: PlayerRegistration.PlayerDataSource.frenchFederation, hasArrived: true) + playerRegistration.storeId = "123" let pr: PlayerRegistration = try await StoreCenter.main.service().post(playerRegistration) + assert(pr.storeId == playerRegistration.storeId) + assert(pr.lastName == playerRegistration.lastName) assert(pr.teamRegistration == playerRegistration.teamRegistration) assert(pr.firstName == playerRegistration.firstName) assert(pr.lastName == playerRegistration.lastName) @@ -267,8 +283,11 @@ final class ServerDataTests: XCTestCase { let parentRoundId = rounds.first?.id let match: Match = Match(round: parentRoundId, groupStage: nil, startDate: Date(), endDate: Date(), index: 2, matchFormat: MatchFormat.twoSets, servingTeamId: teamRegistrationId, winningTeamId: teamRegistrationId, losingTeamId: teamRegistrationId, disabled: true, courtIndex: 1, confirmed: true) + match.storeId = "123" let m: Match = try await StoreCenter.main.service().post(match) + assert(m.storeId == match.storeId) + assert(m.lastUpdate == match.lastUpdate) assert(m.round == match.round) assert(m.groupStage == match.groupStage) assert(m.startDate != nil) @@ -297,8 +316,11 @@ final class ServerDataTests: XCTestCase { return } let teamScore = TeamScore(match: matchId, teamRegistration: teamRegistrationId, score: "6/6", walkOut: 1, luckyLoser: 1) + teamScore.storeId = "!23" let ts: TeamScore = try await StoreCenter.main.service().post(teamScore) + assert(ts.storeId == teamScore.storeId) + assert(ts.lastUpdate == teamScore.lastUpdate) assert(ts.match == teamScore.match) assert(ts.teamRegistration == teamScore.teamRegistration) assert(ts.score == teamScore.score) @@ -318,6 +340,7 @@ final class ServerDataTests: XCTestCase { let court = Court(index: 1, club: clubId, name: "Philippe Chatrier", exitAllowed: true, indoor: true) let c: Court = try await StoreCenter.main.service().post(court) + assert(c.lastUpdate == court.lastUpdate) assert(c.club == court.club) assert(c.name == court.name) assert(c.index == court.index) @@ -337,6 +360,7 @@ final class ServerDataTests: XCTestCase { let dateInterval = DateInterval(event: eventId, courtIndex: 1, startDate: Date(), endDate: Date()) let di: PadelClub.DateInterval = try await StoreCenter.main.service().post(dateInterval) + assert(di.lastUpdate == dateInterval.lastUpdate) assert(di.event == dateInterval.event) assert(di.courtIndex == dateInterval.courtIndex) assert(di.startDate.formatted() == dateInterval.startDate.formatted()) @@ -354,10 +378,12 @@ final class ServerDataTests: XCTestCase { let transactionId = UInt64.random(in: 0...100000) let quantity = Int.random(in: 0...10) - let purchase: Purchase = Purchase(user: userId, transactionId: transactionId, purchaseDate: Date(), productId: "app.padelclub.productId", quantity: quantity, revocationDate: Date(), expirationDate: Date()) + let purchase: Purchase = Purchase(transactionId: transactionId, purchaseDate: Date(), productId: "app.padelclub.productId", quantity: quantity, revocationDate: Date(), expirationDate: Date()) + let p: Purchase = try await StoreCenter.main.service().post(purchase) assert(p.id == purchase.id) + assert(p.lastUpdate == purchase.lastUpdate) assert(p.user == purchase.user) assert(p.productId == purchase.productId) assert(p.purchaseDate.formatted() == purchase.purchaseDate.formatted()) diff --git a/PadelClubTests/SynchronizationTests.swift b/PadelClubTests/SynchronizationTests.swift new file mode 100644 index 0000000..0c2313d --- /dev/null +++ b/PadelClubTests/SynchronizationTests.swift @@ -0,0 +1,33 @@ +// +// SynchronizationTests.swift +// PadelClubTests +// +// Created by Laurent Morvillier on 09/10/2024. +// + +import Testing +import LeStorage +@testable import PadelClub + +struct SynchronizationTests { + + let username: String = "laurent" + let password: String = "StaxKikoo12" + + init() { + StoreCenter.main.synchronizationApiURL = "http://127.0.0.1:8000/roads/" + } + + @Test func synchronizationTest() async throws { + + _ = try await self.login() + try await StoreCenter.main.synchronizeLastUpdates() + + } + + func login() async throws -> CustomUser { + let user: CustomUser = try await StoreCenter.main.service().login(username: self.username, password: self.password) + return user + } + +} diff --git a/PadelClubTests/UserDataTests.swift b/PadelClubTests/UserDataTests.swift index 4980845..8b848d5 100644 --- a/PadelClubTests/UserDataTests.swift +++ b/PadelClubTests/UserDataTests.swift @@ -24,8 +24,8 @@ final class UserDataTests: XCTestCase { func testUserCreation() async throws { - let userCreationForm = UserCreationForm(user: User.placeHolder(), username: self.username, password: self.password, firstName: "jean", lastName: "coco", email: "test@lolomo.com", phone: "0123", country: "France") - let user: User = try await StoreCenter.main.service().createAccount(user: userCreationForm) + let userCreationForm = UserCreationForm(user: CustomUser.placeHolder(), username: self.username, password: self.password, firstName: "jean", lastName: "coco", email: "test@lolomo.com", phone: "0123", country: "France") + let user: CustomUser = try await StoreCenter.main.service().createAccount(user: userCreationForm) assert(user.username == userCreationForm.username) assert(user.firstName == userCreationForm.firstName) @@ -36,8 +36,8 @@ final class UserDataTests: XCTestCase { } - func login() async throws -> User { - let user: User = try await StoreCenter.main.service().login(username: self.username, password: self.password) + func login() async throws -> CustomUser { + let user: CustomUser = try await StoreCenter.main.service().login(username: self.username, password: self.password) return user }