diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index fa033d0..c3a3dd3 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -1919,7 +1919,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 12; + CURRENT_PROJECT_VERSION = 13; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -1957,7 +1957,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 12; + CURRENT_PROJECT_VERSION = 13; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index ad87cfd..7856756 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -147,6 +147,18 @@ class Match: ModelObject, Storable { let scores = tuples.map { $0 + "/" + $1 }.joined(separator: " ") return scores } + + func cleanScheduleAndSave(_ targetStartDate: Date? = nil) { + startDate = targetStartDate + endDate = nil + followingMatch()?.cleanScheduleAndSave(nil) + _loserMatch()?.cleanScheduleAndSave(nil) + do { + try DataStore.shared.matches.addOrUpdate(instance: self) + } catch { + Logger.error(error) + } + } func resetMatch() { losingTeamId = nil @@ -284,11 +296,16 @@ class Match: ModelObject, Storable { bottomPreviousRoundMatch()?._toggleMatchDisableState(state) } } - + func next() -> Match? { Store.main.filter(isIncluded: { $0.round == round && $0.index > index }).sorted(by: \.index).first } + func followingMatch() -> Match? { + guard let nextRoundId = roundObject?.nextRound()?.id else { return nil } + return Store.main.filter(isIncluded: { $0.round == nextRoundId && $0.index == index / 2 }).first + } + func getDuration() -> Int { if let tournament = currentTournament() { matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration) diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index f3da644..f11825c 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -64,6 +64,14 @@ class TeamRegistration: ModelObject, Storable { bracketPosition = seedPosition } + func expectedSummonDate() -> Date? { + if let groupStageStartDate = groupStageObject()?.startDate { + return groupStageStartDate + } else if let roundMatchStartDate = initialMatch()?.startDate { + return roundMatchStartDate + } + return nil + } var initialWeight: Int { lockedWeight ?? weight diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 6d3313f..ea6151a 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -844,13 +844,9 @@ class Tournament : ModelObject, Storable { } func isStartDateIsDifferentThanCallDate(_ team: TeamRegistration) -> Bool { - guard let callDate = team.callDate else { return true } - if let groupStageStartDate = team.groupStageObject()?.startDate { - return Calendar.current.compare(callDate, to: groupStageStartDate, toGranularity: .minute) != ComparisonResult.orderedSame - } else if let roundMatchStartDate = team.initialMatch()?.startDate { - return Calendar.current.compare(callDate, to: roundMatchStartDate, toGranularity: .minute) != ComparisonResult.orderedSame - } - return true + guard let summonDate = team.callDate else { return true } + guard let expectedSummonDate = team.expectedSummonDate() else { return true } + return Calendar.current.compare(summonDate, to: expectedSummonDate, toGranularity: .minute) != ComparisonResult.orderedSame } func availableToStart(_ allMatches: [Match]) -> [Match] { @@ -1082,8 +1078,8 @@ class Tournament : ModelObject, Storable { func callStatus() -> TournamentStatus { let selectedSortedTeams = selectedSortedTeams() - let called = selectedSortedTeams.filter{ $0.called() } - let label = called.count.formatted() + " / " + selectedSortedTeams.count.formatted() + " paires convoquées" + let called = selectedSortedTeams.filter { isStartDateIsDifferentThanCallDate($0) == false } + let label = called.count.formatted() + " / " + selectedSortedTeams.count.formatted() + " convoquées au bon horaire" let completion = (Double(called.count) / Double(selectedSortedTeams.count)) let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0))) return TournamentStatus(label: label, completion: completionLabel) @@ -1121,7 +1117,7 @@ class Tournament : ModelObject, Storable { return [teamCount.formatted() + " équipes", groupStageLabel].compactMap({ $0 }).joined(separator: ", ") } - func buildStructure() { + func deleteAndBuildEverything() { deleteStructure() deleteGroupStages() buildGroupStages() diff --git a/PadelClub/Views/Calling/CallSettingsView.swift b/PadelClub/Views/Calling/CallSettingsView.swift index 2811cd9..64ed7f7 100644 --- a/PadelClub/Views/Calling/CallSettingsView.swift +++ b/PadelClub/Views/Calling/CallSettingsView.swift @@ -43,7 +43,7 @@ struct CallSettingsView: View { RowButtonView("Tout le monde a été convoqué", role: .destructive) { let teams = tournament.unsortedTeams() teams.forEach { team in - team.callDate = Date() + team.callDate = team.expectedSummonDate() } try? dataStore.teamRegistrations.addOrUpdate(contentOfs: teams) } diff --git a/PadelClub/Views/Calling/CallView.swift b/PadelClub/Views/Calling/CallView.swift index da9bd86..e15ba4a 100644 --- a/PadelClub/Views/Calling/CallView.swift +++ b/PadelClub/Views/Calling/CallView.swift @@ -31,7 +31,7 @@ struct CallView: View { Text(startDate.formatted(.dateTime.weekday().day(.twoDigits).month().year())) } Spacer() - Text("paires convoquées") + Text("convoquées au bon horaire") } .font(.caption) .foregroundColor(.secondary) @@ -130,7 +130,6 @@ struct CallView: View { MessageComposeView(recipients: recipients, body: body) { result in switch result { case .cancelled: - _called(true) break case .failed: self.sentError = .messageFailed @@ -153,7 +152,6 @@ struct CallView: View { switch result { case .cancelled, .saved: self.contactType = nil - _called(true) case .failed: self.contactType = nil self.sentError = .mailFailed diff --git a/PadelClub/Views/Club/Shared/ClubCourtSetupView.swift b/PadelClub/Views/Club/Shared/ClubCourtSetupView.swift index 76ba387..9567202 100644 --- a/PadelClub/Views/Club/Shared/ClubCourtSetupView.swift +++ b/PadelClub/Views/Club/Shared/ClubCourtSetupView.swift @@ -17,7 +17,7 @@ struct ClubCourtSetupView: View { @ViewBuilder var body: some View { Section { - TournamentFieldsManagerView(localizedStringKey: "Terrains", count: $club.courtCount) + TournamentFieldsManagerView(localizedStringKey: "Terrains du club", count: $club.courtCount) .disabled(displayContext == .lockedForEditing) .onChange(of: club.courtCount) { if displayContext != .addition { diff --git a/PadelClub/Views/Match/Components/MatchDateView.swift b/PadelClub/Views/Match/Components/MatchDateView.swift index 65727c7..67144e9 100644 --- a/PadelClub/Views/Match/Components/MatchDateView.swift +++ b/PadelClub/Views/Match/Components/MatchDateView.swift @@ -46,15 +46,11 @@ struct MatchDateView: View { let tournament = match.currentTournament() let estimatedDuration = tournament != nil ? match.matchFormat.getEstimatedDuration(tournament!.additionalEstimationDuration) : match.matchFormat.getEstimatedDuration() Button("Décaler de \(estimatedDuration) minutes") { - match.startDate = match.startDate?.addingTimeInterval(Double(estimatedDuration) * 60.0) - match.endDate = nil - _save() + match.cleanScheduleAndSave(match.startDate?.addingTimeInterval(Double(estimatedDuration) * 60.0)) } } Button("Retirer l'horaire") { - match.startDate = nil - match.endDate = nil - _save() + match.cleanScheduleAndSave() } } } label: { diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentClubSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentClubSettingsView.swift index 29e7072..d49d35f 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentClubSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentClubSettingsView.swift @@ -49,6 +49,49 @@ struct TournamentClubSettingsView: View { } } + + Section { + TournamentFieldsManagerView(localizedStringKey: "Terrains pour le tournoi", count: $tournament.courtCount) + + if let event = tournament.eventObject() { + NavigationLink { + CourtAvailabilitySettingsView(event: event) + .environment(tournament) + } label: { + Text("Préciser la disponibilité des terrains") + } + } + } footer: { + if let club = tournament.club() { + if tournament.courtCount < club.courtCount { + let plural = tournament.courtCount.pluralSuffix + let verb = tournament.courtCount > 1 ? "seront" : "sera" + Text("En réduisant les terrains maximum, seul\(plural) le\(plural) \(tournament.courtCount) premier\(plural) terrain\(plural) \(verb) utilisé\(plural)") + Text(", par contre, si vous augmentez le nombre de terrains, vous pourrez plutôt préciser quel terrain n'est pas disponible.") + } else if tournament.courtCount > club.courtCount { + let isCreatedByUser = club.hasBeenCreated(by: dataStore.user.id) + Button { + do { + club.courtCount = tournament.courtCount + try dataStore.clubs.addOrUpdate(instance: club) + } catch { + Logger.error(error) + } + } label: { + if isCreatedByUser { + 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) + } + } + .buttonStyle(.plain) + .disabled(isCreatedByUser == false) + } + } + } + + + if let selectedClub { ClubCourtSetupView(club: selectedClub, displayContext: selectedClub.hasBeenCreated(by: dataStore.user.id) ? .edition : .lockedForEditing, selectedCourt: $selectedCourt) .onChange(of: selectedClub.courtCount) { diff --git a/PadelClub/Views/Tournament/Screen/TableStructureView.swift b/PadelClub/Views/Tournament/Screen/TableStructureView.swift index 3faa36f..c3e61ee 100644 --- a/PadelClub/Views/Tournament/Screen/TableStructureView.swift +++ b/PadelClub/Views/Tournament/Screen/TableStructureView.swift @@ -204,15 +204,15 @@ struct TableStructureView: View { } } .disabled(updatedElements.isEmpty) - .confirmationDialog("Mise à jour de la structure", isPresented: $presentRefreshStructureWarning, actions: { + .confirmationDialog("Refaire la structure", isPresented: $presentRefreshStructureWarning, actions: { if requirements.allSatisfy({ $0 == .groupStage }) { - Button("Mettre à jour les poules") { + Button("Refaire les poules") { _save(rebuildEverything: false) } } - Button("Tout mettre à jour", role: .destructive) { + Button("Tout refaire", role: .destructive) { _save(rebuildEverything: true) } @@ -234,10 +234,6 @@ struct TableStructureView: View { do { let requirements = Set(updatedElements.compactMap { $0.requiresRebuilding }) - if (rebuildEverything == false && requirements.contains(.all)) || rebuildEverything { - tournament.deleteStructure() - } - tournament.teamCount = teamCount tournament.groupStageCount = groupStageCount tournament.teamsPerGroupStage = teamsPerGroupStage @@ -245,8 +241,9 @@ struct TableStructureView: View { tournament.groupStageAdditionalQualified = groupStageAdditionalQualified if (rebuildEverything == false && requirements.contains(.all)) || rebuildEverything { - tournament.buildStructure() + tournament.deleteAndBuildEverything() } else if (rebuildEverything == false && requirements.contains(.groupStage)) { + tournament.deleteGroupStages() tournament.buildGroupStages() } @@ -342,7 +339,7 @@ extension TableStructureView { var rebuildingRequirementMessage: String { switch self { case .groupStage: - return "Si vous le souhaitez, seulement les poules seront mis à jour. Le tableau ne sera pas modifié." + return "Si vous le souhaitez, seulement les poules seront refaites. Le tableau ne sera pas modifié." case .all: return "Tous les matchs seront re-générés. La position des têtes de série sera remise à zéro et les poules seront reconstruites." }