diff --git a/PadelClub/Data/MatchScheduler.swift b/PadelClub/Data/MatchScheduler.swift index 2f306c3..1f9d2ad 100644 --- a/PadelClub/Data/MatchScheduler.swift +++ b/PadelClub/Data/MatchScheduler.swift @@ -386,15 +386,9 @@ final class MatchScheduler : ModelObject, Storable { return true } } else { - if targetedStartDate == minimumTargetedEndDate { - print("Updating minimumTargetedEndDate to minimumPossibleEndDate: \(minimumPossibleEndDate)") - minimumTargetedEndDate = minimumPossibleEndDate - } else { - print("Setting minimumTargetedEndDate to the earlier of \(minimumPossibleEndDate) and \(minimumTargetedEndDate)") - minimumTargetedEndDate = min(minimumPossibleEndDate, minimumTargetedEndDate) - } - print("Targeted start date is before the minimum possible end date, returning false.") - return false + print("Setting minimumTargetedEndDate to the earlier of \(minimumPossibleEndDate) and \(minimumTargetedEndDate)") + minimumTargetedEndDate = minimumPossibleEndDate + return true } } @@ -463,7 +457,7 @@ final class MatchScheduler : ModelObject, Storable { var courts = initialCourts ?? Array(courtsAvailable) var shouldStartAtDispatcherDate = rotationIndex > 0 - while !availableMatchs.isEmpty && !issueFound && rotationIndex < 100 { + while !availableMatchs.isEmpty && !issueFound && rotationIndex < 50 { freeCourtPerRotation[rotationIndex] = [] let previousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 1 }) var rotationStartDate: Date = getNextStartDate(fromPreviousRotationSlots: previousRotationSlots, includeBreakTime: false) ?? dispatcherStartDate @@ -604,7 +598,9 @@ final class MatchScheduler : ModelObject, Storable { if shouldTryToFillUpCourtsAvailable == false { if roundObject.parent == nil && roundObject.index > 1 && indexInRound == 0, let nextMatch = match.next() { - if courtPosition < courts.count - 1 && canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate) { + + var nextMinimumTargetedEndDate = minimumTargetedEndDate + if courtPosition < courts.count - 1 && canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &nextMinimumTargetedEndDate) { print("Returning true: Both current \(match.index) and next match \(nextMatch.index) can be played in rotation \(rotationIndex).") return true } else { @@ -625,7 +621,7 @@ final class MatchScheduler : ModelObject, Storable { matchID: firstMatch.id, rotationIndex: rotationIndex, courtIndex: courtIndex, - startDate: rotationStartDate, + startDate: minimumTargetedEndDate, durationLeft: firstMatch.matchFormat.getEstimatedDuration(additionalEstimationDuration), minimumBreakTime: firstMatch.matchFormat.breakTime.breakTime ) diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index 92d29cf..4ac2e11 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -300,7 +300,7 @@ final class PlayerRegistration: ModelObject, Storable { if let currentLicenceId = licenceId { if currentLicenceId.trimmed.hasSuffix("(\(year-1))") { self.licenceId = currentLicenceId.replacingOccurrences(of: "\(year-1)", with: "\(year)") - } else if let computedLicense = currentLicenceId.strippedLicense { + } else if let computedLicense = currentLicenceId.strippedLicense?.computedLicense { self.licenceId = computedLicense + " (\(year))" } } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 3c70565..1bf494d 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -1021,8 +1021,20 @@ defer { func playersWithoutValidLicense(in players: [PlayerRegistration], isImported: Bool) -> [PlayerRegistration] { let licenseYearValidity = self.licenseYearValidity() - return players.filter({ - ($0.isImported() && $0.isValidLicenseNumber(year: licenseYearValidity) == false) || ($0.isImported() == false && ($0.licenceId == nil || $0.formattedLicense().isLicenseNumber == false || $0.licenceId?.isEmpty == true) || ($0.isImported() == false && isImported)) + return players.filter({ player in + if player.isImported() { + // Player is marked as imported: check if the license is valid + return !player.isValidLicenseNumber(year: licenseYearValidity) + } else { + // Player is not imported: validate license and handle `isImported` flag for non-imported players + let noLicenseId = player.licenceId == nil || player.licenceId?.isEmpty == true + let invalidFormattedLicense = player.formattedLicense().isLicenseNumber == false + + // If global `isImported` is true, check license number as well + let invalidLicenseForImportedFlag = isImported && !player.isValidLicenseNumber(year: licenseYearValidity) + + return noLicenseId || invalidFormattedLicense || invalidLicenseForImportedFlag + } }) } diff --git a/PadelClub/Views/Calling/TeamsCallingView.swift b/PadelClub/Views/Calling/TeamsCallingView.swift index 986322d..832daae 100644 --- a/PadelClub/Views/Calling/TeamsCallingView.swift +++ b/PadelClub/Views/Calling/TeamsCallingView.swift @@ -14,6 +14,8 @@ struct TeamsCallingView: View { var body: some View { List { + PlayersWithoutContactView(players: teams.flatMap({ $0.unsortedPlayers() }).sorted(by: \.computedRank)) + Section { ForEach(teams) { team in Menu { diff --git a/PadelClub/Views/Planning/PlanningSettingsView.swift b/PadelClub/Views/Planning/PlanningSettingsView.swift index c545c81..5eeef89 100644 --- a/PadelClub/Views/Planning/PlanningSettingsView.swift +++ b/PadelClub/Views/Planning/PlanningSettingsView.swift @@ -107,7 +107,7 @@ struct PlanningSettingsView: View { } } label: { if isCreatedByUser { - Text("Vous avez indiqué plus de terrains dans ce tournoi que dans le club.") + Text("Vous avez indiqué plus de terrains dans ce tournoi que dans le club. ") + Text("Mettre à jour le club ?").underline().foregroundStyle(.master) } else { Label("Vous avez indiqué plus de terrains dans ce tournoi que dans le club.", systemImage: "exclamationmark.triangle.fill").foregroundStyle(.logoRed) @@ -257,6 +257,7 @@ struct PlanningSettingsView: View { _save() } .onChange(of: tournament.courtCount) { + matchScheduler.courtsAvailable = Set(tournament.courtsAvailable()) _save() } .onChange(of: tournament.dayDuration) { diff --git a/PadelClub/Views/Planning/PlanningView.swift b/PadelClub/Views/Planning/PlanningView.swift index f2b0348..a20841b 100644 --- a/PadelClub/Views/Planning/PlanningView.swift +++ b/PadelClub/Views/Planning/PlanningView.swift @@ -10,7 +10,8 @@ import SwiftUI struct PlanningView: View { @EnvironmentObject var dataStore: DataStore @Environment(Tournament.self) var tournament: Tournament - + @State private var selectedDay: Date? + let matches: [Match] @Binding var selectedScheduleDestination: ScheduleDestination? @@ -39,7 +40,7 @@ struct PlanningView: View { case .byCourt: return "Par terrain" case .byDefault: - return "Par défaut" + return "Par ordre des matchs" } } } @@ -49,11 +50,38 @@ struct PlanningView: View { _selectedScheduleDestination = selectedScheduleDestination } + private func _computedTitle() -> String { + if let selectedDay { + return selectedDay.formatted(.dateTime.day().weekday().month()) + } else { + if days.count > 1 { + return "Tous les jours" + } else { + return "Horaires" + } + } + } + var body: some View { List { _bySlotView() } + .navigationTitle(Text(_computedTitle())) .toolbar(content: { + if days.count > 1 { + ToolbarTitleMenu { + Picker(selection: $selectedDay) { + Text("Tous les jours").tag(nil as Date?) + ForEach(days, id: \.self) { day in + Text(day.formatted(.dateTime.day().weekday().month())).tag(day as Date?) + } + } label: { + Text("Jour") + } + .pickerStyle(.automatic) + } + } + ToolbarItem(placement: .topBarTrailing) { Menu { Picker(selection: $filterOption) { @@ -89,7 +117,7 @@ struct PlanningView: View { @ViewBuilder func _bySlotView() -> some View { if matches.allSatisfy({ $0.startDate == nil }) == false { - ForEach(days, id: \.self) { day in + 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] { diff --git a/PadelClub/Views/Player/Components/EditablePlayerView.swift b/PadelClub/Views/Player/Components/EditablePlayerView.swift index cc4e393..6569682 100644 --- a/PadelClub/Views/Player/Components/EditablePlayerView.swift +++ b/PadelClub/Views/Player/Components/EditablePlayerView.swift @@ -78,6 +78,13 @@ struct EditablePlayerView: View { Logger.error(error) } } + .onChange(of: player.licenceId) { + do { + try self.tournamentStore.playerRegistrations.addOrUpdate(instance: player) + } catch { + Logger.error(error) + } + } .onChange(of: player.hasArrived) { do { try self.tournamentStore.playerRegistrations.addOrUpdate(instance: player) diff --git a/PadelClub/Views/Player/PlayerDetailView.swift b/PadelClub/Views/Player/PlayerDetailView.swift index c30b70d..046cdc1 100644 --- a/PadelClub/Views/Player/PlayerDetailView.swift +++ b/PadelClub/Views/Player/PlayerDetailView.swift @@ -186,6 +186,27 @@ struct PlayerDetailView: View { } } } + + Section { + if let number = player.phoneNumber?.replacingOccurrences(of: " ", with: "") { + if let url = URL(string: "tel:\(number)") { + Link(destination: url) { + Label("Appeler", systemImage: "phone") + } + } + if let url = URL(string: "sms:\(number)") { + Link(destination: url) { + Label("Message", systemImage: "message") + } + } + } + + if let mail = player.email, let mailURL = URL(string: "mail:\(mail)") { + Link(destination: mailURL) { + Label("Mail", systemImage: "mail") + } + } + } } .toolbar { ToolbarItem(placement: .topBarTrailing) { diff --git a/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift b/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift index c670fbd..fe03ff1 100644 --- a/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift @@ -126,9 +126,7 @@ struct InscriptionInfoView: View { } } .listRowView(color: .logoRed) - } - - Section { + DisclosureGroup { ForEach(homonyms) { player in ImportedPlayerView(player: player) @@ -141,8 +139,20 @@ struct InscriptionInfoView: View { } } .listRowView(color: .logoRed) - } + DisclosureGroup { + ForEach(playersMissing) { + TeamDetailView(team: $0) + } + } label: { + LabeledContent { + Text(playersMissing.count.formatted()) + } label: { + Text("Paires incomplètes") + } + } + .listRowView(color: .pink) + } Section { DisclosureGroup { @@ -200,6 +210,11 @@ struct InscriptionInfoView: View { ForEach(playersWithoutValidLicense) { EditablePlayerView(player: $0, editingOptions: [.licenceId]) .environmentObject(tournament.tournamentStore) + .onChange(of: $0.licenceId) { + players = tournament.unsortedPlayers() + let isImported = players.anySatisfy({ $0.isImported() }) + playersWithoutValidLicense = tournament.playersWithoutValidLicense(in: players, isImported: isImported) + } } } label: { LabeledContent { @@ -212,21 +227,6 @@ struct InscriptionInfoView: View { } footer: { Text("importé du fichier beach-padel sans licence valide ou créé sans licence") } - - Section { - DisclosureGroup { - ForEach(playersMissing) { - TeamDetailView(team: $0) - } - } label: { - LabeledContent { - Text(playersMissing.count.formatted()) - } label: { - Text("Paires incomplètes") - } - } - .listRowView(color: .pink) - } } .task { await _getIssues() diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index 1b724d7..81a6368 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -165,6 +165,7 @@ struct InscriptionManagerView: View { if self.teamsHash == nil, selectedSortedTeams.isEmpty == false { self.teamsHash = _simpleHash(ids: selectedSortedTeams.map { $0.id }) } + self.registrationIssues = nil Task { self.registrationIssues = await tournament.registrationIssues() } @@ -708,6 +709,12 @@ struct InscriptionManagerView: View { NavigationLink { InscriptionInfoView() .environment(tournament) + .onDisappear { + self.registrationIssues = nil + Task { + self.registrationIssues = await tournament.registrationIssues() + } + } } label: { LabeledContent { if let registrationIssues { diff --git a/PadelClub/Views/Tournament/Screen/TableStructureView.swift b/PadelClub/Views/Tournament/Screen/TableStructureView.swift index 10e660a..317bf6a 100644 --- a/PadelClub/Views/Tournament/Screen/TableStructureView.swift +++ b/PadelClub/Views/Tournament/Screen/TableStructureView.swift @@ -106,14 +106,14 @@ struct TableStructureView: View { if structurePreset != .doubleGroupStage { LabeledContent { - StepperView(count: $qualifiedPerGroupStage, minimum: 0, maximum: (teamsPerGroupStage-1)) + StepperView(count: $qualifiedPerGroupStage, minimum: 1, maximum: (teamsPerGroupStage-1)) } label: { Text("Qualifié\(qualifiedPerGroupStage.pluralSuffix) par poule") } if qualifiedPerGroupStage < teamsPerGroupStage - 1 { LabeledContent { - StepperView(count: $groupStageAdditionalQualified, minimum: 1, maximum: maxMoreQualified) + StepperView(count: $groupStageAdditionalQualified, minimum: 0, maximum: maxMoreQualified) } label: { Text("Qualifié\(groupStageAdditionalQualified.pluralSuffix) supplémentaires") Text(moreQualifiedLabel)