first commit

sync2
Laurent 1 year ago
parent 31ee3cef62
commit 671edd3412
  1. 20
      PadelClub.xcodeproj/project.pbxproj
  2. 28
      PadelClub/Data/Club.swift
  3. 21
      PadelClub/Data/Court.swift
  4. 17
      PadelClub/Data/CustomUser.swift
  5. 62
      PadelClub/Data/DataStore.swift
  6. 10
      PadelClub/Data/DateInterval.swift
  7. 16
      PadelClub/Data/Event.swift
  8. 62
      PadelClub/Data/GroupStage.swift
  9. 90
      PadelClub/Data/Match.swift
  10. 4
      PadelClub/Data/MonthData.swift
  11. 22
      PadelClub/Data/PlayerRegistration.swift
  12. 1
      PadelClub/Data/README.md
  13. 63
      PadelClub/Data/Round.swift
  14. 58
      PadelClub/Data/TeamRegistration.swift
  15. 13
      PadelClub/Data/TeamScore.swift
  16. 132
      PadelClub/Data/Tournament.swift
  17. 17
      PadelClub/Data/TournamentStore.swift
  18. 4
      PadelClub/Utils/SourceFileManager.swift
  19. 6
      PadelClub/Views/Club/ClubDetailView.swift
  20. 26
      PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift
  21. 4
      PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift
  22. 53
      PadelClub/Views/Round/RoundSettingsView.swift
  23. 476
      PadelClub/Views/Tournament/Screen/AddTeamView.swift
  24. 1
      PadelClub/Views/Tournament/Subscription/Guard.swift
  25. 10
      PadelClub/Views/Tournament/Subscription/Purchase.swift
  26. 2
      PadelClub/Views/User/AccountView.swift
  27. 4
      PadelClub/Views/User/LoginView.swift
  28. 2
      PadelClub/Views/User/UserCreationView.swift
  29. 32
      PadelClubTests/ServerDataTests.swift
  30. 33
      PadelClubTests/SynchronizationTests.swift
  31. 8
      PadelClubTests/UserDataTests.swift

@ -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 = "<group>"; };
C4A47DA52B83948E00ADC637 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; };
C4A47DA82B85F82100ADC637 /* ChangePasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangePasswordView.swift; sourceTree = "<group>"; };
C4A47DAC2B85FCCD00ADC637 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = "<group>"; };
C4A47DAC2B85FCCD00ADC637 /* CustomUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomUser.swift; sourceTree = "<group>"; };
C4A47DB22B86387500ADC637 /* AccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountView.swift; sourceTree = "<group>"; };
C4B3A1542C2581DA0078EAA8 /* Patcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Patcher.swift; sourceTree = "<group>"; };
C4C01D972C481C0C0059087C /* CapsuleViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapsuleViewModifier.swift; sourceTree = "<group>"; };
C4C33F752C9B1EC5006316DE /* CodingContainer+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CodingContainer+Extensions.swift"; sourceTree = "<group>"; };
C4D477982CB6704C0077713D /* SynchronizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronizationTests.swift; sourceTree = "<group>"; };
C4EC6F562BE92CAC000CEAB4 /* local.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = local.plist; sourceTree = "<group>"; };
C4EC6F582BE92D88000CEAB4 /* PListReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PListReader.swift; sourceTree = "<group>"; };
C4FC2E262C2AABC90021F3BF /* PasswordField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordField.swift; sourceTree = "<group>"; };
@ -1293,6 +1295,7 @@
C411C9C22BEBA453003017AD /* ServerDataTests.swift */,
C411C9C82BF219CB003017AD /* UserDataTests.swift */,
C411C9CF2BF38F41003017AD /* TokenExemptionTests.swift */,
C4D477982CB6704C0077713D /* SynchronizationTests.swift */,
);
path = PadelClubTests;
sourceTree = "<group>";
@ -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 */,

@ -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)

@ -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)

@ -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)

@ -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<DateInterval>
fileprivate(set) var purchases: StoredCollection<Purchase>
fileprivate var userStorage: StoredSingleton<User>
fileprivate var userStorage: StoredSingleton<CustomUser>
fileprivate var _temporaryLocalUser: OptionalStorage<User> = OptionalStorage(fileName: "tmp_local_user.json")
fileprivate var _temporaryLocalUser: OptionalStorage<CustomUser> = OptionalStorage(fileName: "tmp_local_user.json")
fileprivate(set) var appSettingsStorage: MicroStorage<AppSettings> = 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<User> = notification.object as? StoredSingleton<User> {
self.user = userSingleton.item() ?? self._temporaryLocalUser.item ?? User.placeHolder()
if let userSingleton: StoredSingleton<CustomUser> = notification.object as? StoredSingleton<CustomUser> {
self.user = userSingleton.item() ?? self._temporaryLocalUser.item ?? CustomUser.placeHolder()
} else if let clubsCollection: StoredCollection<Club> = notification.object as? StoredCollection<Club> {
self._fixMissingClubCreatorIfNecessary(clubsCollection)
} else if let eventsCollection: StoredCollection<Event> = notification.object as? StoredCollection<Event> {
@ -134,17 +132,13 @@ class DataStore: ObservableObject {
}
fileprivate func _fixMissingClubCreatorIfNecessary(_ clubsCollection: StoredCollection<Club>) {
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)

@ -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)
}
}

@ -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)

@ -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"

@ -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()
}
}

@ -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 {

@ -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

@ -18,4 +18,3 @@ Dans Django:
Enfin, revenir dans Xcode, ouvrir ServerDataTests et lancer le test mis à jour

@ -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..<matchCount).map { //0 is final match
@ -635,11 +623,7 @@ defer {
//initial mode let the roundTitle give a name without considering the playable match
}
do {
try self.tournamentStore.matches.addOrUpdate(contentOfs: matches)
} catch {
Logger.error(error)
}
self.tournamentStore.matches.addOrUpdate(contentOfs: matches)
loserRounds().forEach { round in
round.buildLoserBracket()
@ -672,24 +656,20 @@ defer {
playedMatches.forEach { match in
match.matchFormat = updatedMatchFormat
}
do {
try self.tournamentStore.matches.addOrUpdate(contentOfs: playedMatches)
} catch {
Logger.error(error)
}
self.tournamentStore.matches.addOrUpdate(contentOfs: playedMatches)
}
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)
let loserRounds = self.loserRounds()
for round in loserRounds {
try round.deleteDependencies()
round.deleteDependencies()
}
self.tournamentStore.rounds.deleteDependencies(loserRounds)
@ -697,6 +677,8 @@ defer {
enum CodingKeys: String, CodingKey {
case _id = "id"
case _storeId = "storeId"
case _lastUpdate = "lastUpdate"
case _tournament = "tournament"
case _index = "index"
case _parent = "parent"
@ -709,6 +691,7 @@ defer {
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.decodeIfPresent(Date.self, forKey: ._lastUpdate) ?? Date()
tournament = try container.decode(String.self, forKey: ._tournament)
index = try container.decode(Int.self, forKey: ._index)
parent = try container.decodeIfPresent(String.self, forKey: ._parent)
@ -722,6 +705,8 @@ defer {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: ._id)
try container.encode(lastUpdate, forKey: ._lastUpdate)
try container.encode(storeId, forKey: ._storeId)
try container.encode(tournament, forKey: ._tournament)
try container.encode(index, forKey: ._index)
try container.encode(groupStageLoserBracket, forKey: ._groupStageLoserBracket)
@ -742,11 +727,7 @@ defer {
}
extension Round: Selectable, Equatable {
static func == (lhs: Round, rhs: Round) -> Bool {
lhs.id == rhs.id
}
extension Round: Selectable {
func selectionLabel(index: Int) -> String {
if let parentRound {

@ -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<PlayerRegistration>, 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
}

@ -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)

@ -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..<matchCount).map { //0 is final match
@ -1724,11 +1688,7 @@ defer {
(RoundRule.roundName(fromMatchIndex: $0.index), RoundRule.matchIndexWithinRound(fromMatchIndex: $0.index))
})
do {
try self.tournamentStore.matches.addOrUpdate(contentOfs: matches)
} catch {
Logger.error(error)
}
self.tournamentStore.matches.addOrUpdate(contentOfs: matches)
rounds.forEach { round in
round.buildLoserBracket()
@ -1758,11 +1718,7 @@ defer {
}
func deleteStructure() {
do {
try self.tournamentStore.rounds.delete(contentOfs: rounds())
} catch {
Logger.error(error)
}
self.tournamentStore.rounds.delete(contentOfs: rounds())
}
func resetBracketPosition() {
@ -1770,11 +1726,7 @@ defer {
}
func deleteGroupStages() {
do {
try self.tournamentStore.groupStages.delete(contentOfs: allGroupStages())
} catch {
Logger.error(error)
}
self.tournamentStore.groupStages.delete(contentOfs: allGroupStages())
}
func refreshGroupStages() {
@ -1827,11 +1779,7 @@ defer {
}
}
do {
try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams())
} catch {
Logger.error(error)
}
self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams())
}
func isFree() -> Bool {
@ -2097,11 +2045,7 @@ defer {
let lastStep = lastStep() + 1
for i in 0..<teamsPerGroupStage {
let gs = GroupStage(tournament: id, index: i, size: groupStageCount, step: lastStep)
do {
try tournamentStore.groupStages.addOrUpdate(instance: gs)
} catch {
Logger.error(error)
}
tournamentStore.groupStages.addOrUpdate(instance: gs)
}
groupStages(atStep: 1).forEach { $0.buildMatches() }
@ -2118,11 +2062,7 @@ defer {
let placeCount = i * 2 + 1
let match = Match(round: groupStageLoserBracket.id, index: placeCount, matchFormat: groupStageLoserBracket.matchFormat)
match.name = "\(placeCount)\(placeCount.ordinalFormattedSuffix(feminine: true)) place"
do {
try tournamentStore.matches.addOrUpdate(instance: match)
} catch {
Logger.error(error)
}
tournamentStore.matches.addOrUpdate(instance: match)
if let gs1 = gss.first, let gs2 = gss.last, let score1 = gs1.teams(true)[safe: i], let score2 = gs2.teams(true)[safe: i] {
print("rang \(i)")
@ -2177,7 +2117,7 @@ defer {
if self.payment != nil { return }
if let payment = Guard.main.paymentForNewTournament() {
self.payment = payment
try DataStore.shared.tournaments.addOrUpdate(instance: self)
DataStore.shared.tournaments.addOrUpdate(instance: self)
return
}
throw PaymentError.cantPayTournament
@ -2240,16 +2180,6 @@ fileprivate extension Bool {
// }
//}
extension Tournament: Hashable {
static func == (lhs: Tournament, rhs: Tournament) -> 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 {

@ -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()

@ -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)")

@ -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 {

@ -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)
}
}

@ -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

@ -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()
})

@ -10,7 +10,7 @@ import LeStorage
import CoreData
struct AddTeamView: View {
@Environment(\.dismiss) var dismiss
private var fetchRequest: FetchRequest<ImportedPlayer>
@ -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> = 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<PlayerRegistration> {
var currentSelection = Set<PlayerRegistration>()
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<String>
let createdPlayers: Set<PlayerRegistration>
let unsortedPlayers: [PlayerRegistration]
let fetchPlayers: FetchedResults<ImportedPlayer>
let editedTeam: TeamRegistration?
let pasteString: String?
let tournament: Tournament
let _createTeam: (Bool, Bool) -> Void
let _updateTeam: (Bool) -> Void
let dismiss: DismissAction
let _currentSelection: () -> Set<PlayerRegistration>
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<String>
let createdPlayers: Set<PlayerRegistration>
let unsortedPlayers: [PlayerRegistration]
let fetchPlayers: FetchedResults<ImportedPlayer>
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<String>
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<PlayerRegistration>
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?
"""
]

@ -20,7 +20,6 @@ import LeStorage
var updateListenerTask: Task<Void, Never>? = nil
override init() {
super.init()

@ -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)

@ -10,7 +10,7 @@ import LeStorage
struct AccountView: View {
var user: User
var user: CustomUser
var handler: () -> ()
var body: some View {

@ -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

@ -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

@ -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())

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

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

Loading…
Cancel
Save