diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index c6443db..6e29fde 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -970,6 +970,12 @@ FFE103102C366DCD00684FC9 /* EditSharingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE1030F2C366DCD00684FC9 /* EditSharingView.swift */; }; FFE103122C366E5900684FC9 /* ImagePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE103112C366E5900684FC9 /* ImagePickerView.swift */; }; FFE2D2E22C231BEE00D0C7BE /* SupportButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE2D2E12C231BEE00D0C7BE /* SupportButtonView.swift */; }; + FFE8B5B32DA848D300BDE966 /* OnlineWaitingListFaqSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE8B5B22DA848D300BDE966 /* OnlineWaitingListFaqSheetView.swift */; }; + FFE8B5B42DA848D400BDE966 /* OnlineWaitingListFaqSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE8B5B22DA848D300BDE966 /* OnlineWaitingListFaqSheetView.swift */; }; + FFE8B5B52DA848D400BDE966 /* OnlineWaitingListFaqSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE8B5B22DA848D300BDE966 /* OnlineWaitingListFaqSheetView.swift */; }; + FFE8B5B72DA8763800BDE966 /* PaymentInfoSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE8B5B62DA8763800BDE966 /* PaymentInfoSheetView.swift */; }; + FFE8B5B82DA8763800BDE966 /* PaymentInfoSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE8B5B62DA8763800BDE966 /* PaymentInfoSheetView.swift */; }; + FFE8B5B92DA8763800BDE966 /* PaymentInfoSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE8B5B62DA8763800BDE966 /* PaymentInfoSheetView.swift */; }; FFE8C2C02C7601E80046B243 /* ConfirmButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE8C2BF2C7601E80046B243 /* ConfirmButtonView.swift */; }; FFEF7F4E2BDE69130033D0F0 /* MenuWarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFEF7F4D2BDE69130033D0F0 /* MenuWarningView.swift */; }; FFF0241E2BF48B15001F14B4 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = FFF0241D2BF48B15001F14B4 /* Localizable.strings */; }; @@ -1407,6 +1413,8 @@ FFE1030F2C366DCD00684FC9 /* EditSharingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditSharingView.swift; sourceTree = ""; }; FFE103112C366E5900684FC9 /* ImagePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePickerView.swift; sourceTree = ""; }; FFE2D2E12C231BEE00D0C7BE /* SupportButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportButtonView.swift; sourceTree = ""; }; + FFE8B5B22DA848D300BDE966 /* OnlineWaitingListFaqSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnlineWaitingListFaqSheetView.swift; sourceTree = ""; }; + FFE8B5B62DA8763800BDE966 /* PaymentInfoSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentInfoSheetView.swift; sourceTree = ""; }; FFE8C2BF2C7601E80046B243 /* ConfirmButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmButtonView.swift; sourceTree = ""; }; FFEF7F4D2BDE69130033D0F0 /* MenuWarningView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuWarningView.swift; sourceTree = ""; }; FFF0241C2BF48B15001F14B4 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; @@ -2004,11 +2012,13 @@ FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */, FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */, FF5D0D6F2BB3EFA5005CB568 /* LearnMoreSheetView.swift */, + FFE8B5B22DA848D300BDE966 /* OnlineWaitingListFaqSheetView.swift */, FFCFC0192BBC5A8500B82851 /* MatchFormatRowView.swift */, FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */, FFE2D2E12C231BEE00D0C7BE /* SupportButtonView.swift */, FFE103112C366E5900684FC9 /* ImagePickerView.swift */, FFBFC3942CF05CBB000EBD8D /* DateMenuView.swift */, + FFE8B5B62DA8763800BDE966 /* PaymentInfoSheetView.swift */, ); path = Shared; sourceTree = ""; @@ -2767,6 +2777,7 @@ FF6525C32C8C61B400B9498E /* LoserBracketFromGroupStageView.swift in Sources */, FF5D30512BD94E1000F2B93D /* ImportedPlayer+Extensions.swift in Sources */, FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */, + FFE8B5B72DA8763800BDE966 /* PaymentInfoSheetView.swift in Sources */, FFBFC3962CF05CBB000EBD8D /* DateMenuView.swift in Sources */, FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */, FF70916E2B9108C600AB08DA /* InscriptionManagerView.swift in Sources */, @@ -2838,6 +2849,7 @@ C493B37E2C10AD3600862481 /* LoadingViewModifier.swift in Sources */, FF089EBD2BB0287D00F0AEC7 /* PlayerView.swift in Sources */, FF967D032BAEF0C000A9A3BD /* MatchDetailView.swift in Sources */, + FFE8B5B32DA848D300BDE966 /* OnlineWaitingListFaqSheetView.swift in Sources */, FFF1D2CB2C4A22B200C8D33D /* ExportFormat.swift in Sources */, C488C8012CC7DCB80082001F /* BaseClub.swift in Sources */, FF967D0F2BAF63B000A9A3BD /* PlayerBlockView.swift in Sources */, @@ -3064,6 +3076,7 @@ FF4CBFC52C996C0600151637 /* CashierSettingsView.swift in Sources */, FF4CBFC62C996C0600151637 /* LoserRoundScheduleEditorView.swift in Sources */, FF4CBFC72C996C0600151637 /* Club.swift in Sources */, + FFE8B5B82DA8763800BDE966 /* PaymentInfoSheetView.swift in Sources */, FF4CBFC82C996C0600151637 /* Array+Extensions.swift in Sources */, FF4CBFC92C996C0600151637 /* ToolboxView.swift in Sources */, FF4CBFCA2C996C0600151637 /* Alphabet.swift in Sources */, @@ -3130,6 +3143,7 @@ FF4CBFFC2C996C0600151637 /* UmpireView.swift in Sources */, FF4CBFFD2C996C0600151637 /* CustomUser.swift in Sources */, FF4CBFFE2C996C0600151637 /* MatchSummaryView.swift in Sources */, + FFE8B5B52DA848D400BDE966 /* OnlineWaitingListFaqSheetView.swift in Sources */, FFA252B52CDD2C6C0074E63F /* OngoingDestination.swift in Sources */, FF4CBFFF2C996C0600151637 /* TournamentDurationManagerView.swift in Sources */, FF4CC0002C996C0600151637 /* MockData.swift in Sources */, @@ -3358,6 +3372,7 @@ FF70FB442C90584900129CC2 /* CashierSettingsView.swift in Sources */, FF70FB452C90584900129CC2 /* LoserRoundScheduleEditorView.swift in Sources */, FF70FB462C90584900129CC2 /* Club.swift in Sources */, + FFE8B5B92DA8763800BDE966 /* PaymentInfoSheetView.swift in Sources */, FF70FB472C90584900129CC2 /* Array+Extensions.swift in Sources */, FF70FB482C90584900129CC2 /* ToolboxView.swift in Sources */, FF70FB492C90584900129CC2 /* Alphabet.swift in Sources */, @@ -3424,6 +3439,7 @@ FF70FB7C2C90584900129CC2 /* CustomUser.swift in Sources */, C4C33F772C9B1ED4006316DE /* CodingContainer+Extensions.swift in Sources */, FF70FB7D2C90584900129CC2 /* MatchSummaryView.swift in Sources */, + FFE8B5B42DA848D400BDE966 /* OnlineWaitingListFaqSheetView.swift in Sources */, FFA252B72CDD2C6C0074E63F /* OngoingDestination.swift in Sources */, FF70FB7E2C90584900129CC2 /* TournamentDurationManagerView.swift in Sources */, FF70FB7F2C90584900129CC2 /* MockData.swift in Sources */, diff --git a/PadelClub/Data/CustomUser.swift b/PadelClub/Data/CustomUser.swift index 794aa8c..1184481 100644 --- a/PadelClub/Data/CustomUser.swift +++ b/PadelClub/Data/CustomUser.swift @@ -14,6 +14,79 @@ enum UserRight: Int, Codable { case creation = 2 } +enum RegistrationPaymentMode: Int, Codable { + case disabled = 0 + case corporate = 1 + case noFee = 2 + case stripe = 3 + + func fee() -> Double? { + switch self { + case .disabled: + return nil + case .corporate: + return nil + case .noFee: + return nil + case .stripe: + let fee = 0.0075 + return fee + } + } + + func canEnableOnlinePayment() -> Bool { + switch self { + case .disabled: + return false + case .corporate: + return true + case .noFee: + return true + case .stripe: + return true + } + } + + func localizedRegistrationPaymentFee() -> String? { + switch self { + case .disabled: + return nil + case .corporate: + return nil + case .noFee: + return nil + case .stripe: + if let fee = self.fee() { + return String(format: "%.1f%%", fee * 100) + } else { + return nil + } + } + } + + func sample(entryFee: Double) -> String? { + if let fee = self.fee() { + let feeAmount = entryFee * fee + return String(format: "%.2f€", feeAmount) + } else { + return nil + } + } + + func requiresStripe() -> Bool { + switch self { + case .disabled: + return false + case .corporate: + return true + case .noFee: + return true + case .stripe: + return true + } + } +} + @Observable class CustomUser: BaseCustomUser, UserBase { @@ -138,6 +211,9 @@ class CustomUser: BaseCustomUser, UserBase { } } + func canEnableOnlinePayment() -> Bool { + registrationPaymentMode.canEnableOnlinePayment() + } // enum CodingKeys: String, CodingKey { // case _id = "id" // case _lastUpdate = "lastUpdate" diff --git a/PadelClub/Data/Gen/BaseCustomUser.swift b/PadelClub/Data/Gen/BaseCustomUser.swift index 4295e19..5e20b6b 100644 --- a/PadelClub/Data/Gen/BaseCustomUser.swift +++ b/PadelClub/Data/Gen/BaseCustomUser.swift @@ -32,8 +32,16 @@ class BaseCustomUser: SyncedModelObject, SyncedStorable { var groupStageMatchFormatPreference: MatchFormat? = nil var loserBracketMatchFormatPreference: MatchFormat? = nil var loserBracketMode: LoserBracketMode = .automatic + var disableRankingFederalRuling: Bool = false var deviceId: String? = nil var agents: [String] = [] + var userRole: Int? = nil + var registrationPaymentMode: RegistrationPaymentMode = RegistrationPaymentMode.disabled + var umpireCustomMail: String? = nil + var umpireCustomContact: String? = nil + var umpireCustomPhone: String? = nil + var hideUmpireMail: Bool = false + var hideUmpirePhone: Bool = true init( id: String = Store.randomId(), @@ -57,8 +65,16 @@ class BaseCustomUser: SyncedModelObject, SyncedStorable { groupStageMatchFormatPreference: MatchFormat? = nil, loserBracketMatchFormatPreference: MatchFormat? = nil, loserBracketMode: LoserBracketMode = .automatic, + disableRankingFederalRuling: Bool = false, deviceId: String? = nil, - agents: [String] = [] + agents: [String] = [], + userRole: Int? = nil, + registrationPaymentMode: RegistrationPaymentMode = RegistrationPaymentMode.disabled, + umpireCustomMail: String? = nil, + umpireCustomContact: String? = nil, + umpireCustomPhone: String? = nil, + hideUmpireMail: Bool = false, + hideUmpirePhone: Bool = true ) { super.init() self.id = id @@ -82,8 +98,16 @@ class BaseCustomUser: SyncedModelObject, SyncedStorable { self.groupStageMatchFormatPreference = groupStageMatchFormatPreference self.loserBracketMatchFormatPreference = loserBracketMatchFormatPreference self.loserBracketMode = loserBracketMode + self.disableRankingFederalRuling = disableRankingFederalRuling self.deviceId = deviceId self.agents = agents + self.userRole = userRole + self.registrationPaymentMode = registrationPaymentMode + self.umpireCustomMail = umpireCustomMail + self.umpireCustomContact = umpireCustomContact + self.umpireCustomPhone = umpireCustomPhone + self.hideUmpireMail = hideUmpireMail + self.hideUmpirePhone = hideUmpirePhone } required public override init() { super.init() @@ -111,8 +135,16 @@ class BaseCustomUser: SyncedModelObject, SyncedStorable { case _groupStageMatchFormatPreference = "groupStageMatchFormatPreference" case _loserBracketMatchFormatPreference = "loserBracketMatchFormatPreference" case _loserBracketMode = "loserBracketMode" + case _disableRankingFederalRuling = "disableRankingFederalRuling" case _deviceId = "deviceId" case _agents = "agents" + case _userRole = "userRole" + case _registrationPaymentMode = "registrationPaymentMode" + case _umpireCustomMail = "umpireCustomMail" + case _umpireCustomContact = "umpireCustomContact" + case _umpireCustomPhone = "umpireCustomPhone" + case _hideUmpireMail = "hideUmpireMail" + case _hideUmpirePhone = "hideUmpirePhone" } required init(from decoder: Decoder) throws { @@ -138,8 +170,16 @@ class BaseCustomUser: SyncedModelObject, SyncedStorable { self.groupStageMatchFormatPreference = try container.decodeIfPresent(MatchFormat.self, forKey: ._groupStageMatchFormatPreference) ?? nil self.loserBracketMatchFormatPreference = try container.decodeIfPresent(MatchFormat.self, forKey: ._loserBracketMatchFormatPreference) ?? nil self.loserBracketMode = try container.decodeIfPresent(LoserBracketMode.self, forKey: ._loserBracketMode) ?? .automatic + self.disableRankingFederalRuling = try container.decodeIfPresent(Bool.self, forKey: ._disableRankingFederalRuling) ?? false self.deviceId = try container.decodeIfPresent(String.self, forKey: ._deviceId) ?? nil self.agents = try container.decodeIfPresent([String].self, forKey: ._agents) ?? [] + self.userRole = try container.decodeIfPresent(Int.self, forKey: ._userRole) ?? nil + self.registrationPaymentMode = try container.decodeIfPresent(RegistrationPaymentMode.self, forKey: ._registrationPaymentMode) ?? RegistrationPaymentMode.disabled + self.umpireCustomMail = try container.decodeIfPresent(String.self, forKey: ._umpireCustomMail) ?? nil + self.umpireCustomContact = try container.decodeIfPresent(String.self, forKey: ._umpireCustomContact) ?? nil + self.umpireCustomPhone = try container.decodeIfPresent(String.self, forKey: ._umpireCustomPhone) ?? nil + self.hideUmpireMail = try container.decodeIfPresent(Bool.self, forKey: ._hideUmpireMail) ?? false + self.hideUmpirePhone = try container.decodeIfPresent(Bool.self, forKey: ._hideUmpirePhone) ?? true try super.init(from: decoder) } @@ -166,8 +206,16 @@ class BaseCustomUser: SyncedModelObject, SyncedStorable { try container.encode(self.groupStageMatchFormatPreference, forKey: ._groupStageMatchFormatPreference) try container.encode(self.loserBracketMatchFormatPreference, forKey: ._loserBracketMatchFormatPreference) try container.encode(self.loserBracketMode, forKey: ._loserBracketMode) + try container.encode(self.disableRankingFederalRuling, forKey: ._disableRankingFederalRuling) try container.encode(self.deviceId, forKey: ._deviceId) try container.encode(self.agents, forKey: ._agents) + try container.encode(self.userRole, forKey: ._userRole) + try container.encode(self.registrationPaymentMode, forKey: ._registrationPaymentMode) + try container.encode(self.umpireCustomMail, forKey: ._umpireCustomMail) + try container.encode(self.umpireCustomContact, forKey: ._umpireCustomContact) + try container.encode(self.umpireCustomPhone, forKey: ._umpireCustomPhone) + try container.encode(self.hideUmpireMail, forKey: ._hideUmpireMail) + try container.encode(self.hideUmpirePhone, forKey: ._hideUmpirePhone) try super.encode(to: encoder) } @@ -194,8 +242,16 @@ class BaseCustomUser: SyncedModelObject, SyncedStorable { self.groupStageMatchFormatPreference = customuser.groupStageMatchFormatPreference self.loserBracketMatchFormatPreference = customuser.loserBracketMatchFormatPreference self.loserBracketMode = customuser.loserBracketMode + self.disableRankingFederalRuling = customuser.disableRankingFederalRuling self.deviceId = customuser.deviceId self.agents = customuser.agents + self.userRole = customuser.userRole + self.registrationPaymentMode = customuser.registrationPaymentMode + self.umpireCustomMail = customuser.umpireCustomMail + self.umpireCustomContact = customuser.umpireCustomContact + self.umpireCustomPhone = customuser.umpireCustomPhone + self.hideUmpireMail = customuser.hideUmpireMail + self.hideUmpirePhone = customuser.hideUmpirePhone } static func relationships() -> [Relationship] { diff --git a/PadelClub/Data/Gen/BasePlayerRegistration.swift b/PadelClub/Data/Gen/BasePlayerRegistration.swift index d8c2ab3..3a48e18 100644 --- a/PadelClub/Data/Gen/BasePlayerRegistration.swift +++ b/PadelClub/Data/Gen/BasePlayerRegistration.swift @@ -33,6 +33,9 @@ class BasePlayerRegistration: SyncedModelObject, SyncedStorable { var coach: Bool = false var captain: Bool = false var registeredOnline: Bool = false + var timeToConfirm: Date? = nil + var registrationStatus: PlayerRegistration.RegistrationStatus = PlayerRegistration.RegistrationStatus.waiting + var paymentId: String? = nil init( id: String = Store.randomId(), @@ -56,7 +59,10 @@ class BasePlayerRegistration: SyncedModelObject, SyncedStorable { hasArrived: Bool = false, coach: Bool = false, captain: Bool = false, - registeredOnline: Bool = false + registeredOnline: Bool = false, + timeToConfirm: Date? = nil, + registrationStatus: PlayerRegistration.RegistrationStatus = PlayerRegistration.RegistrationStatus.waiting, + paymentId: String? = nil ) { super.init() self.id = id @@ -81,6 +87,9 @@ class BasePlayerRegistration: SyncedModelObject, SyncedStorable { self.coach = coach self.captain = captain self.registeredOnline = registeredOnline + self.timeToConfirm = timeToConfirm + self.registrationStatus = registrationStatus + self.paymentId = paymentId } required public override init() { super.init() @@ -109,6 +118,9 @@ class BasePlayerRegistration: SyncedModelObject, SyncedStorable { case _coach = "coach" case _captain = "captain" case _registeredOnline = "registeredOnline" + case _timeToConfirm = "timeToConfirm" + case _registrationStatus = "registrationStatus" + case _paymentId = "paymentId" } required init(from decoder: Decoder) throws { @@ -135,6 +147,9 @@ class BasePlayerRegistration: SyncedModelObject, SyncedStorable { self.coach = try container.decodeIfPresent(Bool.self, forKey: ._coach) ?? false self.captain = try container.decodeIfPresent(Bool.self, forKey: ._captain) ?? false self.registeredOnline = try container.decodeIfPresent(Bool.self, forKey: ._registeredOnline) ?? false + self.timeToConfirm = try container.decodeIfPresent(Date.self, forKey: ._timeToConfirm) ?? nil + self.registrationStatus = try container.decodeIfPresent(PlayerRegistration.RegistrationStatus.self, forKey: ._registrationStatus) ?? PlayerRegistration.RegistrationStatus.waiting + self.paymentId = try container.decodeIfPresent(String.self, forKey: ._paymentId) ?? nil try super.init(from: decoder) } @@ -162,6 +177,9 @@ class BasePlayerRegistration: SyncedModelObject, SyncedStorable { try container.encode(self.coach, forKey: ._coach) try container.encode(self.captain, forKey: ._captain) try container.encode(self.registeredOnline, forKey: ._registeredOnline) + try container.encode(self.timeToConfirm, forKey: ._timeToConfirm) + try container.encode(self.registrationStatus, forKey: ._registrationStatus) + try container.encode(self.paymentId, forKey: ._paymentId) try super.encode(to: encoder) } @@ -194,6 +212,9 @@ class BasePlayerRegistration: SyncedModelObject, SyncedStorable { self.coach = playerregistration.coach self.captain = playerregistration.captain self.registeredOnline = playerregistration.registeredOnline + self.timeToConfirm = playerregistration.timeToConfirm + self.registrationStatus = playerregistration.registrationStatus + self.paymentId = playerregistration.paymentId } static func relationships() -> [Relationship] { diff --git a/PadelClub/Data/Gen/BaseTournament.swift b/PadelClub/Data/Gen/BaseTournament.swift index 0b6d3c3..4ca282f 100644 --- a/PadelClub/Data/Gen/BaseTournament.swift +++ b/PadelClub/Data/Gen/BaseTournament.swift @@ -70,6 +70,14 @@ class BaseTournament: SyncedModelObject, SyncedStorable { var hideUmpirePhone: Bool = true var disableRankingFederalRuling: Bool = false var teamCountLimit: Bool = true + var enableOnlinePayment: Bool = false + var onlinePaymentIsMandatory: Bool = false + var enableOnlinePaymentRefund: Bool = false + var refundDateLimit: Date? = nil + var stripeAccountId: String? = nil + var enableTimeToConfirm: Bool = false + var isCorporateTournament: Bool = false + var isTemplate: Bool = false init( id: String = Store.randomId(), @@ -130,7 +138,15 @@ class BaseTournament: SyncedModelObject, SyncedStorable { hideUmpireMail: Bool = false, hideUmpirePhone: Bool = true, disableRankingFederalRuling: Bool = false, - teamCountLimit: Bool = true + teamCountLimit: Bool = true, + enableOnlinePayment: Bool = false, + onlinePaymentIsMandatory: Bool = false, + enableOnlinePaymentRefund: Bool = false, + refundDateLimit: Date? = nil, + stripeAccountId: String? = nil, + enableTimeToConfirm: Bool = false, + isCorporateTournament: Bool = false, + isTemplate: Bool = false ) { super.init() self.id = id @@ -192,6 +208,14 @@ class BaseTournament: SyncedModelObject, SyncedStorable { self.hideUmpirePhone = hideUmpirePhone self.disableRankingFederalRuling = disableRankingFederalRuling self.teamCountLimit = teamCountLimit + self.enableOnlinePayment = enableOnlinePayment + self.onlinePaymentIsMandatory = onlinePaymentIsMandatory + self.enableOnlinePaymentRefund = enableOnlinePaymentRefund + self.refundDateLimit = refundDateLimit + self.stripeAccountId = stripeAccountId + self.enableTimeToConfirm = enableTimeToConfirm + self.isCorporateTournament = isCorporateTournament + self.isTemplate = isTemplate } required public override init() { super.init() @@ -259,6 +283,14 @@ class BaseTournament: SyncedModelObject, SyncedStorable { case _hideUmpirePhone = "hideUmpirePhone" case _disableRankingFederalRuling = "disableRankingFederalRuling" case _teamCountLimit = "teamCountLimit" + case _enableOnlinePayment = "enableOnlinePayment" + case _onlinePaymentIsMandatory = "onlinePaymentIsMandatory" + case _enableOnlinePaymentRefund = "enableOnlinePaymentRefund" + case _refundDateLimit = "refundDateLimit" + case _stripeAccountId = "stripeAccountId" + case _enableTimeToConfirm = "enableTimeToConfirm" + case _isCorporateTournament = "isCorporateTournament" + case _isTemplate = "isTemplate" } private static func _decodePayment(container: KeyedDecodingContainer) throws -> TournamentPayment? { @@ -389,6 +421,14 @@ class BaseTournament: SyncedModelObject, SyncedStorable { self.hideUmpirePhone = try container.decodeIfPresent(Bool.self, forKey: ._hideUmpirePhone) ?? true self.disableRankingFederalRuling = try container.decodeIfPresent(Bool.self, forKey: ._disableRankingFederalRuling) ?? false self.teamCountLimit = try container.decodeIfPresent(Bool.self, forKey: ._teamCountLimit) ?? true + self.enableOnlinePayment = try container.decodeIfPresent(Bool.self, forKey: ._enableOnlinePayment) ?? false + self.onlinePaymentIsMandatory = try container.decodeIfPresent(Bool.self, forKey: ._onlinePaymentIsMandatory) ?? false + self.enableOnlinePaymentRefund = try container.decodeIfPresent(Bool.self, forKey: ._enableOnlinePaymentRefund) ?? false + self.refundDateLimit = try container.decodeIfPresent(Date.self, forKey: ._refundDateLimit) ?? nil + self.stripeAccountId = try container.decodeIfPresent(String.self, forKey: ._stripeAccountId) ?? nil + self.enableTimeToConfirm = try container.decodeIfPresent(Bool.self, forKey: ._enableTimeToConfirm) ?? false + self.isCorporateTournament = try container.decodeIfPresent(Bool.self, forKey: ._isCorporateTournament) ?? false + self.isTemplate = try container.decodeIfPresent(Bool.self, forKey: ._isTemplate) ?? false try super.init(from: decoder) } @@ -453,6 +493,14 @@ class BaseTournament: SyncedModelObject, SyncedStorable { try container.encode(self.hideUmpirePhone, forKey: ._hideUmpirePhone) try container.encode(self.disableRankingFederalRuling, forKey: ._disableRankingFederalRuling) try container.encode(self.teamCountLimit, forKey: ._teamCountLimit) + try container.encode(self.enableOnlinePayment, forKey: ._enableOnlinePayment) + try container.encode(self.onlinePaymentIsMandatory, forKey: ._onlinePaymentIsMandatory) + try container.encode(self.enableOnlinePaymentRefund, forKey: ._enableOnlinePaymentRefund) + try container.encode(self.refundDateLimit, forKey: ._refundDateLimit) + try container.encode(self.stripeAccountId, forKey: ._stripeAccountId) + try container.encode(self.enableTimeToConfirm, forKey: ._enableTimeToConfirm) + try container.encode(self.isCorporateTournament, forKey: ._isCorporateTournament) + try container.encode(self.isTemplate, forKey: ._isTemplate) try super.encode(to: encoder) } @@ -522,6 +570,14 @@ class BaseTournament: SyncedModelObject, SyncedStorable { self.hideUmpirePhone = tournament.hideUmpirePhone self.disableRankingFederalRuling = tournament.disableRankingFederalRuling self.teamCountLimit = tournament.teamCountLimit + self.enableOnlinePayment = tournament.enableOnlinePayment + self.onlinePaymentIsMandatory = tournament.onlinePaymentIsMandatory + self.enableOnlinePaymentRefund = tournament.enableOnlinePaymentRefund + self.refundDateLimit = tournament.refundDateLimit + self.stripeAccountId = tournament.stripeAccountId + self.enableTimeToConfirm = tournament.enableTimeToConfirm + self.isCorporateTournament = tournament.isCorporateTournament + self.isTemplate = tournament.isTemplate } static func relationships() -> [Relationship] { diff --git a/PadelClub/Data/Gen/CustomUser.json b/PadelClub/Data/Gen/CustomUser.json index ac32c3c..763d975 100644 --- a/PadelClub/Data/Gen/CustomUser.json +++ b/PadelClub/Data/Gen/CustomUser.json @@ -119,6 +119,11 @@ "type": "LoserBracketMode", "defaultValue": ".automatic" }, + { + "name": "disableRankingFederalRuling", + "type": "Bool", + "defaultValue": "false" + }, { "name": "deviceId", "type": "String", @@ -129,6 +134,42 @@ "name": "agents", "type": "[String]", "defaultValue": "[]" + }, + { + "name": "userRole", + "type": "Int", + "optional": true, + "defaultValue": "nil" + }, + { + "name": "registrationPaymentMode", + "type": "RegistrationPaymentMode", + "defaultValue": "RegistrationPaymentMode.disabled" + }, + { + "name": "umpireCustomMail", + "type": "String", + "optional": true + }, + { + "name": "umpireCustomContact", + "type": "String", + "optional": true + }, + { + "name": "umpireCustomPhone", + "type": "String", + "optional": true + }, + { + "name": "hideUmpireMail", + "type": "Bool", + "defaultValue": "false" + }, + { + "name": "hideUmpirePhone", + "type": "Bool", + "defaultValue": "true" } ] } diff --git a/PadelClub/Data/Gen/PlayerRegistration.json b/PadelClub/Data/Gen/PlayerRegistration.json index 1f5eb98..72dc484 100644 --- a/PadelClub/Data/Gen/PlayerRegistration.json +++ b/PadelClub/Data/Gen/PlayerRegistration.json @@ -115,6 +115,22 @@ "name": "registeredOnline", "type": "Bool", "defaultValue": "false" + }, + { + "name": "timeToConfirm", + "type": "Date", + "optional": true + }, + { + "name": "registrationStatus", + "type": "PlayerRegistration.RegistrationStatus", + "choices": "PlayerRegistration.RegistrationStatus", + "defaultValue": "PlayerRegistration.RegistrationStatus.waiting" + }, + { + "name": "paymentId", + "type": "String", + "optional": true } ] } diff --git a/PadelClub/Data/Gen/Tournament.json b/PadelClub/Data/Gen/Tournament.json index 1262af0..d287efe 100644 --- a/PadelClub/Data/Gen/Tournament.json +++ b/PadelClub/Data/Gen/Tournament.json @@ -295,14 +295,52 @@ { "name": "disableRankingFederalRuling", "type": "Bool", - "defaultValue": "false", - "optional": false + "defaultValue": "false" }, { "name": "teamCountLimit", "type": "Bool", - "defaultValue": "true", - "optional": false + "defaultValue": "true" + }, + { + "name": "enableOnlinePayment", + "type": "Bool", + "defaultValue": "false" + }, + { + "name": "onlinePaymentIsMandatory", + "type": "Bool", + "defaultValue": "false" + }, + { + "name": "enableOnlinePaymentRefund", + "type": "Bool", + "defaultValue": "false" + }, + { + "name": "refundDateLimit", + "type": "Date", + "optional": true + }, + { + "name": "stripeAccountId", + "type": "String", + "optional": true + }, + { + "name": "enableTimeToConfirm", + "type": "Bool", + "defaultValue": "false" + }, + { + "name": "isCorporateTournament", + "type": "Bool", + "defaultValue": "false" + }, + { + "name": "isTemplate", + "type": "Bool", + "defaultValue": "false" } ] } diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index d190a82..af1a1e9 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -145,7 +145,7 @@ final class PlayerRegistration: BasePlayerRegistration, SideStorable { func pasteData(_ exportFormat: ExportFormat = .rawText) -> String { switch exportFormat { case .rawText: - return [firstName.capitalized, lastName.capitalized, licenceId].compactMap({ $0 }).joined(separator: exportFormat.separator()) + return [firstName.capitalized, lastName.capitalized, licenceId?.computedLicense].compactMap({ $0 }).joined(separator: exportFormat.separator()) case .csv: return [lastName.uppercased() + " " + firstName.capitalized].joined(separator: exportFormat.separator()) } @@ -383,6 +383,13 @@ final class PlayerRegistration: BasePlayerRegistration, SideStorable { case beachPadel = 1 } + enum RegistrationStatus: Int, Codable { + case waiting = 0 + case pending = 1 + case confirmed = 2 + case canceled = 3 + } + static func addon(for playerRank: Int, manMax: Int, womanMax: Int) -> Int { switch playerRank { case 0: return 0 diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 574c8da..dad95cf 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -27,98 +27,6 @@ final class Tournament: BaseTournament { @ObservationIgnored var navigationPath: [Screen] = [] - -// 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, initialSeedRound: Int = 0, initialSeedCount: Int = 0) { -// super.init() -// } - - - internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = true, 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, initialSeedRound: Int = 0, initialSeedCount: Int = 0, enableOnlineRegistration: Bool = false, registrationDateLimit: Date? = nil, openingRegistrationDate: Date? = nil, waitingListLimit: Int? = nil, accountIsRequired: Bool = true, licenseIsRequired: Bool = true, minimumPlayerPerTeam: Int = 2, maximumPlayerPerTeam: Int = 2, information: String? = nil, - umpireCustomMail: String? = nil, - umpireCustomContact: String? = nil, - umpireCustomPhone: String? = nil, - hideUmpireMail: Bool = false, - hideUmpirePhone: Bool = true, - disableRankingFederalRuling: Bool = false, - teamCountLimit: Bool = true - ) { - super.init() - self.event = event - self.name = name - self.startDate = startDate - self.endDate = endDate - self.creationDate = creationDate -#if DEBUG - self.isPrivate = false -#else - self.isPrivate = isPrivate -#endif - self.groupStageFormat = groupStageFormat - self.roundFormat = roundFormat - self.loserRoundFormat = loserRoundFormat - self.groupStageSortMode = groupStageSortMode - self.groupStageCount = groupStageCount - self.rankSourceDate = rankSourceDate - self.dayDuration = dayDuration - self.teamCount = teamCount - self.teamSorting = teamSorting ?? federalLevelCategory.defaultTeamSortingType - self.federalCategory = federalCategory - self.federalLevelCategory = federalLevelCategory - self.federalAgeCategory = federalAgeCategory - self.closedRegistrationDate = closedRegistrationDate - self.groupStageAdditionalQualified = groupStageAdditionalQualified - self.courtCount = courtCount - self.prioritizeClubMembers = prioritizeClubMembers - self.qualifiedPerGroupStage = qualifiedPerGroupStage - self.teamsPerGroupStage = teamsPerGroupStage - self.entryFee = entryFee - self.additionalEstimationDuration = additionalEstimationDuration - self.isDeleted = isDeleted -#if DEBUG - self.publishTeams = true - self.publishSummons = true - self.publishBrackets = true - self.publishGroupStages = true - self.publishRankings = true - self.publishTournament = true -#else - self.publishTeams = publishTeams - self.publishSummons = publishSummons - self.publishBrackets = publishBrackets - self.publishGroupStages = publishGroupStages - self.publishRankings = publishRankings - self.publishTournament = publishTournament -#endif - self.shouldVerifyBracket = shouldVerifyBracket - self.shouldVerifyGroupStage = shouldVerifyGroupStage - self.hideTeamsWeight = hideTeamsWeight - self.hidePointsEarned = hidePointsEarned - self.loserBracketMode = loserBracketMode - self.initialSeedRound = initialSeedRound - self.initialSeedCount = initialSeedCount - self.enableOnlineRegistration = enableOnlineRegistration - self.registrationDateLimit = registrationDateLimit - self.openingRegistrationDate = openingRegistrationDate - self.waitingListLimit = waitingListLimit - - self.accountIsRequired = accountIsRequired - self.licenseIsRequired = licenseIsRequired - self.minimumPlayerPerTeam = minimumPlayerPerTeam - self.maximumPlayerPerTeam = maximumPlayerPerTeam - self.information = information - self.umpireCustomMail = umpireCustomMail - self.umpireCustomContact = umpireCustomContact - self.disableRankingFederalRuling = disableRankingFederalRuling - self.teamCountLimit = teamCountLimit - } - - required init(from decoder: Decoder) throws { - try super.init(from: decoder) - } - - required public init() { - super.init() - } var tournamentStore: TournamentStore? { return TournamentLibrary.shared.store(tournamentId: self.id) @@ -780,7 +688,7 @@ defer { func duplicates(in players: [PlayerRegistration]) -> [PlayerRegistration] { var duplicates = [PlayerRegistration]() Set(players.compactMap({ $0.licenceId })).forEach { licenceId in - let found = players.filter({ $0.licenceId == licenceId }) + let found = players.filter({ $0.licenceId?.strippedLicense == licenceId.strippedLicense }) if found.count > 1 { duplicates.append(found.first!) } @@ -1952,7 +1860,81 @@ defer { return groupStageMatchFormat } } + + func initSettings(templateTournament: Tournament?) { + setupDefaultPrivateSettings(templateTournament: templateTournament) + setupUmpireSettings(defaultTournament: nil) //default is not template, default is for event sharing settings + if let templateTournament { + setupRegistrationSettings(templateTournament: templateTournament) + } + setupFederalSettings() + } + func setupDefaultPrivateSettings(templateTournament: Tournament?) { +#if DEBUG + self.isPrivate = false + self.publishTeams = true + self.publishSummons = true + self.publishBrackets = true + self.publishGroupStages = true + self.publishRankings = true + self.publishTournament = true +#else + var shouldBePrivate = templateTournament?.isPrivate ?? true + + if Guard.main.currentPlan == .monthlyUnlimited { + shouldBePrivate = false + } else if Guard.main.purchasedTransactions.isEmpty == false { + shouldBePrivate = false + } + + self.isPrivate = shouldBePrivate +#endif + } + + func setupUmpireSettings(defaultTournament: Tournament? = nil) { + if let defaultTournament { + self.umpireCustomMail = defaultTournament.umpireCustomMail + self.umpireCustomPhone = defaultTournament.umpireCustomPhone + self.umpireCustomContact = defaultTournament.umpireCustomContact + self.hideUmpireMail = defaultTournament.hideUmpireMail + self.hideUmpirePhone = defaultTournament.hideUmpirePhone + self.disableRankingFederalRuling = defaultTournament.disableRankingFederalRuling + self.loserBracketMode = defaultTournament.loserBracketMode + } else { + let user = DataStore.shared.user + self.umpireCustomMail = user.umpireCustomMail + self.umpireCustomPhone = user.umpireCustomPhone + self.umpireCustomContact = user.umpireCustomContact + self.hideUmpireMail = user.hideUmpireMail + self.hideUmpirePhone = user.hideUmpirePhone + self.disableRankingFederalRuling = user.disableRankingFederalRuling + self.loserBracketMode = user.loserBracketMode + } + } + + func setupRegistrationSettings(templateTournament: Tournament) { + self.enableOnlineRegistration = templateTournament.enableOnlineRegistration + self.accountIsRequired = templateTournament.accountIsRequired + self.licenseIsRequired = templateTournament.licenseIsRequired + self.minimumPlayerPerTeam = templateTournament.minimumPlayerPerTeam + self.maximumPlayerPerTeam = templateTournament.maximumPlayerPerTeam + self.waitingListLimit = templateTournament.waitingListLimit + self.teamCountLimit = templateTournament.teamCountLimit + self.enableOnlinePayment = templateTournament.enableOnlinePayment + self.onlinePaymentIsMandatory = templateTournament.onlinePaymentIsMandatory + self.enableOnlinePaymentRefund = templateTournament.enableOnlinePaymentRefund + self.stripeAccountId = templateTournament.stripeAccountId + self.enableTimeToConfirm = templateTournament.enableTimeToConfirm + self.isCorporateTournament = templateTournament.isCorporateTournament + + if self.registrationDateLimit == nil, templateTournament.registrationDateLimit != nil { + self.registrationDateLimit = startDate.truncateMinutesAndSeconds() + } + self.openingRegistrationDate = templateTournament.openingRegistrationDate != nil ? creationDate.truncateMinutesAndSeconds() : nil + self.refundDateLimit = templateTournament.enableOnlinePaymentRefund ? startDate.truncateMinutesAndSeconds() : nil + } + func setupFederalSettings() { teamSorting = tournamentLevel.defaultTeamSortingType groupStageMatchFormat = groupStageSmartMatchFormat() @@ -2604,6 +2586,10 @@ extension Tournament: TournamentBuildHolder { } extension Tournament { + static func getTemplateTournament() -> Tournament? { + return DataStore.shared.tournaments.filter { $0.isTemplate && $0.isDeleted == false }.sorted(by: \.startDate, order: .descending).first + } + static func newEmptyInstance() -> Tournament { let lastDataSource: String? = DataStore.shared.appSettings.lastDataSource var _mostRecentDateAvailable: Date? { @@ -2612,28 +2598,7 @@ extension Tournament { } let rankSourceDate = _mostRecentDateAvailable - let tournaments : [Tournament] = DataStore.shared.tournaments.filter { $0.endDate != nil && $0.isDeleted == false }.sorted(by: \.endDate!, order: .descending) - - var shouldBePrivate = tournaments.first?.isPrivate ?? true - - if Guard.main.currentPlan == .monthlyUnlimited { - shouldBePrivate = false - } else if Guard.main.purchasedTransactions.isEmpty == false { - shouldBePrivate = false - } - - let disableRankingFederalRuling = tournaments.first?.disableRankingFederalRuling ?? false - let umpireCustomMail = tournaments.first?.umpireCustomMail - let umpireCustomPhone = tournaments.first?.umpireCustomPhone - let umpireCustomContact = tournaments.first?.umpireCustomContact - let hideUmpireMail = tournaments.first?.hideUmpireMail ?? false - let hideUmpirePhone = tournaments.first?.hideUmpirePhone ?? true - - let tournamentLevel = TournamentLevel.mostUsed(inTournaments: tournaments) - let tournamentCategory = TournamentCategory.mostUsed(inTournaments: tournaments) - let federalTournamentAge = FederalTournamentAge.mostUsed(inTournaments: tournaments) -//creator: DataStore.shared.user?.id - return Tournament(isPrivate: shouldBePrivate, groupStageSortMode: .snake, rankSourceDate: rankSourceDate, teamSorting: tournamentLevel.defaultTeamSortingType, federalCategory: tournamentCategory, federalLevelCategory: tournamentLevel, federalAgeCategory: federalTournamentAge, loserBracketMode: DataStore.shared.user.loserBracketMode, umpireCustomMail: umpireCustomMail, umpireCustomContact: umpireCustomContact, umpireCustomPhone: umpireCustomPhone, hideUmpireMail: hideUmpireMail, hideUmpirePhone: hideUmpirePhone, disableRankingFederalRuling: disableRankingFederalRuling) + return Tournament(rankSourceDate: rankSourceDate) } static func fake() -> Tournament { diff --git a/PadelClub/Utils/URLs.swift b/PadelClub/Utils/URLs.swift index eddbe65..12a6429 100644 --- a/PadelClub/Utils/URLs.swift +++ b/PadelClub/Utils/URLs.swift @@ -10,8 +10,8 @@ import Foundation enum URLs: String, Identifiable { // case httpScheme = "https://" #if DEBUG - case activationHost = "xlr.alwaysdata.net" - case main = "https://xlr.alwaysdata.net/" + case activationHost = "http://127.0.0.1:8000" + case main = "http://127.0.0.1:8000/" // case api = "https://xlr.alwaysdata.net/roads/" #elseif TESTFLIGHT case activationHost = "xlr.alwaysdata.net" diff --git a/PadelClub/Views/Cashier/Event/EventCreationView.swift b/PadelClub/Views/Cashier/Event/EventCreationView.swift index 5ba6f87..a6059bf 100644 --- a/PadelClub/Views/Cashier/Event/EventCreationView.swift +++ b/PadelClub/Views/Cashier/Event/EventCreationView.swift @@ -138,12 +138,13 @@ struct EventCreationView: View { Logger.error(error) } + let templateTournament = Tournament.getTemplateTournament() tournaments.forEach { tournament in tournament.event = event.id tournament.courtCount = selectedClub?.courtCount ?? 2 tournament.startDate = startingDate tournament.dayDuration = duration - tournament.setupFederalSettings() + tournament.initSettings(templateTournament: templateTournament) } do { diff --git a/PadelClub/Views/Cashier/Event/EventTournamentsView.swift b/PadelClub/Views/Cashier/Event/EventTournamentsView.swift index 198a313..1b70684 100644 --- a/PadelClub/Views/Cashier/Event/EventTournamentsView.swift +++ b/PadelClub/Views/Cashier/Event/EventTournamentsView.swift @@ -13,6 +13,7 @@ struct EventTournamentsView: View { @Environment(NavigationViewModel.self) private var navigation let event: Event @State private var newTournament: Tournament? + @State private var mainTournament: Tournament? var presentTournamentCreationView: Binding { Binding( get: { newTournament != nil }, @@ -27,17 +28,37 @@ struct EventTournamentsView: View { let tournaments = event.tournaments List { ForEach(tournaments) { tournament in - NavigationLink { - TournamentStatusView(tournament: tournament, eventDismiss: true) - } label: { - TournamentCellView(tournament: tournament) - .contextMenu { - Button { - navigation.openTournamentInOrganizer(tournament) - } label: { - Label("Voir dans le gestionnaire", systemImage: "line.diagonal.arrow") + Section { + NavigationLink { + TournamentStatusView(tournament: tournament, eventDismiss: true) + } label: { + TournamentCellView(tournament: tournament) + .contextMenu { + Button { + navigation.openTournamentInOrganizer(tournament) + } label: { + Label("Voir dans le gestionnaire", systemImage: "line.diagonal.arrow") + } + } + } + } footer: { + if event.tournaments.count > 1 { + if mainTournament == nil { + FooterButtonView("c'est le tournoi principal") { + self.mainTournament = tournament + } + } else if mainTournament == tournament { + FooterButtonView("ce n'est pas le tournoi principal") { + self.mainTournament = tournament + } + } else if let mainTournament { + FooterButtonView("coller les réglages du tournoi principal") { + tournament.setupUmpireSettings(defaultTournament: mainTournament) + tournament.setupRegistrationSettings(templateTournament: mainTournament) + dataStore.tournaments.addOrUpdate(instance: tournament) } } + } } } } @@ -63,13 +84,9 @@ struct EventTournamentsView: View { newTournament.courtCount = event.eventCourtCount() newTournament.startDate = event.eventStartDate() newTournament.dayDuration = event.eventDayDuration() - newTournament.setupFederalSettings() - - do { - try dataStore.tournaments.addOrUpdate(instance: newTournament) - } catch { - Logger.error(error) - } + newTournament.initSettings(templateTournament: Tournament.getTemplateTournament()) + + dataStore.tournaments.addOrUpdate(instance: newTournament) self.newTournament = nil } diff --git a/PadelClub/Views/Navigation/Agenda/CalendarView.swift b/PadelClub/Views/Navigation/Agenda/CalendarView.swift index 1dab0aa..7025f9a 100644 --- a/PadelClub/Views/Navigation/Agenda/CalendarView.swift +++ b/PadelClub/Views/Navigation/Agenda/CalendarView.swift @@ -72,6 +72,7 @@ struct CalendarView: View { if federalDataViewModel.ageCategories.isEmpty == false { tournament.federalTournamentAge = federalDataViewModel.ageCategories.first! } + tournament.initSettings(templateTournament: Tournament.getTemplateTournament()) newTournament = tournament } @@ -173,7 +174,8 @@ struct CalendarView: View { newTournament.federalTournamentAge = build.age newTournament.dayDuration = federalTournament.dayDuration newTournament.startDate = federalTournament.startDate.atBeginningOfDay(hourInt: 9) - newTournament.setupFederalSettings() + newTournament.initSettings(templateTournament: Tournament.getTemplateTournament()) + do { try dataStore.tournaments.addOrUpdate(instance: newTournament) } catch { diff --git a/PadelClub/Views/Navigation/Agenda/EventListView.swift b/PadelClub/Views/Navigation/Agenda/EventListView.swift index ede8b69..fece518 100644 --- a/PadelClub/Views/Navigation/Agenda/EventListView.swift +++ b/PadelClub/Views/Navigation/Agenda/EventListView.swift @@ -133,7 +133,7 @@ struct EventListView: View { try FileManager.default.removeItem(at: chunk.url) } - try dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) + dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) } catch { Logger.error(error) } @@ -147,119 +147,103 @@ struct EventListView: View { Divider() } Menu { - if pcTournaments.anySatisfy({ $0.isPrivate == true }) { - Button { - pcTournaments.forEach { tournament in - tournament.isPrivate = false - } - do { - try dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) - } catch { - Logger.error(error) - } - } label: { - Text("Afficher sur Padel Club") + Button { + pcTournaments.forEach { tournament in + tournament.isPrivate = false } + dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) + } label: { + Text("Afficher sur Padel Club") } - if pcTournaments.anySatisfy({ $0.isPrivate == false }) { - Button { - pcTournaments.forEach { tournament in - tournament.isPrivate = true - } - do { - try dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) - } catch { - Logger.error(error) - } - } label: { - Text("Masquer sur Padel Club") + Button { + pcTournaments.forEach { tournament in + tournament.isPrivate = true } + dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) + } label: { + Text("Masquer sur Padel Club") } } label: { Text("Visibilité sur Padel Club") } Divider() - if pcTournaments.anySatisfy({ $0.hasEnded() == false && $0.enableOnlineRegistration == false && $0.onlineRegistrationCanBeEnabled() }) || pcTournaments.anySatisfy({ $0.enableOnlineRegistration == true && $0.hasEnded() == false }) { - Menu { - if pcTournaments.anySatisfy({ $0.hasEnded() == false && $0.enableOnlineRegistration == false && $0.onlineRegistrationCanBeEnabled() }) { - Button { - pcTournaments.forEach { tournament in - tournament.enableOnlineRegistration = true - } - do { - try dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) - } catch { - Logger.error(error) - } - } label: { - Text("Activer") + Menu { + Button { + Task { + await pcTournaments.concurrentForEach { tournament in + await tournament.refreshTeamList(forced: true) } } - - if pcTournaments.anySatisfy({ $0.enableOnlineRegistration == true && $0.hasEnded() == false }) { - - Button { - Task { - await pcTournaments.concurrentForEach { tournament in - await tournament.refreshTeamList(forced: true) - } - } - } label: { - Text("M-à-j des inscriptions") + } label: { + Text("M-à-j des inscriptions") + } + + Button { + pcTournaments.forEach { tournament in + if tournament.onlineRegistrationCanBeEnabled() { + tournament.enableOnlineRegistration = true } - - - Button { + } + dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) + } label: { + Text("Activer") + } + + Button { + pcTournaments.forEach { tournament in + tournament.enableOnlineRegistration = false + } + dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) + } label: { + Text("Désactiver") + } + } label: { + Text("Inscription en ligne") + } + + Divider() + + if dataStore.user.canEnableOnlinePayment() { + Menu { + Button { + if let templateTournament = Tournament.getTemplateTournament() { pcTournaments.forEach { tournament in - tournament.enableOnlineRegistration = false - } - do { - try dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) - } catch { - Logger.error(error) + if tournament.onlineRegistrationCanBeEnabled() { + tournament.setupRegistrationSettings(templateTournament: templateTournament) + } } - } label: { - Text("Désactiver") + dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) } + } label: { + Text("Utiliser les réglages par défaut") } } label: { - Text("Inscription en ligne") + Text("Inscription et paiement en ligne") } + Divider() } - Divider() Menu { Button { pcTournaments.forEach { tournament in tournament.information = nil } - do { - try dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) - } catch { - Logger.error(error) - } + dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) } label: { Text("Effacer les descriptions") } - let info = Set(pcTournaments.compactMap { tournament in - tournament.information?.trimmedMultiline - }).joined(separator: "\n") - - if info.isEmpty == false { - Button { - pcTournaments.forEach { tournament in - tournament.information = info - } - do { - try dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) - } catch { - Logger.error(error) - } - - } label: { - Text("Mettre '\(info.trunc(length: 12))'") + Button { + let info = Set(pcTournaments.compactMap { tournament in + tournament.information?.trimmedMultiline + }).joined(separator: "\n") + + pcTournaments.forEach { tournament in + tournament.information = info } + dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) + } label: { + Text("Mettre la même description") } PasteButton(payloadType: String.self) { strings in @@ -267,11 +251,7 @@ struct EventListView: View { pcTournaments.forEach { tournament in tournament.information = pasteboard } - do { - try dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) - } catch { - Logger.error(error) - } + dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) } } } label: { @@ -284,69 +264,24 @@ struct EventListView: View { tournament.umpireCustomMail = nil tournament.umpireCustomPhone = nil tournament.umpireCustomContact = nil + tournament.hideUmpireMail = dataStore.user.hideUmpireMail + tournament.hideUmpirePhone = dataStore.user.hideUmpirePhone } dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) } label: { - Text("Effacer les informations du JAP") - } - - let umpireCustomMail = pcTournaments.first(where: { tournament in - tournament.umpireCustomMail != nil - })?.umpireCustomMail - let umpireCustomPhone = pcTournaments.first(where: { tournament in - tournament.umpireCustomPhone != nil - })?.umpireCustomPhone - let umpireCustomContact = pcTournaments.first(where: { tournament in - tournament.umpireCustomContact != nil - })?.umpireCustomContact - Button { - pcTournaments.forEach { tournament in - tournament.umpireCustomMail = umpireCustomMail - tournament.umpireCustomPhone = umpireCustomPhone - tournament.umpireCustomContact = umpireCustomContact - } - dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) - } label: { - Text("Indiquer le même JAP pour tous") + Text("Retirer les informations personnalisées") } Button { pcTournaments.forEach { tournament in - tournament.hideUmpireMail = true - } - dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) - } label: { - Text("Masquer le mail") - } - - Button { - pcTournaments.forEach { tournament in - tournament.hideUmpireMail = false - } - dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) - } label: { - Text("Afficher le mail") - } - - Button { - pcTournaments.forEach { tournament in - tournament.hideUmpirePhone = true - } - dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) - } label: { - Text("Masquer le téléphone") - } - - Button { - pcTournaments.forEach { tournament in - tournament.hideUmpirePhone = false + tournament.setupUmpireSettings() } dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments) } label: { - Text("Afficher le téléphone") + Text("Utiliser les réglages par défaut") } } label: { - Text("Infos JAP") + Text("Informations de contact Juge-Arbitre") } } @@ -400,7 +335,17 @@ struct EventListView: View { } } .listRowView(isActive: tournament.enableOnlineRegistration, color: .green, hideColorVariation: true) + .onChange(of: tournament.isTemplate) { + dataStore.tournaments.addOrUpdate(instance: tournament) + } .contextMenu { + @Bindable var bindableTournament: Tournament = tournament + Toggle(isOn: $bindableTournament.isTemplate) { + Text("Source des réglages d'inscriptions") + } + + Divider() + if tournament.hasEnded() == false { Button { navigation.openTournamentInOrganizer(tournament) @@ -441,14 +386,15 @@ struct EventListView: View { } private func _importFederalTournamentBatch(federalTournament: FederalTournament) { - federalTournament.tournaments.forEach { tournament in - _create(federalTournament: federalTournament, existingTournament: _event(of: federalTournament)?.existingBuild(tournament), build: tournament) + let templateTournament = Tournament.getTemplateTournament() + let newTournaments = federalTournament.tournaments.compactMap { tournament in + _create(federalTournament: federalTournament, existingTournament: _event(of: federalTournament)?.existingBuild(tournament), build: tournament, templateTournament: templateTournament) } + dataStore.tournaments.addOrUpdate(contentOfs: newTournaments) } - private func _create(federalTournament: FederalTournament, existingTournament: Tournament?, build: any TournamentBuildHolder) { - if let existingTournament { - } else { + private func _create(federalTournament: FederalTournament, existingTournament: Tournament?, build: any TournamentBuildHolder, templateTournament: Tournament?) -> Tournament? { + if existingTournament == nil { let event = federalTournament.getEvent() let newTournament = Tournament.newEmptyInstance() newTournament.event = event.id @@ -460,12 +406,10 @@ struct EventListView: View { newTournament.federalTournamentAge = build.age newTournament.dayDuration = federalTournament.dayDuration newTournament.startDate = federalTournament.startDate.atBeginningOfDay(hourInt: 9) - newTournament.setupFederalSettings() - do { - try dataStore.tournaments.addOrUpdate(instance: newTournament) - } catch { - Logger.error(error) - } + newTournament.initSettings(templateTournament: templateTournament) + return newTournament + } else { + return nil } } } diff --git a/PadelClub/Views/Navigation/Umpire/UmpireView.swift b/PadelClub/Views/Navigation/Umpire/UmpireView.swift index 2d184d3..cf2965a 100644 --- a/PadelClub/Views/Navigation/Umpire/UmpireView.swift +++ b/PadelClub/Views/Navigation/Umpire/UmpireView.swift @@ -18,9 +18,22 @@ struct UmpireView: View { @State private var presentSearchView: Bool = false @State private var showSubscriptions: Bool = false @State private var showProductIds: Bool = false - + @State private var umpireCustomMail: String + @State private var umpireCustomPhone: String + @State private var umpireCustomContact: String + @State private var umpireCustomMailIsInvalid: Bool = false + @State private var umpireCustomPhoneIsInvalid: Bool = false + + @FocusState private var focusedField: CustomUser.CodingKeys? + // @State var isConnected: Bool = false + init() { + _umpireCustomMail = State(wrappedValue: DataStore.shared.user.umpireCustomMail ?? "") + _umpireCustomPhone = State(wrappedValue: DataStore.shared.user.umpireCustomPhone ?? "") + _umpireCustomContact = State(wrappedValue: DataStore.shared.user.umpireCustomContact ?? "") + } + enum UmpireScreen { case login } @@ -128,8 +141,41 @@ struct UmpireView: View { // Text("Statistiques de participations") // } // } -// +// + _customUmpireView() + Section { + @Bindable var user = dataStore.user + if dataStore.user.hideUmpireMail, dataStore.user.hideUmpirePhone { + Text("Attention, les emails envoyés automatiquement au regard des inscriptions en ligne ne contiendront aucun moyen de vous contacter.").foregroundStyle(.logoRed) + } + + Toggle(isOn: $user.hideUmpireMail) { + Text("Masquer l'email") + } + Toggle(isOn: $user.hideUmpirePhone) { + Text("Masquer le téléphone") + } + + } footer: { + Text("Ces informations ne seront pas affichées sur la page d'information des tournois sur Padel Club et dans les emails envoyés automatiquement au regard des inscriptions en lignes.") + } + + + Section { + @Bindable var user = dataStore.user + Toggle(isOn: $user.disableRankingFederalRuling) { + Text("Désactiver la règle fédéral") + } + .onChange(of: user.disableRankingFederalRuling) { + dataStore.saveUser() + } + } header: { + Text("Règle fédérale classement finale") + } footer: { + Text("Dernier de poule ≠ dernier du tournoi") + } + Section { @Bindable var user = dataStore.user Picker(selection: $user.loserBracketMode) { @@ -195,6 +241,61 @@ struct UmpireView: View { } #endif } + .navigationBarBackButtonHidden(focusedField != nil) + .toolbar(content: { + if focusedField != nil { + ToolbarItem(placement: .topBarLeading) { + Button("Annuler", role: .cancel) { + focusedField = nil + } + } + } + }) + .toolbar { + if focusedField != nil { + ToolbarItem(placement: .keyboard) { + HStack { + if focusedField == ._umpireCustomMail, umpireCustomMail.isEmpty == false { + Button("Effacer") { + _deleteUmpireMail() + } + .buttonStyle(.borderless) + } else if focusedField == ._umpireCustomPhone, umpireCustomPhone.isEmpty == false { + Button("Effacer") { + _deleteUmpirePhone() + } + .buttonStyle(.borderless) + } else if focusedField == ._umpireCustomContact, umpireCustomContact.isEmpty == false { + Button("Effacer") { + _deleteUmpireContact() + } + .buttonStyle(.borderless) + } + Spacer() + Button("Valider") { + focusedField = nil + } + .buttonStyle(.bordered) + } + } + } + } + .onChange(of: [dataStore.user.umpireCustomMail, dataStore.user.umpireCustomPhone, dataStore.user.umpireCustomContact]) { + self.dataStore.saveUser() + } + .onChange(of: [dataStore.user.hideUmpireMail, dataStore.user.hideUmpirePhone]) { + self.dataStore.saveUser() + } + .onChange(of: focusedField) { old, new in + if old == ._umpireCustomMail { + _confirmUmpireMail() + } else if old == ._umpireCustomPhone { + _confirmUmpirePhone() + } else if old == ._umpireCustomContact { + _confirmUmpireContact() + } + } + .sheet(isPresented: self.$showSubscriptions, content: { NavigationStack { SubscriptionView(isPresented: self.$showSubscriptions) @@ -242,6 +343,108 @@ struct UmpireView: View { } } + private func _confirmUmpireMail() { + umpireCustomMailIsInvalid = false + if umpireCustomMail.isEmpty { + dataStore.user.umpireCustomMail = nil + } else if umpireCustomMail.isValidEmail() { + dataStore.user.umpireCustomMail = umpireCustomMail + } else { + umpireCustomMailIsInvalid = true + } + } + + private func _deleteUmpireMail() { + umpireCustomMailIsInvalid = false + umpireCustomMail = "" + dataStore.user.umpireCustomMail = nil + } + + private func _confirmUmpirePhone() { + umpireCustomPhoneIsInvalid = false + if umpireCustomPhone.isEmpty { + dataStore.user.umpireCustomPhone = nil + } else if umpireCustomPhone.isPhoneNumber() { + dataStore.user.umpireCustomPhone = umpireCustomPhone.prefixMultilineTrimmed(15) + } else { + umpireCustomPhoneIsInvalid = true + } + } + + private func _deleteUmpirePhone() { + umpireCustomPhoneIsInvalid = false + umpireCustomPhone = "" + dataStore.user.umpireCustomPhone = nil + } + + private func _confirmUmpireContact() { + if umpireCustomContact.isEmpty { + dataStore.user.umpireCustomContact = nil + } else { + dataStore.user.umpireCustomContact = umpireCustomContact.prefixMultilineTrimmed(200) + } + } + + private func _deleteUmpireContact() { + umpireCustomContact = "" + dataStore.user.umpireCustomContact = nil + } + + + private func _customUmpireView() -> some View { + Section { + VStack(alignment: .leading) { + TextField(dataStore.user.email, text: $umpireCustomMail) + .frame(maxWidth: .infinity) + .keyboardType(.emailAddress) + .autocapitalization(.none) + .focused($focusedField, equals: ._umpireCustomMail) + .onSubmit { + _confirmUmpireMail() + } + if umpireCustomMailIsInvalid { + Text("Vous n'avez pas indiqué un email valide.").foregroundStyle(.logoRed) + } + } + + VStack(alignment: .leading) { + TextField(dataStore.user.phone ?? "Téléphone", text: $umpireCustomPhone) + .frame(maxWidth: .infinity) + .keyboardType(.phonePad) + .focused($focusedField, equals: ._umpireCustomPhone) + .onSubmit { + _confirmUmpirePhone() + } + if umpireCustomPhoneIsInvalid { + Text("Vous n'avez pas indiqué un téléphone valide.").foregroundStyle(.logoRed) + } + } + + + VStack(alignment: .leading) { + TextField(dataStore.user.fullName(), text: $umpireCustomContact) + .frame(maxWidth: .infinity) + .keyboardType(.default) + .focused($focusedField, equals: ._umpireCustomContact) + .onSubmit { + _confirmUmpireContact() + } + if dataStore.user.getSummonsMessageSignature() != nil, umpireCustomContact != dataStore.user.fullName() { + Text("Attention vous avez une signature personnalisée contenant un contact différent.").foregroundStyle(.logoRed) + + FooterButtonView("retirer la personnalisation ?") { + dataStore.user.summonsMessageSignature = nil + self.dataStore.saveUser() + } + } } + + } header: { + Text("Juge-arbitre") + } footer: { + Text("Ces informations seront utilisées pour vous contacter. Vous pouvez les modifier si vous souhaitez utiliser les informations de contact différentes de votre compte Padel Club.") + } + } + } struct AccountRowView: View { @@ -293,6 +496,7 @@ struct ProductIdsView: View { } } } + } diff --git a/PadelClub/Views/Shared/OnlineWaitingListFaqSheetView.swift b/PadelClub/Views/Shared/OnlineWaitingListFaqSheetView.swift new file mode 100644 index 0000000..3dcae44 --- /dev/null +++ b/PadelClub/Views/Shared/OnlineWaitingListFaqSheetView.swift @@ -0,0 +1,89 @@ +// +// OnlineWaitingListFaqSheetView.swift +// PadelClub +// +// Created by razmig on 10/04/2025. +// + +import SwiftUI + +struct OnlineWaitingListFaqSheetView: View { + @Environment(\.dismiss) private var dismiss + let faqText: String = + """ + FAQ pour les Arbitres - Confirmation des Équipes + + Comment fonctionne le délai de confirmation pour les équipes ? + + Notre système calcule automatiquement un délai de confirmation adapté pour les équipes en fonction de trois facteurs principaux : + - Proximité du tournoi : Plus le tournoi est proche, plus le délai est court + - Pression de la liste d'attente : Plus il y a d'équipes en attente, plus le délai est court + - Heures ouvrables : Les délais respectent généralement les heures ouvrables (8h-21h) + + Quels sont les délais typiques de confirmation ? + - Tournoi dans moins de 24h → 30 minutes + - Tournoi dans moins de 48h → 60 minutes (1 heure) + - Tournoi dans moins de 72h → 120 minutes (2 heures) + - Tournoi dans plus de 72h → 240 minutes (4 heures) + + Ces délais peuvent être raccourcis en fonction du nombre d'équipes en liste d'attente : + - 30+ équipes en attente → 30 minutes + - 20+ équipes en attente → 60 minutes (1 heure) + - 10+ équipes en attente → 120 minutes (2 heures) + + Y a-t-il des exceptions à ces règles ? + + Oui, dans les situations urgentes : + - Si le tournoi commence dans moins de 24h, les restrictions d'heures ouvrables sont ignorées + - Si le tournoi commence dans moins de 12h, toutes les restrictions sont assouplies avec un minimum de 30 minutes de délai + + Comment les délais sont-ils arrondis ? + + Les délais sont toujours arrondis à la demi-heure supérieure pour plus de simplicité. + + Que se passe-t-il si le délai tombe en dehors des heures ouvrables ? + + Si le délai calculé tombe en dehors des heures ouvrables (avant 8h ou après 21h), il est automatiquement reporté au jour ouvrable suivant à 8h du matin. + """ + var body: some View { + NavigationView { + ScrollView { + VStack(alignment: .leading, spacing: 20) { + // Content sections + ForEach(faqText.components(separatedBy: "\n\n"), id: \.self) { section in + if !section.isEmpty { + VStack(alignment: .leading, spacing: 10) { + if section.contains(":") { + Text(section.components(separatedBy: ":")[0]) + .font(.headline) + .foregroundColor(.primary) + + let bulletPoints = section.components(separatedBy: "\n-") + if bulletPoints.count > 1 { + ForEach(bulletPoints.dropFirst(), id: \.self) { point in + HStack(alignment: .top) { + Text("•") + .padding(.trailing, 5) + Text(point) + .fixedSize(horizontal: false, vertical: true) + } + } + } + } else { + Text(section) + } + } + .padding(.bottom, 10) + } + } + } + .padding() + } + .navigationBarItems(trailing: Button("Fermer") { + dismiss() + }) + .toolbarBackground(.visible, for: .navigationBar) + .navigationTitle("FAQ - Liste d'attente") + } + } +} diff --git a/PadelClub/Views/Shared/PaymentInfoSheetView.swift b/PadelClub/Views/Shared/PaymentInfoSheetView.swift new file mode 100644 index 0000000..11e734f --- /dev/null +++ b/PadelClub/Views/Shared/PaymentInfoSheetView.swift @@ -0,0 +1,90 @@ +// +// PaymentInfoSheetView.swift +// PadelClub +// +// Created by razmig on 15/01/2025. +// + +import SwiftUI + +struct PaymentInfoSheetView: View { + @Environment(\.dismiss) private var dismiss + let paymentInfoText: String = + """ + Comment fonctionnent les paiements en ligne ? + + Les paiements en ligne permettent aux joueurs de régler les frais de tournoi directement via la plateforme. Voici les informations importantes à connaître : + + Options de paiement : + - Le paiement en ligne est activé à la discrétion de l'organisateur + - L'organisateur peut rendre le paiement en ligne obligatoire ou optionnel + - Si le paiement n'est pas obligatoire, il est possible de s'inscrire sans payer immédiatement + - Tous les paiements sont traités via Stripe, une plateforme sécurisée de paiement en ligne + + Remboursements : + - Les remboursements peuvent être activés ou désactivés par l'organisateur + - Si activés, une date limite de remboursement peut être définie + - Aucun remboursement n'est possible après cette date limite + - Les remboursements sont automatiquement traités via la même méthode de paiement utilisée + + Commissions et frais : + - Padel Club prélève une commission de 0,75% sur chaque transaction + - Cette commission couvre les frais de service et de maintenance de la plateforme + - Des frais supplémentaires de Stripe peuvent s'appliquer (environ 1,4% + 0,25€ par transaction) + - Le montant total des frais est indiqué clairement avant validation du paiement + + Exigences pour les organisateurs : + - L'organisateur doit avoir un compte Stripe valide pour recevoir les paiements + - Le compte Stripe doit être vérifié et connecté à Padel Club + - Sans compte Stripe connecté, l'option de paiement en ligne ne peut pas être activée + - Les fonds sont directement versés sur le compte bancaire associé au compte Stripe de l'organisateur + + Sécurité : + - Toutes les transactions sont sécurisées et chiffrées + - Padel Club ne stocke pas les informations de carte bancaire + - La conformité RGPD et PCI-DSS est assurée par Stripe + + En cas de problème avec un paiement, veuillez contacter l'organisateur du tournoi ou le support Padel Club. + """ + var body: some View { + NavigationView { + ScrollView { + VStack(alignment: .leading, spacing: 20) { + // Content sections + ForEach(paymentInfoText.components(separatedBy: "\n\n"), id: \.self) { section in + if !section.isEmpty { + VStack(alignment: .leading, spacing: 10) { + if section.contains(":") { + Text(section.components(separatedBy: ":")[0]) + .font(.headline) + .foregroundColor(.primary) + + let bulletPoints = section.components(separatedBy: "\n-") + if bulletPoints.count > 1 { + ForEach(bulletPoints.dropFirst(), id: \.self) { point in + HStack(alignment: .top) { + Text("•") + .padding(.trailing, 5) + Text(point) + .fixedSize(horizontal: false, vertical: true) + } + } + } + } else { + Text(section) + } + } + .padding(.bottom, 10) + } + } + } + .padding() + } + .navigationBarItems(trailing: Button("Fermer") { + dismiss() + }) + .toolbarBackground(.visible, for: .navigationBar) + .navigationTitle("Paiement en ligne") + } + } +} diff --git a/PadelClub/Views/Tournament/Screen/Components/EventClubSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/EventClubSettingsView.swift index c1b56e9..7e57d9c 100644 --- a/PadelClub/Views/Tournament/Screen/Components/EventClubSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/EventClubSettingsView.swift @@ -30,6 +30,10 @@ struct EventClubSettingsView: View { Text("Lieu de l'événement") } footer: { HStack { + if let clubURL = selectedClub.shareURL() { + ShareLink(item: clubURL) + } + Spacer() FooterButtonView("détails du club") { showClubDetail = selectedClub diff --git a/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift b/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift index 69a47f0..16856e0 100644 --- a/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift +++ b/PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift @@ -5,8 +5,8 @@ // Created by razmig on 20/11/2024. // -import SwiftUI import LeStorage +import SwiftUI struct RegistrationSetupView: View { @EnvironmentObject var dataStore: DataStore @@ -24,7 +24,22 @@ struct RegistrationSetupView: View { @State private var licenseIsRequired: Bool @State private var minPlayerPerTeam: Int @State private var maxPlayerPerTeam: Int - @State private var showMoreInfos: Bool = false + @State private var showMoreRegistrationInfos: Bool = false + @State private var showMoreOnlineWaitingListInfos: Bool = false + @State private var showMorePaymentInfos: Bool = false + @State private var enableTimeToConfirm: Bool + @State private var isTemplate: Bool + @State private var isCorporateTournament: Bool + + // Online Payment + @State private var enableOnlinePayment: Bool + @State private var onlinePaymentIsMandatory: Bool + @State private var enableOnlinePaymentRefund: Bool + @State private var refundDateLimit: Date + @State private var refundDateLimitEnabled: Bool + @State private var stripeAccountId: String + @State private var stripeAccountIdIsInvalid: Bool = false + @FocusState private var focusedField: Tournament.CodingKeys? @State private var hasChanges: Bool = false @@ -33,7 +48,8 @@ struct RegistrationSetupView: View { init(tournament: Tournament) { self.tournament = tournament _enableOnlineRegistration = .init(wrappedValue: tournament.enableOnlineRegistration) - + _isTemplate = .init(wrappedValue: tournament.isTemplate) + _isCorporateTournament = .init(wrappedValue: tournament.isCorporateTournament) // Registration Date Limit if let registrationDateLimit = tournament.registrationDateLimit { _registrationDateLimit = .init(wrappedValue: registrationDateLimit) @@ -70,6 +86,21 @@ struct RegistrationSetupView: View { _maxPlayerPerTeam = .init(wrappedValue: tournament.maximumPlayerPerTeam) _minPlayerPerTeam = .init(wrappedValue: tournament.minimumPlayerPerTeam) + // Online Payment + _enableOnlinePayment = .init(wrappedValue: tournament.enableOnlinePayment) + _onlinePaymentIsMandatory = .init(wrappedValue: tournament.onlinePaymentIsMandatory) + _enableOnlinePaymentRefund = .init(wrappedValue: tournament.enableOnlinePaymentRefund) + _stripeAccountId = .init(wrappedValue: tournament.stripeAccountId ?? "") + _enableTimeToConfirm = .init(wrappedValue: tournament.enableTimeToConfirm) + + // Refund Date Limit + if let refundDateLimit = tournament.refundDateLimit { + _refundDateLimit = .init(wrappedValue: refundDateLimit) + _refundDateLimitEnabled = .init(wrappedValue: true) + } else { + _refundDateLimit = .init(wrappedValue: tournament.startDate.truncateMinutesAndSeconds()) + _refundDateLimitEnabled = .init(wrappedValue: false) + } } func displayWarning() -> Bool { @@ -88,13 +119,22 @@ struct RegistrationSetupView: View { Text("Les inscriptions en ligne permettent à des joueurs de s'inscrire à votre tournoi en passant par le site Padel Club. Vous verrez alors votre liste d'inscription s'agrandir dans la vue Gestion des Inscriptions de l'application.") FooterButtonView("En savoir plus") { - self.showMoreInfos = true + self.showMoreRegistrationInfos = true } } } if enableOnlineRegistration { + + Section { + Toggle(isOn: $isTemplate) { + Text("Définir en tant que réglages par défaut") + } + } footer: { + Text("Définisser ce tournoi comme la source de vos réglages concernant l'inscription en ligne. Tous les tournois crées après celui-ci utiliseront ces réglages.") + } + if let shareURL = tournament.shareURL(.info) { Section { Link(destination: shareURL) { @@ -113,6 +153,23 @@ struct RegistrationSetupView: View { } } + if dataStore.user.canEnableOnlinePayment() { + Section { + Toggle(isOn: $enableTimeToConfirm) { + Text("Automatique") + } + } header: { + Text("Gestion des confirmations") + } footer: { + VStack(alignment: .leading) { + Text("Activer la gestion automatique des confirmations pour ne plus vous occuper de la gestion de la file d'attente.") + FooterButtonView("En savoir plus") { + self.showMoreOnlineWaitingListInfos = true + } + } + } + } + Section { Toggle(isOn: $openingRegistrationDateEnabled) { Text("Définir une date") @@ -179,6 +236,10 @@ struct RegistrationSetupView: View { Text("Si une limite à la liste d'attente existe, les inscriptions ne seront plus possibles une fois la liste d'attente pleine. Si aucune limite de liste d'attente n'est active, alors les inscriptions seront toujours possibles. Les joueurs auront une indication comme quoi ils sont en liste d'attente.") } + if dataStore.user.canEnableOnlinePayment() { + _onlinePaymentsView() + } + if tournament.isAnimation() { Section { // Toggle(isOn: $userAccountIsRequired) { @@ -210,9 +271,16 @@ struct RegistrationSetupView: View { ) } } - .sheet(isPresented: $showMoreInfos) { + .sheet(isPresented: $showMoreRegistrationInfos) { RegistrationInfoSheetView() } + .sheet(isPresented: $showMoreOnlineWaitingListInfos) { + OnlineWaitingListFaqSheetView() + } + .sheet(isPresented: $showMorePaymentInfos) { + PaymentInfoSheetView() + } + .toolbar(content: { if hasChanges { ToolbarItem(placement: .topBarLeading) { @@ -229,6 +297,36 @@ struct RegistrationSetupView: View { } } }) + .toolbar(content: { + if focusedField != nil { + ToolbarItem(placement: .topBarLeading) { + Button("Annuler", role: .cancel) { + focusedField = nil + } + } + } + }) + .toolbar { + if focusedField != nil { + ToolbarItem(placement: .keyboard) { + HStack { + if focusedField == ._stripeAccountId, stripeAccountId.isEmpty == false { + Button("Effacer") { + stripeAccountId = "" + tournament.stripeAccountId = nil + } + .buttonStyle(.borderless) + Spacer() + Button("Valider") { + focusedField = nil + } + .buttonStyle(.bordered) + } + } + } + } + } + .toolbarRole(.editor) .headerProminence(.increased) .navigationTitle("Inscription en ligne") @@ -273,11 +371,102 @@ struct RegistrationSetupView: View { .onChange(of: [minPlayerPerTeam, maxPlayerPerTeam]) { _hasChanged() } - .onChange(of: [userAccountIsRequired, licenseIsRequired]) { + .onChange(of: [isTemplate, userAccountIsRequired, licenseIsRequired, enableTimeToConfirm, isCorporateTournament]) { _hasChanged() } } + private func _onlinePaymentsView() -> some View { + Section { + Toggle(isOn: $enableOnlinePayment) { + Text("Activer le paiement en ligne") + } + + if enableOnlinePayment { + + if let fee = dataStore.user.registrationPaymentMode.localizedRegistrationPaymentFee() { + let entryFee = tournament.entryFee ?? 20 + Text("Cette fonction entraîne un coût supplémentaire, en effet Padel Club touchera une commission de \(fee) par paiement en ligne. Soit \(dataStore.user.registrationPaymentMode.sample(entryFee: entryFee)) centimes pour une inscription de \(entryFee)€ par exemple.").foregroundStyle(.logoRed).bold() + } + + Toggle(isOn: $onlinePaymentIsMandatory) { + Text("Paiement obligatoire") + } + } + + Toggle(isOn: $enableOnlinePaymentRefund) { + Text("Autoriser les remboursements") + } + + if enableOnlinePaymentRefund { + Toggle(isOn: $refundDateLimitEnabled) { + Text("Définir une date limite") + } + + if refundDateLimitEnabled { + DatePicker(selection: $refundDateLimit) { + DateMenuView(date: $refundDateLimit) + } + } + } + + if dataStore.user.registrationPaymentMode == .corporate { + Toggle(isOn: $isCorporateTournament) { + Text("Revenu Padel Club") + } + } + + if isCorporateTournament == false, dataStore.user.registrationPaymentMode.requiresStripe() { + VStack(alignment: .leading) { + TextField("Identifiant du compte Stripe", text: $stripeAccountId) + .frame(maxWidth: .infinity) + .keyboardType(.default) + .focused($focusedField, equals: ._stripeAccountId) + if stripeAccountIdIsInvalid { + Text("Format d'identifiant Stripe invalide.").foregroundStyle(.logoRed) + } + } + } + } header: { + Text("Paiement en ligne") + } footer: { + VStack(alignment: .leading) { + Text("Permettez aux joueurs de payer leur inscription en ligne. Vous devez connecter un compte Stripe pour recevoir les paiements.") + + FooterButtonView("En savoir plus") { + self.showMorePaymentInfos = true + } + } + } + + .onChange(of: [enableOnlinePayment, onlinePaymentIsMandatory, enableOnlinePaymentRefund]) { + _hasChanged() + } + .onChange(of: refundDateLimitEnabled) { + _hasChanged() + } + .onChange(of: refundDateLimit) { + _hasChanged() + } + .onChange(of: focusedField) { old, new in + if old == ._stripeAccountId { + _hasChanged() + } + } + + } + + private func _confirmStripeAccountId() { + stripeAccountIdIsInvalid = false + if stripeAccountId.isEmpty { + tournament.stripeAccountId = nil + } else if stripeAccountId.count >= 5, stripeAccountId.starts(with: "acct_") { + tournament.stripeAccountId = stripeAccountId.prefixMultilineTrimmed(255) + } else { + stripeAccountIdIsInvalid = true + } + } + private func _hasChanged() { hasChanges = true } @@ -286,17 +475,40 @@ struct RegistrationSetupView: View { hasChanges = false tournament.enableOnlineRegistration = enableOnlineRegistration - + tournament.isTemplate = isTemplate + tournament.isCorporateTournament = isCorporateTournament + if enableOnlineRegistration { tournament.accountIsRequired = userAccountIsRequired tournament.licenseIsRequired = licenseIsRequired tournament.minimumPlayerPerTeam = minPlayerPerTeam tournament.maximumPlayerPerTeam = maxPlayerPerTeam + + // Online Payment + tournament.enableOnlinePayment = enableOnlinePayment + tournament.onlinePaymentIsMandatory = onlinePaymentIsMandatory + tournament.enableOnlinePaymentRefund = enableOnlinePaymentRefund + + if refundDateLimitEnabled == false { + tournament.refundDateLimit = nil + } else { + tournament.refundDateLimit = refundDateLimit + } + + tournament.enableTimeToConfirm = enableTimeToConfirm + _confirmStripeAccountId() } else { tournament.accountIsRequired = true tournament.licenseIsRequired = true tournament.minimumPlayerPerTeam = 2 tournament.maximumPlayerPerTeam = 2 + tournament.enableTimeToConfirm = false + // When online registration is disabled, also disable online payment + tournament.enableOnlinePayment = false + tournament.onlinePaymentIsMandatory = false + tournament.enableOnlinePaymentRefund = false + tournament.refundDateLimit = nil + tournament.stripeAccountId = nil } if openingRegistrationDateEnabled == false { @@ -320,11 +532,7 @@ struct RegistrationSetupView: View { tournament.waitingListLimit = waitingListLimit } - do { - try self.dataStore.tournaments.addOrUpdate(instance: tournament) - } catch { - Logger.error(error) - } + self.dataStore.tournaments.addOrUpdate(instance: tournament) dismiss() } diff --git a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift index 1437452..b1bcd21 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift @@ -84,7 +84,7 @@ struct TournamentRankView: View { Toggle(isOn: $tournament.disableRankingFederalRuling) { Text("Désactiver la règle fédéral") - Text("Dernier de poule ≠ derner du tournoi") + Text("Dernier de poule ≠ dernier du tournoi") } .onChange(of: tournament.disableRankingFederalRuling) { dataStore.tournaments.addOrUpdate(instance: tournament) diff --git a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift index 1969855..d3bbc49 100644 --- a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift +++ b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift @@ -202,7 +202,7 @@ struct TournamentCellView: View { newTournament.federalTournamentAge = build.age newTournament.dayDuration = federalTournament.dayDuration newTournament.startDate = federalTournament.startDate.atBeginningOfDay(hourInt: 9) - newTournament.setupFederalSettings() + newTournament.initSettings(templateTournament: Tournament.getTemplateTournament()) do { try dataStore.tournaments.addOrUpdate(instance: newTournament) } catch {