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

@ -81,6 +81,11 @@ struct FederalTournament: Identifiable, Codable {
var dateFin, dateValidation: Date?
var codePostalEngagement, codeClub: String?
var prixEspece: Int?
var japPhoneNumber: String?
mutating func updateJapPhoneNumber(phone: String?) {
self.japPhoneNumber = phone
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
@ -249,7 +254,7 @@ struct FederalTournament: Identifiable, Codable {
}
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 {

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

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

@ -1,5 +1,5 @@
<ul class="round">
<li class="spacer">
<li class="spacer" style="transform: translateY(-20px);">
&nbsp;{{roundLabel}}
<div>{{formatLabel}}</div>
</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}}
<div class="match-description-overlay" style="visibility:{{hidden}};">{{matchDescriptionTop}}</div>
</li>
<li class="game game-spacer" style="visibility:{{hidden}}"><div class="multiline">{{matchDescription}}</div></li>
<li class="game game-bottom {{entrantTwoWon}}" style="visibility:{{hidden}}">
{{entrantTwo}}
<li class="game game-spacer" style="visibility:{{hidden}}">
</li>
<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 class="spacer">&nbsp;</li>

@ -92,11 +92,32 @@
overflow: hidden;
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>
</head>
<body>
<h3>{{tournamentTitle}} - {{tournamentStartDate}}</h3>
<h3 style="visibility:{{titleHidden}}">{{tournamentTitle}} - {{tournamentStartDate}}</h3>
<main id="tournament">
{{brackets}}
</main>

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

@ -176,7 +176,7 @@ class HtmlGenerator: ObservableObject {
func generateLoserBracketHtml(upperRound: Round) -> String {
//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? {

@ -12,7 +12,7 @@ enum HtmlService {
case template(tournament: Tournament)
case bracket(round: Round)
case loserBracket(upperRound: Round)
case loserBracket(upperRound: Round, hideTitle: Bool)
case match(match: Match)
case player(entrant: TeamRegistration)
case hiddenPlayer
@ -187,11 +187,17 @@ enum HtmlService {
var template = html
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))
if withScore, let top = match.topPreviousRoundMatch(), top.hasEnded() {
template = template.replacingOccurrences(of: "{{matchDescriptionTop}}", with: [top.scoreLabel(winnerFirst:true)].compactMap({ $0 }).joined(separator: "\n"))
}
} else {
template = template.replacingOccurrences(of: "{{entrantOne}}", with: HtmlService.hiddenPlayer.html(headName: headName, withRank: withRank, withTeamIndex: withTeamIndex, withScore: withScore))
}
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))
if withScore, let bottom = match.bottomPreviousRoundMatch(), bottom.hasEnded() {
template = template.replacingOccurrences(of: "{{matchDescriptionBottom}}", with: [bottom.scoreLabel(winnerFirst:true)].compactMap({ $0 }).joined(separator: "\n"))
}
} else {
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 {
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
case .bracket(let round):
var template = ""
@ -216,16 +223,31 @@ enum HtmlService {
for (_, match) in round._matches().enumerated() {
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 = bracket.replacingOccurrences(of: "{{roundLabel}}", with: round.roundTitle())
bracket = bracket.replacingOccurrences(of: "{{formatLabel}}", with: round.matchFormat.formatTitle())
return bracket
case .loserBracket(let upperRound):
case .loserBracket(let upperRound, let hideTitle):
var template = html
template = template.replacingOccurrences(of: "{{minHeight}}", with: withTeamIndex ? "226" : "156")
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 = ""
for round in upperRound.loserRounds() {
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 winner = """
@ -240,6 +262,14 @@ enum HtmlService {
brackets = brackets.appending(winner)
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
case .template(let tournament):
var template = html

@ -10,12 +10,16 @@ import LeStorage
class StripeValidationService {
static func validateStripeAccountID(_ accountID: String) async throws -> ValidationResponse {
let service = try StoreCenter.main.service()
// MARK: - Validate Stripe Account
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)
let body = ["account_id": accountID]
urlRequest.httpBody = try JSONEncoder().encode(body)
var body: [String: Any] = [:]
body["account_id"] = accountId
urlRequest.httpBody = try JSONSerialization.data(withJSONObject: body)
do {
let (data, response) = try await URLSession.shared.data(for: urlRequest)
@ -23,17 +27,79 @@ class StripeValidationService {
guard let httpResponse = response as? HTTPURLResponse else {
throw ValidationError.invalidResponse
}
switch httpResponse.statusCode {
case 200...299:
let decodedResponse = try JSONDecoder().decode(ValidationResponse.self, from: data)
return decodedResponse
case 400:
// Handle bad request
case 400, 403, 404:
// Handle client errors - still decode as ValidationResponse
let errorResponse = try JSONDecoder().decode(ValidationResponse.self, from: data)
return errorResponse
case 403:
// Handle permission error
let errorResponse = try JSONDecoder().decode(ValidationResponse.self, from: data)
default:
throw ValidationError.invalidResponse
}
} 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
default:
throw ValidationError.invalidResponse
@ -46,17 +112,67 @@ class StripeValidationService {
}
}
// MARK: - Response Models
struct ValidationResponse: Codable {
let valid: Bool
let canProcessPayments: Bool?
let onboardingComplete: Bool?
let needsOnboarding: Bool?
let account: AccountDetails?
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 {
let id: String
let chargesEnabled: Bool?
let payoutsEnabled: Bool?
let detailsSubmitted: Bool?
enum CodingKeys: String, CodingKey {
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 networkError(Error)
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 {
let selectedPlayers = tournament.selectedPlayers()
ForEach(PlayerPaymentType.allCases) { type in
let count = selectedPlayers.filter({ $0.paymentType == type }).count
if count > 0 {
let players = selectedPlayers.filter({ $0.paymentType == type })
if players.count > 0 {
LabeledContent {
if let entryFee = tournament.entryFee {
let sum = Double(count) * entryFee
Text(sum.formatted(.currency(code: Locale.defaultCurrency())))
}
let sum = players.compactMap({ $0.paidAmount(tournament) }).reduce(0.0, +)
Text(sum.formatted(.currency(code: Locale.defaultCurrency())))
} label: {
Text(type.localizedLabel())
Text(count.formatted())
Text(players.count.formatted())
}
}
}

@ -13,25 +13,50 @@ struct CashierSettingsView: View {
@EnvironmentObject var dataStore: DataStore
@State private var entryFee: Double? = nil
@State private var clubMemberFeeDeduction: Double? = nil
@Bindable var tournament: Tournament
@FocusState private var focusedField: Tournament.CodingKeys?
let priceTags: [Double] = [15.0, 20.0, 25.0]
let deductionTags: [Double] = [5.0, 10.0]
init(tournament: Tournament) {
self.tournament = tournament
_entryFee = State(wrappedValue: tournament.entryFee)
_clubMemberFeeDeduction = State(wrappedValue: tournament.clubMemberFeeDeduction)
}
var body: some View {
List {
Section {
TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $entryFee, format: .currency(code: Locale.defaultCurrency()))
.keyboardType(.decimalPad)
.multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity)
.focused($focusedField, equals: ._entryFee)
LabeledContent {
TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $entryFee, format: .currency(code: Locale.defaultCurrency()))
.keyboardType(.decimalPad)
.multilineTextAlignment(.trailing)
.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: {
Text("Prix de l'inscription")
Text("Frais d'inscription")
} 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.")
}
@ -104,27 +129,43 @@ struct CashierSettingsView: View {
ToolbarItem(placement: .keyboard) {
HStack {
if tournament.isFree() {
ForEach(priceTags, id: \.self) { priceTag in
Button(priceTag.formatted(.currency(code: Locale.defaultCurrency()))) {
entryFee = priceTag
tournament.entryFee = priceTag
if focusedField == ._entryFee {
if tournament.isFree() {
ForEach(priceTags, id: \.self) { priceTag in
Button(priceTag.formatted(.currency(code: Locale.defaultCurrency()))) {
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
}
.buttonStyle(.bordered)
}
} else {
Button("Gratuit") {
entryFee = nil
tournament.entryFee = nil
clubMemberFeeDeduction = entryFee
tournament.clubMemberFeeDeduction = clubMemberFeeDeduction
focusedField = nil
}
.buttonStyle(.bordered)
}
Spacer()
Button("Valider") {
tournament.entryFee = entryFee
focusedField = nil
}
.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()
}
}

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

@ -15,6 +15,7 @@ struct EventSettingsView: View {
@State private var eventName: String = ""
@State private var pageLink: PageLink = .teams
@State private var tournamentInformation: String = ""
@State private var eventStartDate: Date
@FocusState private var focusedField: Tournament.CodingKeys?
func eventLinksPasteData() -> String {
@ -22,6 +23,20 @@ struct EventSettingsView: View {
var link = [String]()
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("Retrouvez toutes les infos en suivant le\(tournaments.count.pluralSuffix) lien\(tournaments.count.pluralSuffix) ci-dessous :")
link.append("\n\n")
@ -43,6 +58,7 @@ struct EventSettingsView: View {
init(event: Event) {
self.event = event
_eventName = State(wrappedValue: event.name ?? "")
_eventStartDate = .init(wrappedValue: event.eventStartDate())
_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 {
Section {
RowButtonView("Répartir les tournois") {
@ -99,7 +128,9 @@ struct EventSettingsView: View {
} footer: {
Text("Ce texte sera indiqué dans le champ information de tous les tournois de l'événement")
}
_message(eventPasteMessage: _eventPasteMessage())
if event.club != nil {
let eventLinksPasteData = eventLinksPasteData()
Section {
@ -135,12 +166,8 @@ struct EventSettingsView: View {
})
.toolbarBackground(.visible, for: .navigationBar)
.toolbar {
if let tenupId = event.tenupId {
ToolbarItem(placement: .topBarTrailing) {
Link(destination: URL(string:"https://tenup.fft.fr/tournoi/\(tenupId)")!) {
Text("Tenup")
}
}
ToolbarItem(placement: .topBarTrailing) {
_linkLabel()
}
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() {
dataStore.events.addOrUpdate(instance: event)
}

@ -44,19 +44,68 @@ struct EventTournamentsView: View {
}
} footer: {
if event.tournaments.count > 1 {
if mainTournament == nil {
FooterButtonView("c'est le tournoi principal") {
self.mainTournament = tournament
}
} else if mainTournament == tournament {
FooterButtonView("ce n'est pas le tournoi principal") {
self.mainTournament = tournament
if let mainTournament, mainTournament == tournament {
Menu {
Button("Formats") {
tournaments.forEach { tournament in
tournament.groupStageMatchFormat = mainTournament.groupStageMatchFormat
tournament.loserBracketMatchFormat = mainTournament.loserBracketMatchFormat
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") {
tournament.setupUmpireSettings(defaultTournament: mainTournament)
tournament.setupRegistrationSettings(templateTournament: mainTournament)
dataStore.tournaments.addOrUpdate(instance: tournament)
} else {
Menu {
if tournament != self.mainTournament {
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 tournaments(Event)
case cashier
case eventPlanning
var id: String {
return String(describing: self)
@ -33,6 +34,8 @@ enum EventDestination: Identifiable, Selectable, Equatable {
return "Tournois"
case .cashier:
return "Finance"
case .eventPlanning:
return "Planning"
}
}
@ -42,7 +45,7 @@ enum EventDestination: Identifiable, Selectable, Equatable {
return nil
case .tournaments(let event):
return event.tournaments.count
case .cashier:
case .cashier, .eventPlanning:
return nil
}
}
@ -77,7 +80,7 @@ struct EventView: View {
}
func allDestinations() -> [EventDestination] {
[.club(event), .tournaments(event), .cashier]
[.club(event), .eventPlanning, .tournaments(event), .cashier]
}
var body: some View {
@ -90,6 +93,10 @@ struct EventView: View {
switch selectedEventDestination {
case .club(let event):
EventClubSettingsView(event: event)
case .eventPlanning:
let allMatches = event.tournaments.flatMap { $0.allMatches() }
PlanningView(matches: allMatches, selectedScheduleDestination: .constant(nil))
.environment(\.matchViewStyle, .feedStyle)
case .links:
EventLinksView(event: event)
case .tournaments(let event):

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

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

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

@ -64,7 +64,7 @@ struct GroupStageSettingsView: View {
}
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) {
groupStage.matches().forEach { match in
match.setCourt(courtIndex)
@ -179,7 +179,7 @@ struct GroupStageSettingsView: View {
}
Section {
RowButtonView("Rafraichir", role: .destructive) {
RowButtonView("Rafraîchir", role: .destructive) {
let playedMatches = groupStage.playedMatches()
playedMatches.forEach { match in
match.updateTeamScores()

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

@ -78,7 +78,7 @@ struct GroupStagesSettingsView: View {
Section {
if tournament.groupStageLoserBracket() == nil {
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 {
try tournamentStore?.rounds.addOrUpdate(instance: round)

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

@ -64,7 +64,7 @@ struct MatchSetupView: View {
}
HStack {
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())
if walkOutSpot || team.bracketPosition != nil || matchTypeContext == .loserBracket {
match.setLuckyLoser(team: team, teamPosition: teamPosition)

@ -133,6 +133,9 @@ struct TournamentLookUpView: View {
Menu {
#if DEBUG
if tournaments.isEmpty == false {
Button("Gather Mobile Phone") {
_gatherNumbers()
}
Section {
ShareLink(item: pastedTournaments) {
Label("Par texte", systemImage: "square.and.arrow.up")
@ -180,6 +183,20 @@ struct TournamentLookUpView: View {
private var liguesFound: [String] {
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() {
dataStore.appSettingsStorage.write()

@ -58,9 +58,9 @@ enum OngoingDestination: Int, CaseIterable, Identifiable, Selectable, Equatable
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."))
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:
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:
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:
return "À suivre"
case .court:
return "Terrains"
return "Pistes"
case .free:
return "Libres"
case .over:

@ -89,7 +89,7 @@ struct OngoingCourtView: View {
List {
ForEach(filterMode.sortedCourtIndex, id: \.self) { index in
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) {
Section {
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)
.environment(\.editMode, Binding.constant(EditMode.active))
}

@ -99,9 +99,9 @@ struct CourtAvailabilitySettingsView: View {
.overlay {
if courtsUnavailability.isEmpty {
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: {
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: {
RowButtonView("Ajouter une indisponibilité", systemImage: "plus.circle.fill") {
showingPopover = true
@ -175,7 +175,7 @@ struct CourtAvailabilityEditorView: View {
NavigationStack {
Form {
Section {
CourtPicker(title: "Terrain", selection: $courtIndex, maxCourt: tournament.courtCount)
CourtPicker(title: "Piste", selection: $courtIndex, maxCourt: tournament.courtCount)
}
Section {
@ -256,13 +256,27 @@ struct CourtAvailabilityEditorView: View {
struct DateAdjusterView: View {
@Binding var date: Date
var time: Int?
var matchFormat: MatchFormat?
var body: some View {
HStack {
_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)
HStack(spacing: 4) {
if let matchFormat {
_createButton(label: "-\(matchFormat.defaultEstimatedDuration)m", timeOffset: -matchFormat.defaultEstimatedDuration, component: .minute)
_createButton(label: "+\(matchFormat.defaultEstimatedDuration)m", timeOffset: +matchFormat.defaultEstimatedDuration, component: .minute)
_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)
}
@ -272,6 +286,9 @@ struct DateAdjusterView: View {
date = Calendar.current.date(byAdding: component, value: timeOffset, to: date) ?? date
}) {
Text(label)
.lineLimit(1)
.font(.footnote)
.underline()
.frame(maxWidth: .infinity) // Make buttons take equal space
}
.buttonStyle(.borderedProminent)

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

@ -63,7 +63,7 @@ struct PlanningSettingsView: View {
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() {
NavigationLink {
@ -85,7 +85,7 @@ struct PlanningSettingsView: View {
LabeledContent {
Text(matchScheduler.courtsAvailable.count.formatted() + "/" + tournament.courtCount.formatted())
} label: {
Text("Sélection des terrains")
Text("Sélection des pistes")
if matchScheduler.courtsAvailable.count > tournament.courtCount {
Text("Attention !")
.tint(.red)
@ -97,7 +97,7 @@ struct PlanningSettingsView: View {
if tournament.courtCount < club.courtCount {
let plural = tournament.courtCount.pluralSuffix
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 {
let isCreatedByUser = club.hasBeenCreated(by: StoreCenter.main.userId)
Button {
@ -109,10 +109,10 @@ struct PlanningSettingsView: View {
}
} label: {
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)
} 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)
@ -122,7 +122,7 @@ struct PlanningSettingsView: View {
}
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)
}
@ -141,7 +141,7 @@ struct PlanningSettingsView: View {
}
} footer: {
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 {
Toggle(isOn: $matchScheduler.randomizeCourts) {
Text("Distribuer les terrains au hasard")
Text("Distribuer les pistes au hasard")
}
}
Section {
Toggle(isOn: $matchScheduler.shouldTryToFillUpCourtsAvailable) {
Text("Remplir au maximum les terrains d'une rotation")
Text("Remplir au maximum les pistes d'une rotation")
}
} footer: {
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")
}
} 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 {
@ -462,6 +462,15 @@ struct PlanningSettingsView: View {
} header: {
Text("Classement")
}
Section {
Toggle(isOn: $matchScheduler.accountGroupStageBreakTime) {
Text("Tenir compte des temps de pause réglementaires")
}
} header: {
Text("Poule")
}
Section {
Toggle(isOn: $matchScheduler.rotationDifferenceIsImportant) {
@ -469,18 +478,27 @@ struct PlanningSettingsView: View {
}
LabeledContent {
StepperView(count: $matchScheduler.upperBracketRotationDifference, minimum: 0, maximum: 2)
StepperView(count: $matchScheduler.upperBracketRotationDifference, minimum: 0)
} label: {
Text("Tableau")
}
.disabled(matchScheduler.rotationDifferenceIsImportant == false)
LabeledContent {
StepperView(count: $matchScheduler.loserBracketRotationDifference, minimum: 0, maximum: 2)
StepperView(count: $matchScheduler.loserBracketRotationDifference, minimum: 0)
} label: {
Text("Classement")
}
.disabled(matchScheduler.rotationDifferenceIsImportant == false)
LabeledContent {
StepperView(count: $matchScheduler.groupStageRotationDifference, minimum: 0)
} label: {
Text("Poule")
}
.disabled(matchScheduler.rotationDifferenceIsImportant == false)
} footer: {
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]] {
var matchesByDay = [Date: [Match]]()
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
tournament.groupMatchesByDay(matches: matches)
}
private func _matchCountPerDay(matchesByDay: [Date: [Match]], tournament: Tournament) -> [Date: NSCountedSet] {
let days = matchesByDay.keys
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
tournament.matchCountPerDay(matchesByDay: matchesByDay)
}
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("Capitaine", isOn: $player.captain).disabled(player.hasPaidOnline())
//Toggle("Coach", isOn: $player.coach)
Toggle(isOn: $player.clubMember) {
Text("Membre du club")
}
}
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()
}
.onChange(of: player.sex) {

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

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

@ -27,6 +27,10 @@ struct RoundView: View {
var upperRound: UpperRound
func _refreshRound() {
self.upperRound.playedMatches = self.upperRound.round.playedMatches()
}
init(upperRound: UpperRound) {
self.upperRound = upperRound
// let seeds = upperRound.round.seeds()
@ -135,6 +139,22 @@ struct RoundView: View {
}
}
} 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 availableSeeds = tournament.availableSeeds()
let availableQualifiedTeams = tournament.availableQualifiedTeams()
@ -252,6 +272,9 @@ struct RoundView: View {
}
}
}
.onAppear(perform: {
self._refreshRound()
})
.task {
await MainActor.run {
let seeds = self.upperRound.round.seeds()

@ -69,7 +69,7 @@ struct FollowUpMatchView: View {
case .index:
return "Ordre prévu"
case .court:
return "Terrain"
return "Piste"
case .restingTime:
return "Temps de repos"
case .winner:
@ -285,7 +285,7 @@ struct FollowUpMatchView: View {
}
}
} label: {
Text("Sur le terrain")
Text("Sur la piste")
}
.labelsHidden()
.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
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
- Aucun remboursement n'est possible après cette date limite
- Les remboursements sont automatiquement traités via la même méthode de paiement utilisée
- Aucun remboursement en ligne n'est possible après cette date limite
- Les remboursements en ligne sont automatiquement traités via la même méthode de paiement utilisée
Commissions et frais :
- Padel Club prélève une commission de \(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)
- Le montant total des frais est indiqué clairement avant validation du paiement
Exigences pour les organisateurs :
- L'organisateur doit avoir un compte Stripe valide pour recevoir les paiements
- Le compte Stripe doit être vérifié et connecté à Padel Club
- Sans compte Stripe connecté, l'option de paiement en ligne ne peut pas être activée
- Les fonds sont directement versés sur le compte bancaire associé au compte Stripe de l'organisateur
Sécurité :
- Toutes les transactions sont sécurisées et chiffrées
- Padel Club ne stocke pas les informations de carte bancaire
- La conformité RGPD et PCI-DSS est assurée par Stripe
En cas de problème avec un paiement, veuillez contacter l'organisateur du tournoi ou le support Padel Club.
"""
}

@ -29,6 +29,8 @@ struct EditingTeamView: View {
@State private var isProcessingRefund = false
@State private var refundMessage: String?
@State private var registrationDateModified: Date
@State private var uniqueRandomIndex: Int
var messageSentFailed: Binding<Bool> {
Binding {
@ -48,6 +50,7 @@ struct EditingTeamView: View {
return
registrationDate != team.registrationDate
|| uniqueRandomIndex != team.uniqueRandomIndex
|| walkOut != team.walkOut
|| wildCardBracket != team.wildCardBracket
|| wildCardGroupStage != team.wildCardGroupStage
@ -69,6 +72,7 @@ struct EditingTeamView: View {
_walkOut = State(wrappedValue: team.walkOut)
_wildCardBracket = State(wrappedValue: team.wildCardBracket)
_wildCardGroupStage = State(wrappedValue: team.wildCardGroupStage)
_uniqueRandomIndex = .init(wrappedValue: team.uniqueRandomIndex)
}
private func _resetTeam() {
@ -77,6 +81,7 @@ struct EditingTeamView: View {
team.wildCardGroupStage = false
team.walkOut = false
team.wildCardBracket = false
team.uniqueRandomIndex = 0
}
var body: some View {
@ -96,12 +101,22 @@ struct EditingTeamView: View {
HStack {
CopyPasteButtonView(pasteValue: team.playersPasteData())
Spacer()
NavigationLink {
GroupStageTeamReplacementView(team: team)
.environment(tournament)
} label: {
Text("Chercher à remplacer")
.underline()
if team.isWildCard(), team.unsortedPlayers().isEmpty {
TeamPickerView(pickTypeContext: .wildcard) { teamregistration in
teamregistration.wildCardBracket = team.wildCardBracket
teamregistration.wildCardGroupStage = team.wildCardGroupStage
tournament.tournamentStore?.teamRegistrations.addOrUpdate(instance: teamregistration)
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) {
if registrationDate != registrationDateModified {
HStack {
FooterButtonView("Valider") {
Button("Valider", systemImage: "checkmark.circle") {
registrationDate = registrationDateModified
}
.tint(.green)
Divider()
FooterButtonView("Annuler", role: .cancel) {
Button("Annuler", systemImage: "xmark.circle", role: .cancel) {
registrationDateModified = registrationDate
}
.foregroundStyle(.blue)
.tint(.logoRed)
}
.labelStyle(.iconOnly)
.buttonStyle(.borderedProminent)
} else {
Text("Inscription")
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 {
HStack {
TextField("Nom de l'équipe", text: $name)
@ -287,6 +315,7 @@ struct EditingTeamView: View {
team.wildCardBracket = wildCardBracket
team.wildCardGroupStage = wildCardGroupStage
team.walkOut = walkOut
team.uniqueRandomIndex = uniqueRandomIndex
_save()
}
@ -295,6 +324,7 @@ struct EditingTeamView: View {
walkOut = team.walkOut
wildCardBracket = team.wildCardBracket
wildCardGroupStage = team.wildCardGroupStage
uniqueRandomIndex = team.uniqueRandomIndex
}
}, 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.")
@ -394,6 +424,12 @@ struct EditingTeamView: View {
}
}
}
.onChange(of: uniqueRandomIndex) {
if canSaveWithoutWarning() {
team.uniqueRandomIndex = uniqueRandomIndex
_save()
}
}
.onChange(of: [walkOut, wildCardBracket, wildCardGroupStage]) {
if canSaveWithoutWarning() {
if walkOut == false && team.walkOut == true {

@ -8,6 +8,13 @@
import SwiftUI
import PadelClubData
public enum TeamPickType: String {
case bracket = "bracket"
case groupStage = "groupStage"
case loserBracket = "loserBracket"
case wildcard = "wildcard"
}
struct TeamPickerView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
@ -20,7 +27,7 @@ struct TeamPickerView: View {
var shouldConfirm: Bool = false
var groupStagePosition: Int? = nil
var round: Round? = nil
var matchTypeContext: MatchType = .bracket
var pickTypeContext: TeamPickType = .bracket
var luckyLosers: [TeamRegistration] = []
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 {
ConfirmButtonView(shouldConfirm: shouldConfirm, message: MatchSetupView.confirmationMessage) {
presentTeamPickerView = true
} label: {
Text("Choisir")
Text(wording)
.underline()
}
.sheet(isPresented: $presentTeamPickerView) {
NavigationStack {
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")
}
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")
}
@ -63,7 +83,11 @@ struct TeamPickerView: View {
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")
}
@ -145,7 +169,7 @@ struct TeamPickerView: View {
.frame(maxWidth: .infinity)
.buttonStyle(.plain)
.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) {
// Button("Retirer du tableau", role: .destructive) {
// teamPicked(confirmTeam!)

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

@ -321,6 +321,9 @@ struct BroadcastView: View {
Section {
let club = tournament.club()
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)
} header: {
Text("Autres liens")

@ -22,7 +22,7 @@ struct TournamentClubSettingsView: View {
let selectedClub = event?.clubObject()
Section {
TournamentFieldsManagerView(localizedStringKey: "Terrains pour le tournoi", count: $tournament.courtCount)
TournamentFieldsManagerView(localizedStringKey: "Pistes pour le tournoi", count: $tournament.courtCount)
.onChange(of: tournament.courtCount) {
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
@ -36,7 +36,7 @@ struct TournamentClubSettingsView: View {
CourtAvailabilitySettingsView(event: event)
.environment(tournament)
} label: {
Text("Indisponibilités des terrains")
Text("Indisponibilités des pistes")
}
}
} footer: {
@ -44,7 +44,7 @@ struct TournamentClubSettingsView: View {
if tournament.courtCount < club.courtCount {
let plural = tournament.courtCount.pluralSuffix
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 {
let isCreatedByUser = club.hasBeenCreated(by: StoreCenter.main.userId)
Button {
@ -56,10 +56,10 @@ struct TournamentClubSettingsView: View {
}
} label: {
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)
} 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)

@ -16,6 +16,7 @@ struct TournamentGeneralSettingsView: View {
@State private var tournamentName: String = ""
@State private var tournamentInformation: String = ""
@State private var entryFee: Double? = nil
@State private var clubMemberFeeDeduction: Double? = nil
@State private var umpireCustomMail: String
@State private var umpireCustomPhone: String
@State private var umpireCustomContact: String
@ -24,12 +25,14 @@ struct TournamentGeneralSettingsView: View {
@FocusState private var focusedField: Tournament.CodingKeys?
let priceTags: [Double] = [15.0, 20.0, 25.0]
let deductionTags: [Double] = [5.0, 10.0]
init(tournament: Tournament) {
self.tournament = tournament
_tournamentName = State(wrappedValue: tournament.name ?? "")
_tournamentInformation = State(wrappedValue: tournament.information ?? "")
_entryFee = State(wrappedValue: tournament.entryFee)
_clubMemberFeeDeduction = State(wrappedValue: tournament.clubMemberFeeDeduction)
_umpireCustomMail = State(wrappedValue: tournament.umpireCustomMail ?? "")
_umpireCustomPhone = State(wrappedValue: tournament.umpireCustomPhone ?? "")
_umpireCustomContact = State(wrappedValue: tournament.umpireCustomContact ?? "")
@ -58,6 +61,24 @@ struct TournamentGeneralSettingsView: View {
} label: {
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: {
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)
}
} 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 {
if focusedField == ._name, tournamentName.isEmpty == false {
Button("Effacer") {
@ -214,7 +250,7 @@ struct TournamentGeneralSettingsView: View {
.onChange(of: tournament.startDate) {
_save()
}
.onChange(of: tournament.entryFee) {
.onChange(of: [tournament.entryFee, tournament.clubMemberFeeDeduction]) {
_save()
}
.onChange(of: [tournament.name, tournament.information, tournament.umpireCustomMail, tournament.umpireCustomPhone, tournament.umpireCustomContact]) {
@ -243,6 +279,8 @@ struct TournamentGeneralSettingsView: View {
}
} else if old == ._entryFee {
tournament.entryFee = entryFee
} else if old == ._clubMemberFeeDeduction {
tournament.clubMemberFeeDeduction = clubMemberFeeDeduction
} else if old == ._umpireCustomMail {
_confirmUmpireMail()
} else if old == ._umpireCustomPhone {
@ -301,11 +339,7 @@ struct TournamentGeneralSettingsView: View {
}
private func _save() {
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
dataStore.tournaments.addOrUpdate(instance: tournament)
}
private func _customUmpireView() -> some View {

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

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

@ -10,6 +10,7 @@ import SwiftUI
import PadelClubData
struct RegistrationSetupView: View {
@Environment(\.openURL) private var openURL
@EnvironmentObject var dataStore: DataStore
@Bindable var tournament: Tournament
@State private var enableOnlineRegistration: Bool
@ -32,6 +33,7 @@ struct RegistrationSetupView: View {
@State private var isTemplate: Bool
@State private var isCorporateTournament: Bool
@State private var isValidating = false
@State private var unregisterDeltaInHours: Int
// Online Payment
@State private var enableOnlinePayment: Bool
@ -40,7 +42,7 @@ struct RegistrationSetupView: View {
@State private var refundDateLimit: Date
@State private var refundDateLimitEnabled: Bool
@State private var stripeAccountId: String
@State private var stripeAccountIdIsInvalid: Bool = false
@State private var stripeAccountIdIsInvalid: Bool?
@State private var paymentConfig: PaymentConfig?
@State private var timeToConfirmConfig: TimeToConfirmConfig?
@ -49,6 +51,10 @@ struct RegistrationSetupView: View {
@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
init(tournament: Tournament) {
@ -56,6 +62,7 @@ struct RegistrationSetupView: View {
_enableOnlineRegistration = .init(wrappedValue: tournament.enableOnlineRegistration)
_isTemplate = .init(wrappedValue: tournament.isTemplate)
_isCorporateTournament = .init(wrappedValue: tournament.isCorporateTournament)
_unregisterDeltaInHours = .init(wrappedValue: tournament.unregisterDeltaInHours)
// Registration Date Limit
if let registrationDateLimit = tournament.registrationDateLimit {
_registrationDateLimit = .init(wrappedValue: registrationDateLimit)
@ -184,8 +191,10 @@ struct RegistrationSetupView: View {
}
Section {
Text("Par défaut, sans date définie, les inscriptions en ligne sont possible dès son activation.")
Toggle(isOn: $openingRegistrationDateEnabled) {
Text("Définir une date")
Text("Définir une date ultérieur")
}
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.")
}
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 {
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.")
@ -304,7 +325,6 @@ struct RegistrationSetupView: View {
ToolbarItem(placement: .topBarTrailing) {
ButtonValidateView(role: .destructive) {
_save()
dismiss()
}
}
}
@ -315,7 +335,7 @@ struct RegistrationSetupView: View {
HStack {
Button("Effacer") {
stripeAccountId = ""
stripeAccountIdIsInvalid = false
stripeAccountIdIsInvalid = nil
tournament.stripeAccountId = nil
}
.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)
.headerProminence(.increased)
.navigationTitle("Inscription en ligne")
@ -399,7 +425,7 @@ struct RegistrationSetupView: View {
}
Toggle(isOn: $enableOnlinePaymentRefund) {
Text("Autoriser les remboursements")
Text("Autoriser les remboursements en ligne")
}
if enableOnlinePaymentRefund {
@ -419,32 +445,6 @@ struct RegistrationSetupView: View {
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: {
Text("Paiement en ligne")
} footer: {
@ -472,13 +472,81 @@ struct RegistrationSetupView: View {
.onChange(of: refundDateLimit) {
_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 {
let fixedFee = RegistrationPaymentMode.stripeFixedFee // Fixed fee in euros
let percentageFee = RegistrationPaymentMode.stripePercentageFee
@ -510,11 +578,10 @@ struct RegistrationSetupView: View {
// Text("Aucune commission Padel Club ne sera prélevée.").foregroundStyle(.logoRed).bold()
}
}
}
private func _confirmStripeAccountId() {
stripeAccountIdIsInvalid = false
stripeAccountIdIsInvalid = nil
if stripeAccountId.isEmpty {
tournament.stripeAccountId = nil
} else if stripeAccountId.count >= 5, stripeAccountId.starts(with: "acct_") {
@ -528,13 +595,12 @@ struct RegistrationSetupView: View {
Task {
isValidating = true
do {
let response = try await StripeValidationService.validateStripeAccountID(accId)
let response = try await StripeValidationService.validateStripeAccount(accountId: accId)
print("validateStripeAccount", response)
stripeAccountId = accId
stripeAccountIdIsInvalid = response.valid == false
enableOnlinePayment = response.valid
stripeAccountIdIsInvalid = response.canProcessPayments == false
} catch {
stripeAccountIdIsInvalid = true
enableOnlinePayment = false
}
isValidating = false
}
@ -550,7 +616,9 @@ struct RegistrationSetupView: View {
tournament.enableOnlineRegistration = enableOnlineRegistration
tournament.isTemplate = isTemplate
tournament.isCorporateTournament = isCorporateTournament
tournament.unregisterDeltaInHours = unregisterDeltaInHours
var shouldDismiss = true
if enableOnlineRegistration {
tournament.accountIsRequired = userAccountIsRequired
tournament.licenseIsRequired = licenseIsRequired
@ -573,6 +641,12 @@ struct RegistrationSetupView: View {
tournament.stripeAccountId = stripeAccountId
} else {
tournament.stripeAccountId = nil
if enableOnlinePayment, isCorporateTournament == false, dataStore.user.registrationPaymentMode.requiresStripe() {
enableOnlinePayment = false
tournament.enableOnlinePayment = false
shouldDismiss = false
}
}
} else {
tournament.accountIsRequired = true
@ -610,8 +684,11 @@ struct RegistrationSetupView: View {
}
self.dataStore.tournaments.addOrUpdate(instance: tournament)
dismiss()
if shouldDismiss {
dismiss()
} else {
presentErrorAlert = true
}
}
}

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

@ -26,10 +26,11 @@ extension Product {
return StoreItem(rawValue: self.id)!
}
var formattedPrice: String {
let ttcPrice = "\(self.displayPrice) TTC"
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 {
self.totalPrice = product.displayPrice
}
self.totalPrice += " TTC"
} else {
self.totalPrice = ""
}

@ -78,7 +78,7 @@ struct TournamentInitView: View {
Text(tournament.localizedTournamentType())
} label: {
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()
}
NavigationLink(value: Screen.cashier) {
Text(tournament.isFree() ? "Présence" : "Encaissement")
}
NavigationLink(value: Screen.rankings) {
LabeledContent {
if tournament.publishRankings == false {

@ -236,9 +236,7 @@ final class ServerDataTests: XCTestCase {
let rounds: [Round] = try await StoreCenter.main.service().get()
let parentRoundId = rounds.first?.id
let round = Round(tournament: tournamentId, index: 1, parent: parentRoundId, matchFormat: MatchFormat.nineGames, startDate: Date(), groupStageLoserBracket: false, loserBracketMode: .manual,
plannedStartDate: Date()
)
let round = Round(tournament: tournamentId, index: 1, parent: parentRoundId, format: MatchFormat.nineGames, startDate: Date(), groupStageLoserBracket: false, loserBracketMode: .manual, plannedStartDate: Date())
round.storeId = "abc"
if let r: Round = try await StoreCenter.main.service().post(round) {
@ -270,7 +268,7 @@ final class ServerDataTests: XCTestCase {
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"
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.finalRanking == teamRegistration.finalRanking)
assert(tr.pointsEarned == teamRegistration.pointsEarned)
assert(tr.uniqueRandomIndex == teamRegistration.uniqueRandomIndex)
} else {
XCTFail("missing data")
}

Loading…
Cancel
Save