Raz 1 year ago
parent 640de8a8d0
commit 2f14a16852
  1. 18
      PadelClub.xcodeproj/project.pbxproj
  2. 4
      PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift
  3. 13
      PadelClub/Data/Federal/FederalTournament.swift
  4. 23
      PadelClub/Data/Round.swift
  5. 2
      PadelClub/Data/Tournament.swift
  6. 3
      PadelClub/Utils/ContactManager.swift
  7. 3
      PadelClub/Views/Match/MatchSetupView.swift
  8. 130
      PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift
  9. 105
      PadelClub/Views/Planning/Components/DateUpdateManagerView.swift
  10. 2
      PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift
  11. 13
      PadelClub/Views/Planning/LoserRoundStepScheduleEditorView.swift
  12. 4
      PadelClub/Views/Planning/MatchScheduleEditorView.swift
  13. 2
      PadelClub/Views/Team/EditingTeamView.swift
  14. 10
      PadelClub/Views/Team/TeamPickerView.swift
  15. 125
      PadelClub/Views/Tournament/Screen/AddTeamView.swift
  16. 16
      PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift
  17. 6
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift

@ -2546,7 +2546,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 = 5; 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\"";
@ -2556,6 +2556,7 @@
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club"; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports";
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_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_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
@ -2568,7 +2569,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.8; MARKETING_VERSION = 1.0.9;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -2588,7 +2589,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 = 5; 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;
@ -2597,6 +2598,7 @@
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club"; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports";
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_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_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
@ -2609,7 +2611,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.8; MARKETING_VERSION = 1.0.9;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -2701,7 +2703,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 = 4; 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\"";
@ -2723,7 +2725,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.8; MARKETING_VERSION = 1.0.9;
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 = "";
@ -2743,7 +2745,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 = 4; 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;
@ -2764,7 +2766,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.8; MARKETING_VERSION = 1.0.9;
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 = "";

@ -70,6 +70,10 @@ extension ImportedPlayer: PlayerHolder {
} }
} }
func contains(_ searchField: String) -> Bool {
firstName?.localizedCaseInsensitiveContains(searchField) == true || lastName?.localizedCaseInsensitiveContains(searchField) == true
}
func hitForSearch(_ searchText: String) -> Int { func hitForSearch(_ searchText: String) -> Int {
var trimmedSearchText = searchText.lowercased().trimmingCharacters(in: .whitespaces).folding(options: .diacriticInsensitive, locale: .current) var trimmedSearchText = searchText.lowercased().trimmingCharacters(in: .whitespaces).folding(options: .diacriticInsensitive, locale: .current)
trimmedSearchText = trimmedSearchText.replaceCharactersFromSet(characterSet: .punctuationCharacters, replacementString: " ") trimmedSearchText = trimmedSearchText.replaceCharactersFromSet(characterSet: .punctuationCharacters, replacementString: " ")

@ -151,6 +151,19 @@ struct FederalTournament: Identifiable, Codable {
[libelle, dateDebut?.formatted(date: .complete, time: .omitted)].compactMap({$0}).joined(separator: "\n") + "\n" [libelle, dateDebut?.formatted(date: .complete, time: .omitted)].compactMap({$0}).joined(separator: "\n") + "\n"
} }
var sharePartnerMessage: String {
["Je nous ai inscris au tournoi suivant : ",
libelle,
dateDebut?.formatted(date: .complete, time: .omitted),
"message preparé par Padel Club",
URLs.appStore.rawValue
].compactMap({$0}).joined(separator: "\n") + "\n"
}
func calendarNoteMessage() -> String {
[jugeArbitre?.nom, jugeArbitre?.prenom, courrielEngagement, installation?.telephone].compactMap({$0}).joined(separator: "\n")
}
var japMessage: String { var japMessage: String {
[nomClub, jugeArbitre?.nom, jugeArbitre?.prenom, courrielEngagement, installation?.telephone].compactMap({$0}).joined(separator: ";") [nomClub, jugeArbitre?.nom, jugeArbitre?.prenom, courrielEngagement, installation?.telephone].compactMap({$0}).joined(separator: ";")
} }

@ -154,6 +154,11 @@ final class Round: ModelObject, Storable {
return teamIds.compactMap { self.tournamentStore.teamRegistrations.findById($0) } return teamIds.compactMap { self.tournamentStore.teamRegistrations.findById($0) }
} }
func winners() -> [TeamRegistration] {
let teamIds: [String] = self._matches().compactMap { $0.winningTeamId }
return teamIds.compactMap { self.tournamentStore.teamRegistrations.findById($0) }
}
func teams() -> [TeamRegistration] { func teams() -> [TeamRegistration] {
return playedMatches().flatMap({ $0.teams() }) return playedMatches().flatMap({ $0.teams() })
} }
@ -186,13 +191,13 @@ defer {
case .two: case .two:
if let luckyLoser = match.teamScores.first(where: { $0.luckyLoser == match.index * 2 + 1 }) { if let luckyLoser = match.teamScores.first(where: { $0.luckyLoser == match.index * 2 + 1 }) {
return luckyLoser.team return luckyLoser.team
} else if groupStageLoserBracket == false, let previousMatch = bottomPreviousRoundMatch(ofMatch: match, previousRound: previousRound) { } else if let previousMatch = bottomPreviousRoundMatch(ofMatch: match, previousRound: previousRound) {
if let teamId = previousMatch.winningTeamId { if let teamId = previousMatch.winningTeamId {
return self.tournamentStore.teamRegistrations.findById(teamId) return self.tournamentStore.teamRegistrations.findById(teamId)
} else if previousMatch.disabled { } else if previousMatch.disabled {
return previousMatch.teams().first return previousMatch.teams().first
} }
} else if groupStageLoserBracket == false, let parent = upperBracketBottomMatch(ofMatchIndex: match.index, previousRound: previousRound)?.losingTeamId { } else if let parent = upperBracketBottomMatch(ofMatchIndex: match.index, previousRound: previousRound)?.losingTeamId {
return tournamentStore.findById(parent) return tournamentStore.findById(parent)
} }
} }
@ -208,8 +213,13 @@ defer {
print("func upperBracketTopMatch", matchIndex, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) print("func upperBracketTopMatch", matchIndex, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
} }
#endif #endif
let parentRound = parentRound
if parentRound?.parent == nil, groupStageLoserBracket == false, tournamentObject()?.automaticLoserBracket == false {
return nil
}
let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex)
if isLoserBracket(), previousRound == nil, let parentRound = parentRound, let upperBracketTopMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2) { if isLoserBracket(), previousRound == nil, let upperBracketTopMatch = parentRound?.getMatch(atMatchIndexInRound: indexInRound * 2) {
return upperBracketTopMatch return upperBracketTopMatch
} }
return nil return nil
@ -224,8 +234,13 @@ defer {
} }
#endif #endif
let parentRound = parentRound
if parentRound?.parent == nil, groupStageLoserBracket == false, tournamentObject()?.automaticLoserBracket == false {
return nil
}
let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex)
if isLoserBracket(), previousRound == nil, let parentRound = parentRound, let upperBracketBottomMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2 + 1) { if isLoserBracket(), previousRound == nil, let upperBracketBottomMatch = parentRound?.getMatch(atMatchIndexInRound: indexInRound * 2 + 1) {
return upperBracketBottomMatch return upperBracketBottomMatch
} }
return nil return nil

@ -57,6 +57,7 @@ final class Tournament : ModelObject, Storable {
var publishTournament: Bool = false var publishTournament: Bool = false
var hidePointsEarned: Bool = false var hidePointsEarned: Bool = false
var publishRankings: Bool = false var publishRankings: Bool = false
var automaticLoserBracket: Bool = true
@ObservationIgnored @ObservationIgnored
var navigationPath: [Screen] = [] var navigationPath: [Screen] = []
@ -2189,3 +2190,4 @@ extension Tournament {
} }
} }

@ -15,6 +15,9 @@ enum ContactManagerError: LocalizedError {
case mailNotSent //no network no error case mailNotSent //no network no error
case messageFailed case messageFailed
case messageNotSent //no network no error case messageNotSent //no network no error
case calendarAccessDenied
case calendarEventSaveFailed
case noCalendarAvailable
} }
enum ContactType: Identifiable { enum ContactType: Identifiable {

@ -63,7 +63,7 @@ struct MatchSetupView: View {
} }
HStack { HStack {
let luckyLosers = walkOutSpot ? match.luckyLosers() : [] let luckyLosers = walkOutSpot ? match.luckyLosers() : []
TeamPickerView(shouldConfirm: shouldConfirm, groupStagePosition: nil, matchTypeContext: matchTypeContext, luckyLosers: luckyLosers, teamPicked: { team in TeamPickerView(shouldConfirm: shouldConfirm, round: match.roundObject, matchTypeContext: matchTypeContext, luckyLosers: luckyLosers, teamPicked: { team in
print(team.pasteData()) print(team.pasteData())
if walkOutSpot || team.bracketPosition != nil || matchTypeContext == .loserBracket { if walkOutSpot || team.bracketPosition != nil || matchTypeContext == .loserBracket {
match.setLuckyLoser(team: team, teamPosition: teamPosition) match.setLuckyLoser(team: team, teamPosition: teamPosition)
@ -86,6 +86,7 @@ struct MatchSetupView: View {
} }
} }
}) })
.environment(match)
if matchTypeContext == .bracket, let tournament = match.currentTournament() { if matchTypeContext == .bracket, let tournament = match.currentTournament() {
let availableQualifiedTeams = tournament.availableQualifiedTeams() let availableQualifiedTeams = tournament.availableQualifiedTeams()
let availableSeedGroups = tournament.availableSeedGroups() let availableSeedGroups = tournament.availableSeedGroups()

@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import EventKit
struct TournamentSubscriptionView: View { struct TournamentSubscriptionView: View {
@EnvironmentObject var networkMonitor: NetworkMonitor @EnvironmentObject var networkMonitor: NetworkMonitor
@ -17,6 +18,8 @@ struct TournamentSubscriptionView: View {
@State private var selectedPlayers: [ImportedPlayer] @State private var selectedPlayers: [ImportedPlayer]
@State private var contactType: ContactType? = nil @State private var contactType: ContactType? = nil
@State private var sentError: ContactManagerError? = nil @State private var sentError: ContactManagerError? = nil
@State private var didSendMessage: Bool = false
@State private var didSaveInCalendar: Bool = false
init(federalTournament: FederalTournament, build: any TournamentBuildHolder, user: User) { init(federalTournament: FederalTournament, build: any TournamentBuildHolder, user: User) {
self.federalTournament = federalTournament self.federalTournament = federalTournament
@ -25,8 +28,79 @@ struct TournamentSubscriptionView: View {
_selectedPlayers = .init(wrappedValue: [user.currentPlayerData()].compactMap({ $0 })) _selectedPlayers = .init(wrappedValue: [user.currentPlayerData()].compactMap({ $0 }))
} }
func hasPartner() -> Bool {
selectedPlayers.count == 2
}
func inscriptionSent() -> Bool {
didSendMessage
}
func addEvent() {
let eventStore = EKEventStore()
eventStore.requestWriteOnlyAccessToEvents { (granted, error) in
if granted && error == nil {
print("Access granted")
let startDate = federalTournament.startDate
let endDate = federalTournament.dateFin ?? federalTournament.startDate.endOfDay()
addEventToCalendar(title: messageSubject, startDate: startDate, endDate: endDate)
didSaveInCalendar = true
} else {
print("Access denied or error occurred: \(String(describing: error?.localizedDescription))")
sentError = .calendarAccessDenied
}
}
}
func addEventToCalendar(title: String, startDate: Date, endDate: Date) {
let eventStore = EKEventStore()
if eventStore.defaultCalendarForNewEvents == nil {
sentError = .noCalendarAvailable
return
}
eventStore.requestWriteOnlyAccessToEvents { (granted, error) in
if granted && error == nil {
let event = EKEvent(eventStore: eventStore)
event.title = title
event.isAllDay = true
event.startDate = startDate
event.endDate = endDate
event.calendar = eventStore.defaultCalendarForNewEvents
event.notes = noteCalendar
event.location = federalTournament.clubLabel()
do {
try eventStore.save(event, span: .thisEvent)
didSaveInCalendar = true
print("Event saved")
} catch let error {
print("Failed to save event: \(error.localizedDescription)")
sentError = .calendarEventSaveFailed
}
} else {
print("Access denied or error occurred: \(String(describing: error?.localizedDescription))")
sentError = .calendarAccessDenied
}
}
}
var body: some View { var body: some View {
List { List {
if didSaveInCalendar {
Section {
LabeledContent {
Image(systemName: "checkmark").foregroundStyle(.green)
} label: {
Text("Le tournoi a bien été ajouté dans votre calendrier par défaut")
let eventStore = EKEventStore()
if let defaultCalendarForNewEvents = eventStore.defaultCalendarForNewEvents {
Text(defaultCalendarForNewEvents.title)
}
}
}
}
Section { Section {
LabeledContent("Tournoi") { LabeledContent("Tournoi") {
Text(federalTournament.libelle ?? "Tournoi") Text(federalTournament.libelle ?? "Tournoi")
@ -121,6 +195,17 @@ struct TournamentSubscriptionView: View {
} }
.toolbar(content: { .toolbar(content: {
Menu { Menu {
ShareLink(item: federalTournament.sharePartnerMessage) {
Label("Prévenir votre partenaire", systemImage: "person.2")
}
Button("Ajouter à votre agenda") {
addEvent()
}
ShareLink(item: federalTournament.shareMessage) {
Label("Partager les infos", systemImage: "info")
}
Link(destination: URL(string:"https://tenup.fft.fr/tournoi/\(federalTournament.id)")!) { Link(destination: URL(string:"https://tenup.fft.fr/tournoi/\(federalTournament.id)")!) {
Label("Voir sur Tenup", systemImage: "tennisball") Label("Voir sur Tenup", systemImage: "tennisball")
} }
@ -134,6 +219,13 @@ struct TournamentSubscriptionView: View {
.alert("Un problème est survenu", isPresented: messageSentFailed) { .alert("Un problème est survenu", isPresented: messageSentFailed) {
Button("OK") { Button("OK") {
} }
if sentError == .calendarAccessDenied || sentError == .noCalendarAvailable {
Button("Voir vos réglages") {
openAppSettings()
}
}
} message: { } message: {
Text(_networkErrorMessage) Text(_networkErrorMessage)
} }
@ -150,6 +242,8 @@ struct TournamentSubscriptionView: View {
case .sent: case .sent:
if networkMonitor.connected == false { if networkMonitor.connected == false {
self.sentError = .messageNotSent self.sentError = .messageNotSent
} else {
self.didSendMessage = true
} }
@unknown default: @unknown default:
break break
@ -167,6 +261,8 @@ struct TournamentSubscriptionView: View {
if networkMonitor.connected == false { if networkMonitor.connected == false {
self.contactType = nil self.contactType = nil
self.sentError = .mailNotSent self.sentError = .mailNotSent
} else {
self.didSendMessage = true
} }
@unknown default: @unknown default:
break break
@ -188,12 +284,19 @@ struct TournamentSubscriptionView: View {
var messageBody: String { var messageBody: String {
let bonjourOuBonsoir = Date().timeOfDay.hello let bonjourOuBonsoir = Date().timeOfDay.hello
let bonneSoireeOuBonneJournee = Date().timeOfDay.goodbye let bonneSoireeOuBonneJournee = Date().timeOfDay.goodbye
let body = [["\(bonjourOuBonsoir),\n\nJe souhaiterais inscrire mon équipe au tournoi : ", build.buildHolderTitle(), "du", federalTournament.computedStartDate, "au", federalTournament.clubLabel() + ".\n"].compacted().joined(separator: " "), teamsString, "\nCordialement,\n", user.fullName() ?? bonneSoireeOuBonneJournee, "----------------------------------\nCe message a été préparé grâce à l'application Padel Club !\nVotre tournoi n'est pas encore dessus ? \(URLs.main.rawValue)", "Téléchargez l'app : \(URLs.appStore.rawValue)", "En savoir plus : \(URLs.appDescription.rawValue)"].compactMap { $0 }.joined(separator: "\n") + "\n" let body = [["\(bonjourOuBonsoir),\n\nJe souhaiterais inscrire mon équipe au tournoi : ", build.buildHolderTitle(), "du", federalTournament.computedStartDate, "au", federalTournament.clubLabel() + ".\n"].compacted().joined(separator: " "), teamsString, "\nCordialement,\n", user.fullName() ?? bonneSoireeOuBonneJournee, "----------------------------------\nCe message a été préparé grâce à l'application Padel Club !"].compactMap { $0 }.joined(separator: "\n") + "\n"
return body return body
} }
var messageBodyShort: String { var messageBodyShort: String {
let body = [[build.buildHolderTitle(), federalTournament.clubLabel()].compacted().joined(separator: " "), federalTournament.computedStartDate, teamsString].compacted().joined(separator: "\n") + "\n" let bonjourOuBonsoir = Date().timeOfDay.hello
let bonneSoireeOuBonneJournee = Date().timeOfDay.goodbye
let body = [["\(bonjourOuBonsoir),\n\nJe souhaiterais inscrire mon équipe au tournoi : ", build.buildHolderTitle(), "du", federalTournament.computedStartDate, "au", federalTournament.clubLabel() + ".\n"].compacted().joined(separator: " "), teamsString, "\nCordialement,\n", user.fullName() ?? bonneSoireeOuBonneJournee].compactMap { $0 }.joined(separator: "\n") + "\n"
return body
}
var noteCalendar: String {
let body = [[build.buildHolderTitle(), "du", federalTournament.computedStartDate, "au", federalTournament.clubLabel() + ".\n"].compacted().joined(separator: " "), teamsString, federalTournament.calendarNoteMessage()].compactMap { $0 }.joined(separator: "\n") + "\n"
return body return body
} }
@ -227,6 +330,29 @@ struct TournamentSubscriptionView: View {
if sentError == .mailFailed { if sentError == .mailFailed {
errors.append("Le mail n'a pas été envoyé") errors.append("Le mail n'a pas été envoyé")
} }
if sentError == .calendarAccessDenied {
errors.append("Padel Club n'a pas accès à votre calendrier")
}
if sentError == .calendarEventSaveFailed {
errors.append("Padel Club n'a pas réussi à sauver ce tournoi dans votre calendrier")
}
if sentError == .noCalendarAvailable {
errors.append("Padel Club n'a pas réussi à trouver un calendrier pour y inscrire ce tournoi")
}
return errors.joined(separator: "\n") return errors.joined(separator: "\n")
} }
func openAppSettings() {
if let appSettings = URL(string: UIApplication.openSettingsURLString) {
if UIApplication.shared.canOpenURL(appSettings) {
UIApplication.shared.open(appSettings, options: [:], completionHandler: nil)
}
}
}
} }

@ -93,6 +93,7 @@ struct DatePickingView: View {
} }
struct MatchFormatPickingView: View { struct MatchFormatPickingView: View {
var title: String? = nil
@Binding var matchFormat: MatchFormat @Binding var matchFormat: MatchFormat
var validateAction: (() async -> ()) var validateAction: (() async -> ())
@ -110,6 +111,10 @@ struct MatchFormatPickingView: View {
confirmScheduleUpdate = false confirmScheduleUpdate = false
} }
} }
} header: {
if let title {
Text(title)
}
} footer: { } footer: {
if confirmScheduleUpdate && updatingInProgress == false { if confirmScheduleUpdate && updatingInProgress == false {
FooterButtonView("non, ne pas modifier les horaires") { FooterButtonView("non, ne pas modifier les horaires") {
@ -123,3 +128,103 @@ struct MatchFormatPickingView: View {
} }
} }
} }
struct DatePickingViewWithFormat: View {
@Binding var matchFormat: MatchFormat
let title: String
@Binding var startDate: Date
@Binding var currentDate: Date?
var duration: Int?
var validateAction: ((Bool) async -> ())
@State private var confirmScheduleUpdate: Bool = false
@State private var updatingInProgress : Bool = false
var body: some View {
Section {
MatchFormatPickerView(headerLabel: "Format", matchFormat: $matchFormat)
DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline)
}
if confirmScheduleUpdate {
RowButtonView("Sauver et modifier la suite") {
updatingInProgress = true
await validateAction(true)
updatingInProgress = false
confirmScheduleUpdate = false
}
}
} header: {
Text(title)
} footer: {
if confirmScheduleUpdate && updatingInProgress == false {
HStack {
FooterButtonView("sauver sans modifier la suite") {
Task {
await validateAction(false)
confirmScheduleUpdate = false
}
}
Text("ou")
FooterButtonView("annuler") {
confirmScheduleUpdate = false
}
}
} else {
HStack {
Menu {
Button("de 30 minutes") {
startDate = startDate.addingTimeInterval(1800)
}
Button("d'une heure") {
startDate = startDate.addingTimeInterval(3600)
}
Button("à 9h") {
startDate = startDate.atNine()
}
Button("à demain 9h") {
startDate = startDate.tomorrowAtNine
}
if let duration {
Button("à la prochaine rotation") {
startDate = startDate.addingTimeInterval(Double(duration) * 60)
}
Button("à la précédente rotation") {
startDate = startDate.addingTimeInterval(Double(duration) * -60)
}
}
} label: {
Text("décaler")
.underline()
}
.buttonStyle(.borderless)
Spacer()
if currentDate != nil {
FooterButtonView("retirer l'horaire bloqué") {
currentDate = nil
}
} else {
FooterButtonView("bloquer l'horaire") {
currentDate = startDate
}
}
}
.buttonStyle(.borderless)
}
}
.headerProminence(.increased)
.onChange(of: matchFormat) {
confirmScheduleUpdate = true
}
.onChange(of: startDate) {
confirmScheduleUpdate = true
}
}
}

@ -33,7 +33,7 @@ struct LoserRoundScheduleEditorView: View {
var body: some View { var body: some View {
List { List {
MatchFormatPickingView(matchFormat: $matchFormat) { MatchFormatPickingView(title: "Format des tours par défault", matchFormat: $matchFormat) {
await _updateSchedule() await _updateSchedule()
} }

@ -38,8 +38,11 @@ struct LoserRoundStepScheduleEditorView: View {
var body: some View { var body: some View {
@Bindable var round = round @Bindable var round = round
DatePickingView(title: "Tour #\(stepIndex + 1)", startDate: $startDate, currentDate: .constant(nil), duration: round.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { DatePickingViewWithFormat(matchFormat: $round.matchFormat, title: "Tour #\(stepIndex + 1)", startDate: $startDate, currentDate: .constant(nil), duration: round.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { update in
await _updateSchedule() for match in matches {
match.matchFormat = round.matchFormat
}
await _updateSchedule(update: update)
} }
Section { Section {
@ -64,8 +67,10 @@ struct LoserRoundStepScheduleEditorView: View {
} }
} }
private func _updateSchedule() async { private func _updateSchedule(update: Bool) async {
tournament.matchScheduler()?.updateBracketSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate) if update {
tournament.matchScheduler()?.updateBracketSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate)
}
upperRound.loserRounds(forRoundIndex: round.index).forEach({ round in upperRound.loserRounds(forRoundIndex: round.index).forEach({ round in
round.startDate = startDate round.startDate = startDate
}) })

@ -27,6 +27,10 @@ struct MatchScheduleEditorView: View {
} }
var body: some View { var body: some View {
MatchFormatPickingView(matchFormat: $match.matchFormat) {
await _updateSchedule()
}
DatePickingView(title: title, startDate: $startDate, currentDate: .constant(nil), duration: match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { DatePickingView(title: title, startDate: $startDate, currentDate: .constant(nil), duration: match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) {
await _updateSchedule() await _updateSchedule()
} }

@ -243,7 +243,7 @@ struct EditingTeamView: View {
} }
.tint(.master) .tint(.master)
} }
.sheet(item: $editedTeam) { editedTeam in .fullScreenCover(item: $editedTeam) { editedTeam in
NavigationStack { NavigationStack {
AddTeamView(tournament: tournament, editedTeam: editedTeam) AddTeamView(tournament: tournament, editedTeam: editedTeam)
} }

@ -18,6 +18,7 @@ struct TeamPickerView: View {
var shouldConfirm: Bool = false var shouldConfirm: Bool = false
var groupStagePosition: Int? = nil var groupStagePosition: Int? = nil
var round: Round? = nil
var matchTypeContext: MatchType = .bracket var matchTypeContext: MatchType = .bracket
var luckyLosers: [TeamRegistration] = [] var luckyLosers: [TeamRegistration] = []
let teamPicked: ((TeamRegistration) -> (Void)) let teamPicked: ((TeamRegistration) -> (Void))
@ -39,6 +40,14 @@ struct TeamPickerView: View {
.sheet(isPresented: $presentTeamPickerView) { .sheet(isPresented: $presentTeamPickerView) {
NavigationStack { NavigationStack {
List { List {
if matchTypeContext == .loserBracket, let losers = round?.parentRound?.losers() {
_sectionView(losers.sorted(by: \.weight, order: sortOrder), title: "Perdant du tour précédent")
}
if matchTypeContext == .loserBracket, let losers = round?.previousRound()?.winners() {
_sectionView(losers.sorted(by: \.weight, order: sortOrder), title: "Gagnant du tour précédent")
}
if let groupStagePosition, let replacementRangeExtended = tournament.replacementRangeExtended(groupStagePosition: groupStagePosition) { if let groupStagePosition, let replacementRangeExtended = tournament.replacementRangeExtended(groupStagePosition: groupStagePosition) {
Section { Section {
GroupStageTeamReplacementView.TeamRangeView(teamRange: replacementRangeExtended, playerWeight: 0) GroupStageTeamReplacementView.TeamRangeView(teamRange: replacementRangeExtended, playerWeight: 0)
@ -130,6 +139,7 @@ struct TeamPickerView: View {
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.buttonStyle(.plain) .buttonStyle(.plain)
.listRowView(isActive: matchTypeContext == .loserBracket && round?.teams().map({ $0.id }).contains(team.id) == true, color: .green, hideColorVariation: true)
// .confirmationDialog("Attention", isPresented: confirmationRequest, titleVisibility: .visible) { // .confirmationDialog("Attention", isPresented: confirmationRequest, titleVisibility: .visible) {
// Button("Retirer du tableau", role: .destructive) { // Button("Retirer du tableau", role: .destructive) {
// teamPicked(confirmTeam!) // teamPicked(confirmTeam!)

@ -20,7 +20,11 @@ struct AddTeamView: View {
var tournament: Tournament var tournament: Tournament
var cancelShouldDismiss: Bool = false var cancelShouldDismiss: Bool = false
enum FocusField: Hashable {
case pasteField
}
@FocusState private var focusedField: FocusField?
@State private var searchField: String = "" @State private var searchField: String = ""
@State private var presentSearch: Bool = false @State private var presentSearch: Bool = false
@State private var presentPlayerSearch: Bool = false @State private var presentPlayerSearch: Bool = false
@ -36,6 +40,7 @@ struct AddTeamView: View {
@State private var confirmDuplicate: Bool = false @State private var confirmDuplicate: Bool = false
@State private var homonyms: [PlayerRegistration] = [] @State private var homonyms: [PlayerRegistration] = []
@State private var confirmHomonym: Bool = false @State private var confirmHomonym: Bool = false
@State private var editableTextField: String = ""
var tournamentStore: TournamentStore { var tournamentStore: TournamentStore {
return self.tournament.tournamentStore return self.tournament.tournamentStore
@ -61,13 +66,32 @@ struct AddTeamView: View {
_pasteString = .init(wrappedValue: pasteString) _pasteString = .init(wrappedValue: pasteString)
_fetchPlayers = FetchRequest<ImportedPlayer>(sortDescriptors: [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)], predicate: SearchViewModel.pastePredicate(pasteField: pasteString, mostRecentDate: tournament.rankSourceDate, filterOption: tournament.tournamentCategory.playerFilterOption)) _fetchPlayers = FetchRequest<ImportedPlayer>(sortDescriptors: [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)], predicate: SearchViewModel.pastePredicate(pasteField: pasteString, mostRecentDate: tournament.rankSourceDate, filterOption: tournament.tournamentCategory.playerFilterOption))
_autoSelect = .init(wrappedValue: true) _autoSelect = .init(wrappedValue: true)
_editableTextField = .init(wrappedValue: pasteString)
cancelShouldDismiss = true cancelShouldDismiss = true
} }
} }
var body: some View { var body: some View {
_buildingTeamView() if pasteString == nil {
.navigationBarBackButtonHidden(true) computedBody
} else {
computedBody
.searchable(text: $searchField, placement: .navigationBarDrawer(displayMode: .automatic), prompt: Text("Chercher dans les résultats"))
}
}
var computedBody: some View {
List(selection: $createdPlayerIds) {
_buildingTeamView()
}
.onReceive(fetchPlayers.publisher.count()) { _ in // <-- here
if let pasteString, count == 2, autoSelect == true {
fetchPlayers.filter { $0.hitForSearch(pasteString) >= hitTarget }.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) }).forEach { player in
createdPlayerIds.insert(player.license!)
}
autoSelect = false
}
}
.alert("Présence d'homonyme", isPresented: $confirmHomonym) { .alert("Présence d'homonyme", isPresented: $confirmHomonym) {
Button("Créer l'équipe quand même") { Button("Créer l'équipe quand même") {
_createTeam(checkDuplicates: false, checkHomonym: false) _createTeam(checkDuplicates: false, checkHomonym: false)
@ -121,7 +145,6 @@ struct AddTeamView: View {
} }
.tint(.master) .tint(.master)
} }
.navigationTitle(editedTeam == nil ? "Ajouter une équipe" : "Modifier l'équipe")
.toolbar { .toolbar {
ToolbarItem(placement: .cancellationAction) { ToolbarItem(placement: .cancellationAction) {
Button("Annuler", role: .cancel) { Button("Annuler", role: .cancel) {
@ -130,27 +153,23 @@ struct AddTeamView: View {
} }
if pasteString == nil { if pasteString == nil {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .bottomBar) {
PasteButton(payloadType: String.self) { strings in PasteButton(payloadType: String.self) { strings in
guard let first = strings.first else { return } guard let first = strings.first else { return }
Task { handlePasteString(first)
await MainActor.run {
fetchPlayers.nsPredicate = SearchViewModel.pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption())
fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)]
pasteString = first
autoSelect = true
}
}
} }
.foregroundStyle(.master) .foregroundStyle(.master)
.labelStyle(.iconOnly) .labelStyle(.titleAndIcon)
.buttonBorderShape(.capsule) .buttonBorderShape(.capsule)
} }
} }
} }
.navigationBarBackButtonHidden(_isEditingTeam()) .navigationBarBackButtonHidden(_isEditingTeam())
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.toolbarBackground(.visible, for: .bottomBar)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.navigationTitle(editedTeam == nil ? "Ajouter une équipe" : "Modifier l'équipe")
.environment(\.editMode, Binding.constant(EditMode.active))
} }
private func _isEditingTeam() -> Bool { private func _isEditingTeam() -> Bool {
@ -275,6 +294,8 @@ struct AddTeamView: View {
createdPlayers.removeAll() createdPlayers.removeAll()
createdPlayerIds.removeAll() createdPlayerIds.removeAll()
pasteString = nil pasteString = nil
editableTextField = ""
if team.players().count > 1 { if team.players().count > 1 {
dismiss() dismiss()
} }
@ -302,23 +323,42 @@ struct AddTeamView: View {
createdPlayers.removeAll() createdPlayers.removeAll()
createdPlayerIds.removeAll() createdPlayerIds.removeAll()
pasteString = nil pasteString = nil
editableTextField = ""
self.editedTeam = nil self.editedTeam = nil
if editedTeam.players().count > 1 { if editedTeam.players().count > 1 {
dismiss() dismiss()
} }
} }
@ViewBuilder
private func _buildingTeamView() -> some View { private func _buildingTeamView() -> some View {
List(selection: $createdPlayerIds) {
if let pasteString { if let pasteString {
Section { Section {
Text(pasteString) TextEditor(text: $editableTextField)
.focused($focusedField, equals: .pasteField)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button("Fermer", role: .cancel) {
self.editableTextField = pasteString
self.focusedField = nil
}
Spacer()
Button("Chercher") {
self.handlePasteString(editableTextField)
self.focusedField = nil
}
.buttonStyle(.bordered)
}
}
} header: {
Text("Contenu du presse-papier")
} footer: { } footer: {
HStack { HStack {
Text("contenu du presse-papier")
Spacer() Spacer()
Button("effacer", role: .destructive) { Button("effacer", role: .destructive) {
self.focusedField = nil
self.editableTextField = ""
self.pasteString = nil self.pasteString = nil
self.createdPlayers.removeAll() self.createdPlayers.removeAll()
self.createdPlayerIds.removeAll() self.createdPlayerIds.removeAll()
@ -387,6 +427,8 @@ struct AddTeamView: View {
Text(tournament.cutLabel(index: teamIndex, teamCount: selectedSortedTeams.count)) Text(tournament.cutLabel(index: teamIndex, teamCount: selectedSortedTeams.count))
} }
} }
// } else {
// Text("Préparation de l'équipe")
} }
} }
@ -404,33 +446,16 @@ struct AddTeamView: View {
RowButtonView("Effacer cette recherche") { RowButtonView("Effacer cette recherche") {
self.pasteString = nil self.pasteString = nil
self.editableTextField = ""
} }
} }
} else { } else {
Section { _listOfPlayers(pasteString: pasteString)
let sortedPlayers = fetchPlayers.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) })
ForEach(sortedPlayers) { player in
ImportedPlayerView(player: player).tag(player.license!)
}
} header: {
Text(fetchPlayers.count.formatted() + " résultat" + fetchPlayers.count.pluralSuffix)
}
} }
} else { } else {
_managementView() _managementView()
} }
}
.headerProminence(.increased)
.onReceive(fetchPlayers.publisher.count()) { _ in // <-- here
if let pasteString, count == 2, autoSelect == true {
fetchPlayers.filter { $0.hitForSearch(pasteString) >= hitTarget }.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) }).forEach { player in
createdPlayerIds.insert(player.license!)
}
autoSelect = false
}
}
.environment(\.editMode, Binding.constant(EditMode.active))
} }
private var count: Int { private var count: Int {
@ -453,4 +478,32 @@ struct AddTeamView: View {
Logger.error(error) Logger.error(error)
} }
} }
private func handlePasteString(_ first: String) {
Task {
await MainActor.run {
fetchPlayers.nsPredicate = SearchViewModel.pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption())
fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)]
pasteString = first
editableTextField = first
autoSelect = true
}
}
}
@ViewBuilder
private func _listOfPlayers(pasteString: String) -> some View {
let sortedPlayers = fetchPlayers.filter({ $0.contains(searchField) || searchField.isEmpty }).sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) })
Section {
ForEach(sortedPlayers) { player in
ImportedPlayerView(player: player).tag(player.license!)
}
} header: {
Text(sortedPlayers.count.formatted() + " résultat" + sortedPlayers.count.pluralSuffix)
}
}
} }

@ -11,7 +11,7 @@ import LeStorage
struct TournamentGeneralSettingsView: View { struct TournamentGeneralSettingsView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
var tournament: Tournament @Bindable var tournament: Tournament
@State private var tournamentName: String = "" @State private var tournamentName: String = ""
@State private var entryFee: Double? = nil @State private var entryFee: Double? = nil
@FocusState private var focusedField: Tournament.CodingKeys? @FocusState private var focusedField: Tournament.CodingKeys?
@ -34,6 +34,20 @@ struct TournamentGeneralSettingsView: View {
TournamentLevelPickerView() TournamentLevelPickerView()
} }
Section {
Toggle(isOn: $tournament.automaticLoserBracket) {
Text("Gestion automatique des matchs de classements")
}
// Picker(selection: $tournament.loserBracketMode) {
// ForEach(LoserBracketMode.allCases) { mode in
// Text(mode.loserBracketModeLocalizedLabel()).tag(mode)
// }
// } label: {
// Text("Mode")
// }
}
Section { Section {
LabeledContent { LabeledContent {
TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $entryFee, format: .currency(code: Locale.current.currency?.identifier ?? "EUR")) TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $entryFee, format: .currency(code: Locale.current.currency?.identifier ?? "EUR"))

@ -327,7 +327,7 @@ struct InscriptionManagerView: View {
UpdateSourceRankDateView(currentRankSourceDate: $currentRankSourceDate, confirmUpdateRank: $confirmUpdateRank, tournament: tournament) UpdateSourceRankDateView(currentRankSourceDate: $currentRankSourceDate, confirmUpdateRank: $confirmUpdateRank, tournament: tournament)
.tint(.master) .tint(.master)
} }
.sheet(isPresented: $presentAddTeamView, onDismiss: { .fullScreenCover(isPresented: $presentAddTeamView, onDismiss: {
_setHash() _setHash()
}) { }) {
NavigationStack { NavigationStack {
@ -335,7 +335,7 @@ struct InscriptionManagerView: View {
} }
.tint(.master) .tint(.master)
} }
.sheet(item: $editedTeam, onDismiss: { .fullScreenCover(item: $editedTeam, onDismiss: {
_setHash() _setHash()
}) { editedTeam in }) { editedTeam in
NavigationStack { NavigationStack {
@ -343,7 +343,7 @@ struct InscriptionManagerView: View {
} }
.tint(.master) .tint(.master)
} }
.sheet(item: $pasteString, onDismiss: { .fullScreenCover(item: $pasteString, onDismiss: {
_setHash() _setHash()
}) { pasteString in }) { pasteString in
NavigationStack { NavigationStack {

Loading…
Cancel
Save