diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index de0a683..d2e29a3 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -56,11 +56,11 @@ class Match: ModelObject, Storable { } func matchWarningSubject() -> String { - [roundTitle(), matchTitle()].compacted().joined(separator: " ") + [roundTitle(), matchTitle(.short)].compacted().joined(separator: " ") } func matchWarningMessage() -> String { - [roundTitle(), matchTitle(), startDate?.localizedDate(), courtName()].compacted().joined(separator: "\n") + [roundTitle(), matchTitle(.short), startDate?.localizedDate(), courtName()].compacted().joined(separator: "\n") } func matchTitle(_ displayStyle: DisplayStyle = .wide) -> String { @@ -448,6 +448,10 @@ class Match: ModelObject, Storable { endDate ?? .distantFuture } + func hasSpaceLeft() -> Bool { + teams().count == 1 + } + func isReady() -> Bool { teams().count == 2 } diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index 74d7196..4654725 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -139,9 +139,15 @@ class PlayerRegistration: ModelObject, Storable { func playerLabel(_ displayStyle: DisplayStyle = .wide) -> String { switch displayStyle { case .wide: - lastName.trimmed.capitalized + " " + firstName.trimmed.capitalized + return lastName.trimmed.capitalized + " " + firstName.trimmed.capitalized case .short: - lastName.trimmed.capitalized + " " + firstName.trimmed.prefix(1).capitalized + "." + let names = lastName.components(separatedBy: .whitespaces) + if lastName.components(separatedBy: .whitespaces).count > 1 { + if let firstLongWord = names.first(where: { $0.count > 3 }) { + return firstLongWord.trimmed.capitalized.trunc(length: 10) + " " + firstName.trimmed.prefix(1).capitalized + "." + } + } + return lastName.trimmed.capitalized.trunc(length: 10) + " " + firstName.trimmed.prefix(1).capitalized + "." } } diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index 0c6bdb7..1f5f52d 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -123,7 +123,7 @@ class TeamRegistration: ModelObject, Storable { case .wide: players().map { $0.playerLabel(displayStyle) }.joined(separator: " & ") case .short: - players().map { $0.playerLabel(.wide) }.joined(separator: "\n") + players().map { $0.playerLabel(.short) }.joined(separator: "\n") } } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index ef879c5..1b274c0 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -328,6 +328,10 @@ class Tournament : ModelObject, Storable { let groupStages = groupStages() return groupStages.filter({ $0.hasStarted() && $0.hasEnded() == false }).sorted(by: \.index).first ?? groupStages.first } + + func matchesWithSpace() -> [Match] { + getActiveRound()?.playedMatches().filter({ $0.hasSpaceLeft() }) ?? [] + } func getActiveRound(withSeeds: Bool = false) -> Round? { let rounds = rounds() @@ -979,6 +983,14 @@ class Tournament : ModelObject, Storable { selectedSortedTeams().firstIndex(where: { $0.id == team.id }) } + func labelIndexOf(team: TeamRegistration) -> String? { + if let teamIndex = indexOf(team: team) { + return "#" + (teamIndex + 1).formatted() + } else { + return nil + } + } + func addTeam(_ players: Set, registrationDate: Date? = nil) -> TeamRegistration { let team = TeamRegistration(tournament: id, registrationDate: registrationDate ?? Date()) team.setWeight(from: Array(players), inTournamentCategory: tournamentCategory) diff --git a/PadelClub/Extensions/String+Extensions.swift b/PadelClub/Extensions/String+Extensions.swift index b515c10..f0afc3a 100644 --- a/PadelClub/Extensions/String+Extensions.swift +++ b/PadelClub/Extensions/String+Extensions.swift @@ -7,7 +7,12 @@ import Foundation +// MARK: - Trimming and stuff extension String { + func trunc(length: Int, trailing: String = "…") -> String { + return (self.count > length) ? self.prefix(length) + trailing : self + } + var trimmed: String { trimmingCharacters(in: .whitespacesAndNewlines) } @@ -29,6 +34,7 @@ extension String { } } +// MARK: - Club Name extension String { func acronym() -> String { let acronym = canonicalVersion.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines) @@ -58,16 +64,8 @@ extension String { } } +// MARK: - FFT License extension String { - enum RegexStatic { - static let mobileNumber = /^0[6-7]/ - //static let mobileNumber = /^(?:(?:\+|00)33[\s.-]{0,3}(?:\(0\)[\s.-]{0,3})?|0)[1-9](?:(?:[\s.-]?\d{2}){4}|\d{2}(?:[\s.-]?\d{3}){2})$/ - } - - func isMobileNumber() -> Bool { - firstMatch(of: RegexStatic.mobileNumber) != nil - } - var computedLicense: String { if let licenseKey { return self + licenseKey @@ -138,20 +136,24 @@ extension String { } return nil } -} -extension String { func licencesFound() -> [String] { let matches = self.matches(of: /[1-9][0-9]{5,7}/) return matches.map { String(self[$0.range]) } } } -extension LosslessStringConvertible { - var string: String { .init(self) } -} - +// MARK: - FFT Source Importing extension String { + enum RegexStatic { + static let mobileNumber = /^0[6-7]/ + //static let mobileNumber = /^(?:(?:\+|00)33[\s.-]{0,3}(?:\(0\)[\s.-]{0,3})?|0)[1-9](?:(?:[\s.-]?\d{2}){4}|\d{2}(?:[\s.-]?\d{3}){2})$/ + } + + func isMobileNumber() -> Bool { + firstMatch(of: RegexStatic.mobileNumber) != nil + } + //april 04-2024 bug with accent characters / adobe / fft mutating func replace(characters: [(Character, Character)]) { for (targetChar, replacementChar) in characters { @@ -160,7 +162,13 @@ extension String { } } +// MARK: - Player Names extension StringProtocol { var firstUppercased: String { prefix(1).uppercased() + dropFirst() } var firstCapitalized: String { prefix(1).capitalized + dropFirst() } } + +// MARK: - todo clean up ?? +extension LosslessStringConvertible { + var string: String { .init(self) } +} diff --git a/PadelClub/ViewModel/AgendaDestination.swift b/PadelClub/ViewModel/AgendaDestination.swift index 18982e8..e4a43af 100644 --- a/PadelClub/ViewModel/AgendaDestination.swift +++ b/PadelClub/ViewModel/AgendaDestination.swift @@ -53,7 +53,7 @@ enum AgendaDestination: CaseIterable, Identifiable, Selectable { case .history: DataStore.shared.tournaments.filter { $0.endDate != nil && FederalDataViewModel.shared.isTournamentValidForFilters($0) }.count case .tenup: - FederalDataViewModel.shared.filteredFederalTournaments.count + FederalDataViewModel.shared.filteredFederalTournaments.map { $0.tournaments.count }.reduce(0,+) } } diff --git a/PadelClub/Views/Components/FortuneWheelView.swift b/PadelClub/Views/Components/FortuneWheelView.swift index c31a452..41a9e22 100644 --- a/PadelClub/Views/Components/FortuneWheelView.swift +++ b/PadelClub/Views/Components/FortuneWheelView.swift @@ -8,24 +8,35 @@ import SwiftUI protocol SpinDrawable { - func segmentLabel() -> String + func segmentLabel(_ displayStyle: DisplayStyle) -> [String] } extension String: SpinDrawable { - func segmentLabel() -> String { - self + func segmentLabel(_ displayStyle: DisplayStyle) -> [String] { + [self] } } extension Match: SpinDrawable { - func segmentLabel() -> String { - self.matchTitle(.wide) + func segmentLabel(_ displayStyle: DisplayStyle) -> [String] { + let teams = teams() + if teams.count == 1 { + return teams.first!.segmentLabel(displayStyle) + } else { + return [roundTitle(), matchTitle(displayStyle)].compactMap { $0 } + } } } extension TeamRegistration: SpinDrawable { - func segmentLabel() -> String { - self.teamLabel(.short) + func segmentLabel(_ displayStyle: DisplayStyle) -> [String] { + var strings: [String] = [] + let indexLabel = tournamentObject()?.labelIndexOf(team: self) + if let indexLabel { + strings.append(indexLabel) + } + strings.append(contentsOf: self.players().map { $0.playerLabel(displayStyle) }) + return strings } } @@ -40,15 +51,14 @@ struct DrawOption: Identifiable, SpinDrawable { let initialIndex: Int let option: SpinDrawable - func segmentLabel() -> String { - option.segmentLabel() + func segmentLabel(_ displayStyle: DisplayStyle) -> [String] { + option.segmentLabel(displayStyle) } } struct SpinDrawView: View { @Environment(\.dismiss) private var dismiss - let time: Date = Date() let drawees: [any SpinDrawable] @State var segments: [any SpinDrawable] let completion: ([DrawResult]) -> Void // Completion closure @@ -56,29 +66,20 @@ struct SpinDrawView: View { @State private var drawCount: Int = 0 @State private var draws: [DrawResult] = [DrawResult]() @State private var drawOptions: [DrawOption] = [DrawOption]() - + @State private var selectedIndex: Int? + var autoMode: Bool { drawees.count > 1 } - - func validationLabel(drawee: Int, result: SpinDrawable) -> String { - let draw = drawees[drawee] - return draw.segmentLabel() + " -> " + result.segmentLabel() - } - @State private var selectedIndex: Int? var body: some View { List { - Section { - Text(time.formatted(date: .complete, time: .complete)) - Text(time, style: .timer) - } - if selectedIndex != nil { Section { - Text(validationLabel(drawee: drawCount, result: segments[draws.last!.drawIndex])) + _validationLabelView(drawee: drawCount, result: segments[draws.last!.drawIndex]) if autoMode == false || drawCount == drawees.count { - RowButtonView("ok") { + RowButtonView("Valider le tirage") { + completion(draws) dismiss() } } else { @@ -87,15 +88,15 @@ struct SpinDrawView: View { } } else if drawCount < drawees.count { Section { - Text(drawees[drawCount].segmentLabel()) + _segmentLabelView(segment: drawees[drawCount].segmentLabel(.wide), horizontalAlignment: .center) } Section { - FortuneWheelTestView(segments: drawOptions, autoMode: autoMode) { index in + FortuneWheelContainerView(segments: drawOptions, autoMode: autoMode) { index in self.selectedIndex = index self.draws.append(DrawResult(drawee: drawCount, drawIndex: drawOptions[index].initialIndex)) self.drawOptions.remove(at: index) - + if autoMode && drawCount < drawees.count { DispatchQueue.main.asyncAfter(deadline: .now() + 1) { self.drawCount += 1 @@ -117,7 +118,7 @@ struct SpinDrawView: View { Section { Text("Tous les tirages sont terminés") ForEach(draws) { drawResult in - Text(validationLabel(drawee: drawResult.drawee, result: segments[drawResult.drawIndex])) + _validationLabelView(drawee: drawResult.drawee, result: segments[drawResult.drawIndex]) } } @@ -135,6 +136,18 @@ struct SpinDrawView: View { Text("Comité du tournoi") } } + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Annuler", role: .cancel) { + dismiss() + } + } + } + .navigationBarBackButtonHidden() + .navigationTitle("Tirage au sort") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .toolbar(.hidden, for: .tabBar) .listStyle(.insetGrouped) .scrollDisabled(true) .onAppear { @@ -143,9 +156,29 @@ struct SpinDrawView: View { } } } + + + private func _segmentLabelView(segment: [String], horizontalAlignment: HorizontalAlignment = .leading) -> some View { + VStack(alignment: horizontalAlignment, spacing: 0.0) { + ForEach(segment, id: \.self) { string in + Text(string) + .frame(maxWidth: .infinity) + } + } + } + + @ViewBuilder + private func _validationLabelView(drawee: Int, result: SpinDrawable) -> some View { + HStack(spacing: 0.0) { + let draw = drawees[drawee] + _segmentLabelView(segment: draw.segmentLabel(.wide), horizontalAlignment: .leading) + Image(systemName: "arrowshape.forward.fill") + _segmentLabelView(segment: result.segmentLabel(.wide), horizontalAlignment: .trailing) + } + } } -struct FortuneWheelTestView: View { +struct FortuneWheelContainerView: View { @State private var rotation: Double = 0 let segments: [any SpinDrawable] let autoMode: Bool @@ -162,7 +195,6 @@ struct FortuneWheelTestView: View { .stroke(Color.black, lineWidth: 2) .frame(width: 20, height: 20) .rotationEffect(.degrees(180)) - } .onAppear { if autoMode { @@ -186,7 +218,6 @@ struct FortuneWheelTestView: View { } func rollWheel() { - rotation = 0 // Generate a random angle for the wheel to rotate @@ -249,10 +280,16 @@ struct FortuneWheelView: View { } .fill(getColor(forIndex:index)) - Text(segments[index].segmentLabel()).multilineTextAlignment(.trailing) - .rotationEffect(.degrees(Double(index) * (360 / Double(segments.count)) + (360 / Double(segments.count) / 2))) - .foregroundColor(.white) - .position(arcPosition(index: index, radius: radius)) + VStack(alignment: .trailing, spacing: 0.0) { + let strings = segments[index].segmentLabel(.short) + ForEach(strings, id: \.self) { string in + Text(string).bold() + } + } + .padding(.trailing, 30) + .rotationEffect(.degrees(Double(index) * (360 / Double(segments.count)) + (360 / Double(segments.count) / 2))) + .foregroundColor(.white) + .position(arcPosition(index: index, radius: radius)) } } } diff --git a/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift b/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift index afd06f7..01e3d1d 100644 --- a/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift +++ b/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift @@ -22,11 +22,30 @@ struct GroupStageTeamView: View { } if groupStage.tournamentObject()?.hasEnded() == false { - Section { - NavigationLink { - GroupStageTeamReplacementView(team: team) - } label: { - Text("Chercher à remplacer") + if team.qualified && team.bracketPosition == nil, let tournament = team.tournamentObject() { + Section { + NavigationLink { + SpinDrawView(drawees: [team], segments: tournament.matchesWithSpace()) { results in + + } + } label: { + Text("Tirage au sort visuel") + } + } + + Section { + RowButtonView("Tirage au sort automatique", role: .destructive) { + } + } + } + + if team.qualified == false { + Section { + NavigationLink { + GroupStageTeamReplacementView(team: team) + } label: { + Text("Chercher à remplacer") + } } } @@ -46,11 +65,13 @@ struct GroupStageTeamView: View { } } - Section { - RowButtonView("Retirer de la poule", role: .destructive) { - team.groupStagePosition = nil - team.groupStage = nil - _save() + if team.qualified == false { + Section { + RowButtonView("Retirer de la poule", role: .destructive) { + team.groupStagePosition = nil + team.groupStage = nil + _save() + } } } } diff --git a/PadelClub/Views/Round/RoundView.swift b/PadelClub/Views/Round/RoundView.swift index 4fba924..2998506 100644 --- a/PadelClub/Views/Round/RoundView.swift +++ b/PadelClub/Views/Round/RoundView.swift @@ -18,7 +18,9 @@ struct RoundView: View { List { let loserRounds = round.loserRounds() - + let availableQualifiedTeams = tournament.availableQualifiedTeams() + let displayableMatches = round.displayableMatches() + let spaceLeft = displayableMatches.filter({ $0.hasSpaceLeft() }) if isEditingTournamentSeed.wrappedValue == false { //(where: { $0.isDisabled() == false || isEditingTournamentSeed.wrappedValue }) if loserRounds.isEmpty == false { @@ -33,6 +35,19 @@ struct RoundView: View { } } } + } else if availableQualifiedTeams.isEmpty == false && spaceLeft.isEmpty == false { + NavigationLink("Tirer au sort la position d'un qualifié") { + SpinDrawView(drawees: availableQualifiedTeams, segments: spaceLeft) { results in + results.forEach { drawResult in + print(availableQualifiedTeams[drawResult.drawee].teamLabel()) + print(spaceLeft[drawResult.drawIndex].matchTitle()) + availableQualifiedTeams[drawResult.drawee].setSeedPosition(inSpot: spaceLeft[drawResult.drawIndex], slot: nil, opposingSeeding: true) + } + try? dataStore.matches.addOrUpdate(contentOfs: spaceLeft) + try? dataStore.teamRegistrations.addOrUpdate(contentOfs: availableQualifiedTeams) + isEditingTournamentSeed.wrappedValue.toggle() + } + } } else if let availableSeedGroup = tournament.seedGroupAvailable(atRoundIndex: round.index) { RowButtonView("Placer \(availableSeedGroup.localizedLabel())" + ((availableSeedGroup.isFixed() == false) ? " au hasard" : "")) { @@ -62,7 +77,7 @@ struct RoundView: View { } } - ForEach(round.displayableMatches()) { match in + ForEach(displayableMatches) { match in Section { MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle) } header: { diff --git a/PadelClub/Views/Round/RoundsView.swift b/PadelClub/Views/Round/RoundsView.swift index db3b83d..76df209 100644 --- a/PadelClub/Views/Round/RoundsView.swift +++ b/PadelClub/Views/Round/RoundsView.swift @@ -15,7 +15,7 @@ struct RoundsView: View { init(tournament: Tournament) { self.tournament = tournament _selectedRound = State(wrappedValue: tournament.getActiveRound()) - if tournament.availableSeeds().isEmpty == false { + if tournament.availableSeeds().isEmpty == false || tournament.availableQualifiedTeams().isEmpty == false { _isEditingTournamentSeed = State(wrappedValue: true) } } diff --git a/PadelClub/Views/Tournament/Screen/BroadcastView.swift b/PadelClub/Views/Tournament/Screen/BroadcastView.swift index 5b89cf0..6351a46 100644 --- a/PadelClub/Views/Tournament/Screen/BroadcastView.swift +++ b/PadelClub/Views/Tournament/Screen/BroadcastView.swift @@ -126,9 +126,13 @@ struct BroadcastView: View { Label("Partager le lien", systemImage: "link") } } label: { - Text("lien") - .underline() + HStack { + Spacer() + Text("lien") + .underline() + } } + .frame(maxWidth: .infinity) .buttonStyle(.borderless) }