Merge branch 'main'

#Conflicts:
#	PadelClub/Views/Tournament/Screen/TableStructureView.swift
sync2
Raz 1 year ago
commit 290aad40a4
  1. 26
      PadelClub.xcodeproj/project.pbxproj
  2. 5
      PadelClub/Data/PlayerRegistration.swift
  3. 32
      PadelClub/Data/Tournament.swift
  4. 4
      PadelClub/Extensions/String+Extensions.swift
  5. 2
      PadelClub/PadelClubApp.swift
  6. 54
      PadelClub/Utils/FileImportManager.swift
  7. 2
      PadelClub/Utils/HtmlGenerator.swift
  8. 15
      PadelClub/Utils/LocationManager.swift
  9. 16
      PadelClub/Utils/PadelRule.swift
  10. 25
      PadelClub/Utils/Tips.swift
  11. 10
      PadelClub/ViewModel/AgendaDestination.swift
  12. 2
      PadelClub/ViewModel/FederalDataViewModel.swift
  13. 33
      PadelClub/ViewModel/Selectable.swift
  14. 10
      PadelClub/Views/Calling/CallMessageCustomizationView.swift
  15. 16
      PadelClub/Views/Cashier/Event/EventSettingsView.swift
  16. 3
      PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift
  17. 10
      PadelClub/Views/Club/ClubDetailView.swift
  18. 12
      PadelClub/Views/Club/CourtView.swift
  19. 4
      PadelClub/Views/Components/GenericDestinationPickerView.swift
  20. 13
      PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift
  21. 62
      PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift
  22. 4
      PadelClub/Views/Navigation/Toolbox/RankCalculatorView.swift
  23. 2
      PadelClub/Views/Planning/PlanningSettingsView.swift
  24. 13
      PadelClub/Views/Player/PlayerDetailView.swift
  25. 2
      PadelClub/Views/Shared/TournamentFilterView.swift
  26. 20
      PadelClub/Views/Team/EditingTeamView.swift
  27. 18
      PadelClub/Views/Tournament/FileImportView.swift
  28. 9
      PadelClub/Views/Tournament/Screen/AddTeamView.swift
  29. 24
      PadelClub/Views/Tournament/Screen/BroadcastView.swift
  30. 44
      PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift
  31. 11
      PadelClub/Views/Tournament/Screen/Components/TournamentLevelPickerView.swift
  32. 17
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  33. 2
      PadelClub/Views/Tournament/Screen/TableStructureView.swift
  34. 3
      PadelClub/Views/Tournament/Screen/TournamentRankView.swift
  35. 2
      PadelClub/Views/Tournament/Shared/TournamentCellView.swift
  36. 4
      PadelClub/Views/Tournament/Subscription/Guard.swift
  37. 2
      PadelClub/Views/Tournament/Subscription/PurchaseListView.swift
  38. 2
      PadelClub/Views/Tournament/TournamentView.swift

@ -743,6 +743,7 @@
FFB1C98B2C10255100B154A7 /* TournamentBroadcastRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */; }; FFB1C98B2C10255100B154A7 /* TournamentBroadcastRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */; };
FFB9C8712BBADDE200A0EF4F /* Selectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB9C8702BBADDE200A0EF4F /* Selectable.swift */; }; FFB9C8712BBADDE200A0EF4F /* Selectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB9C8702BBADDE200A0EF4F /* Selectable.swift */; };
FFB9C8752BBADDF700A0EF4F /* SeedInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */; }; FFB9C8752BBADDF700A0EF4F /* SeedInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */; };
FFBA2D2D2CA2CE9E00D5BBDD /* CodingContainer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C33F752C9B1EC5006316DE /* CodingContainer+Extensions.swift */; };
FFBF065C2BBD2657009D6715 /* GroupStageTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF065B2BBD2657009D6715 /* GroupStageTeamView.swift */; }; FFBF065C2BBD2657009D6715 /* GroupStageTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF065B2BBD2657009D6715 /* GroupStageTeamView.swift */; };
FFBF065E2BBD8040009D6715 /* MatchListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF065D2BBD8040009D6715 /* MatchListView.swift */; }; FFBF065E2BBD8040009D6715 /* MatchListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF065D2BBD8040009D6715 /* MatchListView.swift */; };
FFBF06602BBD9F6D009D6715 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */; }; FFBF06602BBD9F6D009D6715 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */; };
@ -2671,6 +2672,7 @@
FF4CBFF82C996C0600151637 /* TabItemModifier.swift in Sources */, FF4CBFF82C996C0600151637 /* TabItemModifier.swift in Sources */,
FF4CBFF92C996C0600151637 /* DeferredViewModifier.swift in Sources */, FF4CBFF92C996C0600151637 /* DeferredViewModifier.swift in Sources */,
FF4CBFFA2C996C0600151637 /* TournamentScheduleView.swift in Sources */, FF4CBFFA2C996C0600151637 /* TournamentScheduleView.swift in Sources */,
FFBA2D2D2CA2CE9E00D5BBDD /* CodingContainer+Extensions.swift in Sources */,
FF4CBFFB2C996C0600151637 /* MatchFormatStorageView.swift in Sources */, FF4CBFFB2C996C0600151637 /* MatchFormatStorageView.swift in Sources */,
FF4CBFFC2C996C0600151637 /* UmpireView.swift in Sources */, FF4CBFFC2C996C0600151637 /* UmpireView.swift in Sources */,
FF4CBFFD2C996C0600151637 /* User.swift in Sources */, FF4CBFFD2C996C0600151637 /* User.swift in Sources */,
@ -3132,7 +3134,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2; CURRENT_PROJECT_VERSION = 3;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
@ -3155,7 +3157,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.12; MARKETING_VERSION = 1.0.14;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3176,7 +3178,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2; CURRENT_PROJECT_VERSION = 3;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -3198,7 +3200,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.12; MARKETING_VERSION = 1.0.14;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3291,7 +3293,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 4;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
@ -3313,7 +3315,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.10; MARKETING_VERSION = 1.0.14;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3333,7 +3335,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 4;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -3354,7 +3356,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.10; MARKETING_VERSION = 1.0.14;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3375,7 +3377,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
@ -3397,7 +3399,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.9; MARKETING_VERSION = 1.0.14;
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 = "";
@ -3417,7 +3419,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -3438,7 +3440,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.9; MARKETING_VERSION = 1.0.14;
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 = "";

@ -283,6 +283,11 @@ final class PlayerRegistration: ModelObject, Storable {
} }
func setComputedRank(in tournament: Tournament) { func setComputedRank(in tournament: Tournament) {
if tournament.isAnimation() {
computedRank = rank ?? 0
return
}
let currentRank = rank ?? tournament.unrankValue(for: isMalePlayer()) ?? 70_000 let currentRank = rank ?? tournament.unrankValue(for: isMalePlayer()) ?? 70_000
switch tournament.tournamentCategory { switch tournament.tournamentCategory {
case .men: case .men:

@ -1063,6 +1063,11 @@ defer {
registrationDate = previousTeamRegistrationDate registrationDate = previousTeamRegistrationDate
} }
let newTeam = addTeam(team.players, registrationDate: registrationDate, name: team.name) let newTeam = addTeam(team.players, registrationDate: registrationDate, name: team.name)
if isAnimation() {
if newTeam.weight == 0 {
newTeam.weight = team.index(in: teams) ?? 0
}
}
teamsToImport.append(newTeam) teamsToImport.append(newTeam)
} }
} }
@ -1396,10 +1401,14 @@ defer {
} }
func tournamentTitle(_ displayStyle: DisplayStyle = .wide) -> String { func tournamentTitle(_ displayStyle: DisplayStyle = .wide) -> String {
if tournamentLevel == .unlisted, displayStyle == .title, let name { if tournamentLevel == .unlisted, displayStyle == .title {
if let name {
return name return name
} else {
return tournamentLevel.localizedLevelLabel(.title)
} }
let title: String = [tournamentLevel.localizedLabel(displayStyle), tournamentCategory.localizedLabel(displayStyle), federalTournamentAge.localizedLabel(displayStyle)].joined(separator: " ") }
let title: String = [tournamentLevel.localizedLevelLabel(displayStyle), tournamentCategory.localizedLabel(displayStyle), federalTournamentAge.localizedLabel(displayStyle)].filter({ $0.isEmpty == false }).joined(separator: " ")
if displayStyle == .wide, let name { if displayStyle == .wide, let name {
return [title, name].joined(separator: " - ") return [title, name].joined(separator: " - ")
} else { } else {
@ -1410,9 +1419,9 @@ defer {
func localizedTournamentType() -> String { func localizedTournamentType() -> String {
switch tournamentLevel { switch tournamentLevel {
case .unlisted: case .unlisted:
return tournamentLevel.localizedLabel(.short) return tournamentLevel.localizedLevelLabel(.short)
default: default:
return tournamentLevel.localizedLabel(.short) + tournamentCategory.localizedLabel(.short) return tournamentLevel.localizedLevelLabel(.short) + tournamentCategory.localizedLabel(.short)
} }
} }
@ -1430,7 +1439,9 @@ defer {
func formattedDate(_ displayStyle: DisplayStyle = .wide) -> String { func formattedDate(_ displayStyle: DisplayStyle = .wide) -> String {
switch displayStyle { switch displayStyle {
case .wide, .title: case .title:
startDate.formatted(.dateTime.weekday(.abbreviated).day().month(.abbreviated).year())
case .wide:
startDate.formatted(date: Date.FormatStyle.DateStyle.complete, time: Date.FormatStyle.TimeStyle.omitted) startDate.formatted(date: Date.FormatStyle.DateStyle.complete, time: Date.FormatStyle.TimeStyle.omitted)
case .short: case .short:
startDate.formatted(date: .numeric, time: .omitted) startDate.formatted(date: .numeric, time: .omitted)
@ -1811,6 +1822,11 @@ defer {
players.forEach { player in players.forEach { player in
player.teamRegistration = team.id player.teamRegistration = team.id
} }
if isAnimation() {
if team.weight == 0 {
team.weight = unsortedTeams().count
}
}
return team return team
} }
@ -1926,9 +1942,9 @@ defer {
private func _defaultSorting() -> [MySortDescriptor<TeamRegistration>] { private func _defaultSorting() -> [MySortDescriptor<TeamRegistration>] {
switch teamSorting { switch teamSorting {
case .rank: case .rank:
[.keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.registrationDate!)] [.keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.registrationDate!), .keyPath(\TeamRegistration.id)]
case .inscriptionDate: case .inscriptionDate:
[.keyPath(\TeamRegistration.registrationDate!), .keyPath(\TeamRegistration.initialWeight)] [.keyPath(\TeamRegistration.registrationDate!), .keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.id)]
} }
} }
@ -1938,7 +1954,7 @@ defer {
&& federalTournamentAge == build.age && federalTournamentAge == build.age
} }
private let _currentSelectionSorting : [MySortDescriptor<TeamRegistration>] = [.keyPath(\.weight), .keyPath(\.registrationDate!)] private let _currentSelectionSorting : [MySortDescriptor<TeamRegistration>] = [.keyPath(\.weight), .keyPath(\.registrationDate!), .keyPath(\.id)]
private func _matchSchedulers() -> [MatchScheduler] { private func _matchSchedulers() -> [MatchScheduler] {
return self.tournamentStore.matchSchedulers.filter { $0.tournament == self.id } return self.tournamentStore.matchSchedulers.filter { $0.tournament == self.id }

@ -13,6 +13,10 @@ extension String {
return (self.count > length) ? self.prefix(length) + trailing : self return (self.count > length) ? self.prefix(length) + trailing : self
} }
func prefixTrimmed(_ length: Int) -> String {
String(trimmed.prefix(length))
}
var trimmed: String { var trimmed: String {
replaceCharactersFromSet(characterSet: .newlines, replacementString: " ").trimmingCharacters(in: .whitespacesAndNewlines) replaceCharactersFromSet(characterSet: .newlines, replacementString: " ").trimmingCharacters(in: .whitespacesAndNewlines)
} }

@ -100,7 +100,7 @@ print("Running in Release mode")
//try? Tips.resetDatastore() //try? Tips.resetDatastore()
try? Tips.configure([ try? Tips.configure([
.displayFrequency(.immediate), .displayFrequency(.daily),
.datastoreLocation(.applicationDefault) .datastoreLocation(.applicationDefault)
]) ])
} }

@ -434,39 +434,77 @@ class FileImportManager {
let fetchRequest = ImportedPlayer.fetchRequest() let fetchRequest = ImportedPlayer.fetchRequest()
let federalContext = PersistenceController.shared.localContainer.viewContext let federalContext = PersistenceController.shared.localContainer.viewContext
let results: [TeamHolder] = lines.chunked(into: 2).map { team in let results: [TeamHolder] = lines.chunked(byParameterAt: 1).map { team in
var teamName: String? = nil var teamName: String? = nil
let players = team.map { player in let players = team.map { player in
let data = player.components(separatedBy: separator) let data = player.components(separatedBy: separator)
let lastName : String = data[safe: 2]?.trimmed ?? "" let lastName : String = data[safe: 2]?.prefixTrimmed(50) ?? ""
let firstName : String = data[safe: 3]?.trimmed ?? "" let firstName : String = data[safe: 3]?.prefixTrimmed(50) ?? ""
let sex: PlayerRegistration.PlayerSexType = data[safe: 0] == "f" ? PlayerRegistration.PlayerSexType.female : PlayerRegistration.PlayerSexType.male let sex: PlayerRegistration.PlayerSexType = data[safe: 0] == "f" ? PlayerRegistration.PlayerSexType.female : PlayerRegistration.PlayerSexType.male
if data[safe: 1]?.trimmed != nil { if data[safe: 1]?.trimmed != nil {
teamName = data[safe: 1]?.trimmed teamName = data[safe: 1]?.trimmed
} }
let phoneNumber : String? = data[safe: 4]?.trimmed let phoneNumber : String? = data[safe: 4]?.trimmed.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines).prefixTrimmed(50)
let email : String? = data[safe: 5]?.trimmed let email : String? = data[safe: 5]?.prefixTrimmed(50)
let rank : Int? = data[safe: 6]?.trimmed.toInt() let rank : Int? = data[safe: 6]?.trimmed.toInt()
let licenceId : String? = data[safe: 7]?.trimmed let licenceId : String? = data[safe: 7]?.prefixTrimmed(50)
let club : String? = data[safe: 8]?.trimmed let club : String? = data[safe: 8]?.prefixTrimmed(200)
let predicate = NSPredicate(format: "firstName like[cd] %@ && lastName like[cd] %@", firstName, lastName) let predicate = NSPredicate(format: "firstName like[cd] %@ && lastName like[cd] %@", firstName, lastName)
fetchRequest.predicate = predicate fetchRequest.predicate = predicate
let found = try? federalContext.fetch(fetchRequest).first let found = try? federalContext.fetch(fetchRequest).first
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.email = email
player.phoneNumber = phoneNumber
return player return player
} else { } else {
let player = PlayerRegistration(firstName: firstName, lastName: lastName, licenceId: licenceId, rank: rank, sex: sex, clubName: club, phoneNumber: phoneNumber, email: email) let player = PlayerRegistration(firstName: firstName, lastName: lastName, licenceId: licenceId, rank: rank, sex: sex, clubName: club, phoneNumber: phoneNumber, email: email)
if rank == nil, autoSearch { if rank == nil, autoSearch {
player.setComputedRank(in: tournament) player.setComputedRank(in: tournament)
} else {
player.computedRank = rank ?? 0
} }
return player return player
} }
} }
return TeamHolder(players: players, tournamentCategory: .men, tournamentAgeCategory: .senior, previousTeam: nil, name: teamName, tournament: tournament) return TeamHolder(players: players, tournamentCategory: tournament.tournamentCategory, tournamentAgeCategory: tournament.federalTournamentAge, previousTeam: nil, name: teamName, tournament: tournament)
} }
return results return results
} }
} }
extension Array where Element == String {
/// Groups the array of CSV lines based on the same value at the specified column index.
/// If no key is found, it defaults to chunking the array into groups of 2 lines.
/// - Parameter index: The index of the CSV column to group by.
/// - Returns: An array of arrays, where each inner array contains lines grouped by the CSV parameter or by default chunks of 2.
func chunked(byParameterAt index: Int) -> [[String]] {
var groups: [String: [String]] = [:]
for line in self {
let columns = line.split(separator: ";", omittingEmptySubsequences: false).map { String($0) }
if index < columns.count {
let key = columns[index]
if groups[key] == nil {
groups[key] = []
}
groups[key]?.append(line)
} else {
// Handle out-of-bounds by continuing
print("Warning: Index \(index) out of bounds for line: \(line)")
}
}
// If no valid groups found, chunk into groups of 2 lines
if groups.isEmpty {
return self.chunked(into: 2)
} else {
// Append groups by parameter value, converting groups.values into an array of arrays
return groups.map { $0.value }
}
}
}

@ -186,7 +186,7 @@ class HtmlGenerator: ObservableObject {
.day() .day()
.dateSeparator(.dash)) .dateSeparator(.dash))
let name = tournament.tournamentLevel.localizedLabel() + "-" + tournament.tournamentCategory.importingRawValue let name = tournament.tournamentLevel.localizedLevelLabel() + "-" + tournament.tournamentCategory.importingRawValue
return pdfFolderURL.appendingPathComponent(stringDate + "-" + name + ".pdf") return pdfFolderURL.appendingPathComponent(stringDate + "-" + name + ".pdf")
} }

@ -16,7 +16,18 @@ class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
@Published var postalCode: String? @Published var postalCode: String?
@Published var requestStarted: Bool = false @Published var requestStarted: Bool = false
@Published var userReadableCityOrZipcode: String = "" @Published var userReadableCityOrZipcode: String = ""
@Published var lastError: Error? = nil @Published var lastError: LocalizedError? = nil
enum LocationError: LocalizedError {
case unknownError(error: Error)
var errorDescription: String? {
switch self {
case .unknownError(let error):
return "Padel Club n'a pas réussi à vous localiser."
}
}
}
override init() { override init() {
super.init() super.init()
@ -49,7 +60,7 @@ class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("locationManager didFailWithError", error) print("locationManager didFailWithError", error)
requestStarted = false requestStarted = false
self.lastError = error self.lastError = LocationError.unknownError(error: error)
} }
func geocodeCity(cityOrZipcode: String, completion: @escaping (_ placemark: [CLPlacemark]?, _ error: Error?) -> Void) { func geocodeCity(cityOrZipcode: String, completion: @escaping (_ placemark: [CLPlacemark]?, _ error: Error?) -> Void) {

@ -48,7 +48,7 @@ struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identifiable {
} }
var identifier: String { var identifier: String {
level.localizedLabel()+":"+category.localizedLabel()+":"+age.localizedLabel() level.localizedLevelLabel()+":"+category.localizedLabel()+":"+age.localizedLabel()
} }
var computedLabel: String { var computedLabel: String {
@ -57,11 +57,11 @@ struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identifiable {
} }
func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
level.localizedLabel() + category.localizedLabel(.short) level.localizedLevelLabel() + category.localizedLabel(.short)
} }
var localizedTitle: String { var localizedTitle: String {
level.localizedLabel() + " " + category.localizedLabel() level.localizedLevelLabel() + " " + category.localizedLabel()
} }
var localizedAge: String { var localizedAge: String {
@ -72,7 +72,7 @@ struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identifiable {
extension TournamentBuild { extension TournamentBuild {
init?(category: String, level: String, age: FederalTournamentAge = .senior) { init?(category: String, level: String, age: FederalTournamentAge = .senior) {
guard let levelFound = TournamentLevel.allCases.first(where: { $0.localizedLabel() == level }) else { return nil } guard let levelFound = TournamentLevel.allCases.first(where: { $0.localizedLevelLabel() == level }) else { return nil }
var c = category var c = category
if c.hasPrefix("ME") { if c.hasPrefix("ME") {
@ -465,7 +465,7 @@ enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable {
} }
} }
func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { func localizedLevelLabel(_ displayStyle: DisplayStyle = .wide) -> String {
if self == .unlisted { return displayStyle == .title ? "Animation" : "Anim." } if self == .unlisted { return displayStyle == .title ? "Animation" : "Anim." }
return String(describing: self).capitalized return String(describing: self).capitalized
} }
@ -837,7 +837,7 @@ enum TournamentCategory: Int, Hashable, Codable, CaseIterable, Identifiable {
case .men: case .men:
switch displayStyle { switch displayStyle {
case .title: case .title:
return "DH" return "Hommes"
case .wide: case .wide:
return "Hommes" return "Hommes"
case .short: case .short:
@ -846,7 +846,7 @@ enum TournamentCategory: Int, Hashable, Codable, CaseIterable, Identifiable {
case .women: case .women:
switch displayStyle { switch displayStyle {
case .title: case .title:
return "DD" return "Dames"
case .wide: case .wide:
return "Dames" return "Dames"
case .short: case .short:
@ -855,7 +855,7 @@ enum TournamentCategory: Int, Hashable, Codable, CaseIterable, Identifiable {
case .mix: case .mix:
switch displayStyle { switch displayStyle {
case .title: case .title:
return "MX" return "Mixte"
case .wide: case .wide:
return "Mixte" return "Mixte"
case .short: case .short:

@ -430,7 +430,7 @@ struct CreateAccountTip: Tip {
Action(id: ActionKey.createAccount.rawValue, title: "Créer votre compte") Action(id: ActionKey.createAccount.rawValue, title: "Créer votre compte")
//todo //todo
//Action(id: ActionKey.learnMore.rawValue, title: "En savoir plus") //Action(id: ActionKey.learnMore.rawValue, title: "En savoir plus")
Action(id: ActionKey.accessPadelClubWebPage.rawValue, title: "Jeter un oeil au site Padel Club") Action(id: ActionKey.accessPadelClubWebPage.rawValue, title: "Voir le site Padel Club")
} }
enum ActionKey: String { enum ActionKey: String {
@ -549,6 +549,29 @@ struct TeamsExportTip: Tip {
} }
} }
struct PlayerTournamentSearchTip: Tip {
var title: Text {
Text("Cherchez un tournoi autour de vous !")
}
var message: Text? {
Text("Padel Club facilite la recherche de tournois et l'inscription !")
}
var image: Image? {
Image(systemName: "trophy.circle")
}
var actions: [Action] {
Action(id: ActionKey.selectAction.rawValue, title: "Éssayer")
}
enum ActionKey: String {
case selectAction = "selectAction"
}
}
struct TipStyleModifier: ViewModifier { struct TipStyleModifier: ViewModifier {
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
var tint: Color? var tint: Color?

@ -7,6 +7,7 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
import TipKit
enum AgendaDestination: Int, CaseIterable, Identifiable, Selectable, Equatable { enum AgendaDestination: Int, CaseIterable, Identifiable, Selectable, Equatable {
var id: Int { self.rawValue } var id: Int { self.rawValue }
@ -33,6 +34,15 @@ enum AgendaDestination: Int, CaseIterable, Identifiable, Selectable, Equatable {
} }
} }
func associatedTip() -> (any Tip)? {
switch self {
case .around:
return PlayerTournamentSearchTip()
default:
return nil
}
}
func selectionLabel(index: Int) -> String { func selectionLabel(index: Int) -> String {
localizedTitleKey localizedTitleKey
} }

@ -25,7 +25,7 @@ class FederalDataViewModel {
func filterStatus() -> String { func filterStatus() -> String {
var labels: [String] = [] var labels: [String] = []
labels.append(contentsOf: levels.map { $0.localizedLabel() }.formatList()) labels.append(contentsOf: levels.map { $0.localizedLevelLabel() }.formatList())
labels.append(contentsOf: categories.map { $0.localizedLabel() }.formatList()) labels.append(contentsOf: categories.map { $0.localizedLabel() }.formatList())
labels.append(contentsOf: ageCategories.map { $0.localizedLabel() }.formatList()) labels.append(contentsOf: ageCategories.map { $0.localizedLabel() }.formatList())
let clubNames = selectedClubs.compactMap { codeClub in let clubNames = selectedClubs.compactMap { codeClub in

@ -7,6 +7,7 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
import TipKit
protocol Selectable { protocol Selectable {
func selectionLabel(index: Int) -> String func selectionLabel(index: Int) -> String
@ -15,9 +16,14 @@ protocol Selectable {
func badgeValueColor() -> Color? func badgeValueColor() -> Color?
func displayImageIfValueZero() -> Bool func displayImageIfValueZero() -> Bool
func systemImage() -> String? func systemImage() -> String?
func associatedTip() -> (any Tip)?
} }
extension Selectable { extension Selectable {
func associatedTip() -> (any Tip)? {
return nil
}
func systemImage() -> String? { func systemImage() -> String? {
return nil return nil
} }
@ -54,3 +60,30 @@ enum Badge {
} }
} }
} }
struct SelectionTipViewModifier: ViewModifier {
let selectable: Selectable
let action: () -> Void
func body(content: Content) -> some View {
if let tip = selectable.associatedTip() {
if #available(iOS 18.0, *) {
content
.popoverTip(tip, arrowEdge: .top) { _ in
action()
tip.invalidate(reason: .tipClosed)
}
} else {
content
}
} else {
content
}
}
}
extension View {
func selectableTipViewModifier(selectable: Selectable, action: @escaping () -> Void) -> some View {
modifier(SelectionTipViewModifier(selectable: selectable, action: action))
}
}

@ -95,6 +95,16 @@ struct CallMessageCustomizationView: View {
} }
.headerProminence(.increased) .headerProminence(.increased)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(focusedField != nil)
.toolbar(content: {
if focusedField != nil {
ToolbarItem(placement: .topBarLeading) {
Button("Annuler", role: .cancel) {
focusedField = nil
}
}
}
})
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Message de convocation") .navigationTitle("Message de convocation")
.toolbar { .toolbar {

@ -27,7 +27,7 @@ struct EventSettingsView: View {
link.append(tournaments.compactMap({ tournament in link.append(tournaments.compactMap({ tournament in
if let url = tournament.shareURL(pageLink) { if let url = tournament.shareURL(pageLink) {
var tournamentLink = [String]() var tournamentLink = [String]()
tournamentLink.append(tournament.tournamentTitle()) tournamentLink.append(tournament.tournamentTitle(.title))
tournamentLink.append(url.absoluteString) tournamentLink.append(url.absoluteString)
return tournamentLink.joined(separator: "\n") return tournamentLink.joined(separator: "\n")
} else { } else {
@ -46,12 +46,14 @@ struct EventSettingsView: View {
var body: some View { var body: some View {
Form { Form {
Section { Section {
TextField("Description de l'événement", text: $eventName, axis: .vertical) TextField("Nom de l'événement", text: $eventName, axis: .vertical)
.lineLimit(2) .lineLimit(2)
.keyboardType(.alphabet) .keyboardType(.alphabet)
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.focused($textFieldIsFocus) .focused($textFieldIsFocus)
} header: {
Text("Nom de l'événement")
} footer: { } footer: {
if eventName.isEmpty == false { if eventName.isEmpty == false {
FooterButtonView("effacer le nom") { FooterButtonView("effacer le nom") {
@ -85,6 +87,16 @@ struct EventSettingsView: View {
} }
} }
} }
.navigationBarBackButtonHidden(textFieldIsFocus)
.toolbar(content: {
if textFieldIsFocus {
ToolbarItem(placement: .topBarLeading) {
Button("Annuler", role: .cancel) {
textFieldIsFocus = false
}
}
}
})
.toolbar { .toolbar {
if textFieldIsFocus { if textFieldIsFocus {
ToolbarItem(placement: .keyboard) { ToolbarItem(placement: .keyboard) {

@ -22,11 +22,12 @@ struct TournamentConfigurationView: View {
var body: some View { var body: some View {
Picker(selection: $tournament.federalLevelCategory, label: Text("Niveau")) { Picker(selection: $tournament.federalLevelCategory, label: Text("Niveau")) {
ForEach(TournamentLevel.allCases) { type in ForEach(TournamentLevel.allCases) { type in
Text(type.localizedLabel(.title)).tag(type) Text(type.localizedLevelLabel(.title)).tag(type)
} }
} }
.onChange(of: tournament.federalLevelCategory) { .onChange(of: tournament.federalLevelCategory) {
if tournament.federalLevelCategory == .unlisted { if tournament.federalLevelCategory == .unlisted {
tournament.hideTeamsWeight = true
tournament.federalCategory = .unlisted tournament.federalCategory = .unlisted
tournament.federalAgeCategory = .unlisted tournament.federalAgeCategory = .unlisted
} else { } else {

@ -213,6 +213,16 @@ struct ClubDetailView: View {
} }
} }
} }
.navigationBarBackButtonHidden(focusedField != nil)
.toolbar(content: {
if focusedField != nil {
ToolbarItem(placement: .topBarLeading) {
Button("Annuler", role: .cancel) {
focusedField = nil
}
}
}
})
.keyboardType(.alphabet) .keyboardType(.alphabet)
.autocorrectionDisabled() .autocorrectionDisabled()
.defaultFocus($focusedField, ._name, priority: .automatic) .defaultFocus($focusedField, ._name, priority: .automatic)

@ -12,6 +12,7 @@ struct CourtView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Bindable var court: Court @Bindable var court: Court
@State private var name: String = "" @State private var name: String = ""
@FocusState var focusedField: Court.CodingKeys?
init(court: Court) { init(court: Court) {
self.court = court self.court = court
@ -23,6 +24,7 @@ struct CourtView: View {
Section { Section {
LabeledContent { LabeledContent {
TextField("Nom", text: $name) TextField("Nom", text: $name)
.focused($focusedField, equals: ._name)
.autocorrectionDisabled() .autocorrectionDisabled()
.keyboardType(.alphabet) .keyboardType(.alphabet)
.multilineTextAlignment(.trailing) .multilineTextAlignment(.trailing)
@ -71,6 +73,16 @@ struct CourtView: View {
Logger.error(error) Logger.error(error)
} }
} }
.navigationBarBackButtonHidden(focusedField != nil)
.toolbar(content: {
if focusedField != nil {
ToolbarItem(placement: .topBarLeading) {
Button("Annuler", role: .cancel) {
focusedField = nil
}
}
}
})
.navigationTitle(court.courtTitle()) .navigationTitle(court.courtTitle())
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)

@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import TipKit
struct GenericDestinationPickerView<T: Identifiable & Selectable & Equatable >: View { struct GenericDestinationPickerView<T: Identifiable & Selectable & Equatable >: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@ -49,6 +50,9 @@ struct GenericDestinationPickerView<T: Identifiable & Selectable & Equatable >:
.contentShape(Capsule()) .contentShape(Capsule())
} }
} }
.selectableTipViewModifier(selectable: destination) {
selectedDestination = destination
}
.padding() .padding()
.background { .background {
Capsule() Capsule()

@ -20,6 +20,7 @@ struct GroupStageSettingsView: View {
@State private var presentConfirmationButton: Bool = false @State private var presentConfirmationButton: Bool = false
@State private var size: Int @State private var size: Int
@State private var courtIndex: Int @State private var courtIndex: Int
@FocusState var focusedField: GroupStage.CodingKeys?
init(groupStage: GroupStage) { init(groupStage: GroupStage) {
_groupStage = Bindable(groupStage) _groupStage = Bindable(groupStage)
@ -37,6 +38,8 @@ struct GroupStageSettingsView: View {
Section { Section {
TextField("Nom de la poule", text: $groupStageName) TextField("Nom de la poule", text: $groupStageName)
.keyboardType(.alphabet) .keyboardType(.alphabet)
.focused($focusedField, equals: ._name)
.submitLabel(.done)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.onSubmit { .onSubmit {
groupStageName = groupStageName.trimmed groupStageName = groupStageName.trimmed
@ -152,6 +155,16 @@ struct GroupStageSettingsView: View {
presentConfirmationButton = true presentConfirmationButton = true
} }
} }
.navigationBarBackButtonHidden(focusedField != nil)
.toolbar(content: {
if focusedField != nil {
ToolbarItem(placement: .topBarLeading) {
Button("Annuler", role: .cancel) {
focusedField = nil
}
}
}
})
.navigationTitle("Paramètres") .navigationTitle("Paramètres")
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
} }

@ -23,15 +23,44 @@ struct TournamentLookUpView: View {
@State private var requestedToGetAllPages: Bool = false @State private var requestedToGetAllPages: Bool = false
@State private var revealSearchParameters: Bool = true @State private var revealSearchParameters: Bool = true
@State private var presentAlert: Bool = false @State private var presentAlert: Bool = false
@State private var confirmSearch: Bool = false
var tournaments: [FederalTournament] { var tournaments: [FederalTournament] {
federalDataViewModel.searchedFederalTournaments federalDataViewModel.searchedFederalTournaments
} }
var showLastError: Binding<Bool> {
Binding {
locationManager.lastError != nil
} set: { value in
if value == false {
locationManager.lastError = nil
}
}
}
var body: some View { var body: some View {
List { List {
searchParametersView searchParametersView
} }
.alert(isPresented: showLastError, error: locationManager.lastError as? LocationManager.LocationError, actions: {
Button("Annuler", role: .cancel) {
}
})
.confirmationDialog("Attention", isPresented: $confirmSearch, titleVisibility: .visible) {
Button("Cherchez quand même") {
requestedToGetAllPages = true
runSearch()
}
Button("Annuler", role: .cancel) {
}
} message: {
Text("Aucune ville n'a été indiqué, il est préférable de se localiser ou d'indiquer une ville pour réduire le nombre de résultat.")
}
.alert("Attention", isPresented: $presentAlert, actions: { .alert("Attention", isPresented: $presentAlert, actions: {
Button { Button {
presentAlert = false presentAlert = false
@ -70,8 +99,12 @@ struct TournamentLookUpView: View {
ToolbarItem(placement: .bottomBar) { ToolbarItem(placement: .bottomBar) {
if revealSearchParameters { if revealSearchParameters {
FooterButtonView("Lancer la recherche") { FooterButtonView("Lancer la recherche") {
if dataStore.appSettings.city.isEmpty {
confirmSearch = true
} else {
runSearch() runSearch()
} }
}
.disabled(searching) .disabled(searching)
} else if searching { } else if searching {
HStack(spacing: 20) { HStack(spacing: 20) {
@ -230,31 +263,6 @@ struct TournamentLookUpView: View {
} }
} }
@ViewBuilder
var searchContollerView: some View {
Section {
Button {
runSearch()
} label: {
HStack {
Label("Chercher un tournoi", systemImage: "magnifyingglass")
if searching {
Spacer()
ProgressView()
}
}
}
Button {
dataStore.appSettings.resetSearch()
locationManager.location = nil
locationManager.city = nil
revealSearchParameters = true
} label: {
Label("Ré-initialiser la recherche", systemImage: "xmark.circle")
}
}
}
@ViewBuilder @ViewBuilder
var searchParametersView: some View { var searchParametersView: some View {
@Bindable var appSettings = dataStore.appSettings @Bindable var appSettings = dataStore.appSettings
@ -335,7 +343,7 @@ struct TournamentLookUpView: View {
NavigationLink { NavigationLink {
List([TournamentLevel.p25, TournamentLevel.p100, TournamentLevel.p250, TournamentLevel.p500, TournamentLevel.p1000, TournamentLevel.p1500, TournamentLevel.p2000], selection: $appSettings.tournamentLevels) { type in List([TournamentLevel.p25, TournamentLevel.p100, TournamentLevel.p250, TournamentLevel.p500, TournamentLevel.p1000, TournamentLevel.p1500, TournamentLevel.p2000], selection: $appSettings.tournamentLevels) { type in
Text(type.localizedLabel()) Text(type.localizedLevelLabel())
} }
.navigationTitle("Niveaux") .navigationTitle("Niveaux")
.environment(\.editMode, Binding.constant(EditMode.active)) .environment(\.editMode, Binding.constant(EditMode.active))
@ -413,7 +421,7 @@ struct TournamentLookUpView: View {
if dataStore.appSettings.tournamentLevels.isEmpty || dataStore.appSettings.tournamentLevels.count == TournamentLevel.allCases.count { if dataStore.appSettings.tournamentLevels.isEmpty || dataStore.appSettings.tournamentLevels.count == TournamentLevel.allCases.count {
Text("Tous les niveaux") Text("Tous les niveaux")
} else { } else {
Text(levels.map({ $0.localizedLabel() }).joined(separator: ", ")) Text(levels.map({ $0.localizedLevelLabel() }).joined(separator: ", "))
} }
} }

@ -17,7 +17,7 @@ struct RankCalculatorView: View {
Section { Section {
HStack { HStack {
let ordinal = NumberFormatter.ordinal.string(from: NSNumber(value:rank))! let ordinal = NumberFormatter.ordinal.string(from: NSNumber(value:rank))!
Text("\(ordinal) d'un \(tournamentLevel.localizedLabel()) de \(count.localizedLabel()) équipes:") Text("\(ordinal) d'un \(tournamentLevel.localizedLevelLabel()) de \(count.localizedLabel()) équipes:")
Spacer() Spacer()
Text(tournamentLevel.points(for: rank-1, count: count.rawValue).formatted(.number.sign(strategy: .always()))) Text(tournamentLevel.points(for: rank-1, count: count.rawValue).formatted(.number.sign(strategy: .always())))
} }
@ -25,7 +25,7 @@ struct RankCalculatorView: View {
Section { Section {
Picker(selection: $tournamentLevel) { Picker(selection: $tournamentLevel) {
ForEach(TournamentLevel.allCases) { level in ForEach(TournamentLevel.allCases) { level in
Text(level.localizedLabel()).tag(level) Text(level.localizedLevelLabel()).tag(level)
} }
} label: { } label: {
Label("Niveau", systemImage: "gauge.medium") Label("Niveau", systemImage: "gauge.medium")

@ -52,7 +52,7 @@ struct PlanningSettingsView: View {
Section { Section {
DatePicker(selection: $tournament.startDate) { DatePicker(selection: $tournament.startDate) {
Text(tournament.startDate.formatted(.dateTime.weekday(.wide)).capitalized) Text(tournament.startDate.formatted(.dateTime.weekday(.wide)).capitalized).lineLimit(1)
} }
LabeledContent { LabeledContent {
StepperView(count: $tournament.dayDuration, minimum: 1) StepperView(count: $tournament.dayDuration, minimum: 1)

@ -168,17 +168,22 @@ struct PlayerDetailView: View {
} }
} }
} }
.scrollDismissesKeyboard(.immediately)
.onChange(of: player.hasArrived) { .onChange(of: player.hasArrived) {
_save() _save()
} }
.onChange(of: player.sex) { .onChange(of: player.sex) {
_save() _save()
} }
.onChange(of: player.computedRank) { .navigationBarBackButtonHidden(focusedField != nil)
player.team()?.updateWeight(inTournamentCategory: tournament.tournamentCategory) .toolbar(content: {
_save() if focusedField != nil {
ToolbarItem(placement: .topBarLeading) {
Button("Annuler", role: .cancel) {
focusedField = nil
}
}
} }
})
.headerProminence(.increased) .headerProminence(.increased)
.navigationTitle("Édition") .navigationTitle("Édition")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)

@ -63,7 +63,7 @@ struct TournamentFilterView: View {
} }
} }
} label: { } label: {
Text(level.localizedLabel(.title)) Text(level.localizedLevelLabel(.title))
} }
} }
} header: { } header: {

@ -21,6 +21,7 @@ struct EditingTeamView: View {
@State private var registrationDate : Date @State private var registrationDate : Date
@State private var callDate : Date @State private var callDate : Date
@State private var name: String @State private var name: String
@FocusState private var focusedField: TeamRegistration.CodingKeys?
var messageSentFailed: Binding<Bool> { var messageSentFailed: Binding<Bool> {
Binding { Binding {
@ -137,9 +138,12 @@ struct EditingTeamView: View {
})) { })) {
Text("Forfait") Text("Forfait")
} }
}
Section {
TextField("Nom de l'équipe", text: $name) TextField("Nom de l'équipe", text: $name)
.autocorrectionDisabled() .autocorrectionDisabled()
.focused($focusedField, equals: ._name)
.keyboardType(.alphabet) .keyboardType(.alphabet)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.submitLabel(.done) .submitLabel(.done)
@ -153,6 +157,8 @@ struct EditingTeamView: View {
_save() _save()
} }
} header: {
Text("Nom de l'équipe")
} }
Section { Section {
@ -182,6 +188,16 @@ struct EditingTeamView: View {
} }
} }
} }
.navigationBarBackButtonHidden(focusedField != nil)
.toolbar(content: {
if focusedField != nil {
ToolbarItem(placement: .topBarLeading) {
Button("Annuler", role: .cancel) {
focusedField = nil
}
}
}
})
.alert("Un problème est survenu", isPresented: messageSentFailed) { .alert("Un problème est survenu", isPresented: messageSentFailed) {
Button("OK") { Button("OK") {
} }
@ -200,8 +216,6 @@ struct EditingTeamView: View {
case .failed: case .failed:
self.sentError = .messageFailed self.sentError = .messageFailed
case .sent: case .sent:
let uncalledTeams = team.getPhoneNumbers().isEmpty
if networkMonitor.connected == false { if networkMonitor.connected == false {
self.contactType = nil self.contactType = nil
if team.getPhoneNumbers().isEmpty == false { if team.getPhoneNumbers().isEmpty == false {
@ -232,8 +246,6 @@ struct EditingTeamView: View {
self.contactType = nil self.contactType = nil
self.sentError = .mailFailed self.sentError = .mailFailed
case .sent: case .sent:
let uncalledTeams = team.getMail().isEmpty
if networkMonitor.connected == false { if networkMonitor.connected == false {
self.contactType = nil self.contactType = nil
if team.getMail().isEmpty == false { if team.getMail().isEmpty == false {

@ -85,11 +85,18 @@ struct FileImportView: View {
@State private var presentFormatHelperView: Bool = false @State private var presentFormatHelperView: Bool = false
@State private var validatedTournamentIds: Set<String> = Set() @State private var validatedTournamentIds: Set<String> = Set()
init(defaultFileProvider: FileImportManager.FileProvider = .frenchFederation) {
_fileProvider = .init(wrappedValue: defaultFileProvider)
}
var tournamentStore: TournamentStore { var tournamentStore: TournamentStore {
return self.tournament.tournamentStore return self.tournament.tournamentStore
} }
private func filteredTeams(tournament: Tournament) -> [FileImportManager.TeamHolder] { private func filteredTeams(tournament: Tournament) -> [FileImportManager.TeamHolder] {
if tournament.isAnimation() {
return teams.sorted(by: \.weight)
}
return teams.filter { $0.tournamentCategory == tournament.tournamentCategory && $0.tournamentAgeCategory == tournament.federalTournamentAge }.sorted(by: \.weight) return teams.filter { $0.tournamentCategory == tournament.tournamentCategory && $0.tournamentAgeCategory == tournament.federalTournamentAge }.sorted(by: \.weight)
} }
@ -309,10 +316,14 @@ struct FileImportView: View {
LabeledContent { LabeledContent {
Text(_filteredTeams.count.formatted()) Text(_filteredTeams.count.formatted())
} label: { } label: {
Text("Équipe\(_filteredTeams.count.pluralSuffix) \(tournament.tournamentCategory.importingRawValue) \(tournament.federalTournamentAge.importingRawValue) détectée\(_filteredTeams.count.pluralSuffix)") if tournament.isAnimation() {
Text("Équipe\(_filteredTeams.count.pluralSuffix) détectée\(_filteredTeams.count.pluralSuffix)")
} else {
Text("Équipe\(_filteredTeams.count.pluralSuffix) \(tournament.tournamentCategory.importingRawValue) \(tournament.federalTournamentAge.importingRawValue.lowercased()) détectée\(_filteredTeams.count.pluralSuffix)")
}
} }
} footer: { } footer: {
if previousTeams.isEmpty == false { if previousTeams.isEmpty == false, tournament.isAnimation() == false {
Text("La liste ci-dessous n'est qu'une indication d'évolution par rapport au seul poids d'équipe. Cela ne tient pas compte des dates d'inscriptions, WCs et autres éléments.").foregroundStyle(.logoRed) Text("La liste ci-dessous n'est qu'une indication d'évolution par rapport au seul poids d'équipe. Cela ne tient pas compte des dates d'inscriptions, WCs et autres éléments.").foregroundStyle(.logoRed)
} }
} }
@ -535,6 +546,9 @@ struct FileImportView: View {
Section { Section {
HStack { HStack {
VStack(alignment: .leading) { VStack(alignment: .leading) {
if let teamName = team.name {
Text(teamName).foregroundStyle(.secondary)
}
ForEach(team.players.sorted(by: \.computedRank)) { ForEach(team.players.sorted(by: \.computedRank)) {
Text($0.playerLabel()) Text($0.playerLabel())
} }

@ -149,7 +149,7 @@ struct AddTeamView: View {
} }
.toolbar { .toolbar {
ToolbarItem(placement: .cancellationAction) { ToolbarItem(placement: .cancellationAction) {
Button("Terminer", role: .cancel) { Button("Annuler", role: .cancel) {
dismiss() dismiss()
} }
} }
@ -258,6 +258,7 @@ struct AddTeamView: View {
} }
private func _isDuplicate() -> Bool { private func _isDuplicate() -> Bool {
if tournament.isAnimation() { return false }
let ids : [String?] = _currentSelectionIds() let ids : [String?] = _currentSelectionIds()
if tournament.selectedSortedTeams().anySatisfy({ $0.containsExactlyPlayerLicenses(ids) }) { if tournament.selectedSortedTeams().anySatisfy({ $0.containsExactlyPlayerLicenses(ids) }) {
return true return true
@ -391,8 +392,8 @@ struct AddTeamView: View {
ForEach(createdPlayerIds.sorted(), id: \.self) { id in ForEach(createdPlayerIds.sorted(), id: \.self) { id in
if let p = createdPlayers.first(where: { $0.id == id }) { if let p = createdPlayers.first(where: { $0.id == id }) {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
if let player = unsortedPlayers.first(where: { $0.licenceId == p.licenceId }), editedTeam?.includes(player: player) == false { if let player = unsortedPlayers.first(where: { ($0.licenceId == p.licenceId && $0.licenceId != nil) }), editedTeam?.includes(player: player) == false {
Text("Déjà inscrit !").foregroundStyle(.logoRed).bold() Text("Déjà inscrit !!").foregroundStyle(.logoRed).bold()
} }
PlayerView(player: p).tag(p.id) PlayerView(player: p).tag(p.id)
.environment(tournament) .environment(tournament)
@ -420,7 +421,7 @@ struct AddTeamView: View {
} else { } else {
RowButtonView("Confirmer") { RowButtonView("Confirmer") {
_updateTeam(checkDuplicates: false) _updateTeam(checkDuplicates: false)
editedTeam = nil dismiss()
} }
} }
} header: { } header: {

@ -42,7 +42,7 @@ struct BroadcastView: View {
navigation.selectedTab = .umpire navigation.selectedTab = .umpire
} }
RowButtonView("Jeter un oeil au site Padel Club") { RowButtonView("Voir le site Padel Club") {
UIApplication.shared.open(URLs.main.url) UIApplication.shared.open(URLs.main.url)
} }
} }
@ -104,12 +104,12 @@ struct BroadcastView: View {
Section { Section {
Toggle(isOn: $tournament.isPrivate) { Toggle(isOn: $tournament.isPrivate) {
Text("Tournoi privé") Text("Tournoi privé")
if (tournament.isPrivate && Guard.main.purchasedTransactions.isEmpty) {
Text("Vous devez disposer d'une offre pour rendre publique ce tournoi.")
.foregroundStyle(.logoRed)
} }
Toggle(isOn: $tournament.hideTeamsWeight) {
Text("Masquer les poids des équipes")
} }
.disabled(_disablePrivateToggle())
} footer: { } footer: {
let verb : String = tournament.isPrivate ? "est" : "sera" let verb : String = tournament.isPrivate ? "est" : "sera"
let footerString = " Le tournoi \(verb) masqué sur le site [Padel Club](\(URLs.main.rawValue))" let footerString = " Le tournoi \(verb) masqué sur le site [Padel Club](\(URLs.main.rawValue))"
@ -160,10 +160,6 @@ struct BroadcastView: View {
Text("Publication prévue") Text("Publication prévue")
} }
} }
Toggle(isOn: $tournament.hideTeamsWeight) {
Text("Masquer les poids des équipes")
}
} header: { } header: {
Text("Liste des équipes") Text("Liste des équipes")
} footer: { } footer: {
@ -273,6 +269,7 @@ struct BroadcastView: View {
} }
} }
.toolbar(content: { .toolbar(content: {
if StoreCenter.main.userId != nil, tournament.isPrivate == false, tournament.club() != nil {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
Menu { Menu {
Section { Section {
@ -301,6 +298,7 @@ struct BroadcastView: View {
Label("Partager les liens", systemImage: "square.and.arrow.up") Label("Partager les liens", systemImage: "square.and.arrow.up")
} }
} }
}
}) })
.headerProminence(.increased) .headerProminence(.increased)
.navigationTitle("Publication") .navigationTitle("Publication")
@ -321,14 +319,6 @@ struct BroadcastView: View {
} }
} }
private func _disablePrivateToggle() -> Bool {
#if DEBUG
return false
#else
return (tournament.isPrivate && Guard.main.purchasedTransactions.isEmpty)
#endif
}
private func _save() { private func _save() {
do { do {
if [tournament.publishTeams, tournament.publishSummons, tournament.publishBrackets, tournament.publishGroupStages].anySatisfy({ $0 == true }) { if [tournament.publishTeams, tournament.publishSummons, tournament.publishBrackets, tournament.publishGroupStages].anySatisfy({ $0 == true }) {

@ -28,6 +28,26 @@ struct TournamentGeneralSettingsView: View {
Section { Section {
TournamentDatePickerView() TournamentDatePickerView()
TournamentDurationManagerView() TournamentDurationManagerView()
LabeledContent {
TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $entryFee, format: .currency(code: Locale.current.currency?.identifier ?? "EUR"))
.keyboardType(.decimalPad)
.multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity)
.focused($focusedField, equals: ._entryFee)
} label: {
Text("Inscription")
}
}
Section {
TextField("Nom du tournoi", text: $tournamentName, axis: .vertical)
.lineLimit(2)
.frame(maxWidth: .infinity)
.keyboardType(.alphabet)
.focused($focusedField, equals: ._name)
} header: {
Text("Nom du tournoi")
} }
Section { Section {
@ -70,27 +90,17 @@ struct TournamentGeneralSettingsView: View {
Text(tournament.loserBracketMode.localizedLoserBracketModeDescription()) Text(tournament.loserBracketMode.localizedLoserBracketModeDescription())
} }
} }
Section {
LabeledContent {
TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $entryFee, format: .currency(code: Locale.current.currency?.identifier ?? "EUR"))
.keyboardType(.decimalPad)
.multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity)
.focused($focusedField, equals: ._entryFee)
} label: {
Text("Inscription")
} }
.navigationBarBackButtonHidden(focusedField != nil)
.toolbar(content: {
if focusedField != nil {
ToolbarItem(placement: .topBarLeading) {
Button("Annuler", role: .cancel) {
focusedField = nil
} }
Section {
TextField("Nom du tournoi", text: $tournamentName, axis: .vertical)
.lineLimit(2)
.frame(maxWidth: .infinity)
.keyboardType(.alphabet)
.focused($focusedField, equals: ._name)
} }
} }
})
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.toolbar { .toolbar {
if focusedField != nil { if focusedField != nil {

@ -15,13 +15,22 @@ struct TournamentLevelPickerView: View {
Picker(selection: $tournament.tournamentLevel, label: Text("Niveau")) { Picker(selection: $tournament.tournamentLevel, label: Text("Niveau")) {
ForEach(TournamentLevel.allCases) { type in ForEach(TournamentLevel.allCases) { type in
Text(type.localizedLabel(.title)).tag(type) Text(type.localizedLevelLabel(.title)).tag(type)
} }
} }
.onChange(of: tournament.federalLevelCategory) { .onChange(of: tournament.federalLevelCategory) {
if tournament.federalLevelCategory == .unlisted { if tournament.federalLevelCategory == .unlisted {
tournament.hideTeamsWeight = true
tournament.federalCategory = .unlisted tournament.federalCategory = .unlisted
tournament.federalAgeCategory = .unlisted tournament.federalAgeCategory = .unlisted
} else {
tournament.hideTeamsWeight = false
if tournament.federalCategory == .unlisted {
tournament.federalCategory = .men
}
if tournament.federalAgeCategory == .unlisted {
tournament.federalAgeCategory = .senior
}
} }
} }

@ -231,7 +231,7 @@ struct InscriptionManagerView: View {
_setHash() _setHash()
}) { }) {
NavigationStack { NavigationStack {
FileImportView() FileImportView(defaultFileProvider: tournament.isAnimation() ? .custom : .frenchFederation)
} }
.tint(.master) .tint(.master)
} }
@ -308,6 +308,7 @@ struct InscriptionManagerView: View {
.symbolVariant(filterMode == .all ? .none : .fill) .symbolVariant(filterMode == .all ? .none : .fill)
} }
Menu { Menu {
if tournament.isAnimation() == false {
if tournament.inscriptionClosed() == false { if tournament.inscriptionClosed() == false {
Menu { Menu {
_sortingTypePickerView() _sortingTypePickerView()
@ -348,6 +349,13 @@ struct InscriptionManagerView: View {
Label("Ré-ouvrir", systemImage: "lock.open") Label("Ré-ouvrir", systemImage: "lock.open")
} }
} }
} else {
Button {
presentImportView = true
} label: {
Label("Importer un fichier", systemImage: "square.and.arrow.down")
}
}
} label: { } label: {
if tournament.inscriptionClosed() == false { if tournament.inscriptionClosed() == false {
LabelOptions() LabelOptions()
@ -438,9 +446,12 @@ struct InscriptionManagerView: View {
if presentSearch == false { if presentSearch == false {
_informationView() _informationView()
if tournament.isAnimation() == false {
_rankHandlerView() _rankHandlerView()
_relatedTips() _relatedTips()
} }
}
let teams = searchField.isEmpty ? filteredTeams : filteredTeams.filter({ $0.contains(searchField.canonicalVersion) }) let teams = searchField.isEmpty ? filteredTeams : filteredTeams.filter({ $0.contains(searchField.canonicalVersion) })
@ -650,7 +661,7 @@ struct InscriptionManagerView: View {
.listRowSeparator(.hidden) .listRowSeparator(.hidden)
let registrationIssues = tournament.registrationIssues() let registrationIssues = tournament.registrationIssues()
if registrationIssues > 0 { if tournament.isAnimation() == false, registrationIssues > 0 {
NavigationLink { NavigationLink {
InscriptionInfoView() InscriptionInfoView()
.environment(tournament) .environment(tournament)
@ -660,7 +671,7 @@ struct InscriptionManagerView: View {
.foregroundStyle(.logoRed) .foregroundStyle(.logoRed)
.fontWeight(.bold) .fontWeight(.bold)
} label: { } label: {
Text("Problèmes détéctés") Text("Problèmes détectés")
} }
} }
} }

@ -94,6 +94,8 @@ struct TableStructureView: View {
} label: { } label: {
Text("Nombre de poules") Text("Nombre de poules")
} }
} footer: {
Text("Vous pourrez modifier la taille de vos poules de manière spécifique dans l'écran des poules.")
} }
if groupStageCount > 0 { if groupStageCount > 0 {

@ -250,8 +250,9 @@ struct TournamentRankView: View {
} }
} }
} }
if tournament.isAnimation() == false && key > 0 {
Spacer() Spacer()
if tournament.isAnimation() == false && key > 0 {
VStack(alignment: .trailing) { VStack(alignment: .trailing) {
HStack(alignment: .lastTextBaseline, spacing: 0.0) { HStack(alignment: .lastTextBaseline, spacing: 0.0) {
Text(tournament.tournamentLevel.points(for: key - 1, count: tournament.teamCount).formatted(.number.sign(strategy: .always()))) Text(tournament.tournamentLevel.points(for: key - 1, count: tournament.teamCount).formatted(.number.sign(strategy: .always())))

@ -88,7 +88,7 @@ struct TournamentCellView: View {
.font(.caption) .font(.caption)
} }
HStack(alignment: .bottom) { HStack(alignment: .bottom) {
Text(build.level.localizedLabel()) Text(build.level.localizedLevelLabel())
.fontWeight(.semibold) .fontWeight(.semibold)
if displayStyle == .wide { if displayStyle == .wide {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {

@ -190,7 +190,7 @@ import LeStorage
} }
func userFilteredPurchases() -> [StoreKit.Transaction] { func userFilteredPurchases() -> [StoreKit.Transaction] {
Logger.log("self.purchasedTransactions = \(self.purchasedTransactions.count)") // Logger.log("self.purchasedTransactions = \(self.purchasedTransactions.count)")
guard let userId = StoreCenter.main.userId, let currentUserUUID: UUID = UUID(uuidString: userId) else { guard let userId = StoreCenter.main.userId, let currentUserUUID: UUID = UUID(uuidString: userId) else {
return [] return []
} }
@ -279,7 +279,7 @@ import LeStorage
let tournamentCreditCount = self._purchasedTournamentCount() let tournamentCreditCount = self._purchasedTournamentCount()
// let notDeletedTournamentCount = DataStore.shared.tournaments.filter { $0.isDeleted == false }.count // let notDeletedTournamentCount = DataStore.shared.tournaments.filter { $0.isDeleted == false }.count
Logger.log("unitlyPayed = \(unitlyPayed), purchased = \(tournamentCreditCount) ") // Logger.log("unitlyPayed = \(unitlyPayed), purchased = \(tournamentCreditCount) ")
return tournamentCreditCount - unitlyPayed return tournamentCreditCount - unitlyPayed
} }

@ -68,7 +68,7 @@ class PurchaseManager: ObservableObject {
} }
} }
Logger.log("purchases = \(rows)") // Logger.log("purchases = \(rows)")
self.purchaseRows = rows self.purchaseRows = rows
} }

@ -141,7 +141,7 @@ struct TournamentView: View {
ToolbarItem(placement: .principal) { ToolbarItem(placement: .principal) {
VStack(spacing: -4.0) { VStack(spacing: -4.0) {
Text(tournament.tournamentTitle(.title)).font(.headline) Text(tournament.tournamentTitle(.title)).font(.headline)
Text(tournament.formattedDate()) Text(tournament.formattedDate(.title))
.font(.subheadline).foregroundStyle(.secondary) .font(.subheadline).foregroundStyle(.secondary)
} }
.popoverTip(tournamentSelectionTip) .popoverTip(tournamentSelectionTip)

Loading…
Cancel
Save