online_payment
Raz 7 months ago
parent 3ea327cad2
commit 629dfe5120
  1. 16
      PadelClub.xcodeproj/project.pbxproj
  2. 76
      PadelClub/Data/CustomUser.swift
  3. 58
      PadelClub/Data/Gen/BaseCustomUser.swift
  4. 23
      PadelClub/Data/Gen/BasePlayerRegistration.swift
  5. 58
      PadelClub/Data/Gen/BaseTournament.swift
  6. 41
      PadelClub/Data/Gen/CustomUser.json
  7. 16
      PadelClub/Data/Gen/PlayerRegistration.json
  8. 46
      PadelClub/Data/Gen/Tournament.json
  9. 9
      PadelClub/Data/PlayerRegistration.swift
  10. 195
      PadelClub/Data/Tournament.swift
  11. 4
      PadelClub/Utils/URLs.swift
  12. 3
      PadelClub/Views/Cashier/Event/EventCreationView.swift
  13. 49
      PadelClub/Views/Cashier/Event/EventTournamentsView.swift
  14. 4
      PadelClub/Views/Navigation/Agenda/CalendarView.swift
  15. 248
      PadelClub/Views/Navigation/Agenda/EventListView.swift
  16. 208
      PadelClub/Views/Navigation/Umpire/UmpireView.swift
  17. 89
      PadelClub/Views/Shared/OnlineWaitingListFaqSheetView.swift
  18. 90
      PadelClub/Views/Shared/PaymentInfoSheetView.swift
  19. 4
      PadelClub/Views/Tournament/Screen/Components/EventClubSettingsView.swift
  20. 232
      PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift
  21. 2
      PadelClub/Views/Tournament/Screen/TournamentRankView.swift
  22. 2
      PadelClub/Views/Tournament/Shared/TournamentCellView.swift

@ -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 = "<group>"; };
FFE103112C366E5900684FC9 /* ImagePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePickerView.swift; sourceTree = "<group>"; };
FFE2D2E12C231BEE00D0C7BE /* SupportButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportButtonView.swift; sourceTree = "<group>"; };
FFE8B5B22DA848D300BDE966 /* OnlineWaitingListFaqSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnlineWaitingListFaqSheetView.swift; sourceTree = "<group>"; };
FFE8B5B62DA8763800BDE966 /* PaymentInfoSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentInfoSheetView.swift; sourceTree = "<group>"; };
FFE8C2BF2C7601E80046B243 /* ConfirmButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmButtonView.swift; sourceTree = "<group>"; };
FFEF7F4D2BDE69130033D0F0 /* MenuWarningView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuWarningView.swift; sourceTree = "<group>"; };
FFF0241C2BF48B15001F14B4 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -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 = "<group>";
@ -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 */,

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

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

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

@ -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<CodingKeys>) 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] {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save