Merge branch 'main' into sync3

sync3
Laurent 5 months ago
commit 07eb633ce6
  1. 13
      PadelClub.xcodeproj/project.pbxproj
  2. 7
      PadelClub/Data/Federal/FederalTournament.swift
  3. 1
      PadelClub/Extensions/PlayerRegistration+Extensions.swift
  4. 6
      PadelClub/Extensions/TeamRegistration+Extensions.swift
  5. 20
      PadelClub/Extensions/Tournament+Extensions.swift
  6. 2
      PadelClub/HTML Templates/bracket-template.html
  7. 13
      PadelClub/HTML Templates/match-template.html
  8. 23
      PadelClub/HTML Templates/tournament-template.html
  9. 10
      PadelClub/Utils/FileImportManager.swift
  10. 2
      PadelClub/Utils/HtmlGenerator.swift
  11. 38
      PadelClub/Utils/HtmlService.swift
  12. 157
      PadelClub/Utils/Network/StripeValidationService.swift
  13. 12
      PadelClub/Views/Cashier/CashierDetailView.swift
  14. 84
      PadelClub/Views/Cashier/CashierSettingsView.swift
  15. 25
      PadelClub/Views/Cashier/Event/EventCreationView.swift
  16. 94
      PadelClub/Views/Cashier/Event/EventSettingsView.swift
  17. 73
      PadelClub/Views/Cashier/Event/EventTournamentsView.swift
  18. 11
      PadelClub/Views/Cashier/Event/EventView.swift
  19. 4
      PadelClub/Views/Club/ClubDetailView.swift
  20. 4
      PadelClub/Views/Club/CourtView.swift
  21. 4
      PadelClub/Views/Club/Shared/ClubCourtSetupView.swift
  22. 4
      PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift
  23. 2
      PadelClub/Views/GroupStage/GroupStageView.swift
  24. 2
      PadelClub/Views/GroupStage/GroupStagesSettingsView.swift
  25. 8
      PadelClub/Views/Match/MatchDetailView.swift
  26. 2
      PadelClub/Views/Match/MatchSetupView.swift
  27. 17
      PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift
  28. 6
      PadelClub/Views/Navigation/Ongoing/OngoingDestination.swift
  29. 2
      PadelClub/Views/Navigation/Ongoing/OngoingView.swift
  30. 2
      PadelClub/Views/Planning/Components/MultiCourtPickerView.swift
  31. 33
      PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift
  32. 9
      PadelClub/Views/Planning/PlanningByCourtView.swift
  33. 118
      PadelClub/Views/Planning/PlanningSettingsView.swift
  34. 817
      PadelClub/Views/Planning/PlanningView.swift
  35. 6
      PadelClub/Views/Player/PlayerDetailView.swift
  36. 1
      PadelClub/Views/Round/LoserRoundView.swift
  37. 2
      PadelClub/Views/Round/LoserRoundsView.swift
  38. 23
      PadelClub/Views/Round/RoundView.swift
  39. 4
      PadelClub/Views/Score/FollowUpMatchView.swift
  40. 12
      PadelClub/Views/Shared/PaymentInfoSheetView.swift
  41. 54
      PadelClub/Views/Team/EditingTeamView.swift
  42. 36
      PadelClub/Views/Team/TeamPickerView.swift
  43. 4
      PadelClub/Views/Tournament/Screen/AddTeamView.swift
  44. 3
      PadelClub/Views/Tournament/Screen/BroadcastView.swift
  45. 10
      PadelClub/Views/Tournament/Screen/Components/TournamentClubSettingsView.swift
  46. 46
      PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift
  47. 2
      PadelClub/Views/Tournament/Screen/Components/UpdateSourceRankDateView.swift
  48. 37
      PadelClub/Views/Tournament/Screen/PrintSettingsView.swift
  49. 171
      PadelClub/Views/Tournament/Screen/RegistrationSetupView.swift
  50. 2
      PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift
  51. 6
      PadelClub/Views/Tournament/Subscription/SubscriptionView.swift
  52. 2
      PadelClub/Views/Tournament/TournamentInitView.swift
  53. 4
      PadelClub/Views/Tournament/TournamentView.swift
  54. 7
      PadelClubTests/ServerDataTests.swift

@ -3099,6 +3099,7 @@
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
ENABLE_DEBUG_DYLIB = NO;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PadelClub/Info.plist; INFOPLIST_FILE = PadelClub/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club"; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club";
@ -3120,7 +3121,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.2.24; MARKETING_VERSION = 1.2.33;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3166,7 +3167,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.2.24; MARKETING_VERSION = 1.2.33;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3285,7 +3286,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.2.24; MARKETING_VERSION = 1.2.33;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3330,7 +3331,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.2.24; MARKETING_VERSION = 1.2.33;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3374,7 +3375,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.2.24; MARKETING_VERSION = 1.2.33;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3416,7 +3417,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.2.24; MARKETING_VERSION = 1.2.33;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";

@ -81,6 +81,11 @@ struct FederalTournament: Identifiable, Codable {
var dateFin, dateValidation: Date? var dateFin, dateValidation: Date?
var codePostalEngagement, codeClub: String? var codePostalEngagement, codeClub: String?
var prixEspece: Int? var prixEspece: Int?
var japPhoneNumber: String?
mutating func updateJapPhoneNumber(phone: String?) {
self.japPhoneNumber = phone
}
init(from decoder: Decoder) throws { init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
@ -249,7 +254,7 @@ struct FederalTournament: Identifiable, Codable {
} }
var japMessage: String { var japMessage: String {
[nomClub, jugeArbitre?.nom, jugeArbitre?.prenom, courrielEngagement, installation?.telephone].compactMap({$0}).joined(separator: ";") [nomClub, jugeArbitre?.nom, jugeArbitre?.prenom, courrielEngagement, japPhoneNumber].compactMap({$0}).joined(separator: ";")
} }
func umpireLabel() -> String { func umpireLabel() -> String {

@ -21,6 +21,7 @@ extension PlayerRegistration {
self.tournamentPlayed = importedPlayer.tournamentPlayed self.tournamentPlayed = importedPlayer.tournamentPlayed
self.points = importedPlayer.getPoints() self.points = importedPlayer.getPoints()
self.clubName = importedPlayer.clubName?.prefixTrimmed(200) self.clubName = importedPlayer.clubName?.prefixTrimmed(200)
self.clubCode = importedPlayer.clubCode?.replaceCharactersFromSet(characterSet: .whitespaces).prefixTrimmed(20)
self.ligueName = importedPlayer.ligueName?.prefixTrimmed(200) self.ligueName = importedPlayer.ligueName?.prefixTrimmed(200)
self.assimilation = importedPlayer.assimilation?.prefixTrimmed(50) self.assimilation = importedPlayer.assimilation?.prefixTrimmed(50)
self.source = .frenchFederation self.source = .frenchFederation

@ -45,6 +45,12 @@ extension TeamRegistration {
player.captain = oldPlayer.captain player.captain = oldPlayer.captain
player.assimilation = oldPlayer.assimilation player.assimilation = oldPlayer.assimilation
player.ligueName = oldPlayer.ligueName player.ligueName = oldPlayer.ligueName
player.registrationStatus = oldPlayer.registrationStatus
player.timeToConfirm = oldPlayer.timeToConfirm
player.sex = oldPlayer.sex
player.paymentType = oldPlayer.paymentType
player.paymentId = oldPlayer.paymentId
player.clubMember = oldPlayer.clubMember
} }
} }
} }

@ -43,7 +43,7 @@ extension Tournament {
} }
func addTeam(_ players: Set<PlayerRegistration>, registrationDate: Date? = nil, name: String? = nil) -> TeamRegistration { func addTeam(_ players: Set<PlayerRegistration>, registrationDate: Date? = nil, name: String? = nil) -> TeamRegistration {
let team = TeamRegistration(tournament: id, registrationDate: registrationDate, name: name) let team = TeamRegistration(tournament: id, registrationDate: registrationDate ?? Date(), name: name)
team.setWeight(from: Array(players), inTournamentCategory: tournamentCategory) team.setWeight(from: Array(players), inTournamentCategory: tournamentCategory)
players.forEach { player in players.forEach { player in
player.teamRegistration = team.id player.teamRegistration = team.id
@ -76,7 +76,7 @@ extension Tournament {
guard let tournamentStore = self.tournamentStore else { return } guard let tournamentStore = self.tournamentStore else { return }
let teams = (0..<count).map { _ in let teams = (0..<count).map { _ in
let team = TeamRegistration(tournament: id) let team = TeamRegistration(tournament: id, registrationDate: Date())
team.setWeight(from: [], inTournamentCategory: self.tournamentCategory) team.setWeight(from: [], inTournamentCategory: self.tournamentCategory)
return team return team
} }
@ -90,7 +90,7 @@ extension Tournament {
func addWildCard(_ count: Int, _ type: MatchType) { func addWildCard(_ count: Int, _ type: MatchType) {
let wcs = (0..<count).map { _ in let wcs = (0..<count).map { _ in
let team = TeamRegistration(tournament: id) let team = TeamRegistration(tournament: id, registrationDate: Date())
if type == .bracket { if type == .bracket {
team.wildCardBracket = true team.wildCardBracket = true
} else { } else {
@ -277,13 +277,7 @@ extension Tournament {
newMonthData.maleUnrankedValue = await lastRankMan newMonthData.maleUnrankedValue = await lastRankMan
newMonthData.femaleUnrankedValue = await lastRankWoman newMonthData.femaleUnrankedValue = await lastRankWoman
DataStore.shared.monthData.addOrUpdate(instance: newMonthData)
do {
try DataStore.shared.monthData.addOrUpdate(instance: newMonthData)
} catch {
Logger.error(error)
}
monthData = newMonthData monthData = newMonthData
} }
@ -303,7 +297,7 @@ extension Tournament {
} }
let players = unsortedPlayers() let players = unsortedPlayers()
try await players.concurrentForEach { player in for player in players {
let lastRank = (player.sex == .female) ? lastRankWoman : lastRankMan let lastRank = (player.sex == .female) ? lastRankWoman : lastRankMan
try await player.updateRank(from: chunkedParsers, lastRank: lastRank) try await player.updateRank(from: chunkedParsers, lastRank: lastRank)
player.setComputedRank(in: self) player.setComputedRank(in: self)
@ -315,7 +309,7 @@ extension Tournament {
} }
} }
try tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: players) tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: players)
let unsortedTeams = unsortedTeams() let unsortedTeams = unsortedTeams()
unsortedTeams.forEach { team in unsortedTeams.forEach { team in
@ -324,7 +318,7 @@ extension Tournament {
team.lockedWeight = team.weight team.lockedWeight = team.weight
} }
} }
try tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams) tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams)
refreshRanking = false refreshRanking = false
} }

@ -1,5 +1,5 @@
<ul class="round"> <ul class="round">
<li class="spacer"> <li class="spacer" style="transform: translateY(-20px);">
&nbsp;{{roundLabel}} &nbsp;{{roundLabel}}
<div>{{formatLabel}}</div> <div>{{formatLabel}}</div>
</li> </li>

@ -1,8 +1,13 @@
<li class="game game-top {{entrantOneWon}}" style="visibility:{{hidden}}"> <li class="game game-top {{entrantOneWon}}" style="visibility:{{hidden}}; position: relative;">
{{entrantOne}} {{entrantOne}}
<div class="match-description-overlay" style="visibility:{{hidden}};">{{matchDescriptionTop}}</div>
</li> </li>
<li class="game game-spacer" style="visibility:{{hidden}}"><div class="multiline">{{matchDescription}}</div></li> <li class="game game-spacer" style="visibility:{{hidden}}">
<li class="game game-bottom {{entrantTwoWon}}" style="visibility:{{hidden}}"> </li>
{{entrantTwo}} <li class="game game-bottom {{entrantTwoWon}}" style="visibility:{{hidden}}; position: relative;">
<div style="transform: translateY(-100%);">
{{entrantTwo}}
</div>
<div class="match-description-overlay" style="visibility:{{hidden}};">{{matchDescriptionBottom}}</div>
</li> </li>
<li class="spacer">&nbsp;</li> <li class="spacer">&nbsp;</li>

@ -92,11 +92,32 @@
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.game {
/* Ensure the game container is a positioning context for the overlay */
position: relative;
/* Add any other existing styles for your game list items */
}
.match-description-overlay {
/* Position the overlay directly on top of the game item */
position: absolute;
top: 0;
left: 0;
transform: translateY(100%);
width: 100%;
height: 100%;
display: flex; /* Enable flexbox for centering */
justify-content: center; /* Center horizontally */
align-items: center; /* Center vertically (if needed) */
font-size: 1em; /* Optional: Adjust font size */
/* Add any other desired styling for the overlay */
}
</style> </style>
</head> </head>
<body> <body>
<h3>{{tournamentTitle}} - {{tournamentStartDate}}</h3> <h3 style="visibility:{{titleHidden}}">{{tournamentTitle}} - {{tournamentStartDate}}</h3>
<main id="tournament"> <main id="tournament">
{{brackets}} {{brackets}}
</main> </main>

@ -307,9 +307,11 @@ class FileImportManager {
if (tournamentCategory == tournament.tournamentCategory && tournamentAgeCategory == tournament.federalTournamentAge) || checkingCategoryDisabled { if (tournamentCategory == tournament.tournamentCategory && tournamentAgeCategory == tournament.federalTournamentAge) || checkingCategoryDisabled {
let playerOne = PlayerRegistration(federalData: Array(resultOne[0...7]), sex: sexPlayerOne, sexUnknown: sexUnknown) let playerOne = PlayerRegistration(federalData: Array(resultOne[0...7]), sex: sexPlayerOne, sexUnknown: sexUnknown)
playerOne?.setComputedRank(in: tournament) playerOne?.setComputedRank(in: tournament)
playerOne?.setClubMember(for: tournament)
let playerTwo = PlayerRegistration(federalData: Array(resultTwo[0...7]), sex: sexPlayerTwo, sexUnknown: sexUnknown) let playerTwo = PlayerRegistration(federalData: Array(resultTwo[0...7]), sex: sexPlayerTwo, sexUnknown: sexUnknown)
playerTwo?.setComputedRank(in: tournament) playerTwo?.setComputedRank(in: tournament)
playerTwo?.setClubMember(for: tournament)
let players = [playerOne, playerTwo].compactMap({ $0 }) let players = [playerOne, playerTwo].compactMap({ $0 })
if players.isEmpty == false { if players.isEmpty == false {
let team = TeamHolder(players: players, tournamentCategory: tournamentCategory, tournamentAgeCategory: tournamentAgeCategory, previousTeam: tournament.findTeam(players), tournament: tournament) let team = TeamHolder(players: players, tournamentCategory: tournamentCategory, tournamentAgeCategory: tournamentAgeCategory, previousTeam: tournament.findTeam(players), tournament: tournament)
@ -368,9 +370,11 @@ class FileImportManager {
let playerOne = PlayerRegistration(federalData: Array(result[0...7]), sex: sexPlayerOne, sexUnknown: sexUnknown) let playerOne = PlayerRegistration(federalData: Array(result[0...7]), sex: sexPlayerOne, sexUnknown: sexUnknown)
playerOne?.setComputedRank(in: tournament) playerOne?.setComputedRank(in: tournament)
playerOne?.setClubMember(for: tournament)
let playerTwo = PlayerRegistration(federalData: Array(result[8...]), sex: sexPlayerTwo, sexUnknown: sexUnknown) let playerTwo = PlayerRegistration(federalData: Array(result[8...]), sex: sexPlayerTwo, sexUnknown: sexUnknown)
playerTwo?.setComputedRank(in: tournament) playerTwo?.setComputedRank(in: tournament)
playerTwo?.setClubMember(for: tournament)
let players = [playerOne, playerTwo].compactMap({ $0 }) let players = [playerOne, playerTwo].compactMap({ $0 })
if players.isEmpty == false { if players.isEmpty == false {
let team = TeamHolder(players: players, tournamentCategory: tournamentCategory, tournamentAgeCategory: tournamentAgeCategory, previousTeam: tournament.findTeam(players), tournament: tournament) let team = TeamHolder(players: players, tournamentCategory: tournamentCategory, tournamentAgeCategory: tournamentAgeCategory, previousTeam: tournament.findTeam(players), tournament: tournament)
@ -404,6 +408,7 @@ class FileImportManager {
let registeredPlayers = found?.map({ importedPlayer in let registeredPlayers = found?.map({ importedPlayer in
let player = PlayerRegistration(importedPlayer: importedPlayer) let player = PlayerRegistration(importedPlayer: importedPlayer)
player.setComputedRank(in: tournament) player.setComputedRank(in: tournament)
player.setClubMember(for: tournament)
return player return player
}) })
if let registeredPlayers, registeredPlayers.isEmpty == false { if let registeredPlayers, registeredPlayers.isEmpty == false {
@ -466,6 +471,7 @@ class FileImportManager {
if let found, autoSearch { if let found, autoSearch {
let player = PlayerRegistration(importedPlayer: found) let player = PlayerRegistration(importedPlayer: found)
player.setComputedRank(in: tournament) player.setComputedRank(in: tournament)
player.setClubMember(for: tournament)
player.email = email player.email = email
player.phoneNumber = phoneNumber player.phoneNumber = phoneNumber
return player return player

@ -176,7 +176,7 @@ class HtmlGenerator: ObservableObject {
func generateLoserBracketHtml(upperRound: Round) -> String { func generateLoserBracketHtml(upperRound: Round) -> String {
//HtmlService.groupstage(bracket: tournament.orderedBrackets.first!).html() //HtmlService.groupstage(bracket: tournament.orderedBrackets.first!).html()
HtmlService.loserBracket(upperRound: upperRound).html(headName: displayHeads, withRank: displayRank, withTeamIndex: displayTeamIndex, withScore: displayScore) HtmlService.loserBracket(upperRound: upperRound, hideTitle: false).html(headName: displayHeads, withRank: displayRank, withTeamIndex: displayTeamIndex, withScore: displayScore)
} }
var pdfURL: URL? { var pdfURL: URL? {

@ -12,7 +12,7 @@ enum HtmlService {
case template(tournament: Tournament) case template(tournament: Tournament)
case bracket(round: Round) case bracket(round: Round)
case loserBracket(upperRound: Round) case loserBracket(upperRound: Round, hideTitle: Bool)
case match(match: Match) case match(match: Match)
case player(entrant: TeamRegistration) case player(entrant: TeamRegistration)
case hiddenPlayer case hiddenPlayer
@ -187,11 +187,17 @@ enum HtmlService {
var template = html var template = html
if let entrantOne = match.team(.one) { if let entrantOne = match.team(.one) {
template = template.replacingOccurrences(of: "{{entrantOne}}", with: HtmlService.player(entrant: entrantOne).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore)) template = template.replacingOccurrences(of: "{{entrantOne}}", with: HtmlService.player(entrant: entrantOne).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
if withScore, let top = match.topPreviousRoundMatch(), top.hasEnded() {
template = template.replacingOccurrences(of: "{{matchDescriptionTop}}", with: [top.scoreLabel(winnerFirst:true)].compactMap({ $0 }).joined(separator: "\n"))
}
} else { } else {
template = template.replacingOccurrences(of: "{{entrantOne}}", with: HtmlService.hiddenPlayer.html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore)) template = template.replacingOccurrences(of: "{{entrantOne}}", with: HtmlService.hiddenPlayer.html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
} }
if let entrantTwo = match.team(.two) { if let entrantTwo = match.team(.two) {
template = template.replacingOccurrences(of: "{{entrantTwo}}", with: HtmlService.player(entrant: entrantTwo).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore)) template = template.replacingOccurrences(of: "{{entrantTwo}}", with: HtmlService.player(entrant: entrantTwo).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
if withScore, let bottom = match.bottomPreviousRoundMatch(), bottom.hasEnded() {
template = template.replacingOccurrences(of: "{{matchDescriptionBottom}}", with: [bottom.scoreLabel(winnerFirst:true)].compactMap({ $0 }).joined(separator: "\n"))
}
} else { } else {
template = template.replacingOccurrences(of: "{{entrantTwo}}", with: HtmlService.hiddenPlayer.html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore)) template = template.replacingOccurrences(of: "{{entrantTwo}}", with: HtmlService.hiddenPlayer.html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
} }
@ -206,9 +212,10 @@ enum HtmlService {
} else if match.teamWon(atPosition: .two) == true { } else if match.teamWon(atPosition: .two) == true {
template = template.replacingOccurrences(of: "{{entrantTwoWon}}", with: "winner") template = template.replacingOccurrences(of: "{{entrantTwoWon}}", with: "winner")
} }
template = template.replacingOccurrences(of: "{{matchDescription}}", with: [match.localizedStartDate(), match.scoreLabel()].joined(separator: "\n")) // template = template.replacingOccurrences(of: "{{matchDescription}}", with: [match.localizedStartDate(), match.scoreLabel()].joined(separator: "\n"))
} }
template = template.replacingOccurrences(of: "{{matchDescription}}", with: "") template = template.replacingOccurrences(of: "{{matchDescriptionTop}}", with: "")
template = template.replacingOccurrences(of: "{{matchDescriptionBottom}}", with: "")
return template return template
case .bracket(let round): case .bracket(let round):
var template = "" var template = ""
@ -216,16 +223,31 @@ enum HtmlService {
for (_, match) in round._matches().enumerated() { for (_, match) in round._matches().enumerated() {
template = template.appending(HtmlService.match(match: match).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore)) template = template.appending(HtmlService.match(match: match).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
} }
bracket = html.replacingOccurrences(of: "{{match-template}}", with: template) bracket = html.replacingOccurrences(of: "{{match-template}}", with: template)
bracket = bracket.replacingOccurrences(of: "{{roundLabel}}", with: round.roundTitle()) bracket = bracket.replacingOccurrences(of: "{{roundLabel}}", with: round.roundTitle())
bracket = bracket.replacingOccurrences(of: "{{formatLabel}}", with: round.matchFormat.formatTitle()) bracket = bracket.replacingOccurrences(of: "{{formatLabel}}", with: round.matchFormat.formatTitle())
return bracket return bracket
case .loserBracket(let upperRound): case .loserBracket(let upperRound, let hideTitle):
var template = html var template = html
template = template.replacingOccurrences(of: "{{minHeight}}", with: withTeamIndex ? "226" : "156")
template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: upperRound.correspondingLoserRoundTitle()) template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: upperRound.correspondingLoserRoundTitle())
if let tournamentStartDate = upperRound.initialStartDate()?.localizedDate() {
template = template.replacingOccurrences(of: "{{tournamentStartDate}}", with: tournamentStartDate)
} else {
template = template.replacingOccurrences(of: "{{tournamentStartDate}}", with: "")
}
template = template.replacingOccurrences(of: "{{titleHidden}}", with: hideTitle ? "hidden" : "")
var brackets = "" var brackets = ""
for round in upperRound.loserRounds() { for round in upperRound.loserRounds() {
brackets = brackets.appending(HtmlService.bracket(round: round).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore)) brackets = brackets.appending(HtmlService.bracket(round: round).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
if round.index == 1 {
let sub = HtmlService.loserBracket(upperRound: round, hideTitle: true).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore)
template = template.appending(sub)
}
} }
let winnerName = "" let winnerName = ""
let winner = """ let winner = """
@ -240,6 +262,14 @@ enum HtmlService {
brackets = brackets.appending(winner) brackets = brackets.appending(winner)
template = template.replacingOccurrences(of: "{{brackets}}", with: brackets) template = template.replacingOccurrences(of: "{{brackets}}", with: brackets)
for round in upperRound.loserRounds() {
if round.index > 1 {
let sub = HtmlService.loserBracket(upperRound: round, hideTitle: true).html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore)
template = template.appending(sub)
}
}
return template return template
case .template(let tournament): case .template(let tournament):
var template = html var template = html

@ -10,12 +10,16 @@ import LeStorage
class StripeValidationService { class StripeValidationService {
static func validateStripeAccountID(_ accountID: String) async throws -> ValidationResponse { // MARK: - Validate Stripe Account
let service = try StoreCenter.main.service() static func validateStripeAccount(accountId: String) async throws -> ValidationResponse {
let service = try StoreCenter.main.service()
var urlRequest = try service._baseRequest(servicePath: "validate-stripe-account/", method: .post, requiresToken: true) var urlRequest = try service._baseRequest(servicePath: "validate-stripe-account/", method: .post, requiresToken: true)
let body = ["account_id": accountID] var body: [String: Any] = [:]
urlRequest.httpBody = try JSONEncoder().encode(body)
body["account_id"] = accountId
urlRequest.httpBody = try JSONSerialization.data(withJSONObject: body)
do { do {
let (data, response) = try await URLSession.shared.data(for: urlRequest) let (data, response) = try await URLSession.shared.data(for: urlRequest)
@ -23,17 +27,79 @@ class StripeValidationService {
guard let httpResponse = response as? HTTPURLResponse else { guard let httpResponse = response as? HTTPURLResponse else {
throw ValidationError.invalidResponse throw ValidationError.invalidResponse
} }
switch httpResponse.statusCode { switch httpResponse.statusCode {
case 200...299: case 200...299:
let decodedResponse = try JSONDecoder().decode(ValidationResponse.self, from: data) let decodedResponse = try JSONDecoder().decode(ValidationResponse.self, from: data)
return decodedResponse return decodedResponse
case 400: case 400, 403, 404:
// Handle bad request // Handle client errors - still decode as ValidationResponse
let errorResponse = try JSONDecoder().decode(ValidationResponse.self, from: data) let errorResponse = try JSONDecoder().decode(ValidationResponse.self, from: data)
return errorResponse return errorResponse
case 403: default:
// Handle permission error throw ValidationError.invalidResponse
let errorResponse = try JSONDecoder().decode(ValidationResponse.self, from: data) }
} catch let error as ValidationError {
throw error
} catch {
throw ValidationError.networkError(error)
}
}
// MARK: - Create Stripe Connect Account
static func createStripeConnectAccount() async throws -> CreateAccountResponse {
let service = try StoreCenter.main.service()
let urlRequest = try service._baseRequest(servicePath: "stripe/create-account/", method: .post, requiresToken: true)
do {
let (data, response) = try await URLSession.shared.data(for: urlRequest)
guard let httpResponse = response as? HTTPURLResponse else {
throw ValidationError.invalidResponse
}
switch httpResponse.statusCode {
case 200...299:
let decodedResponse = try JSONDecoder().decode(CreateAccountResponse.self, from: data)
return decodedResponse
case 400, 403, 404:
let errorResponse = try JSONDecoder().decode(CreateAccountResponse.self, from: data)
return errorResponse
default:
throw ValidationError.invalidResponse
}
} catch let error as ValidationError {
throw error
} catch {
throw ValidationError.networkError(error)
}
}
// MARK: - Create Stripe Account Link
static func createStripeAccountLink(_ accountId: String? = nil) async throws -> CreateLinkResponse {
let service = try StoreCenter.main.service()
var urlRequest = try service._baseRequest(servicePath: "stripe/create-account-link/", method: .post, requiresToken: true)
var body: [String: Any] = [:]
if let accountId = accountId {
body["account_id"] = accountId
}
urlRequest.httpBody = try JSONSerialization.data(withJSONObject: body)
do {
let (data, response) = try await URLSession.shared.data(for: urlRequest)
guard let httpResponse = response as? HTTPURLResponse else {
throw ValidationError.invalidResponse
}
switch httpResponse.statusCode {
case 200...299:
let decodedResponse = try JSONDecoder().decode(CreateLinkResponse.self, from: data)
return decodedResponse
case 400, 403, 404:
let errorResponse = try JSONDecoder().decode(CreateLinkResponse.self, from: data)
return errorResponse return errorResponse
default: default:
throw ValidationError.invalidResponse throw ValidationError.invalidResponse
@ -46,17 +112,67 @@ class StripeValidationService {
} }
} }
// MARK: - Response Models
struct ValidationResponse: Codable { struct ValidationResponse: Codable {
let valid: Bool let valid: Bool
let canProcessPayments: Bool?
let onboardingComplete: Bool?
let needsOnboarding: Bool?
let account: AccountDetails? let account: AccountDetails?
let error: String? let error: String?
enum CodingKeys: String, CodingKey {
case valid
case canProcessPayments = "can_process_payments"
case onboardingComplete = "onboarding_complete"
case needsOnboarding = "needs_onboarding"
case account
case error
}
} }
struct AccountDetails: Codable { struct AccountDetails: Codable {
let id: String let id: String
let chargesEnabled: Bool?
let payoutsEnabled: Bool?
let detailsSubmitted: Bool?
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case id case id
case chargesEnabled = "charges_enabled"
case payoutsEnabled = "payouts_enabled"
case detailsSubmitted = "details_submitted"
}
}
struct CreateAccountResponse: Codable {
let success: Bool
let accountId: String?
let message: String?
let existing: Bool?
let error: String?
enum CodingKeys: String, CodingKey {
case success
case accountId = "account_id"
case message
case existing
case error
}
}
struct CreateLinkResponse: Codable {
let success: Bool
let url: URL?
let accountId: String?
let error: String?
enum CodingKeys: String, CodingKey {
case success
case url
case accountId = "account_id"
case error
} }
} }
@ -64,5 +180,28 @@ enum ValidationError: Error {
case invalidResponse case invalidResponse
case networkError(Error) case networkError(Error)
case invalidData case invalidData
case encodingError
case urlNotFound
case accountNotFound
case onlinePaymentNotEnabled
var localizedDescription: String {
switch self {
case .invalidResponse:
return "Réponse du serveur invalide"
case .networkError(let error):
return "Erreur réseau : \(error.localizedDescription)"
case .invalidData:
return "Données reçues invalides"
case .encodingError:
return "Échec de l'encodage des données de la requête"
case .accountNotFound:
return "Le compte n'a pas pu être généré"
case .urlNotFound:
return "Le lien pour utiliser un compte stripe n'a pas pu être généré"
case .onlinePaymentNotEnabled:
return "Le paiement en ligne n'a pas pu être activé pour ce tournoi"
}
}
} }

@ -203,16 +203,14 @@ struct CashierDetailView: View {
DisclosureGroup { DisclosureGroup {
let selectedPlayers = tournament.selectedPlayers() let selectedPlayers = tournament.selectedPlayers()
ForEach(PlayerPaymentType.allCases) { type in ForEach(PlayerPaymentType.allCases) { type in
let count = selectedPlayers.filter({ $0.paymentType == type }).count let players = selectedPlayers.filter({ $0.paymentType == type })
if count > 0 { if players.count > 0 {
LabeledContent { LabeledContent {
if let entryFee = tournament.entryFee { let sum = players.compactMap({ $0.paidAmount(tournament) }).reduce(0.0, +)
let sum = Double(count) * entryFee Text(sum.formatted(.currency(code: Locale.defaultCurrency())))
Text(sum.formatted(.currency(code: Locale.defaultCurrency())))
}
} label: { } label: {
Text(type.localizedLabel()) Text(type.localizedLabel())
Text(count.formatted()) Text(players.count.formatted())
} }
} }
} }

@ -13,25 +13,50 @@ struct CashierSettingsView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@State private var entryFee: Double? = nil @State private var entryFee: Double? = nil
@State private var clubMemberFeeDeduction: Double? = nil
@Bindable var tournament: Tournament @Bindable var tournament: Tournament
@FocusState private var focusedField: Tournament.CodingKeys? @FocusState private var focusedField: Tournament.CodingKeys?
let priceTags: [Double] = [15.0, 20.0, 25.0] let priceTags: [Double] = [15.0, 20.0, 25.0]
let deductionTags: [Double] = [5.0, 10.0]
init(tournament: Tournament) { init(tournament: Tournament) {
self.tournament = tournament self.tournament = tournament
_entryFee = State(wrappedValue: tournament.entryFee) _entryFee = State(wrappedValue: tournament.entryFee)
_clubMemberFeeDeduction = State(wrappedValue: tournament.clubMemberFeeDeduction)
} }
var body: some View { var body: some View {
List { List {
Section { Section {
TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $entryFee, format: .currency(code: Locale.defaultCurrency())) LabeledContent {
.keyboardType(.decimalPad) TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $entryFee, format: .currency(code: Locale.defaultCurrency()))
.multilineTextAlignment(.trailing) .keyboardType(.decimalPad)
.frame(maxWidth: .infinity) .multilineTextAlignment(.trailing)
.focused($focusedField, equals: ._entryFee) .frame(maxWidth: .infinity)
.focused($focusedField, equals: ._entryFee)
} label: {
Text("Frais d'inscription")
}
LabeledContent {
TextField("Réduction", value: $clubMemberFeeDeduction, format: .currency(code: Locale.defaultCurrency()))
.keyboardType(.decimalPad)
.multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity)
.focused($focusedField, equals: ._clubMemberFeeDeduction)
.onChange(of: focusedField) {
if focusedField == ._clubMemberFeeDeduction {
DispatchQueue.main.async {
UIApplication.shared.sendAction(#selector(UIResponder.selectAll(_:)), to: nil, from: nil, for: nil)
}
}
}
} label: {
Text("Réduction membre")
}
.disabled(tournament.isFree())
} header: { } header: {
Text("Prix de l'inscription") Text("Frais d'inscription")
} footer: { } footer: {
Text("Si vous souhaitez que Padel Club vous aide à suivre les encaissements, indiquer un prix d'inscription. Sinon Padel Club vous aidera à suivre simplement l'arrivée et la présence des joueurs.") Text("Si vous souhaitez que Padel Club vous aide à suivre les encaissements, indiquer un prix d'inscription. Sinon Padel Club vous aidera à suivre simplement l'arrivée et la présence des joueurs.")
} }
@ -104,27 +129,43 @@ struct CashierSettingsView: View {
ToolbarItem(placement: .keyboard) { ToolbarItem(placement: .keyboard) {
HStack { HStack {
if tournament.isFree() { if focusedField == ._entryFee {
ForEach(priceTags, id: \.self) { priceTag in if tournament.isFree() {
Button(priceTag.formatted(.currency(code: Locale.defaultCurrency()))) { ForEach(priceTags, id: \.self) { priceTag in
entryFee = priceTag Button(priceTag.formatted(.currency(code: Locale.defaultCurrency()))) {
tournament.entryFee = priceTag entryFee = priceTag
tournament.entryFee = priceTag
focusedField = nil
}
.buttonStyle(.bordered)
}
} else {
Button("Gratuit") {
entryFee = nil
tournament.entryFee = nil
focusedField = nil
}
.buttonStyle(.bordered)
}
} else if focusedField == ._clubMemberFeeDeduction {
ForEach(deductionTags, id: \.self) { deductionTag in
Button(deductionTag.formatted(.currency(code: Locale.defaultCurrency()).precision(.fractionLength(0)))) {
clubMemberFeeDeduction = deductionTag
tournament.clubMemberFeeDeduction = deductionTag
focusedField = nil focusedField = nil
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
} }
} else {
Button("Gratuit") { Button("Gratuit") {
entryFee = nil clubMemberFeeDeduction = entryFee
tournament.entryFee = nil tournament.clubMemberFeeDeduction = clubMemberFeeDeduction
focusedField = nil focusedField = nil
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
} }
Spacer() Spacer()
Button("Valider") { Button("Valider") {
tournament.entryFee = entryFee
focusedField = nil focusedField = nil
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
@ -132,7 +173,14 @@ struct CashierSettingsView: View {
} }
} }
} }
.onChange(of: tournament.entryFee) { .onChange(of: focusedField) { old, new in
if old == ._entryFee {
tournament.entryFee = entryFee
} else if old == ._clubMemberFeeDeduction {
tournament.clubMemberFeeDeduction = clubMemberFeeDeduction
}
}
.onChange(of: [tournament.entryFee, tournament.clubMemberFeeDeduction]) {
_save() _save()
} }
} }

@ -71,6 +71,19 @@ struct EventCreationView: View {
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.focused($textFieldIsFocus) .focused($textFieldIsFocus)
.toolbar {
if textFieldIsFocus {
ToolbarItem(placement: .keyboard) {
HStack {
Spacer()
Button("Valider") {
textFieldIsFocus = false
}
.buttonStyle(.bordered)
}
}
}
}
LabeledContent { LabeledContent {
Text(tournaments.count.formatted()) Text(tournaments.count.formatted())
} label: { } label: {
@ -93,18 +106,6 @@ struct EventCreationView: View {
} }
} }
.toolbar { .toolbar {
if textFieldIsFocus {
ToolbarItem(placement: .keyboard) {
HStack {
Spacer()
Button("Valider") {
textFieldIsFocus = false
}
.buttonStyle(.bordered)
}
}
}
ToolbarItem(placement: .cancellationAction) { ToolbarItem(placement: .cancellationAction) {
Button("Annuler", role: .cancel) { Button("Annuler", role: .cancel) {
dismiss() dismiss()

@ -15,6 +15,7 @@ struct EventSettingsView: View {
@State private var eventName: String = "" @State private var eventName: String = ""
@State private var pageLink: PageLink = .teams @State private var pageLink: PageLink = .teams
@State private var tournamentInformation: String = "" @State private var tournamentInformation: String = ""
@State private var eventStartDate: Date
@FocusState private var focusedField: Tournament.CodingKeys? @FocusState private var focusedField: Tournament.CodingKeys?
func eventLinksPasteData() -> String { func eventLinksPasteData() -> String {
@ -22,6 +23,20 @@ struct EventSettingsView: View {
var link = [String]() var link = [String]()
link.append(event.eventTitle()) link.append(event.eventTitle())
link.append("\n\n")
link.append(tournamentInformation)
link.append("\n\n")
if let url = event.shareURL() {
var tournamentLink = [String]()
tournamentLink.append("Lien de l'événement")
tournamentLink.append(url.absoluteString)
let eventLink = tournamentLink.joined(separator: "\n")
link.append(eventLink)
}
link.append("\n\n") link.append("\n\n")
link.append("Retrouvez toutes les infos en suivant le\(tournaments.count.pluralSuffix) lien\(tournaments.count.pluralSuffix) ci-dessous :") link.append("Retrouvez toutes les infos en suivant le\(tournaments.count.pluralSuffix) lien\(tournaments.count.pluralSuffix) ci-dessous :")
link.append("\n\n") link.append("\n\n")
@ -43,6 +58,7 @@ struct EventSettingsView: View {
init(event: Event) { init(event: Event) {
self.event = event self.event = event
_eventName = State(wrappedValue: event.name ?? "") _eventName = State(wrappedValue: event.name ?? "")
_eventStartDate = .init(wrappedValue: event.eventStartDate())
_tournamentInformation = State(wrappedValue: event.tournaments.first?.information ?? "") _tournamentInformation = State(wrappedValue: event.tournaments.first?.information ?? "")
} }
@ -67,6 +83,19 @@ struct EventSettingsView: View {
} }
} }
Section {
DatePicker(selection: $eventStartDate) {
Text(eventStartDate.formatted(.dateTime.weekday(.wide)).capitalized).lineLimit(1)
}
.onChange(of: eventStartDate) {
event.tournaments.forEach { tournament in
tournament.startDate = eventStartDate
}
dataStore.tournaments.addOrUpdate(contentOfs: event.tournaments)
}
}
if event.tournaments.first?.dayDuration == 3, event.tournaments.count == 3 { if event.tournaments.first?.dayDuration == 3, event.tournaments.count == 3 {
Section { Section {
RowButtonView("Répartir les tournois") { RowButtonView("Répartir les tournois") {
@ -99,7 +128,9 @@ struct EventSettingsView: View {
} footer: { } footer: {
Text("Ce texte sera indiqué dans le champ information de tous les tournois de l'événement") Text("Ce texte sera indiqué dans le champ information de tous les tournois de l'événement")
} }
_message(eventPasteMessage: _eventPasteMessage())
if event.club != nil { if event.club != nil {
let eventLinksPasteData = eventLinksPasteData() let eventLinksPasteData = eventLinksPasteData()
Section { Section {
@ -135,12 +166,8 @@ struct EventSettingsView: View {
}) })
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.toolbar { .toolbar {
if let tenupId = event.tenupId { ToolbarItem(placement: .topBarTrailing) {
ToolbarItem(placement: .topBarTrailing) { _linkLabel()
Link(destination: URL(string:"https://tenup.fft.fr/tournoi/\(tenupId)")!) {
Text("Tenup")
}
}
} }
if focusedField != nil { if focusedField != nil {
@ -185,6 +212,59 @@ struct EventSettingsView: View {
} }
} }
private func _eventPasteMessage() -> String {
var paste = [String]()
if let name = event.name {
paste.append(name)
paste.append("\n")
}
paste.append(event.formattedDateInterval())
paste.append("\n")
paste.append(tournamentInformation)
paste.append("\n")
if let url = event.shareURL() {
paste.append(url.absoluteString)
paste.append("\n")
}
return paste.joined(separator: "\n")
}
private func _message(eventPasteMessage: String) -> some View {
Section {
Text(eventPasteMessage).foregroundStyle(.secondary)
} header: {
Text("Message à partager")
} footer: {
HStack {
CopyPasteButtonView(pasteValue: eventPasteMessage)
Spacer()
ShareLink(item: eventPasteMessage)
}
}
}
private func _linkLabel() -> some View {
Menu {
if let url = event.shareURL() {
ShareLink(item: url) {
Text("Lien de l'événement sur Padel Club")
}
}
if let tenupId = event.tenupId {
ShareLink(item: URL(string:"https://tenup.fft.fr/tournoi/\(tenupId)")!) {
Text("Tenup")
}
}
} label: {
Text("Liens")
}
}
private func _save() { private func _save() {
dataStore.events.addOrUpdate(instance: event) dataStore.events.addOrUpdate(instance: event)
} }

@ -44,19 +44,68 @@ struct EventTournamentsView: View {
} }
} footer: { } footer: {
if event.tournaments.count > 1 { if event.tournaments.count > 1 {
if mainTournament == nil { if let mainTournament, mainTournament == tournament {
FooterButtonView("c'est le tournoi principal") { Menu {
self.mainTournament = tournament Button("Formats") {
} tournaments.forEach { tournament in
} else if mainTournament == tournament { tournament.groupStageMatchFormat = mainTournament.groupStageMatchFormat
FooterButtonView("ce n'est pas le tournoi principal") { tournament.loserBracketMatchFormat = mainTournament.loserBracketMatchFormat
self.mainTournament = tournament tournament.matchFormat = mainTournament.matchFormat
}
dataStore.tournaments.addOrUpdate(contentOfs: tournaments)
}
Button("Infos JAP") {
tournaments.forEach { tournament in
tournament.setupUmpireSettings(defaultTournament: mainTournament)
}
dataStore.tournaments.addOrUpdate(contentOfs: tournaments)
}
Button("Réglages Inscriptions") {
tournaments.forEach { tournament in
tournament.setupRegistrationSettings(templateTournament: mainTournament)
}
dataStore.tournaments.addOrUpdate(contentOfs: tournaments)
}
} label: {
Text("Copier des réglages sur les autres tournois")
.underline()
.multilineTextAlignment(.leading)
} }
} else if let mainTournament {
FooterButtonView("coller les réglages du tournoi principal") { } else {
tournament.setupUmpireSettings(defaultTournament: mainTournament) Menu {
tournament.setupRegistrationSettings(templateTournament: mainTournament) if tournament != self.mainTournament {
dataStore.tournaments.addOrUpdate(instance: tournament) Button("Définir comme tournoi principal") {
self.mainTournament = tournament
}
}
if let mainTournament {
Divider()
Button("Copier les formats du tournoi principal") {
tournament.groupStageMatchFormat = mainTournament.groupStageMatchFormat
tournament.loserBracketMatchFormat = mainTournament.loserBracketMatchFormat
tournament.matchFormat = mainTournament.matchFormat
dataStore.tournaments.addOrUpdate(instance: tournament)
}
Button("Copier les infos JAP du tournoi principal") {
tournament.setupUmpireSettings(defaultTournament: mainTournament)
dataStore.tournaments.addOrUpdate(instance: tournament)
}
Button("Copier les réglages des inscriptions du tournoi principal") {
tournament.setupRegistrationSettings(templateTournament: mainTournament)
dataStore.tournaments.addOrUpdate(instance: tournament)
}
}
} label: {
Text("Options rapides pour certains réglages")
.underline()
} }
} }
} }

@ -18,6 +18,7 @@ enum EventDestination: Identifiable, Selectable, Equatable {
case links case links
case tournaments(Event) case tournaments(Event)
case cashier case cashier
case eventPlanning
var id: String { var id: String {
return String(describing: self) return String(describing: self)
@ -33,6 +34,8 @@ enum EventDestination: Identifiable, Selectable, Equatable {
return "Tournois" return "Tournois"
case .cashier: case .cashier:
return "Finance" return "Finance"
case .eventPlanning:
return "Planning"
} }
} }
@ -42,7 +45,7 @@ enum EventDestination: Identifiable, Selectable, Equatable {
return nil return nil
case .tournaments(let event): case .tournaments(let event):
return event.tournaments.count return event.tournaments.count
case .cashier: case .cashier, .eventPlanning:
return nil return nil
} }
} }
@ -77,7 +80,7 @@ struct EventView: View {
} }
func allDestinations() -> [EventDestination] { func allDestinations() -> [EventDestination] {
[.club(event), .tournaments(event), .cashier] [.club(event), .eventPlanning, .tournaments(event), .cashier]
} }
var body: some View { var body: some View {
@ -90,6 +93,10 @@ struct EventView: View {
switch selectedEventDestination { switch selectedEventDestination {
case .club(let event): case .club(let event):
EventClubSettingsView(event: event) EventClubSettingsView(event: event)
case .eventPlanning:
let allMatches = event.tournaments.flatMap { $0.allMatches() }
PlanningView(matches: allMatches, selectedScheduleDestination: .constant(nil))
.environment(\.matchViewStyle, .feedStyle)
case .links: case .links:
EventLinksView(event: event) EventLinksView(event: event)
case .tournaments(let event): case .tournaments(let event):

@ -245,10 +245,10 @@ struct ClubDetailView: View {
CourtView(court: court) CourtView(court: court)
} }
.onChange(of: zipCode) { .onChange(of: zipCode) {
club.zipCode = zipCode club.zipCode = zipCode.prefixTrimmed(10)
} }
.onChange(of: city) { .onChange(of: city) {
club.city = city club.city = city.prefixTrimmed(100)
} }
.onDisappear { .onDisappear {
if displayContext == .edition && clubDeleted == false { if displayContext == .edition && clubDeleted == false {

@ -44,7 +44,7 @@ struct CourtView: View {
} }
} }
} label: { } label: {
Text("Nom du terrain") Text("Nom de la piste")
} }
} footer: { } footer: {
if court.name?.isEmpty == false { if court.name?.isEmpty == false {
@ -65,7 +65,7 @@ struct CourtView: View {
Text("Sortie autorisée") Text("Sortie autorisée")
} }
Toggle(isOn: $court.indoor) { Toggle(isOn: $court.indoor) {
Text("Terrain intérieur") Text("Piste intérieur")
} }
} }
} }

@ -19,7 +19,7 @@ struct ClubCourtSetupView: View {
@ViewBuilder @ViewBuilder
var body: some View { var body: some View {
Section { Section {
TournamentFieldsManagerView(localizedStringKey: "Terrains du club", count: $club.courtCount) TournamentFieldsManagerView(localizedStringKey: "Pistes du club", count: $club.courtCount)
.disabled(displayContext == .lockedForEditing) .disabled(displayContext == .lockedForEditing)
.onChange(of: club.courtCount) { .onChange(of: club.courtCount) {
if displayContext != .addition { if displayContext != .addition {
@ -53,7 +53,7 @@ struct ClubCourtSetupView: View {
_courtView(atIndex: courtIndex, tournamentClub: club) _courtView(atIndex: courtIndex, tournamentClub: club)
} }
} header: { } header: {
Text("Nom des terrains") Text("Nom des pistes")
} footer: { } footer: {
if displayContext == .lockedForEditing && hideLockForEditingMessage == false { if displayContext == .lockedForEditing && hideLockForEditingMessage == false {
Text("Édition impossible, vous n'êtes pas le créateur de ce club.").foregroundStyle(.logoRed) Text("Édition impossible, vous n'êtes pas le créateur de ce club.").foregroundStyle(.logoRed)

@ -64,7 +64,7 @@ struct GroupStageSettingsView: View {
} }
Section { Section {
CourtPicker(title: "Terrain dédié", selection: $courtIndex, maxCourt: tournament.courtCount) CourtPicker(title: "Piste dédié", selection: $courtIndex, maxCourt: tournament.courtCount)
RowButtonView("Confirmer", role: .destructive) { RowButtonView("Confirmer", role: .destructive) {
groupStage.matches().forEach { match in groupStage.matches().forEach { match in
match.setCourt(courtIndex) match.setCourt(courtIndex)
@ -179,7 +179,7 @@ struct GroupStageSettingsView: View {
} }
Section { Section {
RowButtonView("Rafraichir", role: .destructive) { RowButtonView("Rafraîchir", role: .destructive) {
let playedMatches = groupStage.playedMatches() let playedMatches = groupStage.playedMatches()
playedMatches.forEach { match in playedMatches.forEach { match in
match.updateTeamScores() match.updateTeamScores()

@ -241,7 +241,7 @@ struct GroupStageView: View {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
Text("#\(index + 1)") Text("#\(index + 1)")
.font(.caption) .font(.caption)
TeamPickerView(groupStagePosition: index, matchTypeContext: .groupStage, teamPicked: { team in TeamPickerView(groupStagePosition: index, pickTypeContext: .groupStage, teamPicked: { team in
print(team.pasteData()) print(team.pasteData())
team.groupStage = groupStage.id team.groupStage = groupStage.id
team.groupStagePosition = index team.groupStagePosition = index

@ -78,7 +78,7 @@ struct GroupStagesSettingsView: View {
Section { Section {
if tournament.groupStageLoserBracket() == nil { if tournament.groupStageLoserBracket() == nil {
RowButtonView("Ajouter des matchs de classements", role: .destructive) { RowButtonView("Ajouter des matchs de classements", role: .destructive) {
let round = Round(tournament: tournament.id, index: 0, matchFormat: tournament.loserRoundFormat, groupStageLoserBracket: true) let round = Round(tournament: tournament.id, index: 0, format: tournament.loserRoundFormat, groupStageLoserBracket: true)
do { do {
try tournamentStore?.rounds.addOrUpdate(instance: round) try tournamentStore?.rounds.addOrUpdate(instance: round)

@ -332,7 +332,7 @@ struct MatchDetailView: View {
match.removeCourt() match.removeCourt()
save() save()
} label: { } label: {
Text("Supprimer le terrain") Text("Supprimer la piste")
} }
} }
Button(role: .destructive) { Button(role: .destructive) {
@ -412,7 +412,7 @@ struct MatchDetailView: View {
} }
} label: { } label: {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("terrain").font(.footnote).foregroundStyle(.secondary) Text("piste").font(.footnote).foregroundStyle(.secondary)
if let courtName = match.courtName() { if let courtName = match.courtName() {
Text(courtName) Text(courtName)
.foregroundStyle(Color.master) .foregroundStyle(Color.master)
@ -479,7 +479,7 @@ struct MatchDetailView: View {
DisclosureGroup(isExpanded: $isEditing) { DisclosureGroup(isExpanded: $isEditing) {
startingOptionView startingOptionView
} label: { } label: {
Text("Modifier l'horaire et le terrain") Text("Modifier l'horaire et la piste")
} }
} }
@ -543,7 +543,7 @@ struct MatchDetailView: View {
} }
} }
} label: { } label: {
Text("Terrain") Text("Piste")
} }
.onChange(of: fieldSetup) { .onChange(of: fieldSetup) {
if let courtIndex = fieldSetup.courtIndex { if let courtIndex = fieldSetup.courtIndex {

@ -64,7 +64,7 @@ struct MatchSetupView: View {
} }
HStack { HStack {
let luckyLosers = walkOutSpot ? match.luckyLosers() : [] let luckyLosers = walkOutSpot ? match.luckyLosers() : []
TeamPickerView(shouldConfirm: shouldConfirm, round: match.roundObject, matchTypeContext: matchTypeContext, luckyLosers: luckyLosers, teamPicked: { team in TeamPickerView(shouldConfirm: shouldConfirm, round: match.roundObject, pickTypeContext: matchTypeContext == .bracket ? .bracket : .loserBracket, luckyLosers: luckyLosers, teamPicked: { team in
print(team.pasteData()) print(team.pasteData())
if walkOutSpot || team.bracketPosition != nil || matchTypeContext == .loserBracket { if walkOutSpot || team.bracketPosition != nil || matchTypeContext == .loserBracket {
match.setLuckyLoser(team: team, teamPosition: teamPosition) match.setLuckyLoser(team: team, teamPosition: teamPosition)

@ -133,6 +133,9 @@ struct TournamentLookUpView: View {
Menu { Menu {
#if DEBUG #if DEBUG
if tournaments.isEmpty == false { if tournaments.isEmpty == false {
Button("Gather Mobile Phone") {
_gatherNumbers()
}
Section { Section {
ShareLink(item: pastedTournaments) { ShareLink(item: pastedTournaments) {
Label("Par texte", systemImage: "square.and.arrow.up") Label("Par texte", systemImage: "square.and.arrow.up")
@ -180,6 +183,20 @@ struct TournamentLookUpView: View {
private var liguesFound: [String] { private var liguesFound: [String] {
Set(tournaments.compactMap { $0.nomLigue }).sorted() Set(tournaments.compactMap { $0.nomLigue }).sorted()
} }
private func _gatherNumbers() {
Task {
print("Doing.....")
for i in 0..<tournaments.count {
print(i, "/", tournaments.count)
let phone = try? await NetworkFederalService.shared.getUmpireData(idTournament: tournaments[i].id).phone
federalDataViewModel.searchedFederalTournaments[i].updateJapPhoneNumber(phone: phone)
print(federalDataViewModel.searchedFederalTournaments[i].japMessage)
}
print(".....Done")
}
}
private func runSearch() { private func runSearch() {
dataStore.appSettingsStorage.write() dataStore.appSettingsStorage.write()

@ -58,9 +58,9 @@ enum OngoingDestination: Int, CaseIterable, Identifiable, Selectable, Equatable
case .followUp: case .followUp:
ContentUnavailableView("Aucun match à suivre", systemImage: "figure.tennis", description: Text("Tous vos matchs planifiés et confirmés, seront visibles ici, quelque soit le tournoi.")) ContentUnavailableView("Aucun match à suivre", systemImage: "figure.tennis", description: Text("Tous vos matchs planifiés et confirmés, seront visibles ici, quelque soit le tournoi."))
case .court: case .court:
ContentUnavailableView("Aucun match en cours", systemImage: "sportscourt", description: Text("Tous vos terrains correspondant aux matchs en cours seront visibles ici, quelque soit le tournoi.")) ContentUnavailableView("Aucun match en cours", systemImage: "sportscourt", description: Text("Toutes vos pistes correspondant aux matchs en cours seront visibles ici, quelque soit le tournoi."))
case .free: case .free:
ContentUnavailableView("Aucun terrain libre", systemImage: "sportscourt", description: Text("Les terrains libres seront visibles ici, quelque soit le tournoi.")) ContentUnavailableView("Aucune piste libre", systemImage: "sportscourt", description: Text("Les pistes libres seront visibles ici, quelque soit le tournoi."))
case .over: case .over:
ContentUnavailableView("Aucun match terminé", systemImage: "clock.badge.xmark", description: Text("Les matchs terminés seront visibles ici, quelque soit le tournoi.")) ContentUnavailableView("Aucun match terminé", systemImage: "clock.badge.xmark", description: Text("Les matchs terminés seront visibles ici, quelque soit le tournoi."))
} }
@ -73,7 +73,7 @@ enum OngoingDestination: Int, CaseIterable, Identifiable, Selectable, Equatable
case .followUp: case .followUp:
return "À suivre" return "À suivre"
case .court: case .court:
return "Terrains" return "Pistes"
case .free: case .free:
return "Libres" return "Libres"
case .over: case .over:

@ -89,7 +89,7 @@ struct OngoingCourtView: View {
List { List {
ForEach(filterMode.sortedCourtIndex, id: \.self) { index in ForEach(filterMode.sortedCourtIndex, id: \.self) { index in
let courtFilteredMatches = filteredMatches.filter({ $0.courtIndex == index }) let courtFilteredMatches = filteredMatches.filter({ $0.courtIndex == index })
let title : String = (index == nil ? "Aucun terrain défini" : "Terrain #\(index! + 1)") let title : String = (index == nil ? "Aucune piste définie" : "Piste #\(index! + 1)")
if (filterMode == .free && courtFilteredMatches.isEmpty) || (filterMode == .court && courtFilteredMatches.isEmpty == false) { if (filterMode == .free && courtFilteredMatches.isEmpty) || (filterMode == .court && courtFilteredMatches.isEmpty == false) {
Section { Section {
MatchListView(section: "En cours", matches: courtFilteredMatches, hideWhenEmpty: true, isExpanded: false) MatchListView(section: "En cours", matches: courtFilteredMatches, hideWhenEmpty: true, isExpanded: false)

@ -32,7 +32,7 @@ struct MultiCourtPickerView: View {
} }
} }
} }
.navigationTitle("Terrains disponibles") .navigationTitle("Pistes disponibles")
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.environment(\.editMode, Binding.constant(EditMode.active)) .environment(\.editMode, Binding.constant(EditMode.active))
} }

@ -99,9 +99,9 @@ struct CourtAvailabilitySettingsView: View {
.overlay { .overlay {
if courtsUnavailability.isEmpty { if courtsUnavailability.isEmpty {
ContentUnavailableView { ContentUnavailableView {
Label("Tous les terrains sont disponibles", systemImage: "checkmark.circle.fill").tint(.green) Label("Tous les pistes sont disponibles", systemImage: "checkmark.circle.fill").tint(.green)
} description: { } description: {
Text("Vous pouvez précisez l'indisponibilité d'une ou plusieurs terrains, que ce soit pour une journée entière ou un créneau précis.") Text("Vous pouvez précisez l'indisponibilité d'une ou plusieurs pistes, que ce soit pour une journée entière ou un créneau précis.")
} actions: { } actions: {
RowButtonView("Ajouter une indisponibilité", systemImage: "plus.circle.fill") { RowButtonView("Ajouter une indisponibilité", systemImage: "plus.circle.fill") {
showingPopover = true showingPopover = true
@ -175,7 +175,7 @@ struct CourtAvailabilityEditorView: View {
NavigationStack { NavigationStack {
Form { Form {
Section { Section {
CourtPicker(title: "Terrain", selection: $courtIndex, maxCourt: tournament.courtCount) CourtPicker(title: "Piste", selection: $courtIndex, maxCourt: tournament.courtCount)
} }
Section { Section {
@ -256,13 +256,27 @@ struct CourtAvailabilityEditorView: View {
struct DateAdjusterView: View { struct DateAdjusterView: View {
@Binding var date: Date @Binding var date: Date
var time: Int?
var matchFormat: MatchFormat?
var body: some View { var body: some View {
HStack { HStack(spacing: 4) {
_createButton(label: "-1h", timeOffset: -1, component: .hour) if let matchFormat {
_createButton(label: "-30m", timeOffset: -30, component: .minute) _createButton(label: "-\(matchFormat.defaultEstimatedDuration)m", timeOffset: -matchFormat.defaultEstimatedDuration, component: .minute)
_createButton(label: "+30m", timeOffset: 30, component: .minute) _createButton(label: "+\(matchFormat.defaultEstimatedDuration)m", timeOffset: +matchFormat.defaultEstimatedDuration, component: .minute)
_createButton(label: "+1h", timeOffset: 1, component: .hour) _createButton(label: "-\(matchFormat.estimatedTimeWithBreak)m", timeOffset: -matchFormat.estimatedTimeWithBreak, component: .minute)
_createButton(label: "+\(matchFormat.estimatedTimeWithBreak)m", timeOffset: +matchFormat.estimatedTimeWithBreak, component: .minute)
} else if let time {
_createButton(label: "-\(time)m", timeOffset: -time, component: .minute)
_createButton(label: "-\(time/2)m", timeOffset: -time/2, component: .minute)
_createButton(label: "+\(time/2)m", timeOffset: time/2, component: .minute)
_createButton(label: "+\(time)m", timeOffset: time, component: .minute)
} else {
_createButton(label: "-1h", timeOffset: -1, component: .hour)
_createButton(label: "-30m", timeOffset: -30, component: .minute)
_createButton(label: "+30m", timeOffset: 30, component: .minute)
_createButton(label: "+1h", timeOffset: 1, component: .hour)
}
} }
.font(.headline) .font(.headline)
} }
@ -272,6 +286,9 @@ struct DateAdjusterView: View {
date = Calendar.current.date(byAdding: component, value: timeOffset, to: date) ?? date date = Calendar.current.date(byAdding: component, value: timeOffset, to: date) ?? date
}) { }) {
Text(label) Text(label)
.lineLimit(1)
.font(.footnote)
.underline()
.frame(maxWidth: .infinity) // Make buttons take equal space .frame(maxWidth: .infinity) // Make buttons take equal space
} }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)

@ -91,13 +91,13 @@ struct PlanningByCourtView: View {
Picker(selection: $selectedCourt) { Picker(selection: $selectedCourt) {
ForEach(courts, id: \.self) { courtIndex in ForEach(courts, id: \.self) { courtIndex in
if courtIndex == Int.max { if courtIndex == Int.max {
Label("Sans terrain", systemImage: "rectangle.slash").tag(Int.max) Label("Sans piste", systemImage: "rectangle.slash").tag(Int.max)
} else { } else {
Text(tournament.courtName(atIndex: courtIndex)).tag(courtIndex) Text(tournament.courtName(atIndex: courtIndex)).tag(courtIndex)
} }
} }
} label: { } label: {
Text("Terrain") Text("Piste")
} }
.pickerStyle(.automatic) .pickerStyle(.automatic)
} }
@ -114,7 +114,6 @@ struct PlanningByCourtView: View {
let match = _sortedMatches[index] let match = _sortedMatches[index]
Section { Section {
MatchRowView(match: match) MatchRowView(match: match)
.matchViewStyle(.feedStyle)
} header: { } header: {
if let startDate = match.startDate { if let startDate = match.startDate {
if index > 0 { if index > 0 {
@ -136,7 +135,7 @@ struct PlanningByCourtView: View {
ContentUnavailableView { ContentUnavailableView {
Label("Aucun match planifié", systemImage: "clock.badge.questionmark") Label("Aucun match planifié", systemImage: "clock.badge.questionmark")
} description: { } description: {
Text("Aucun match n'a été planifié sur ce terrain et au jour sélectionné") Text("Aucun match n'a été planifié sur cette piste et au jour sélectionné")
} actions: { } actions: {
} }
} }
@ -144,7 +143,7 @@ struct PlanningByCourtView: View {
ContentUnavailableView { ContentUnavailableView {
Label("Aucun match planifié", systemImage: "clock.badge.questionmark") Label("Aucun match planifié", systemImage: "clock.badge.questionmark")
} description: { } description: {
Text("Aucun match n'a été planifié sur ce terrain et au jour sélectionné") Text("Aucun match n'a été planifié sur cette piste et au jour sélectionné")
} actions: { } actions: {
} }
} }

@ -63,7 +63,7 @@ struct PlanningSettingsView: View {
Text("\(tournament.dayDuration) jour" + tournament.dayDuration.pluralSuffix) Text("\(tournament.dayDuration) jour" + tournament.dayDuration.pluralSuffix)
} }
TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount) TournamentFieldsManagerView(localizedStringKey: "Pistes maximum", count: $tournament.courtCount)
if let event = tournament.eventObject() { if let event = tournament.eventObject() {
NavigationLink { NavigationLink {
@ -85,7 +85,7 @@ struct PlanningSettingsView: View {
LabeledContent { LabeledContent {
Text(matchScheduler.courtsAvailable.count.formatted() + "/" + tournament.courtCount.formatted()) Text(matchScheduler.courtsAvailable.count.formatted() + "/" + tournament.courtCount.formatted())
} label: { } label: {
Text("Sélection des terrains") Text("Sélection des pistes")
if matchScheduler.courtsAvailable.count > tournament.courtCount { if matchScheduler.courtsAvailable.count > tournament.courtCount {
Text("Attention !") Text("Attention !")
.tint(.red) .tint(.red)
@ -97,7 +97,7 @@ struct PlanningSettingsView: View {
if tournament.courtCount < club.courtCount { if tournament.courtCount < club.courtCount {
let plural = tournament.courtCount.pluralSuffix let plural = tournament.courtCount.pluralSuffix
let verb = tournament.courtCount > 1 ? "seront" : "sera" let verb = tournament.courtCount > 1 ? "seront" : "sera"
Text("En réduisant les terrains maximum, seul\(plural) le\(plural) \(tournament.courtCount) premier\(plural) terrain\(plural) \(verb) utilisé\(plural)") + Text(", par contre, si vous gardez le nombre de terrains du club, vous pourrez plutôt préciser quel terrain n'est pas disponible.") Text("En réduisant les pistes maximum, seule\(plural) le\(plural) \(tournament.courtCount) première\(plural) piste\(plural) \(verb) utilisée\(plural)") + Text(", par contre, si vous gardez le nombre de pistes du club, vous pourrez plutôt préciser quelle piste n'est pas disponible.")
} else if tournament.courtCount > club.courtCount { } else if tournament.courtCount > club.courtCount {
let isCreatedByUser = club.hasBeenCreated(by: StoreCenter.main.userId) let isCreatedByUser = club.hasBeenCreated(by: StoreCenter.main.userId)
Button { Button {
@ -109,10 +109,10 @@ struct PlanningSettingsView: View {
} }
} label: { } label: {
if isCreatedByUser { if isCreatedByUser {
Text("Vous avez indiqué plus de terrains dans ce tournoi que dans le club. ") Text("Vous avez indiqué plus de pistes dans ce tournoi que dans le club. ")
+ Text("Mettre à jour le club ?").underline().foregroundStyle(.master) + Text("Mettre à jour le club ?").underline().foregroundStyle(.master)
} else { } else {
Label("Vous avez indiqué plus de terrains dans ce tournoi que dans le club.", systemImage: "exclamationmark.triangle.fill").foregroundStyle(.logoRed) Label("Vous avez indiqué plus de pistes dans ce tournoi que dans le club.", systemImage: "exclamationmark.triangle.fill").foregroundStyle(.logoRed)
} }
} }
.buttonStyle(.plain) .buttonStyle(.plain)
@ -122,7 +122,7 @@ struct PlanningSettingsView: View {
} }
if issueFound { if issueFound {
Text("Padel Club n'a pas réussi à définir un horaire pour tous les matchs de ce tournoi, à cause de la programmation d'autres tournois ou de l'indisponibilité des terrains.") Text("Padel Club n'a pas réussi à définir un horaire pour tous les matchs de ce tournoi, à cause de la programmation d'autres tournois ou de l'indisponibilité des pistes.")
.foregroundStyle(.logoRed) .foregroundStyle(.logoRed)
} }
@ -141,7 +141,7 @@ struct PlanningSettingsView: View {
} }
} footer: { } footer: {
if let event, event.tournaments.count > 1 { if let event, event.tournaments.count > 1 {
Text("Cette option fait en sorte qu'un terrain pris par un match d'un autre tournoi de cet événement soit toujours considéré comme libre.") Text("Cette option fait en sorte qu'une piste prise par un match d'un autre tournoi de cet événement soit toujours considéré comme libre.")
} }
} }
@ -421,13 +421,13 @@ struct PlanningSettingsView: View {
Section { Section {
Toggle(isOn: $matchScheduler.randomizeCourts) { Toggle(isOn: $matchScheduler.randomizeCourts) {
Text("Distribuer les terrains au hasard") Text("Distribuer les pistes au hasard")
} }
} }
Section { Section {
Toggle(isOn: $matchScheduler.shouldTryToFillUpCourtsAvailable) { Toggle(isOn: $matchScheduler.shouldTryToFillUpCourtsAvailable) {
Text("Remplir au maximum les terrains d'une rotation") Text("Remplir au maximum les pistes d'une rotation")
} }
} footer: { } footer: {
Text("Tout en tenant compte de l'option ci-dessous, Padel Club essaiera de remplir les créneaux à chaque rotation.") Text("Tout en tenant compte de l'option ci-dessous, Padel Club essaiera de remplir les créneaux à chaque rotation.")
@ -438,7 +438,7 @@ struct PlanningSettingsView: View {
Text("Équilibrer les matchs d'une manche") Text("Équilibrer les matchs d'une manche")
} }
} footer: { } footer: {
Text("Cette option permet de programmer une manche sur plusieurs rotation de manière équilibrée dans le cas où il y a plus de matchs à jouer dans cette manche que de terrains.") Text("Cette option permet de programmer une manche sur plusieurs rotation de manière équilibrée dans le cas où il y a plus de matchs à jouer dans cette manche que de pistes.")
} }
Section { Section {
@ -462,6 +462,15 @@ struct PlanningSettingsView: View {
} header: { } header: {
Text("Classement") Text("Classement")
} }
Section {
Toggle(isOn: $matchScheduler.accountGroupStageBreakTime) {
Text("Tenir compte des temps de pause réglementaires")
}
} header: {
Text("Poule")
}
Section { Section {
Toggle(isOn: $matchScheduler.rotationDifferenceIsImportant) { Toggle(isOn: $matchScheduler.rotationDifferenceIsImportant) {
@ -469,18 +478,27 @@ struct PlanningSettingsView: View {
} }
LabeledContent { LabeledContent {
StepperView(count: $matchScheduler.upperBracketRotationDifference, minimum: 0, maximum: 2) StepperView(count: $matchScheduler.upperBracketRotationDifference, minimum: 0)
} label: { } label: {
Text("Tableau") Text("Tableau")
} }
.disabled(matchScheduler.rotationDifferenceIsImportant == false) .disabled(matchScheduler.rotationDifferenceIsImportant == false)
LabeledContent { LabeledContent {
StepperView(count: $matchScheduler.loserBracketRotationDifference, minimum: 0, maximum: 2) StepperView(count: $matchScheduler.loserBracketRotationDifference, minimum: 0)
} label: { } label: {
Text("Classement") Text("Classement")
} }
.disabled(matchScheduler.rotationDifferenceIsImportant == false) .disabled(matchScheduler.rotationDifferenceIsImportant == false)
LabeledContent {
StepperView(count: $matchScheduler.groupStageRotationDifference, minimum: 0)
} label: {
Text("Poule")
}
.disabled(matchScheduler.rotationDifferenceIsImportant == false)
} footer: { } footer: {
Text("Cette option ajoute du temps entre 2 rotations, permettant ainsi de mieux configurer plusieurs tournois se déroulant en même temps.") Text("Cette option ajoute du temps entre 2 rotations, permettant ainsi de mieux configurer plusieurs tournois se déroulant en même temps.")
} }
@ -517,83 +535,11 @@ struct PlanningSettingsView: View {
} }
private func _groupMatchesByDay(matches: [Match]) -> [Date: [Match]] { private func _groupMatchesByDay(matches: [Match]) -> [Date: [Match]] {
var matchesByDay = [Date: [Match]]() tournament.groupMatchesByDay(matches: matches)
let calendar = Calendar.current
for match in matches {
// Extract day/month/year and create a date with only these components
let components = calendar.dateComponents([.year, .month, .day], from: match.computedStartDateForSorting)
let strippedDate = calendar.date(from: components)!
// Group matches by the strippedDate (only day/month/year)
if matchesByDay[strippedDate] == nil {
matchesByDay[strippedDate] = []
}
let shouldIncludeMatch: Bool
switch match.matchType {
case .groupStage:
shouldIncludeMatch = !matchesByDay[strippedDate]!.filter { $0.groupStage != nil }.compactMap { $0.groupStage }.contains(match.groupStage!)
case .bracket:
shouldIncludeMatch = !matchesByDay[strippedDate]!.filter { $0.round != nil }.compactMap { $0.round }.contains(match.round!)
case .loserBracket:
shouldIncludeMatch = true
}
if shouldIncludeMatch {
matchesByDay[strippedDate]!.append(match)
}
}
return matchesByDay
} }
private func _matchCountPerDay(matchesByDay: [Date: [Match]], tournament: Tournament) -> [Date: NSCountedSet] { private func _matchCountPerDay(matchesByDay: [Date: [Match]], tournament: Tournament) -> [Date: NSCountedSet] {
let days = matchesByDay.keys tournament.matchCountPerDay(matchesByDay: matchesByDay)
var matchCountPerDay = [Date: NSCountedSet]()
for day in days {
if let matches = matchesByDay[day] {
var groupStageCount = 0
let countedSet = NSCountedSet()
for match in matches {
switch match.matchType {
case .groupStage:
if let groupStage = match.groupStageObject {
if groupStageCount < groupStage.size - 1 {
groupStageCount = groupStage.size - 1
}
}
case .bracket:
countedSet.add(match.matchFormat)
case .loserBracket:
break
}
}
if groupStageCount > 0 {
for _ in 0..<groupStageCount {
countedSet.add(tournament.groupStageMatchFormat)
}
}
if let loserRounds = matches.filter({ $0.round != nil }).filter({ $0.roundObject?.parent == nil }).sorted(by: \.computedStartDateForSorting).last?.roundObject?.loserRounds() {
let ids = matches.map { $0.id }
for loserRound in loserRounds {
if let first = loserRound.playedMatches().first {
if ids.contains(first.id) {
countedSet.add(first.matchFormat)
}
}
}
}
matchCountPerDay[day] = countedSet
}
}
return matchCountPerDay
} }
private func _formatPerDayView(matchCountPerDay: [Date: NSCountedSet]) -> some View { private func _formatPerDayView(matchCountPerDay: [Date: NSCountedSet]) -> some View {

File diff suppressed because it is too large Load Diff

@ -80,6 +80,10 @@ struct PlayerDetailView: View {
Toggle("Joueur sur place", isOn: $player.hasArrived) Toggle("Joueur sur place", isOn: $player.hasArrived)
Toggle("Capitaine", isOn: $player.captain).disabled(player.hasPaidOnline()) Toggle("Capitaine", isOn: $player.captain).disabled(player.hasPaidOnline())
//Toggle("Coach", isOn: $player.coach) //Toggle("Coach", isOn: $player.coach)
Toggle(isOn: $player.clubMember) {
Text("Membre du club")
}
} }
Section { Section {
@ -256,7 +260,7 @@ struct PlayerDetailView: View {
// } // }
// } // }
} }
.onChange(of: [player.hasArrived, player.captain, player.coach]) { .onChange(of: [player.hasArrived, player.captain, player.coach, player.clubMember]) {
_save() _save()
} }
.onChange(of: player.sex) { .onChange(of: player.sex) {

@ -48,6 +48,7 @@ struct LoserRoundView: View {
if isEditingTournamentSeed.wrappedValue == true { if isEditingTournamentSeed.wrappedValue == true {
RowButtonView(match.disabled ? "Jouer ce match" : "Ne pas jouer ce match", role: .destructive) { RowButtonView(match.disabled ? "Jouer ce match" : "Ne pas jouer ce match", role: .destructive) {
match._toggleMatchDisableState(!match.disabled, single: true) match._toggleMatchDisableState(!match.disabled, single: true)
loserBracket.updateEnabledMatches()
} }
} }
} }

@ -13,7 +13,7 @@ class UpperRound: Identifiable, Selectable {
let round: Round let round: Round
var loserRounds: [LoserRound] = [] var loserRounds: [LoserRound] = []
let title: String let title: String
let playedMatches: [Match] var playedMatches: [Match]
var correspondingLoserRoundTitle: String var correspondingLoserRoundTitle: String
init(round: Round) { init(round: Round) {

@ -27,6 +27,10 @@ struct RoundView: View {
var upperRound: UpperRound var upperRound: UpperRound
func _refreshRound() {
self.upperRound.playedMatches = self.upperRound.round.playedMatches()
}
init(upperRound: UpperRound) { init(upperRound: UpperRound) {
self.upperRound = upperRound self.upperRound = upperRound
// let seeds = upperRound.round.seeds() // let seeds = upperRound.round.seeds()
@ -135,6 +139,22 @@ struct RoundView: View {
} }
} }
} else { } else {
let seeds = upperRound.round.seeds()
if upperRound.round.seeds().isEmpty == false {
RowButtonView("Retirer les têtes de séries", role: .destructive) {
seeds.forEach { tr in
tr.bracketPosition = nil
}
let teamScores = upperRound.playedMatches.flatMap { match in
match.teamScores
}
tournamentStore?.teamScores.delete(contentOfs: teamScores)
tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: seeds)
}
}
let isRoundValidForSeeding = tournament.isRoundValidForSeeding(roundIndex: upperRound.round.index) let isRoundValidForSeeding = tournament.isRoundValidForSeeding(roundIndex: upperRound.round.index)
let availableSeeds = tournament.availableSeeds() let availableSeeds = tournament.availableSeeds()
let availableQualifiedTeams = tournament.availableQualifiedTeams() let availableQualifiedTeams = tournament.availableQualifiedTeams()
@ -252,6 +272,9 @@ struct RoundView: View {
} }
} }
} }
.onAppear(perform: {
self._refreshRound()
})
.task { .task {
await MainActor.run { await MainActor.run {
let seeds = self.upperRound.round.seeds() let seeds = self.upperRound.round.seeds()

@ -69,7 +69,7 @@ struct FollowUpMatchView: View {
case .index: case .index:
return "Ordre prévu" return "Ordre prévu"
case .court: case .court:
return "Terrain" return "Piste"
case .restingTime: case .restingTime:
return "Temps de repos" return "Temps de repos"
case .winner: case .winner:
@ -285,7 +285,7 @@ struct FollowUpMatchView: View {
} }
} }
} label: { } label: {
Text("Sur le terrain") Text("Sur la piste")
} }
.labelsHidden() .labelsHidden()
.underline() .underline()

@ -29,29 +29,23 @@ struct PaymentInfoSheetView: View {
- Tous les paiements sont traités via Stripe, une plateforme sécurisée de paiement en ligne - Tous les paiements sont traités via Stripe, une plateforme sécurisée de paiement en ligne
Remboursements : Remboursements :
- Les remboursements peuvent être activés ou désactivés par l'organisateur - Les remboursements en ligne peuvent être activés ou désactivés par l'organisateur
- Si activés, une date limite de remboursement peut être définie - Si activés, une date limite de remboursement peut être définie
- Aucun remboursement n'est possible après cette date limite - Aucun remboursement en ligne n'est possible après cette date limite
- Les remboursements sont automatiquement traités via la même méthode de paiement utilisée - Les remboursements en ligne sont automatiquement traités via la même méthode de paiement utilisée
Commissions et frais : Commissions et frais :
- Padel Club prélève une commission de \(stripePlatformFee)% sur chaque transaction - Padel Club prélève une commission de \(stripePlatformFee)% sur chaque transaction
- Cette commission couvre les frais de service et de maintenance de la plateforme
- Des frais supplémentaires de Stripe s'appliquent (\(stripePercentageFee)% + \(stripeFixedFee) par transaction) - Des frais supplémentaires de Stripe s'appliquent (\(stripePercentageFee)% + \(stripeFixedFee) par transaction)
- Le montant total des frais est indiqué clairement avant validation du paiement
Exigences pour les organisateurs : Exigences pour les organisateurs :
- L'organisateur doit avoir un compte Stripe valide pour recevoir les paiements - L'organisateur doit avoir un compte Stripe valide pour recevoir les paiements
- Le compte Stripe doit être vérifié et connecté à Padel Club - 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 - 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é : Sécurité :
- Toutes les transactions sont sécurisées et chiffrées - Toutes les transactions sont sécurisées et chiffrées
- Padel Club ne stocke pas les informations de carte bancaire - 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.
""" """
} }

@ -29,6 +29,8 @@ struct EditingTeamView: View {
@State private var isProcessingRefund = false @State private var isProcessingRefund = false
@State private var refundMessage: String? @State private var refundMessage: String?
@State private var registrationDateModified: Date @State private var registrationDateModified: Date
@State private var uniqueRandomIndex: Int
var messageSentFailed: Binding<Bool> { var messageSentFailed: Binding<Bool> {
Binding { Binding {
@ -48,6 +50,7 @@ struct EditingTeamView: View {
return return
registrationDate != team.registrationDate registrationDate != team.registrationDate
|| uniqueRandomIndex != team.uniqueRandomIndex
|| walkOut != team.walkOut || walkOut != team.walkOut
|| wildCardBracket != team.wildCardBracket || wildCardBracket != team.wildCardBracket
|| wildCardGroupStage != team.wildCardGroupStage || wildCardGroupStage != team.wildCardGroupStage
@ -69,6 +72,7 @@ struct EditingTeamView: View {
_walkOut = State(wrappedValue: team.walkOut) _walkOut = State(wrappedValue: team.walkOut)
_wildCardBracket = State(wrappedValue: team.wildCardBracket) _wildCardBracket = State(wrappedValue: team.wildCardBracket)
_wildCardGroupStage = State(wrappedValue: team.wildCardGroupStage) _wildCardGroupStage = State(wrappedValue: team.wildCardGroupStage)
_uniqueRandomIndex = .init(wrappedValue: team.uniqueRandomIndex)
} }
private func _resetTeam() { private func _resetTeam() {
@ -77,6 +81,7 @@ struct EditingTeamView: View {
team.wildCardGroupStage = false team.wildCardGroupStage = false
team.walkOut = false team.walkOut = false
team.wildCardBracket = false team.wildCardBracket = false
team.uniqueRandomIndex = 0
} }
var body: some View { var body: some View {
@ -96,12 +101,22 @@ struct EditingTeamView: View {
HStack { HStack {
CopyPasteButtonView(pasteValue: team.playersPasteData()) CopyPasteButtonView(pasteValue: team.playersPasteData())
Spacer() Spacer()
NavigationLink { if team.isWildCard(), team.unsortedPlayers().isEmpty {
GroupStageTeamReplacementView(team: team) TeamPickerView(pickTypeContext: .wildcard) { teamregistration in
.environment(tournament) teamregistration.wildCardBracket = team.wildCardBracket
} label: { teamregistration.wildCardGroupStage = team.wildCardGroupStage
Text("Chercher à remplacer") tournament.tournamentStore?.teamRegistrations.addOrUpdate(instance: teamregistration)
.underline() tournament.tournamentStore?.teamRegistrations.delete(instance: team)
dismiss()
}
} else {
NavigationLink {
GroupStageTeamReplacementView(team: team)
.environment(tournament)
} label: {
Text("Chercher à remplacer")
.underline()
}
} }
} }
} }
@ -173,15 +188,18 @@ struct EditingTeamView: View {
DatePicker(selection: $registrationDateModified) { DatePicker(selection: $registrationDateModified) {
if registrationDate != registrationDateModified { if registrationDate != registrationDateModified {
HStack { HStack {
FooterButtonView("Valider") { Button("Valider", systemImage: "checkmark.circle") {
registrationDate = registrationDateModified registrationDate = registrationDateModified
} }
.tint(.green)
Divider() Divider()
FooterButtonView("Annuler", role: .cancel) { Button("Annuler", systemImage: "xmark.circle", role: .cancel) {
registrationDateModified = registrationDate registrationDateModified = registrationDate
} }
.foregroundStyle(.blue) .tint(.logoRed)
} }
.labelStyle(.iconOnly)
.buttonStyle(.borderedProminent)
} else { } else {
Text("Inscription") Text("Inscription")
Text(registrationDateModified.localizedWeekDay().capitalized) Text(registrationDateModified.localizedWeekDay().capitalized)
@ -206,6 +224,16 @@ struct EditingTeamView: View {
} }
} }
Section {
LabeledContent {
StepperView(count: $uniqueRandomIndex, minimum: 0)
} label: {
Text("Ordre à poids de paire égal")
}
} footer: {
Text("Si plusieurs équipes ont le même poids et que leur position est tiré au sort, ce champ permet de les positionner correctement dans l'ordre croissant.")
}
Section { Section {
HStack { HStack {
TextField("Nom de l'équipe", text: $name) TextField("Nom de l'équipe", text: $name)
@ -287,6 +315,7 @@ struct EditingTeamView: View {
team.wildCardBracket = wildCardBracket team.wildCardBracket = wildCardBracket
team.wildCardGroupStage = wildCardGroupStage team.wildCardGroupStage = wildCardGroupStage
team.walkOut = walkOut team.walkOut = walkOut
team.uniqueRandomIndex = uniqueRandomIndex
_save() _save()
} }
@ -295,6 +324,7 @@ struct EditingTeamView: View {
walkOut = team.walkOut walkOut = team.walkOut
wildCardBracket = team.wildCardBracket wildCardBracket = team.wildCardBracket
wildCardGroupStage = team.wildCardGroupStage wildCardGroupStage = team.wildCardGroupStage
uniqueRandomIndex = team.uniqueRandomIndex
} }
}, message: { }, message: {
Text("Ce changement peut entraîner l'entrée ou la sortie d'une équipe de votre sélection. Padel Club préviendra automatiquement une équipe inscrite en ligne de son nouveau statut.") Text("Ce changement peut entraîner l'entrée ou la sortie d'une équipe de votre sélection. Padel Club préviendra automatiquement une équipe inscrite en ligne de son nouveau statut.")
@ -394,6 +424,12 @@ struct EditingTeamView: View {
} }
} }
} }
.onChange(of: uniqueRandomIndex) {
if canSaveWithoutWarning() {
team.uniqueRandomIndex = uniqueRandomIndex
_save()
}
}
.onChange(of: [walkOut, wildCardBracket, wildCardGroupStage]) { .onChange(of: [walkOut, wildCardBracket, wildCardGroupStage]) {
if canSaveWithoutWarning() { if canSaveWithoutWarning() {
if walkOut == false && team.walkOut == true { if walkOut == false && team.walkOut == true {

@ -8,6 +8,13 @@
import SwiftUI import SwiftUI
import PadelClubData import PadelClubData
public enum TeamPickType: String {
case bracket = "bracket"
case groupStage = "groupStage"
case loserBracket = "loserBracket"
case wildcard = "wildcard"
}
struct TeamPickerView: View { struct TeamPickerView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament @Environment(Tournament.self) var tournament: Tournament
@ -20,7 +27,7 @@ struct TeamPickerView: View {
var shouldConfirm: Bool = false var shouldConfirm: Bool = false
var groupStagePosition: Int? = nil var groupStagePosition: Int? = nil
var round: Round? = nil var round: Round? = nil
var matchTypeContext: MatchType = .bracket var pickTypeContext: TeamPickType = .bracket
var luckyLosers: [TeamRegistration] = [] var luckyLosers: [TeamRegistration] = []
let teamPicked: ((TeamRegistration) -> (Void)) let teamPicked: ((TeamRegistration) -> (Void))
@ -31,21 +38,34 @@ struct TeamPickerView: View {
} }
} }
var wording: String {
switch pickTypeContext {
case .bracket:
return "Choisir"
case .groupStage:
return "Choisir"
case .loserBracket:
return "Choisir"
case .wildcard:
return "Choisir la wildcard"
}
}
var body: some View { var body: some View {
ConfirmButtonView(shouldConfirm: shouldConfirm, message: MatchSetupView.confirmationMessage) { ConfirmButtonView(shouldConfirm: shouldConfirm, message: MatchSetupView.confirmationMessage) {
presentTeamPickerView = true presentTeamPickerView = true
} label: { } label: {
Text("Choisir") Text(wording)
.underline() .underline()
} }
.sheet(isPresented: $presentTeamPickerView) { .sheet(isPresented: $presentTeamPickerView) {
NavigationStack { NavigationStack {
List { List {
if matchTypeContext == .loserBracket, let losers = round?.parentRound?.losers() { if pickTypeContext == .loserBracket, let losers = round?.parentRound?.losers() {
_sectionView(losers.sorted(by: \.weight, order: sortOrder), title: "Perdant du tour précédent") _sectionView(losers.sorted(by: \.weight, order: sortOrder), title: "Perdant du tour précédent")
} }
if matchTypeContext == .loserBracket, let losers = round?.previousRound()?.winners() { if pickTypeContext == .loserBracket, let losers = round?.previousRound()?.winners() {
_sectionView(losers.sorted(by: \.weight, order: sortOrder), title: "Gagnant du tour précédent") _sectionView(losers.sorted(by: \.weight, order: sortOrder), title: "Gagnant du tour précédent")
} }
@ -63,7 +83,11 @@ struct TeamPickerView: View {
let teams = tournament.selectedSortedTeams() let teams = tournament.selectedSortedTeams()
if matchTypeContext == .loserBracket { if pickTypeContext == .wildcard {
_sectionView(tournament.waitingListSortedTeams(selectedSortedTeams: teams).sorted(by: \.weight, order: sortOrder), title: "Liste d'attente")
}
if pickTypeContext == .loserBracket {
_sectionView(teams.filter({ $0.inGroupStage() && $0.qualified == false }).sorted(by: \.weight, order: sortOrder), title: "Non qualifié de poules") _sectionView(teams.filter({ $0.inGroupStage() && $0.qualified == false }).sorted(by: \.weight, order: sortOrder), title: "Non qualifié de poules")
} }
@ -145,7 +169,7 @@ struct TeamPickerView: View {
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.buttonStyle(.plain) .buttonStyle(.plain)
.id(team.id) .id(team.id)
.listRowView(isActive: matchTypeContext == .loserBracket && round?.teams().map({ $0.id }).contains(team.id) == true, color: .green, hideColorVariation: true) .listRowView(isActive: pickTypeContext == .loserBracket && round?.teams().map({ $0.id }).contains(team.id) == true, color: .green, hideColorVariation: true)
// .confirmationDialog("Attention", isPresented: confirmationRequest, titleVisibility: .visible) { // .confirmationDialog("Attention", isPresented: confirmationRequest, titleVisibility: .visible) {
// Button("Retirer du tableau", role: .destructive) { // Button("Retirer du tableau", role: .destructive) {
// teamPicked(confirmTeam!) // teamPicked(confirmTeam!)

@ -155,6 +155,7 @@ struct AddTeamView: View {
players.forEach { player in players.forEach { player in
let newPlayer = PlayerRegistration(importedPlayer: player) let newPlayer = PlayerRegistration(importedPlayer: player)
newPlayer.setComputedRank(in: tournament) newPlayer.setComputedRank(in: tournament)
newPlayer.setClubMember(for: tournament)
createdPlayers = Set<PlayerRegistration>() createdPlayers = Set<PlayerRegistration>()
createdPlayerIds = Set<String>() createdPlayerIds = Set<String>()
createdPlayers.insert(newPlayer) createdPlayers.insert(newPlayer)
@ -176,6 +177,7 @@ struct AddTeamView: View {
players.forEach { player in players.forEach { player in
let newPlayer = PlayerRegistration(importedPlayer: player) let newPlayer = PlayerRegistration(importedPlayer: player)
newPlayer.setComputedRank(in: tournament) newPlayer.setComputedRank(in: tournament)
newPlayer.setClubMember(for: tournament)
createdPlayers.insert(newPlayer) createdPlayers.insert(newPlayer)
createdPlayerIds.insert(newPlayer.id) createdPlayerIds.insert(newPlayer.id)
} }
@ -183,6 +185,7 @@ struct AddTeamView: View {
searchViewModel.selectedPlayers.forEach { player in searchViewModel.selectedPlayers.forEach { player in
let newPlayer = PlayerRegistration(importedPlayer: player) let newPlayer = PlayerRegistration(importedPlayer: player)
newPlayer.setComputedRank(in: tournament) newPlayer.setComputedRank(in: tournament)
newPlayer.setClubMember(for: tournament)
createdPlayers.insert(newPlayer) createdPlayers.insert(newPlayer)
createdPlayerIds.insert(newPlayer.id) createdPlayerIds.insert(newPlayer.id)
} }
@ -336,6 +339,7 @@ struct AddTeamView: View {
}.forEach { player in }.forEach { player in
let player = PlayerRegistration(importedPlayer: player) let player = PlayerRegistration(importedPlayer: player)
player.setComputedRank(in: tournament) player.setComputedRank(in: tournament)
player.setClubMember(for: tournament)
currentSelection.insert(player) currentSelection.insert(player)
} }

@ -321,6 +321,9 @@ struct BroadcastView: View {
Section { Section {
let club = tournament.club() let club = tournament.club()
actionForURL(title: (club == nil) ? "Aucun club indiqué pour ce tournoi" : club!.clubTitle(), description: "Page du club", url: club?.shareURL()) actionForURL(title: (club == nil) ? "Aucun club indiqué pour ce tournoi" : club!.clubTitle(), description: "Page du club", url: club?.shareURL())
if let event = tournament.eventObject() {
actionForURL(title: event.eventTitle(), description: "Page de l'évémement", url: event.shareURL())
}
actionForURL(title: "Padel Club", url: URLs.main.url) actionForURL(title: "Padel Club", url: URLs.main.url)
} header: { } header: {
Text("Autres liens") Text("Autres liens")

@ -22,7 +22,7 @@ struct TournamentClubSettingsView: View {
let selectedClub = event?.clubObject() let selectedClub = event?.clubObject()
Section { Section {
TournamentFieldsManagerView(localizedStringKey: "Terrains pour le tournoi", count: $tournament.courtCount) TournamentFieldsManagerView(localizedStringKey: "Pistes pour le tournoi", count: $tournament.courtCount)
.onChange(of: tournament.courtCount) { .onChange(of: tournament.courtCount) {
do { do {
try dataStore.tournaments.addOrUpdate(instance: tournament) try dataStore.tournaments.addOrUpdate(instance: tournament)
@ -36,7 +36,7 @@ struct TournamentClubSettingsView: View {
CourtAvailabilitySettingsView(event: event) CourtAvailabilitySettingsView(event: event)
.environment(tournament) .environment(tournament)
} label: { } label: {
Text("Indisponibilités des terrains") Text("Indisponibilités des pistes")
} }
} }
} footer: { } footer: {
@ -44,7 +44,7 @@ struct TournamentClubSettingsView: View {
if tournament.courtCount < club.courtCount { if tournament.courtCount < club.courtCount {
let plural = tournament.courtCount.pluralSuffix let plural = tournament.courtCount.pluralSuffix
let verb = tournament.courtCount > 1 ? "seront" : "sera" let verb = tournament.courtCount > 1 ? "seront" : "sera"
Text("En réduisant les terrains maximum, seul\(plural) le\(plural) \(tournament.courtCount) premier\(plural) terrain\(plural) \(verb) utilisé\(plural)") + Text(", par contre, si vous gardez le nombre de terrains du club, vous pourrez plutôt préciser quel terrain n'est pas disponible.") Text("En réduisant les pistes maximum, seule\(plural) le\(plural) \(tournament.courtCount) première\(plural) piste\(plural) \(verb) utilisée\(plural)") + Text(", par contre, si vous gardez le nombre de pistes du club, vous pourrez plutôt préciser quelle piste n'est pas disponible.")
} else if tournament.courtCount > club.courtCount { } else if tournament.courtCount > club.courtCount {
let isCreatedByUser = club.hasBeenCreated(by: StoreCenter.main.userId) let isCreatedByUser = club.hasBeenCreated(by: StoreCenter.main.userId)
Button { Button {
@ -56,10 +56,10 @@ struct TournamentClubSettingsView: View {
} }
} label: { } label: {
if isCreatedByUser { if isCreatedByUser {
Text("Vous avez indiqué plus de terrains dans ce tournoi que dans le club.") Text("Vous avez indiqué plus de pistes dans ce tournoi que dans le club.")
+ Text("Mettre à jour le club ?").underline().foregroundStyle(.master) + Text("Mettre à jour le club ?").underline().foregroundStyle(.master)
} else { } else {
Label("Vous avez indiqué plus de terrains dans ce tournoi que dans le club.", systemImage: "exclamationmark.triangle.fill").foregroundStyle(.logoRed) Label("Vous avez indiqué plus de pistes dans ce tournoi que dans le club.", systemImage: "exclamationmark.triangle.fill").foregroundStyle(.logoRed)
} }
} }
.buttonStyle(.plain) .buttonStyle(.plain)

@ -16,6 +16,7 @@ struct TournamentGeneralSettingsView: View {
@State private var tournamentName: String = "" @State private var tournamentName: String = ""
@State private var tournamentInformation: String = "" @State private var tournamentInformation: String = ""
@State private var entryFee: Double? = nil @State private var entryFee: Double? = nil
@State private var clubMemberFeeDeduction: Double? = nil
@State private var umpireCustomMail: String @State private var umpireCustomMail: String
@State private var umpireCustomPhone: String @State private var umpireCustomPhone: String
@State private var umpireCustomContact: String @State private var umpireCustomContact: String
@ -24,12 +25,14 @@ struct TournamentGeneralSettingsView: View {
@FocusState private var focusedField: Tournament.CodingKeys? @FocusState private var focusedField: Tournament.CodingKeys?
let priceTags: [Double] = [15.0, 20.0, 25.0] let priceTags: [Double] = [15.0, 20.0, 25.0]
let deductionTags: [Double] = [5.0, 10.0]
init(tournament: Tournament) { init(tournament: Tournament) {
self.tournament = tournament self.tournament = tournament
_tournamentName = State(wrappedValue: tournament.name ?? "") _tournamentName = State(wrappedValue: tournament.name ?? "")
_tournamentInformation = State(wrappedValue: tournament.information ?? "") _tournamentInformation = State(wrappedValue: tournament.information ?? "")
_entryFee = State(wrappedValue: tournament.entryFee) _entryFee = State(wrappedValue: tournament.entryFee)
_clubMemberFeeDeduction = State(wrappedValue: tournament.clubMemberFeeDeduction)
_umpireCustomMail = State(wrappedValue: tournament.umpireCustomMail ?? "") _umpireCustomMail = State(wrappedValue: tournament.umpireCustomMail ?? "")
_umpireCustomPhone = State(wrappedValue: tournament.umpireCustomPhone ?? "") _umpireCustomPhone = State(wrappedValue: tournament.umpireCustomPhone ?? "")
_umpireCustomContact = State(wrappedValue: tournament.umpireCustomContact ?? "") _umpireCustomContact = State(wrappedValue: tournament.umpireCustomContact ?? "")
@ -58,6 +61,24 @@ struct TournamentGeneralSettingsView: View {
} label: { } label: {
Text("Inscription") Text("Inscription")
} }
LabeledContent {
TextField("Réduction", value: $clubMemberFeeDeduction, format: .currency(code: Locale.defaultCurrency()))
.keyboardType(.decimalPad)
.multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity)
.focused($focusedField, equals: ._clubMemberFeeDeduction)
.onChange(of: focusedField) {
if focusedField == ._clubMemberFeeDeduction {
DispatchQueue.main.async {
UIApplication.shared.sendAction(#selector(UIResponder.selectAll(_:)), to: nil, from: nil, for: nil)
}
}
}
} label: {
Text("Réduction membre")
}
.disabled(tournament.isFree())
} footer: { } footer: {
Text("Si vous souhaitez que Padel Club vous aide à suivre les encaissements, indiquer un prix d'inscription. Sinon Padel Club vous aidera à suivre simplement l'arrivée et la présence des joueurs.") Text("Si vous souhaitez que Padel Club vous aide à suivre les encaissements, indiquer un prix d'inscription. Sinon Padel Club vous aidera à suivre simplement l'arrivée et la présence des joueurs.")
} }
@ -172,6 +193,21 @@ struct TournamentGeneralSettingsView: View {
.buttonStyle(.bordered) .buttonStyle(.bordered)
} }
} else if focusedField == ._clubMemberFeeDeduction {
ForEach(deductionTags, id: \.self) { deductionTag in
Button(deductionTag.formatted(.currency(code: Locale.defaultCurrency()).precision(.fractionLength(0)))) {
clubMemberFeeDeduction = deductionTag
tournament.clubMemberFeeDeduction = deductionTag
focusedField = nil
}
.buttonStyle(.bordered)
}
Button("Gratuit") {
clubMemberFeeDeduction = entryFee
tournament.clubMemberFeeDeduction = clubMemberFeeDeduction
focusedField = nil
}
.buttonStyle(.bordered)
} else { } else {
if focusedField == ._name, tournamentName.isEmpty == false { if focusedField == ._name, tournamentName.isEmpty == false {
Button("Effacer") { Button("Effacer") {
@ -214,7 +250,7 @@ struct TournamentGeneralSettingsView: View {
.onChange(of: tournament.startDate) { .onChange(of: tournament.startDate) {
_save() _save()
} }
.onChange(of: tournament.entryFee) { .onChange(of: [tournament.entryFee, tournament.clubMemberFeeDeduction]) {
_save() _save()
} }
.onChange(of: [tournament.name, tournament.information, tournament.umpireCustomMail, tournament.umpireCustomPhone, tournament.umpireCustomContact]) { .onChange(of: [tournament.name, tournament.information, tournament.umpireCustomMail, tournament.umpireCustomPhone, tournament.umpireCustomContact]) {
@ -243,6 +279,8 @@ struct TournamentGeneralSettingsView: View {
} }
} else if old == ._entryFee { } else if old == ._entryFee {
tournament.entryFee = entryFee tournament.entryFee = entryFee
} else if old == ._clubMemberFeeDeduction {
tournament.clubMemberFeeDeduction = clubMemberFeeDeduction
} else if old == ._umpireCustomMail { } else if old == ._umpireCustomMail {
_confirmUmpireMail() _confirmUmpireMail()
} else if old == ._umpireCustomPhone { } else if old == ._umpireCustomPhone {
@ -301,11 +339,7 @@ struct TournamentGeneralSettingsView: View {
} }
private func _save() { private func _save() {
do { dataStore.tournaments.addOrUpdate(instance: tournament)
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
} }
private func _customUmpireView() -> some View { private func _customUmpireView() -> some View {

@ -45,7 +45,7 @@ struct UpdateSourceRankDateView: View {
do { do {
try await tournament.updateRank(to: currentRankSourceDate, forceRefreshLockWeight: forceRefreshLockWeight, providedSources: nil) try await tournament.updateRank(to: currentRankSourceDate, forceRefreshLockWeight: forceRefreshLockWeight, providedSources: nil)
try dataStore.tournaments.addOrUpdate(instance: tournament) dataStore.tournaments.addOrUpdate(instance: tournament)
} catch { } catch {
Logger.error(error) Logger.error(error)
} }

@ -47,9 +47,9 @@ struct PrintSettingsView: View {
Text("Tableau") Text("Tableau")
}) })
// Toggle(isOn: $generator.includeLoserBracket, label: { Toggle(isOn: $generator.includeLoserBracket, label: {
// Text("Tableau des matchs de classements") Text("Tableau des matchs de classements")
// }) })
if tournament.groupStages().isEmpty == false { if tournament.groupStages().isEmpty == false {
Toggle(isOn: $generator.includeGroupStage, label: { Toggle(isOn: $generator.includeGroupStage, label: {
@ -132,18 +132,18 @@ struct PrintSettingsView: View {
} label: { } label: {
Text("Aperçu du tableau") Text("Aperçu du tableau")
} }
//
// ForEach(tournament.rounds()) { round in ForEach(tournament.rounds()) { round in
// if round.index > 0 { if round.index > 0 {
// NavigationLink { NavigationLink {
// WebViewPreview(round: round) WebViewPreview(round: round)
// .environmentObject(generator) .environmentObject(generator)
// } label: { } label: {
// Text("Aperçu \(round.correspondingLoserRoundTitle())") Text("Aperçu \(round.correspondingLoserRoundTitle())")
// } }
// } }
// } }
//
ForEach(tournament.groupStages()) { groupStage in ForEach(tournament.groupStages()) { groupStage in
NavigationLink { NavigationLink {
WebViewPreview(groupStage: groupStage) WebViewPreview(groupStage: groupStage)
@ -182,6 +182,13 @@ struct PrintSettingsView: View {
Text("Poule") Text("Poule")
} }
} }
if let round = tournament.rounds().first {
ShareLink(item: generator.generateLoserBracketHtml(upperRound: round)) {
Text("Classement")
}
}
} header: { } header: {
Text("Partager le code source HTML") Text("Partager le code source HTML")
} }

@ -10,6 +10,7 @@ import SwiftUI
import PadelClubData import PadelClubData
struct RegistrationSetupView: View { struct RegistrationSetupView: View {
@Environment(\.openURL) private var openURL
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Bindable var tournament: Tournament @Bindable var tournament: Tournament
@State private var enableOnlineRegistration: Bool @State private var enableOnlineRegistration: Bool
@ -32,6 +33,7 @@ struct RegistrationSetupView: View {
@State private var isTemplate: Bool @State private var isTemplate: Bool
@State private var isCorporateTournament: Bool @State private var isCorporateTournament: Bool
@State private var isValidating = false @State private var isValidating = false
@State private var unregisterDeltaInHours: Int
// Online Payment // Online Payment
@State private var enableOnlinePayment: Bool @State private var enableOnlinePayment: Bool
@ -40,7 +42,7 @@ struct RegistrationSetupView: View {
@State private var refundDateLimit: Date @State private var refundDateLimit: Date
@State private var refundDateLimitEnabled: Bool @State private var refundDateLimitEnabled: Bool
@State private var stripeAccountId: String @State private var stripeAccountId: String
@State private var stripeAccountIdIsInvalid: Bool = false @State private var stripeAccountIdIsInvalid: Bool?
@State private var paymentConfig: PaymentConfig? @State private var paymentConfig: PaymentConfig?
@State private var timeToConfirmConfig: TimeToConfirmConfig? @State private var timeToConfirmConfig: TimeToConfirmConfig?
@ -49,6 +51,10 @@ struct RegistrationSetupView: View {
@State private var hasChanges: Bool = false @State private var hasChanges: Bool = false
@State private var stripeOnBoardingURL: URL? = nil
@State private var errorMessage: String? = nil
@State private var presentErrorAlert: Bool = false
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
init(tournament: Tournament) { init(tournament: Tournament) {
@ -56,6 +62,7 @@ struct RegistrationSetupView: View {
_enableOnlineRegistration = .init(wrappedValue: tournament.enableOnlineRegistration) _enableOnlineRegistration = .init(wrappedValue: tournament.enableOnlineRegistration)
_isTemplate = .init(wrappedValue: tournament.isTemplate) _isTemplate = .init(wrappedValue: tournament.isTemplate)
_isCorporateTournament = .init(wrappedValue: tournament.isCorporateTournament) _isCorporateTournament = .init(wrappedValue: tournament.isCorporateTournament)
_unregisterDeltaInHours = .init(wrappedValue: tournament.unregisterDeltaInHours)
// Registration Date Limit // Registration Date Limit
if let registrationDateLimit = tournament.registrationDateLimit { if let registrationDateLimit = tournament.registrationDateLimit {
_registrationDateLimit = .init(wrappedValue: registrationDateLimit) _registrationDateLimit = .init(wrappedValue: registrationDateLimit)
@ -184,8 +191,10 @@ struct RegistrationSetupView: View {
} }
Section { Section {
Text("Par défaut, sans date définie, les inscriptions en ligne sont possible dès son activation.")
Toggle(isOn: $openingRegistrationDateEnabled) { Toggle(isOn: $openingRegistrationDateEnabled) {
Text("Définir une date") Text("Définir une date ultérieur")
} }
if openingRegistrationDateEnabled { if openingRegistrationDateEnabled {
@ -215,6 +224,18 @@ struct RegistrationSetupView: View {
Text("Si une date de fermeture des inscriptions en ligne est définie, alors plus aucune inscription ne sera possible après cette date. Sinon, la date du début du tournoi ou la date de clôture des inscriptions seront utilisées.") Text("Si une date de fermeture des inscriptions en ligne est définie, alors plus aucune inscription ne sera possible après cette date. Sinon, la date du début du tournoi ou la date de clôture des inscriptions seront utilisées.")
} }
Section {
LabeledContent {
StepperView(count: $unregisterDeltaInHours)
} label: {
Text("\(unregisterDeltaInHours)h avant")
}
} header: {
Text("Limite de désinscription")
} footer: {
Text("Empêche la désinscription plusieurs heures avant le début du tournoi")
}
Section { Section {
if displayWarning() { if displayWarning() {
Text("Attention, l'inscription en ligne est activée et vous avez des équipes inscrites en ligne, en modifiant la structure ces équipes seront intégrées ou retirées de votre sélection d'équipes. Padel Club saura prévenir les équipes inscrites en ligne automatiquement.") Text("Attention, l'inscription en ligne est activée et vous avez des équipes inscrites en ligne, en modifiant la structure ces équipes seront intégrées ou retirées de votre sélection d'équipes. Padel Club saura prévenir les équipes inscrites en ligne automatiquement.")
@ -304,7 +325,6 @@ struct RegistrationSetupView: View {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
ButtonValidateView(role: .destructive) { ButtonValidateView(role: .destructive) {
_save() _save()
dismiss()
} }
} }
} }
@ -315,7 +335,7 @@ struct RegistrationSetupView: View {
HStack { HStack {
Button("Effacer") { Button("Effacer") {
stripeAccountId = "" stripeAccountId = ""
stripeAccountIdIsInvalid = false stripeAccountIdIsInvalid = nil
tournament.stripeAccountId = nil tournament.stripeAccountId = nil
} }
.buttonStyle(.borderless) .buttonStyle(.borderless)
@ -328,7 +348,13 @@ struct RegistrationSetupView: View {
} }
} }
} }
.alert("Paiement en ligne", isPresented: $presentErrorAlert, actions: {
Button("Fermer") {
self.presentErrorAlert = false
}
}, message: {
Text(ValidationError.onlinePaymentNotEnabled.localizedDescription)
})
.toolbarRole(.editor) .toolbarRole(.editor)
.headerProminence(.increased) .headerProminence(.increased)
.navigationTitle("Inscription en ligne") .navigationTitle("Inscription en ligne")
@ -399,7 +425,7 @@ struct RegistrationSetupView: View {
} }
Toggle(isOn: $enableOnlinePaymentRefund) { Toggle(isOn: $enableOnlinePaymentRefund) {
Text("Autoriser les remboursements") Text("Autoriser les remboursements en ligne")
} }
if enableOnlinePaymentRefund { if enableOnlinePaymentRefund {
@ -419,32 +445,6 @@ struct RegistrationSetupView: View {
Text("Revenu Padel Club") Text("Revenu Padel Club")
} }
} }
if isCorporateTournament == false, dataStore.user.registrationPaymentMode.requiresStripe() {
VStack(alignment: .leading) {
LabeledContent {
if isValidating {
ProgressView()
} else if focusedField == nil, stripeAccountIdIsInvalid == false, stripeAccountId.isEmpty == false, isValidating == false {
Image(systemName: "checkmark.circle.fill").foregroundStyle(.green)
}
} label: {
TextField("Identifiant du compte Stripe", text: $stripeAccountId)
.frame(maxWidth: .infinity)
.focused($focusedField, equals: ._stripeAccountId)
.disabled(isValidating)
.keyboardType(.alphabet)
.textContentType(nil)
.autocorrectionDisabled()
}
if stripeAccountIdIsInvalid {
Text("Identifiant Stripe invalide. Vous ne pouvez pas activer le paiement en ligne.").foregroundStyle(.logoRed)
Button("Ré-essayer") {
_confirmStripeAccountId()
}
}
}
}
} header: { } header: {
Text("Paiement en ligne") Text("Paiement en ligne")
} footer: { } footer: {
@ -472,13 +472,81 @@ struct RegistrationSetupView: View {
.onChange(of: refundDateLimit) { .onChange(of: refundDateLimit) {
_hasChanged() _hasChanged()
} }
.onChange(of: focusedField) { old, new in
if old == ._stripeAccountId {
_confirmStripeAccountId()
}
}
if dataStore.user.registrationPaymentMode.requiresStripe() { if isCorporateTournament == false, dataStore.user.registrationPaymentMode.requiresStripe() {
Section {
LabeledContent {
if isValidating {
ProgressView()
} else if focusedField == nil, stripeAccountIdIsInvalid == false, stripeAccountId.isEmpty == false, isValidating == false {
Image(systemName: "checkmark.circle.fill").foregroundStyle(.green)
}
} label: {
TextField("Identifiant du compte Stripe", text: $stripeAccountId)
.frame(maxWidth: .infinity)
.focused($focusedField, equals: ._stripeAccountId)
.disabled(isValidating)
.keyboardType(.alphabet)
.textContentType(nil)
.autocorrectionDisabled()
}
.onChange(of: focusedField) { old, new in
if old == ._stripeAccountId {
_confirmStripeAccountId()
}
}
if stripeAccountIdIsInvalid == true {
Text("Identifiant Stripe invalide. Vous ne pouvez pas activer le paiement en ligne.").foregroundStyle(.logoRed)
}
if stripeAccountId.isEmpty == false {
Button("Vérifier le compte Stripe") {
_confirmStripeAccountId()
}
.disabled(isValidating)
}
if let errorMessage {
Text(errorMessage).foregroundStyle(.logoRed)
}
RowButtonView("Connecter ou créer un compte Stripe", role: .destructive) {
errorMessage = nil
stripeAccountIdIsInvalid = nil
stripeAccountId = ""
stripeOnBoardingURL = nil
do {
let createStripeAccountResponse = try await StripeValidationService.createStripeConnectAccount()
print("createStripeAccountResponse", createStripeAccountResponse)
guard let accounId = createStripeAccountResponse.accountId else {
throw ValidationError.accountNotFound
}
let createStripeAccountLinkResponse = try await StripeValidationService.createStripeAccountLink(accounId)
print("createStripeAccountLinkResponse", createStripeAccountLinkResponse)
stripeOnBoardingURL = createStripeAccountLinkResponse.url
stripeAccountIdIsInvalid = nil
stripeAccountId = accounId
if let stripeOnBoardingURL {
openURL(stripeOnBoardingURL)
} else {
throw ValidationError.urlNotFound
}
} catch {
self.errorMessage = error.localizedDescription
Logger.error(error)
}
}
} header: {
Text("Compte Stripe")
} footer: {
Text("Vous devez connecter un compte Stripe à Padel Club. En cliquant sur le bouton ci-dessus, vous serez dirigé vers Stripe pour choisir votre compte Stripe à connecter ou pour en créer un.")
}
Section { Section {
let fixedFee = RegistrationPaymentMode.stripeFixedFee // Fixed fee in euros let fixedFee = RegistrationPaymentMode.stripeFixedFee // Fixed fee in euros
let percentageFee = RegistrationPaymentMode.stripePercentageFee let percentageFee = RegistrationPaymentMode.stripePercentageFee
@ -510,11 +578,10 @@ struct RegistrationSetupView: View {
// Text("Aucune commission Padel Club ne sera prélevée.").foregroundStyle(.logoRed).bold() // Text("Aucune commission Padel Club ne sera prélevée.").foregroundStyle(.logoRed).bold()
} }
} }
} }
private func _confirmStripeAccountId() { private func _confirmStripeAccountId() {
stripeAccountIdIsInvalid = false stripeAccountIdIsInvalid = nil
if stripeAccountId.isEmpty { if stripeAccountId.isEmpty {
tournament.stripeAccountId = nil tournament.stripeAccountId = nil
} else if stripeAccountId.count >= 5, stripeAccountId.starts(with: "acct_") { } else if stripeAccountId.count >= 5, stripeAccountId.starts(with: "acct_") {
@ -528,13 +595,12 @@ struct RegistrationSetupView: View {
Task { Task {
isValidating = true isValidating = true
do { do {
let response = try await StripeValidationService.validateStripeAccountID(accId) let response = try await StripeValidationService.validateStripeAccount(accountId: accId)
print("validateStripeAccount", response)
stripeAccountId = accId stripeAccountId = accId
stripeAccountIdIsInvalid = response.valid == false stripeAccountIdIsInvalid = response.canProcessPayments == false
enableOnlinePayment = response.valid
} catch { } catch {
stripeAccountIdIsInvalid = true stripeAccountIdIsInvalid = true
enableOnlinePayment = false
} }
isValidating = false isValidating = false
} }
@ -550,7 +616,9 @@ struct RegistrationSetupView: View {
tournament.enableOnlineRegistration = enableOnlineRegistration tournament.enableOnlineRegistration = enableOnlineRegistration
tournament.isTemplate = isTemplate tournament.isTemplate = isTemplate
tournament.isCorporateTournament = isCorporateTournament tournament.isCorporateTournament = isCorporateTournament
tournament.unregisterDeltaInHours = unregisterDeltaInHours
var shouldDismiss = true
if enableOnlineRegistration { if enableOnlineRegistration {
tournament.accountIsRequired = userAccountIsRequired tournament.accountIsRequired = userAccountIsRequired
tournament.licenseIsRequired = licenseIsRequired tournament.licenseIsRequired = licenseIsRequired
@ -573,6 +641,12 @@ struct RegistrationSetupView: View {
tournament.stripeAccountId = stripeAccountId tournament.stripeAccountId = stripeAccountId
} else { } else {
tournament.stripeAccountId = nil tournament.stripeAccountId = nil
if enableOnlinePayment, isCorporateTournament == false, dataStore.user.registrationPaymentMode.requiresStripe() {
enableOnlinePayment = false
tournament.enableOnlinePayment = false
shouldDismiss = false
}
} }
} else { } else {
tournament.accountIsRequired = true tournament.accountIsRequired = true
@ -610,8 +684,11 @@ struct RegistrationSetupView: View {
} }
self.dataStore.tournaments.addOrUpdate(instance: tournament) self.dataStore.tournaments.addOrUpdate(instance: tournament)
if shouldDismiss {
dismiss() dismiss()
} else {
presentErrorAlert = true
}
} }
} }

@ -30,7 +30,7 @@ enum TournamentSettings: Identifiable, Selectable, Equatable {
case .general: case .general:
return "Général" return "Général"
case .club: case .club:
return "Terrains" return "Pistes"
case .tournamentType: case .tournamentType:
return "Type" return "Type"
} }

@ -26,10 +26,11 @@ extension Product {
return StoreItem(rawValue: self.id)! return StoreItem(rawValue: self.id)!
} }
var formattedPrice: String { var formattedPrice: String {
let ttcPrice = "\(self.displayPrice) TTC"
if let period = self.subscription?.subscriptionPeriod { if let period = self.subscription?.subscriptionPeriod {
return self.displayPrice + " / " + period.unit.label return "\(ttcPrice) / \(period.unit.label)"
} }
return self.displayPrice return ttcPrice
} }
} }
@ -101,6 +102,7 @@ class SubscriptionModel: ObservableObject, StoreDelegate {
} else { } else {
self.totalPrice = product.displayPrice self.totalPrice = product.displayPrice
} }
self.totalPrice += " TTC"
} else { } else {
self.totalPrice = "" self.totalPrice = ""
} }

@ -78,7 +78,7 @@ struct TournamentInitView: View {
Text(tournament.localizedTournamentType()) Text(tournament.localizedTournamentType())
} label: { } label: {
Text("Réglages du tournoi") Text("Réglages du tournoi")
Text("Formats, terrains, prix et plus") Text("Formats, pistes, prix et plus")
} }
} }

@ -261,6 +261,10 @@ struct TournamentView: View {
LabelStructure() LabelStructure()
} }
NavigationLink(value: Screen.cashier) {
Text(tournament.isFree() ? "Présence" : "Encaissement")
}
NavigationLink(value: Screen.rankings) { NavigationLink(value: Screen.rankings) {
LabeledContent { LabeledContent {
if tournament.publishRankings == false { if tournament.publishRankings == false {

@ -236,9 +236,7 @@ final class ServerDataTests: XCTestCase {
let rounds: [Round] = try await StoreCenter.main.service().get() let rounds: [Round] = try await StoreCenter.main.service().get()
let parentRoundId = rounds.first?.id let parentRoundId = rounds.first?.id
let round = Round(tournament: tournamentId, index: 1, parent: parentRoundId, matchFormat: MatchFormat.nineGames, startDate: Date(), groupStageLoserBracket: false, loserBracketMode: .manual, let round = Round(tournament: tournamentId, index: 1, parent: parentRoundId, format: MatchFormat.nineGames, startDate: Date(), groupStageLoserBracket: false, loserBracketMode: .manual, plannedStartDate: Date())
plannedStartDate: Date()
)
round.storeId = "abc" round.storeId = "abc"
if let r: Round = try await StoreCenter.main.service().post(round) { if let r: Round = try await StoreCenter.main.service().post(round) {
@ -270,7 +268,7 @@ final class ServerDataTests: XCTestCase {
return return
} }
let teamRegistration = TeamRegistration(tournament: tournamentId, groupStage: groupStageId, registrationDate: Date(), callDate: Date(), bracketPosition: 1, groupStagePosition: 2, comment: "comment", source: "source", sourceValue: "source V", logo: "logo", name: "Stax", walkOut: true, wildCardBracket: true, wildCardGroupStage: true, weight: 1, lockedWeight: 11, confirmationDate: Date(), qualified: true) let teamRegistration = TeamRegistration(tournament: tournamentId, groupStage: groupStageId, registrationDate: Date(), callDate: Date(), bracketPosition: 1, groupStagePosition: 2, comment: "comment", source: "source", sourceValue: "source V", logo: "logo", name: "Stax", walkOut: true, wildCardBracket: true, wildCardGroupStage: true, weight: 1, lockedWeight: 11, confirmationDate: Date(), qualified: true, finalRanking: 100, pointsEarned: 10, uniqueRandomIndex: 1)
teamRegistration.storeId = "123" teamRegistration.storeId = "123"
if let tr: TeamRegistration = try await StoreCenter.main.service().post(teamRegistration) { if let tr: TeamRegistration = try await StoreCenter.main.service().post(teamRegistration) {
@ -297,6 +295,7 @@ final class ServerDataTests: XCTestCase {
assert(tr.qualified == teamRegistration.qualified) assert(tr.qualified == teamRegistration.qualified)
assert(tr.finalRanking == teamRegistration.finalRanking) assert(tr.finalRanking == teamRegistration.finalRanking)
assert(tr.pointsEarned == teamRegistration.pointsEarned) assert(tr.pointsEarned == teamRegistration.pointsEarned)
assert(tr.uniqueRandomIndex == teamRegistration.uniqueRandomIndex)
} else { } else {
XCTFail("missing data") XCTFail("missing data")
} }

Loading…
Cancel
Save