Laurent 10 months ago
commit 3ef38fdaa2
  1. 24
      PadelClub.xcodeproj/project.pbxproj
  2. 6
      PadelClub/Data/MatchScheduler.swift
  3. 25
      PadelClub/Data/PlayerRegistration.swift
  4. 2
      PadelClub/Data/TeamRegistration.swift
  5. 39
      PadelClub/Data/Tournament.swift
  6. 2
      PadelClub/PadelClubApp.swift
  7. 4
      PadelClub/Utils/ContactManager.swift
  8. 23
      PadelClub/Utils/Tips.swift
  9. 4
      PadelClub/Utils/URLs.swift
  10. 2
      PadelClub/Views/Calling/CallMessageCustomizationView.swift
  11. 2
      PadelClub/Views/Calling/CallView.swift
  12. 2
      PadelClub/Views/Calling/Components/PlayersWithoutContactView.swift
  13. 2
      PadelClub/Views/Calling/SendToAllView.swift
  14. 28
      PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift
  15. 458
      PadelClub/Views/Planning/PlanningView.swift
  16. 41
      PadelClub/Views/Player/PlayerDetailView.swift
  17. 2
      PadelClub/Views/Team/EditingTeamView.swift
  18. 13
      PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift
  19. 15
      PadelClub/Views/Tournament/Screen/Components/UpdateSourceRankDateView.swift
  20. 13
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift

@ -3275,7 +3275,10 @@
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi"; INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi";
INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationAlwaysUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
@ -3286,7 +3289,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.39; MARKETING_VERSION = 1.0.44;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3319,7 +3322,10 @@
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi"; INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi";
INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationAlwaysUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
@ -3330,7 +3336,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.39; MARKETING_VERSION = 1.0.44;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3436,7 +3442,10 @@
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi"; INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi";
INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationAlwaysUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
@ -3480,7 +3489,10 @@
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi"; INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi";
INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationAlwaysUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club utilise votre position simplement pour faciliter la recherche de tournois et de clubs autour de vous.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";

@ -229,14 +229,10 @@ final class MatchScheduler : ModelObject, Storable {
return teamsAvailable return teamsAvailable
})) }))
if rotationIndex > 0 { if rotationIndex > 0, simultaneousStart == false {
rotationMatches = rotationMatches.sorted(by: { rotationMatches = rotationMatches.sorted(by: {
if counts[$0.groupStageObject!.index] ?? 0 == counts[$1.groupStageObject!.index] ?? 0 { if counts[$0.groupStageObject!.index] ?? 0 == counts[$1.groupStageObject!.index] ?? 0 {
if simultaneousStart {
return $0.groupStageObject!.orderedIndexOfMatch($0) < $1.groupStageObject!.orderedIndexOfMatch($1)
} else {
return $0.groupStageObject!.index < $1.groupStageObject!.index return $0.groupStageObject!.index < $1.groupStageObject!.index
}
} else { } else {
return counts[$0.groupStageObject!.index] ?? 0 < counts[$1.groupStageObject!.index] ?? 0 return counts[$0.groupStageObject!.index] ?? 0 < counts[$1.groupStageObject!.index] ?? 0
} }

@ -239,8 +239,15 @@ final class PlayerRegistration: ModelObject, Storable {
} }
} }
@MainActor
func updateRank(from sources: [CSVParser], lastRank: Int) async throws { func updateRank(from sources: [CSVParser], lastRank: Int) async throws {
#if DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func updateRank()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
if let dataFound = try await history(from: sources) { if let dataFound = try await history(from: sources) {
rank = dataFound.rankValue?.toInt() rank = dataFound.rankValue?.toInt()
points = dataFound.points points = dataFound.points
@ -251,6 +258,14 @@ final class PlayerRegistration: ModelObject, Storable {
} }
func history(from sources: [CSVParser]) async throws -> Line? { func history(from sources: [CSVParser]) async throws -> Line? {
#if DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func history()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
guard let license = licenceId?.strippedLicense else { guard let license = licenceId?.strippedLicense else {
return try await historyFromName(from: sources) return try await historyFromName(from: sources)
} }
@ -276,6 +291,14 @@ final class PlayerRegistration: ModelObject, Storable {
} }
func historyFromName(from sources: [CSVParser]) async throws -> Line? { func historyFromName(from sources: [CSVParser]) async throws -> Line? {
#if DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func historyFromName()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
return await withTaskGroup(of: Line?.self) { group in return await withTaskGroup(of: Line?.self) { group in
for source in sources.filter({ $0.maleData == isMalePlayer() }) { for source in sources.filter({ $0.maleData == isMalePlayer() }) {
group.addTask { [lastName, firstName] in group.addTask { [lastName, firstName] in

@ -173,7 +173,7 @@ final class TeamRegistration: ModelObject, Storable {
} }
func getPhoneNumbers() -> [String] { func getPhoneNumbers() -> [String] {
return players().compactMap { $0.phoneNumber }.filter({ $0.isMobileNumber() }) return players().compactMap { $0.phoneNumber }.filter({ $0.isEmpty == false })
} }
func getMail() -> [String] { func getMail() -> [String] {

@ -1136,7 +1136,7 @@ defer {
} }
} }
func registrationIssues() async -> Int { func registrationIssues() -> Int {
let players : [PlayerRegistration] = unsortedPlayers() let players : [PlayerRegistration] = unsortedPlayers()
let selectedTeams : [TeamRegistration] = selectedSortedTeams() let selectedTeams : [TeamRegistration] = selectedSortedTeams()
let callDateIssue : [TeamRegistration] = selectedTeams.filter { $0.callDate != nil && isStartDateIsDifferentThanCallDate($0) } let callDateIssue : [TeamRegistration] = selectedTeams.filter { $0.callDate != nil && isStartDateIsDifferentThanCallDate($0) }
@ -1394,6 +1394,14 @@ defer {
Logger.error(error) Logger.error(error)
} }
if self.publishRankings == false {
self.publishRankings = true
do {
try DataStore.shared.tournaments.addOrUpdate(instance: self)
} catch {
Logger.error(error)
}
}
return rankings return rankings
} }
@ -1449,6 +1457,15 @@ defer {
} }
func updateRank(to newDate: Date?) async throws { func updateRank(to newDate: Date?) async throws {
#if DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func updateRank()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
guard let newDate else { return } guard let newDate else { return }
rankSourceDate = newDate rankSourceDate = newDate
@ -1472,8 +1489,8 @@ defer {
let lastRankWoman = currentMonthData()?.femaleUnrankedValue let lastRankWoman = currentMonthData()?.femaleUnrankedValue
let dataURLs = SourceFileManager.shared.allFiles.filter { $0.dateFromPath == newDate } let dataURLs = SourceFileManager.shared.allFiles.filter { $0.dateFromPath == newDate }
let sources = dataURLs.map { CSVParser(url: $0) } let sources = dataURLs.map { CSVParser(url: $0) }
let players = unsortedPlayers()
try await unsortedPlayers().concurrentForEach { player in try await players.concurrentForEach { player in
try await player.updateRank(from: sources, lastRank: (player.sex == .female ? lastRankWoman : lastRankMan) ?? 0) try await player.updateRank(from: sources, lastRank: (player.sex == .female ? lastRankWoman : lastRankMan) ?? 0)
} }
} }
@ -1486,7 +1503,7 @@ defer {
return unsortedTeams().first(where: { $0.includes(players: players) }) return unsortedTeams().first(where: { $0.includes(players: players) })
} }
func tournamentTitle(_ displayStyle: DisplayStyle = .wide) -> String { func tournamentTitle(_ displayStyle: DisplayStyle = .wide, hideSenior: Bool = false) -> String {
if tournamentLevel == .unlisted, displayStyle == .title { if tournamentLevel == .unlisted, displayStyle == .title {
if let name { if let name {
return name return name
@ -1494,7 +1511,13 @@ defer {
return tournamentLevel.localizedLevelLabel(.title) return tournamentLevel.localizedLevelLabel(.title)
} }
} }
let title: String = [tournamentLevel.localizedLevelLabel(displayStyle), tournamentCategory.localizedLabel(displayStyle), federalTournamentAge.localizedFederalAgeLabel(displayStyle)].filter({ $0.isEmpty == false }).joined(separator: " ") let displayStyleCategory = hideSenior ? .short : displayStyle
var levelCategory = [tournamentLevel.localizedLevelLabel(displayStyle), tournamentCategory.localizedLabel(displayStyle)]
if displayStyle == .short {
levelCategory = [tournamentLevel.localizedLevelLabel(displayStyle) + tournamentCategory.localizedLabel(displayStyle)]
}
let array = levelCategory + [federalTournamentAge.localizedFederalAgeLabel(displayStyleCategory)]
let title: String = array.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 {
@ -2623,7 +2646,11 @@ extension Tournament {
func deadline(for type: TournamentDeadlineType) -> Date? { func deadline(for type: TournamentDeadlineType) -> Date? {
guard [.p500, .p1000, .p1500, .p2000].contains(tournamentLevel) else { return nil } guard [.p500, .p1000, .p1500, .p2000].contains(tournamentLevel) else { return nil }
if let date = Calendar.current.date(byAdding: .day, value: type.daysOffset, to: startDate) { var daysOffset = type.daysOffset
if tournamentLevel == .p500 {
daysOffset += 7
}
if let date = Calendar.current.date(byAdding: .day, value: daysOffset, to: startDate) {
let startOfDay = Calendar.current.startOfDay(for: date) let startOfDay = Calendar.current.startOfDay(for: date)
return Calendar.current.date(byAdding: type.timeOffset, to: startOfDay) return Calendar.current.date(byAdding: type.timeOffset, to: startOfDay)
} }

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

@ -82,7 +82,7 @@ Il est conseillé de vous présenter 10 minutes avant de jouer.\n\nMerci de me c
let date = startDate ?? tournament?.startDate ?? Date() let date = startDate ?? tournament?.startDate ?? Date()
if let tournament { if let tournament {
text = text.replacingOccurrences(of: "#titre", with: tournament.tournamentTitle(.short)) text = text.replacingOccurrences(of: "#titre", with: tournament.tournamentTitle(.title, hideSenior: true))
text = text.replacingOccurrences(of: "#prix", with: tournament.entryFeeMessage) text = text.replacingOccurrences(of: "#prix", with: tournament.entryFeeMessage)
} }
@ -132,7 +132,7 @@ Il est conseillé de vous présenter 10 minutes avant de jouer.\n\nMerci de me c
let intro = reSummon ? "Suite à des forfaits, vous êtes finalement" : "Vous êtes" let intro = reSummon ? "Suite à des forfaits, vous êtes finalement" : "Vous êtes"
if let tournament { if let tournament {
return "Bonjour,\n\n\(intro) \(localizedCalled) pour jouer en \(roundLabel.lowercased()) du \(tournament.tournamentTitle(.short)) au \(clubName) le \(date.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(date.formatted(Date.FormatStyle().hour().minute())).\n\n" + computedMessage + "\n\n\(signature)" return "Bonjour,\n\n\(intro) \(localizedCalled) pour jouer en \(roundLabel.lowercased()) du \(tournament.tournamentTitle(.title, hideSenior: true)) au \(clubName) le \(date.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(date.formatted(Date.FormatStyle().hour().minute())).\n\n" + computedMessage + "\n\n\(signature)"
} else { } else {
return "Bonjour,\n\n\(intro) \(localizedCalled) \(roundLabel) au \(clubName) le \(date.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(date.formatted(Date.FormatStyle().hour().minute())).\n\nMerci de confirmer en répondant à ce message et de prévenir votre partenaire !\n\n\(signature)" return "Bonjour,\n\n\(intro) \(localizedCalled) \(roundLabel) au \(clubName) le \(date.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(date.formatted(Date.FormatStyle().hour().minute())).\n\nMerci de confirmer en répondant à ce message et de prévenir votre partenaire !\n\n\(signature)"
} }

@ -549,29 +549,36 @@ struct TeamsExportTip: Tip {
} }
} }
struct PlayerTournamentSearchTip: Tip { struct TimeSlotMoveTip: Tip {
var title: Text { var title: Text {
Text("Cherchez un tournoi autour de vous !") Text("Réorganisez vos créneaux horaires !")
} }
var message: Text? { var message: Text? {
Text("Padel Club facilite la recherche de tournois et l'inscription !") Text("Vous pouvez déplacer les créneaux horaires dans la liste en glissant-déposant.")
} }
var image: Image? { var image: Image? {
Image(systemName: "trophy.circle") Image(systemName: "arrow.up.arrow.down.circle")
} }
}
var actions: [Action] { struct TimeSlotMoveOptionTip: Tip {
Action(id: ActionKey.selectAction.rawValue, title: "Éssayer") var title: Text {
Text("Réorganisez vos créneaux horaires !")
} }
enum ActionKey: String { var message: Text? {
case selectAction = "selectAction" Text("En cliquant ici, vous pouvez déplacer les créneaux horaires dans la liste en glissant-déposant.")
} }
var image: Image? {
Image(systemName: "sparkles")
}
} }
struct TipStyleModifier: ViewModifier { struct TipStyleModifier: ViewModifier {
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
var tint: Color? var tint: Color?

@ -31,8 +31,8 @@ enum URLs: String, Identifiable {
case beachPadel = "https://beach-padel.app.fft.fr/beachja/index/" case beachPadel = "https://beach-padel.app.fft.fr/beachja/index/"
//case padelClub = "https://padelclub.app" //case padelClub = "https://padelclub.app"
case tenup = "https://tenup.fft.fr" case tenup = "https://tenup.fft.fr"
case padelCompetitionGeneralGuide = "https://fft-site.cdn.prismic.io/fft-site/Zqi2PB5LeNNTxlrS_1-REGLESGENERALESDELACOMPETITION-ANNEESPORTIVE2025.pdf" case padelCompetitionGeneralGuide = "https://fft-site.cdn.prismic.io/fft-site/Z2mH0ZbqstJ98yso_CHAPITREIRèglesgénérales.pdf"
case padelCompetitionSpecificGuide = "https://fft-site.cdn.prismic.io/fft-site/Zqi4ax5LeNNTxlsu_3-CAHIERDESCHARGESDESTOURNOIS-ANNEESPORTIVE2025.pdf" case padelCompetitionSpecificGuide = "https://fft-site.cdn.prismic.io/fft-site/Z2mHz5bqstJ98ysm_CHAPITREIIICahierdeschargesdestournois.pdf"
case padelRules = "https://xlr.alwaysdata.net/static/rules/padel-rules-2024.pdf" case padelRules = "https://xlr.alwaysdata.net/static/rules/padel-rules-2024.pdf"
case restingDischarge = "https://club.fft.fr/tennisfirmidecazeville/60120370_d/data_1/pdf/fo/formlairededechargederesponsabilitetournoidepadel.pdf" case restingDischarge = "https://club.fft.fr/tennisfirmidecazeville/60120370_d/data_1/pdf/fo/formlairededechargederesponsabilitetournoidepadel.pdf"
case appReview = "https://apps.apple.com/app/padel-club/id6484163558?mt=8&action=write-review" case appReview = "https://apps.apple.com/app/padel-club/id6484163558?mt=8&action=write-review"

@ -56,7 +56,7 @@ struct CallMessageCustomizationView: View {
var finalMessage: String? { var finalMessage: String? {
let localizedCalled = "convoqué" + (tournament.tournamentCategory == .women ? "e" : "") + "s" let localizedCalled = "convoqué" + (tournament.tournamentCategory == .women ? "e" : "") + "s"
return "Bonjour,\n\nVous êtes \(localizedCalled) pour jouer en \(RoundRule.roundName(fromRoundIndex: 2).lowercased()) du \(tournament.tournamentTitle(.short)) au \(clubName) le \(tournament.startDate.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(tournament.startDate.formatted(Date.FormatStyle().hour().minute())).\n\n" + computedMessage + "\n\n\(customCallMessageSignature)" return "Bonjour,\n\nVous êtes \(localizedCalled) pour jouer en \(RoundRule.roundName(fromRoundIndex: 2).lowercased()) du \(tournament.tournamentTitle(.title, hideSenior: true)) au \(clubName) le \(tournament.startDate.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(tournament.startDate.formatted(Date.FormatStyle().hour().minute())).\n\n" + computedMessage + "\n\n\(customCallMessageSignature)"
} }
var body: some View { var body: some View {

@ -387,7 +387,7 @@ struct CallView: View {
recipients: tournament.umpireMail(), recipients: tournament.umpireMail(),
bccRecipients: teams.flatMap { $0.getMail() }, bccRecipients: teams.flatMap { $0.getMail() },
body: finalMessage(reSummon: reSummon, forcedEmptyMessage: forcedEmptyMessage), body: finalMessage(reSummon: reSummon, forcedEmptyMessage: forcedEmptyMessage),
subject: tournament.tournamentTitle(), subject: tournament.tournamentTitle(hideSenior: true),
tournamentBuild: nil) tournamentBuild: nil)
} }

@ -45,7 +45,7 @@ struct PlayersWithoutContactView: View {
LabeledContent { LabeledContent {
Text(withoutPhones.count.formatted()) Text(withoutPhones.count.formatted())
} label: { } label: {
Text("Joueurs sans téléphone portable") Text("Joueurs sans téléphone portable français")
} }
} }
} header: { } header: {

@ -273,7 +273,7 @@ struct SendToAllView: View {
if contactMethod == 0 { if contactMethod == 0 {
contactType = .message(date: nil, recipients: _teams().flatMap { $0.unsortedPlayers() }.compactMap { $0.phoneNumber }, body: finalMessage(), tournamentBuild: nil) contactType = .message(date: nil, recipients: _teams().flatMap { $0.unsortedPlayers() }.compactMap { $0.phoneNumber }, body: finalMessage(), tournamentBuild: nil)
} else { } else {
contactType = .mail(date: nil, recipients: tournament.umpireMail(), bccRecipients: _teams().flatMap { $0.unsortedPlayers() }.compactMap { $0.email }, body: finalMessage(), subject: tournament.tournamentTitle(), tournamentBuild: nil) contactType = .mail(date: nil, recipients: tournament.umpireMail(), bccRecipients: _teams().flatMap { $0.unsortedPlayers() }.compactMap { $0.email }, body: finalMessage(), subject: tournament.tournamentTitle(hideSenior: true), tournamentBuild: nil)
} }
} }
} }

@ -18,7 +18,7 @@ struct TournamentLookUpView: View {
@State private var searchField: String = "" @State private var searchField: String = ""
@State var page: Int = 0 @State var page: Int = 0
@State var total: Int = 0 @State var total: Int = 0
@State private var showingSettingsAlert = false
@State private var searching: Bool = false @State private var searching: Bool = false
@State private var requestedToGetAllPages: Bool = false @State private var requestedToGetAllPages: Bool = false
@State private var revealSearchParameters: Bool = true @State private var revealSearchParameters: Bool = true
@ -57,6 +57,16 @@ struct TournamentLookUpView: View {
} message: { } 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.") 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(isPresented: $showingSettingsAlert) {
Alert(
title: Text("Réglages"),
message: Text("Pour trouver les clubs autour de vous, vous devez l'autorisation à Padel Club de récupérer votre position."),
primaryButton: .default(Text("Ouvrir les réglages"), action: {
_openSettings()
}),
secondaryButton: .cancel()
)
}
.alert("Attention", isPresented: $presentAlert, actions: { .alert("Attention", isPresented: $presentAlert, actions: {
Button { Button {
presentAlert = false presentAlert = false
@ -305,10 +315,16 @@ struct TournamentLookUpView: View {
} }
if locationManager.requestStarted { if locationManager.requestStarted {
ProgressView() ProgressView()
} else { } else if locationManager.manager.authorizationStatus != .restricted {
LocationButton { LocationButton {
if locationManager.manager.authorizationStatus == .notDetermined {
locationManager.manager.requestWhenInUseAuthorization()
} else if locationManager.manager.authorizationStatus == .denied {
showingSettingsAlert = true
} else {
locationManager.requestLocation() locationManager.requestLocation()
} }
}
.symbolVariant(.fill) .symbolVariant(.fill)
.foregroundColor (Color.white) .foregroundColor (Color.white)
.cornerRadius (20) .cornerRadius (20)
@ -485,4 +501,12 @@ struct TournamentLookUpView: View {
return "Distance" return "Distance"
} }
} }
private func _openSettings() {
guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else {
return
}
UIApplication.shared.open(settingsURL)
}
} }

@ -6,16 +6,21 @@
// //
import SwiftUI import SwiftUI
import LeStorage
import TipKit
struct PlanningView: View { struct PlanningView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament @Environment(Tournament.self) var tournament: Tournament
@State private var selectedDay: Date? @State private var selectedDay: Date?
@Binding var selectedScheduleDestination: ScheduleDestination? @Binding var selectedScheduleDestination: ScheduleDestination?
@State private var filterOption: PlanningFilterOption = .byDefault @State private var filterOption: PlanningFilterOption = .byDefault
@State private var showFinishedMatches: Bool = false @State private var showFinishedMatches: Bool = false
@State private var enableMove: Bool = false
let allMatches: [Match] let allMatches: [Match]
let timeSlotMoveOptionTip = TimeSlotMoveOptionTip()
init(matches: [Match], selectedScheduleDestination: Binding<ScheduleDestination?>) { init(matches: [Match], selectedScheduleDestination: Binding<ScheduleDestination?>) {
self.allMatches = matches self.allMatches = matches
@ -38,22 +43,6 @@ struct PlanningView: View {
timeSlots.keys.sorted() timeSlots.keys.sorted()
} }
enum PlanningFilterOption: Int, CaseIterable, Identifiable {
var id: Int { self.rawValue }
case byDefault
case byCourt
func localizedPlanningLabel() -> String {
switch self {
case .byCourt:
return "Par terrain"
case .byDefault:
return "Par ordre des matchs"
}
}
}
private func _computedTitle(days: [Date]) -> String { private func _computedTitle(days: [Date]) -> String {
if let selectedDay { if let selectedDay {
return selectedDay.formatted(.dateTime.day().weekday().month()) return selectedDay.formatted(.dateTime.day().weekday().month())
@ -71,8 +60,13 @@ struct PlanningView: View {
let keys = self.keys(timeSlots: timeSlots) let keys = self.keys(timeSlots: timeSlots)
let days = self.days(timeSlots: timeSlots) let days = self.days(timeSlots: timeSlots)
let matches = matches let matches = matches
BySlotView(days: days, keys: keys, timeSlots: timeSlots, matches: matches, selectedDay: selectedDay, filterOption: filterOption, showFinishedMatches: showFinishedMatches) let notSlots = matches.allSatisfy({ $0.startDate == nil })
BySlotView(days: days, keys: keys, timeSlots: timeSlots, matches: matches, selectedDay: selectedDay)
.environment(\.filterOption, filterOption)
.environment(\.showFinishedMatches, showFinishedMatches)
.environment(\.enableMove, enableMove)
.navigationTitle(Text(_computedTitle(days: days))) .navigationTitle(Text(_computedTitle(days: days)))
.navigationBarBackButtonHidden(enableMove)
.toolbar(content: { .toolbar(content: {
if days.count > 1 { if days.count > 1 {
ToolbarTitleMenu { ToolbarTitleMenu {
@ -89,11 +83,41 @@ struct PlanningView: View {
Text("Jour") Text("Jour")
} }
.pickerStyle(.automatic) .pickerStyle(.automatic)
.disabled(enableMove)
}
}
if enableMove {
ToolbarItem(placement: .topBarLeading) {
Button("Annuler") {
enableMove = false
}
}
ToolbarItem(placement: .topBarTrailing) {
Button("Sauver") {
do {
try self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: allMatches)
} catch {
Logger.error(error)
}
enableMove = false
} }
} }
} else {
ToolbarItemGroup(placement: .topBarTrailing) { ToolbarItemGroup(placement: .topBarTrailing) {
if notSlots == false {
Toggle(isOn: $enableMove) {
Label("Déplacer", systemImage: "rectangle.2.swap")
}
.popoverTip(timeSlotMoveOptionTip)
}
Menu { Menu {
Section {
Picker(selection: $showFinishedMatches) { Picker(selection: $showFinishedMatches) {
Text("Afficher tous les matchs").tag(true) Text("Afficher tous les matchs").tag(true)
Text("Masquer les matchs terminés").tag(false) Text("Masquer les matchs terminés").tag(false)
@ -102,11 +126,13 @@ struct PlanningView: View {
} }
.labelsHidden() .labelsHidden()
.pickerStyle(.inline) .pickerStyle(.inline)
} label: { } header: {
Label("Filtrer", systemImage: "clock.badge.checkmark") Text("Option de filtrage")
.symbolVariant(showFinishedMatches ? .fill : .none)
} }
Menu {
Divider()
Section {
Picker(selection: $filterOption) { Picker(selection: $filterOption) {
ForEach(PlanningFilterOption.allCases) { ForEach(PlanningFilterOption.allCases) {
Text($0.localizedPlanningLabel()).tag($0) Text($0.localizedPlanningLabel()).tag($0)
@ -116,15 +142,20 @@ struct PlanningView: View {
} }
.labelsHidden() .labelsHidden()
.pickerStyle(.inline) .pickerStyle(.inline)
} header: {
Text("Option de triage")
}
} label: { } label: {
Label("Trier", systemImage: "line.3.horizontal.decrease.circle") Label("Trier", systemImage: "line.3.horizontal.decrease.circle")
.symbolVariant(filterOption == .byCourt ? .fill : .none) .symbolVariant(filterOption == .byCourt || showFinishedMatches ? .fill : .none)
} }
} }
}
}) })
.overlay { .overlay {
if matches.allSatisfy({ $0.startDate == nil }) { if notSlots {
ContentUnavailableView { ContentUnavailableView {
Label("Aucun horaire défini", systemImage: "clock.badge.questionmark") Label("Aucun horaire défini", systemImage: "clock.badge.questionmark")
} description: { } description: {
@ -140,28 +171,146 @@ struct PlanningView: View {
struct BySlotView: View { struct BySlotView: View {
@Environment(Tournament.self) var tournament: Tournament @Environment(Tournament.self) var tournament: Tournament
@Environment(\.filterOption) private var filterOption
@Environment(\.showFinishedMatches) private var showFinishedMatches
@Environment(\.enableMove) private var enableMove
let days: [Date] let days: [Date]
let keys: [Date] let keys: [Date]
let timeSlots: [Date:[Match]] let timeSlots: [Date: [Match]]
let matches: [Match] let matches: [Match]
let selectedDay: Date? let selectedDay: Date?
let filterOption: PlanningFilterOption let timeSlotMoveTip = TimeSlotMoveTip()
let showFinishedMatches: Bool
var body: some View { var body: some View {
List { List {
if matches.allSatisfy({ $0.startDate == nil }) == false {
if enableMove {
TipView(timeSlotMoveTip)
.tipStyle(tint: .logoYellow, asSection: true)
}
if !matches.allSatisfy({ $0.startDate == nil }) {
ForEach(days.filter({ selectedDay == nil || selectedDay == $0 }), id: \.self) { day in ForEach(days.filter({ selectedDay == nil || selectedDay == $0 }), id: \.self) { day in
DaySectionView(
day: day,
keys: keys.filter({ $0.dayInt == day.dayInt }),
timeSlots: timeSlots,
selectedDay: selectedDay
)
}
}
}
}
}
struct DaySectionView: View {
@Environment(Tournament.self) var tournament: Tournament
@Environment(\.filterOption) private var filterOption
@Environment(\.showFinishedMatches) private var showFinishedMatches
@Environment(\.enableMove) private var enableMove
let day: Date
let keys: [Date]
let timeSlots: [Date: [Match]]
let selectedDay: Date?
var body: some View {
Section { Section {
ForEach(keys.filter({ $0.dayInt == day.dayInt }), id: \.self) { key in ForEach(keys, id: \.self) { key in
if let _matches = timeSlots[key]?.sorted(by: filterOption == .byDefault ? \.computedOrder : \.courtIndexForSorting) { TimeSlotSectionView(
key: key,
matches: timeSlots[key]?.sorted(by: filterOption == .byDefault ? \.computedOrder : \.courtIndexForSorting) ?? []
)
}
.onMove(perform: enableMove ? moveSection : nil)
} header: {
HeaderView(day: day, timeSlots: timeSlots)
} footer: {
if day.monthYearFormatted == Date.distantFuture.monthYearFormatted {
Text("Il s'agit des matchs qui n'ont pas réussi à être placé par Padel Club. Peut-être à cause de créneaux indisponibles, d'autres tournois ou des réglages.")
}
}
}
func moveSection(from source: IndexSet, to destination: Int) {
let daySlots = keys.filter { $0.dayInt == day.dayInt }.sorted()
guard let sourceIdx = source.first,
sourceIdx < daySlots.count,
destination <= daySlots.count else {
return
}
// Create a mutable copy of the time slots for this day
var slotsToUpdate = daySlots
let updateRange = min(sourceIdx, destination)...max(sourceIdx, destination)
// Perform the move in the array
let sourceTime = slotsToUpdate.remove(at: sourceIdx)
if sourceIdx < destination {
slotsToUpdate.insert(sourceTime, at: destination - 1)
} else {
slotsToUpdate.insert(sourceTime, at: destination)
}
// Update matches by swapping their startDates
for index in updateRange {
// Find the new time slot for these matches
let oldStartTime = slotsToUpdate[index]
let newStartTime = daySlots[index]
guard let matchesToUpdate = timeSlots[oldStartTime] else { continue }
// Update each match with the new start time
for match in matchesToUpdate {
match.startDate = newStartTime
}
}
}
}
struct TimeSlotSectionView: View {
@Environment(\.enableMove) private var enableMove
let key: Date
let matches: [Match]
var body: some View {
if !matches.isEmpty {
if enableMove {
TimeSlotHeaderView(key: key, matches: matches)
} else {
DisclosureGroup { DisclosureGroup {
ForEach(_matches) { match in MatchListView(matches: matches)
} label: {
TimeSlotHeaderView(key: key, matches: matches)
}
}
}
}
}
struct MatchListView: View {
let matches: [Match]
var body: some View {
ForEach(matches) { match in
NavigationLink { NavigationLink {
MatchDetailView(match: match) MatchDetailView(match: match)
.matchViewStyle(.sectionedStandardStyle) .matchViewStyle(.sectionedStandardStyle)
} label: { } label: {
MatchRowView(match: match)
}
}
}
}
struct MatchRowView: View {
let match: Match
var body: some View {
LabeledContent { LabeledContent {
if let courtName = match.courtName() { if let courtName = match.courtName() {
Text(courtName) Text(courtName)
@ -176,12 +325,17 @@ struct PlanningView: View {
} }
} }
} }
} label: {
_timeSlotView(key: key, matches: _matches)
} struct HeaderView: View {
} @Environment(\.filterOption) private var filterOption
} @Environment(\.showFinishedMatches) private var showFinishedMatches
} header: { @Environment(\.enableMove) private var enableMove
let day: Date
let timeSlots: [Date: [Match]]
var body: some View {
HStack { HStack {
if day.monthYearFormatted == Date.distantFuture.monthYearFormatted { if day.monthYearFormatted == Date.distantFuture.monthYearFormatted {
Text("Sans horaire") Text("Sans horaire")
@ -191,35 +345,39 @@ struct PlanningView: View {
Spacer() Spacer()
let count = _matchesCount(inDayInt: day.dayInt, timeSlots: timeSlots) let count = _matchesCount(inDayInt: day.dayInt, timeSlots: timeSlots)
if showFinishedMatches { if showFinishedMatches {
Text(self._formattedMatchCount(count)) Text(_formattedMatchCount(count))
} else { } else {
Text(self._formattedMatchCount(count) + " restant\(count.pluralSuffix)") Text("\(_formattedMatchCount(count)) restant\(count.pluralSuffix)")
} }
} }
} footer: {
if day.monthYearFormatted == Date.distantFuture.monthYearFormatted {
Text("Il s'agit des matchs qui n'ont pas réussi à être placé par Padel Club. Peut-être à cause de créneaux indisponibles, d'autres tournois ou des réglages.")
}
}
.headerProminence(.increased)
} }
private func _matchesCount(inDayInt dayInt: Int, timeSlots: [Date: [Match]]) -> Int {
timeSlots.filter { $0.key.dayInt == dayInt }.flatMap({ $0.value }).count
} }
private func _formattedMatchCount(_ count: Int) -> String {
return "\(count.formatted()) match\(count.pluralSuffix)"
} }
} }
private func _matchesCount(inDayInt dayInt: Int, timeSlots: [Date:[Match]]) -> Int { struct TimeSlotHeaderView: View {
timeSlots.filter { $0.key.dayInt == dayInt }.flatMap({ $0.value }).count let key: Date
} let matches: [Match]
@Environment(Tournament.self) var tournament: Tournament
private func _timeSlotView(key: Date, matches: [Match]) -> some View { var body: some View {
LabeledContent { LabeledContent {
Text(self._formattedMatchCount(matches.count)) Text("\(matches.count.formatted()) match\(matches.count.pluralSuffix)")
} label: { } label: {
if key.monthYearFormatted == Date.distantFuture.monthYearFormatted { if key.monthYearFormatted == Date.distantFuture.monthYearFormatted {
Text("Aucun horaire") Text("Aucun horaire")
} else { } else {
Text(key.formatted(date: .omitted, time: .shortened)).font(.title).fontWeight(.semibold) Text(key.formatted(date: .omitted, time: .shortened))
.font(.title)
.fontWeight(.semibold)
} }
if matches.count <= tournament.courtCount { if matches.count <= tournament.courtCount {
let names = matches.sorted(by: \.computedOrder) let names = matches.sorted(by: \.computedOrder)
.compactMap({ $0.roundTitle() }) .compactMap({ $0.roundTitle() })
@ -232,15 +390,203 @@ struct PlanningView: View {
} else { } else {
Text(matches.count.formatted().appending(" matchs")) Text(matches.count.formatted().appending(" matchs"))
} }
}
} }
} }
fileprivate func _formattedMatchCount(_ count: Int) -> String {
return "\(count.formatted()) match\(count.pluralSuffix)" // struct BySlotView: View {
// @Environment(Tournament.self) var tournament: Tournament
// let days: [Date]
// let keys: [Date]
// let timeSlots: [Date:[Match]]
// let matches: [Match]
// let selectedDay: Date?
// let filterOption: PlanningFilterOption
// let showFinishedMatches: Bool
//
// var body: some View {
// List {
// if matches.allSatisfy({ $0.startDate == nil }) == false {
// ForEach(days.filter({ selectedDay == nil || selectedDay == $0 }), id: \.self) { day in
// Section {
// ForEach(keys.filter({ $0.dayInt == day.dayInt }), id: \.self) { key in
// if let _matches = timeSlots[key]?.sorted(by: filterOption == .byDefault ? \.computedOrder : \.courtIndexForSorting) {
// DisclosureGroup {
// ForEach(_matches) { match in
// NavigationLink {
// MatchDetailView(match: match)
// .matchViewStyle(.sectionedStandardStyle)
//
// } label: {
// LabeledContent {
// if let courtName = match.courtName() {
// Text(courtName)
// }
// } label: {
// if let groupStage = match.groupStageObject {
// Text(groupStage.groupStageTitle(.title))
// } else if let round = match.roundObject {
// Text(round.roundTitle())
// }
// Text(match.matchTitle())
// }
// }
// }
// } label: {
// _timeSlotView(key: key, matches: _matches)
// }
// }
// }
// .onMove(perform: moveSection)
// } header: {
// HStack {
// if day.monthYearFormatted == Date.distantFuture.monthYearFormatted {
// Text("Sans horaire")
// } else {
// Text(day.formatted(.dateTime.day().weekday().month()))
// }
// Spacer()
// let count = _matchesCount(inDayInt: day.dayInt, timeSlots: timeSlots)
// if showFinishedMatches {
// Text(self._formattedMatchCount(count))
// } else {
// Text(self._formattedMatchCount(count) + " restant\(count.pluralSuffix)")
// }
// }
// } footer: {
// if day.monthYearFormatted == Date.distantFuture.monthYearFormatted {
// Text("Il s'agit des matchs qui n'ont pas réussi à être placé par Padel Club. Peut-être à cause de créneaux indisponibles, d'autres tournois ou des réglages.")
// }
// }
// .headerProminence(.increased)
// }
// }
// }
// }
//
// func moveSection(from source: IndexSet, to destination: Int) {
// let daySlots = keys.filter { selectedDay == nil || $0.dayInt == selectedDay?.dayInt }.sorted()
//
// guard let sourceIdx = source.first,
// sourceIdx < daySlots.count,
// destination <= daySlots.count else {
// return
// }
//
// // Create a mutable copy of the time slots for this day
// var slotsToUpdate = daySlots
//
// let updateRange = min(sourceIdx, destination)...max(sourceIdx, destination) - 1
// print(updateRange)
//
// // Perform the move in the array
// let sourceTime = slotsToUpdate.remove(at: sourceIdx)
// if sourceIdx < destination {
// slotsToUpdate.insert(sourceTime, at: destination - 1)
// } else {
// slotsToUpdate.insert(sourceTime, at: destination)
// }
//
// // Update matches by swapping their startDates
// for index in updateRange {
// // Find the new time slot for these matches
// let oldStartTime = slotsToUpdate[index]
// let newStartTime = daySlots[index]
// guard let matchesToUpdate = timeSlots[oldStartTime] else { continue }
// print("moving", oldStartTime, "to", newStartTime)
//
// // Update each match with the new start time
// for match in matchesToUpdate {
// match.startDate = newStartTime
// }
// }
//
// try? self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: matches)
// }
//
//
// private func _matchesCount(inDayInt dayInt: Int, timeSlots: [Date:[Match]]) -> Int {
// timeSlots.filter { $0.key.dayInt == dayInt }.flatMap({ $0.value }).count
// }
//
// private func _timeSlotView(key: Date, matches: [Match]) -> some View {
// LabeledContent {
// Text(self._formattedMatchCount(matches.count))
// } label: {
// if key.monthYearFormatted == Date.distantFuture.monthYearFormatted {
// Text("Aucun horaire")
// } else {
// Text(key.formatted(date: .omitted, time: .shortened)).font(.title).fontWeight(.semibold)
// }
// if matches.count <= tournament.courtCount {
// let names = matches.sorted(by: \.computedOrder)
// .compactMap({ $0.roundTitle() })
// .reduce(into: [String]()) { uniqueNames, name in
// if !uniqueNames.contains(name) {
// uniqueNames.append(name)
// }
// }
// Text(names.joined(separator: ", ")).lineLimit(1).truncationMode(.tail)
// } else {
// Text(matches.count.formatted().appending(" matchs"))
// }
// }
// }
//
// fileprivate func _formattedMatchCount(_ count: Int) -> String {
// return "\(count.formatted()) match\(count.pluralSuffix)"
// }
// }
}
enum PlanningFilterOption: Int, CaseIterable, Identifiable {
var id: Int { self.rawValue }
case byDefault
case byCourt
func localizedPlanningLabel() -> String {
switch self {
case .byCourt:
return "Par terrain"
case .byDefault:
return "Par ordre des matchs"
}
}
}
struct FilterOptionKey: EnvironmentKey {
static let defaultValue: PlanningFilterOption = .byDefault
}
extension EnvironmentValues {
var filterOption: PlanningFilterOption {
get { self[FilterOptionKey.self] }
set { self[FilterOptionKey.self] = newValue }
} }
}
struct ShowFinishedMatchesKey: EnvironmentKey {
static let defaultValue: Bool = false
}
extension EnvironmentValues {
var showFinishedMatches: Bool {
get { self[ShowFinishedMatchesKey.self] }
set { self[ShowFinishedMatchesKey.self] = newValue }
} }
} }
//#Preview { struct EnableMoveKey: EnvironmentKey {
// PlanningView(matches: [], selectedScheduleDestination: .constant(nil)) static let defaultValue: Bool = false
//} }
extension EnvironmentValues {
var enableMove: Bool {
get { self[EnableMoveKey.self] }
set { self[EnableMoveKey.self] = newValue }
}
}

@ -152,10 +152,6 @@ struct PlayerDetailView: View {
Menu { Menu {
CopyPasteButtonView(pasteValue: player.phoneNumber) CopyPasteButtonView(pasteValue: player.phoneNumber)
PasteButtonView(text: $phoneNumber) PasteButtonView(text: $phoneNumber)
.onChange(of: phoneNumber) {
player.phoneNumber = phoneNumber.prefixTrimmed(50)
_save()
}
} label: { } label: {
Text("Téléphone") Text("Téléphone")
} }
@ -177,10 +173,6 @@ struct PlayerDetailView: View {
Menu { Menu {
CopyPasteButtonView(pasteValue: player.email) CopyPasteButtonView(pasteValue: player.email)
PasteButtonView(text: $email) PasteButtonView(text: $email)
.onChange(of: email) {
player.email = email.prefixTrimmed(50)
_save()
}
} label: { } label: {
Text("Email") Text("Email")
} }
@ -216,13 +208,6 @@ struct PlayerDetailView: View {
} }
} }
} }
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
ShareLink(item: player.pasteData()) {
Label("Partager", systemImage: "square.and.arrow.up")
}
}
}
.onChange(of: player.hasArrived) { .onChange(of: player.hasArrived) {
_save() _save()
} }
@ -230,7 +215,17 @@ struct PlayerDetailView: View {
_save() _save()
} }
.navigationBarBackButtonHidden(focusedField != nil) .navigationBarBackButtonHidden(focusedField != nil)
.toolbar(content: { .headerProminence(.increased)
.navigationTitle("Édition")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
ShareLink(item: player.pasteData()) {
Label("Partager", systemImage: "square.and.arrow.up")
}
}
if focusedField != nil { if focusedField != nil {
ToolbarItem(placement: .topBarLeading) { ToolbarItem(placement: .topBarLeading) {
Button("Annuler", role: .cancel) { Button("Annuler", role: .cancel) {
@ -238,14 +233,9 @@ struct PlayerDetailView: View {
} }
} }
} }
}) if focusedField == ._rank || focusedField == ._computedRank || focusedField == ._phoneNumber {
.headerProminence(.increased) ToolbarItemGroup(placement: .keyboard) {
.navigationTitle("Édition") Spacer()
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.toolbar {
if focusedField == ._rank || focusedField == ._computedRank {
ToolbarItem(placement: .keyboard) {
Button("Valider") { Button("Valider") {
if focusedField == ._rank { if focusedField == ._rank {
player.setComputedRank(in: tournament) player.setComputedRank(in: tournament)
@ -254,6 +244,9 @@ struct PlayerDetailView: View {
} else if focusedField == ._computedRank { } else if focusedField == ._computedRank {
player.team()?.updateWeight(inTournamentCategory: tournament.tournamentCategory) player.team()?.updateWeight(inTournamentCategory: tournament.tournamentCategory)
_save() _save()
} else if focusedField == ._phoneNumber {
player.phoneNumber = phoneNumber.prefixTrimmed(50)
_save()
} }
focusedField = nil focusedField = nil
} }

@ -20,7 +20,6 @@ struct EditingTeamView: View {
@State private var sentError: ContactManagerError? = nil @State private var sentError: ContactManagerError? = nil
@State private var showSubscriptionView: Bool = false @State private var showSubscriptionView: Bool = false
@State private var registrationDate : Date @State private var registrationDate : Date
@State private var callDate : Date
@State private var name: String @State private var name: String
@FocusState private var focusedField: TeamRegistration.CodingKeys? @FocusState private var focusedField: TeamRegistration.CodingKeys?
@ -42,7 +41,6 @@ struct EditingTeamView: View {
self.team = team self.team = team
_name = .init(wrappedValue: team.name ?? "") _name = .init(wrappedValue: team.name ?? "")
_registrationDate = State(wrappedValue: team.registrationDate ?? Date()) _registrationDate = State(wrappedValue: team.registrationDate ?? Date())
_callDate = State(wrappedValue: team.callDate ?? Date())
} }
var body: some View { var body: some View {

@ -9,7 +9,7 @@ import SwiftUI
struct InscriptionInfoView: View { struct InscriptionInfoView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament let tournament: Tournament
@State private var players : [PlayerRegistration] = [] @State private var players : [PlayerRegistration] = []
@State private var selectedTeams : [TeamRegistration] = [] @State private var selectedTeams : [TeamRegistration] = []
@ -81,6 +81,7 @@ struct InscriptionInfoView: View {
DisclosureGroup { DisclosureGroup {
ForEach(callDateIssue) { team in ForEach(callDateIssue) { team in
TeamCallView(team: team) TeamCallView(team: team)
.environment(tournament)
} }
} label: { } label: {
LabeledContent { LabeledContent {
@ -244,16 +245,17 @@ struct InscriptionInfoView: View {
Text("importé du fichier beach-padel sans licence valide ou créé sans licence") Text("importé du fichier beach-padel sans licence valide ou créé sans licence")
} }
} }
.task { .onAppear {
await _getIssues() DispatchQueue.main.async {
_getIssues()
}
} }
.navigationTitle("Synthèse") .navigationTitle("Synthèse")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
} }
private func _getIssues() async { private func _getIssues() {
Task {
players = tournament.unsortedPlayers() players = tournament.unsortedPlayers()
selectedTeams = tournament.selectedSortedTeams() selectedTeams = tournament.selectedSortedTeams()
callDateIssue = selectedTeams.filter { $0.callDate != nil && tournament.isStartDateIsDifferentThanCallDate($0) } callDateIssue = selectedTeams.filter { $0.callDate != nil && tournament.isStartDateIsDifferentThanCallDate($0) }
@ -268,7 +270,6 @@ struct InscriptionInfoView: View {
entriesFromBeachPadel = tournament.unsortedTeams().filter({ $0.isImported() }) entriesFromBeachPadel = tournament.unsortedTeams().filter({ $0.isImported() })
playersMissing = selectedTeams.filter({ $0.unsortedPlayers().count < 2 }) playersMissing = selectedTeams.filter({ $0.unsortedPlayers().count < 2 })
} }
}
} }
//#Preview { //#Preview {

@ -43,30 +43,29 @@ struct UpdateSourceRankDateView: View {
Task { Task {
do { do {
try await tournament.updateRank(to: currentRankSourceDate) try await tournament.updateRank(to: currentRankSourceDate)
try await MainActor.run { let unsortedPlayers = tournament.unsortedPlayers()
tournament.unsortedPlayers().forEach { player in tournament.unsortedPlayers().forEach { player in
player.setComputedRank(in: tournament) player.setComputedRank(in: tournament)
} }
try tournamentStore.playerRegistrations.addOrUpdate(contentOfs: tournament.unsortedPlayers()) try tournamentStore.playerRegistrations.addOrUpdate(contentOfs: unsortedPlayers)
tournament.unsortedTeams().forEach { team in let unsortedTeams = tournament.unsortedTeams()
unsortedTeams.forEach { team in
team.setWeight(from: team.players(), inTournamentCategory: tournament.tournamentCategory) team.setWeight(from: team.players(), inTournamentCategory: tournament.tournamentCategory)
if forceRefreshLockWeight { if forceRefreshLockWeight {
team.lockedWeight = team.weight team.lockedWeight = team.weight
} }
} }
try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams()) try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams)
try dataStore.tournaments.addOrUpdate(instance: tournament) try dataStore.tournaments.addOrUpdate(instance: tournament)
updatingRank = false
confirmUpdateRank = false
}
} catch { } catch {
Logger.error(error) Logger.error(error)
} }
updatingRank = false
confirmUpdateRank = false
} }
}.disabled(updatingRank) }.disabled(updatingRank)

@ -173,8 +173,8 @@ struct InscriptionManagerView: View {
self.teamsHash = _simpleHash(ids: selectedSortedTeams.map { $0.id }) self.teamsHash = _simpleHash(ids: selectedSortedTeams.map { $0.id })
} }
self.registrationIssues = nil self.registrationIssues = nil
Task { DispatchQueue.main.async {
self.registrationIssues = await tournament.registrationIssues() self.registrationIssues = tournament.registrationIssues()
} }
} }
@ -718,14 +718,7 @@ struct InscriptionManagerView: View {
if tournament.isAnimation() == false { if tournament.isAnimation() == false {
NavigationLink { NavigationLink {
InscriptionInfoView() InscriptionInfoView(tournament: tournament)
.environment(tournament)
.onDisappear {
self.registrationIssues = nil
Task {
self.registrationIssues = await tournament.registrationIssues()
}
}
} label: { } label: {
LabeledContent { LabeledContent {
if let registrationIssues { if let registrationIssues {

Loading…
Cancel
Save