Merge branch 'main' of https://stax.alwaysdata.net/gitea/staxriver/PadelClub
commit
edc99d1e79
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,206 @@ |
|||||||
|
// |
||||||
|
// LoserBracketFromGroupStageView.swift |
||||||
|
// PadelClub |
||||||
|
// |
||||||
|
// Created by razmig on 07/09/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import SwiftUI |
||||||
|
import LeStorage |
||||||
|
|
||||||
|
struct LoserBracketFromGroupStageView: View { |
||||||
|
|
||||||
|
@Environment(Tournament.self) var tournament: Tournament |
||||||
|
@EnvironmentObject var dataStore: DataStore |
||||||
|
@Environment(NavigationViewModel.self) private var navigation: NavigationViewModel |
||||||
|
@State var loserBracket: Round |
||||||
|
@State private var isEditingLoserBracketGroupStage: Bool |
||||||
|
|
||||||
|
init(loserBracket: Round) { |
||||||
|
self.loserBracket = loserBracket |
||||||
|
_isEditingLoserBracketGroupStage = .init(wrappedValue: loserBracket._matches().isEmpty) |
||||||
|
} |
||||||
|
|
||||||
|
var tournamentStore: TournamentStore { |
||||||
|
return self.tournament.tournamentStore |
||||||
|
} |
||||||
|
|
||||||
|
var displayableMatches: [Match] { |
||||||
|
loserBracket.playedMatches().sorted(by: \.index) |
||||||
|
} |
||||||
|
|
||||||
|
var body: some View { |
||||||
|
List { |
||||||
|
if isEditingLoserBracketGroupStage == true && displayableMatches.isEmpty == false { |
||||||
|
Section { |
||||||
|
RowButtonView("Ajouter un match", role: .destructive) { |
||||||
|
_addNewMatch() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
ForEach(displayableMatches) { match in |
||||||
|
Section { |
||||||
|
MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle) |
||||||
|
.environment(\.isEditingTournamentSeed, $isEditingLoserBracketGroupStage) |
||||||
|
} header: { |
||||||
|
let tournamentTeamCount = tournament.teamCount |
||||||
|
let seedIntervalPointRange = tournament.tournamentLevel.pointsRange(first: match.index, last: match.index + displayableMatches.filter({ $0.index == match.index }).count, teamsCount: tournamentTeamCount) |
||||||
|
HStack { |
||||||
|
Text(match.matchTitle(.wide)) |
||||||
|
Spacer() |
||||||
|
Text(seedIntervalPointRange) |
||||||
|
.font(.caption) |
||||||
|
} |
||||||
|
} footer: { |
||||||
|
if isEditingLoserBracketGroupStage == true { |
||||||
|
GroupStageLoserBracketMatchFooterView(match: match, samePlaceThanAboveOption: displayableMatches.count > 1) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
Section { |
||||||
|
if displayableMatches.count > 1 && isEditingLoserBracketGroupStage == true { |
||||||
|
Section { |
||||||
|
RowButtonView("Effacer tous les matchs", role: .destructive) { |
||||||
|
_deleteAllMatches() |
||||||
|
} |
||||||
|
} footer: { |
||||||
|
Text("Efface tous les matchs de classement de poules ci-dessus.") |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
.overlay { |
||||||
|
if displayableMatches.isEmpty { |
||||||
|
ContentUnavailableView { |
||||||
|
Label("Aucun match de classement", systemImage: "figure.tennis") |
||||||
|
} description: { |
||||||
|
Text("Vous n'avez créé aucun match de classement entre les perdants de poules.") |
||||||
|
} actions: { |
||||||
|
RowButtonView("Ajouter un match") { |
||||||
|
isEditingLoserBracketGroupStage = true |
||||||
|
_addNewMatch() |
||||||
|
} |
||||||
|
.padding(.horizontal) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
.headerProminence(.increased) |
||||||
|
.navigationTitle("Classement de poules") |
||||||
|
.toolbar { |
||||||
|
ToolbarItem(placement: .topBarTrailing) { |
||||||
|
if displayableMatches.isEmpty == false { |
||||||
|
Button(isEditingLoserBracketGroupStage == true ? "Valider" : "Modifier") { |
||||||
|
if isEditingLoserBracketGroupStage == true { |
||||||
|
isEditingLoserBracketGroupStage = false |
||||||
|
} else { |
||||||
|
isEditingLoserBracketGroupStage = true |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private func _addNewMatch() { |
||||||
|
let currentGroupStageLoserBracketsInitialPlace = tournament.groupStageLoserBracketsInitialPlace() |
||||||
|
let placeCount = displayableMatches.isEmpty ? currentGroupStageLoserBracketsInitialPlace : max(currentGroupStageLoserBracketsInitialPlace, displayableMatches.map({ $0.index }).max()! + 2) |
||||||
|
let match = Match(round: loserBracket.id, index: placeCount, matchFormat: loserBracket.matchFormat) |
||||||
|
match.name = "\(placeCount)\(placeCount.ordinalFormattedSuffix()) place" |
||||||
|
do { |
||||||
|
try tournamentStore.matches.addOrUpdate(instance: match) |
||||||
|
} catch { |
||||||
|
Logger.error(error) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private func _deleteAllMatches() { |
||||||
|
let displayableMatches = loserBracket.playedMatches().sorted(by: \.index) |
||||||
|
|
||||||
|
do { |
||||||
|
for match in displayableMatches { |
||||||
|
try match.deleteDependencies() |
||||||
|
} |
||||||
|
try tournamentStore.matches.delete(contentOfs: displayableMatches) |
||||||
|
} catch { |
||||||
|
Logger.error(error) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
struct GroupStageLoserBracketMatchFooterView: View { |
||||||
|
@Environment(Tournament.self) var tournament: Tournament |
||||||
|
@Bindable var match: Match |
||||||
|
let samePlaceThanAboveOption: Bool |
||||||
|
@State private var selectPlacePlayed: Bool = false |
||||||
|
@State private var index: Int = 0 |
||||||
|
|
||||||
|
var body: some View { |
||||||
|
HStack { |
||||||
|
Menu { |
||||||
|
if samePlaceThanAboveOption { |
||||||
|
Button("Même place qu'au-dessus") { |
||||||
|
_updateIndex(match.index-2) |
||||||
|
} |
||||||
|
} |
||||||
|
Button("Choisir la place") { |
||||||
|
index = match.index |
||||||
|
selectPlacePlayed = true |
||||||
|
} |
||||||
|
} label: { |
||||||
|
Text("Éditer la place jouée") |
||||||
|
.underline() |
||||||
|
} |
||||||
|
|
||||||
|
Spacer() |
||||||
|
FooterButtonView("Effacer", role: .destructive) { |
||||||
|
do { |
||||||
|
try match.deleteDependencies() |
||||||
|
try match.tournamentStore.matches.delete(instance: match) |
||||||
|
} catch { |
||||||
|
Logger.error(error) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
.alert("Place jouée", isPresented: $selectPlacePlayed) { |
||||||
|
TextField("Place jouée", value: $index, format: .number) |
||||||
|
.keyboardType(.numberPad) |
||||||
|
.multilineTextAlignment(.trailing) |
||||||
|
.onSubmit { |
||||||
|
_updateIndex(index) |
||||||
|
} |
||||||
|
Button("Confirmer") { |
||||||
|
_updateIndex(index) |
||||||
|
} |
||||||
|
Button("Annuler", role: .cancel) { |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private func _updateIndex(_ newIndex: Int) { |
||||||
|
let newIndexValidated = max(1,abs(newIndex)) |
||||||
|
let teamScores = match.teamScores |
||||||
|
teamScores.forEach { ts in |
||||||
|
if let luckyLoser = ts.luckyLoser { |
||||||
|
ts.luckyLoser = (luckyLoser - match.index * 2) % 2 + newIndexValidated * 2 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
match.index = newIndexValidated |
||||||
|
|
||||||
|
match.name = "\(newIndexValidated)\(newIndexValidated.ordinalFormattedSuffix()) place" |
||||||
|
|
||||||
|
|
||||||
|
do { |
||||||
|
try match.tournamentStore.teamScores.addOrUpdate(contentOfs: teamScores) |
||||||
|
} catch { |
||||||
|
Logger.error(error) |
||||||
|
} |
||||||
|
do { |
||||||
|
try match.tournamentStore.matches.addOrUpdate(instance: match) |
||||||
|
} catch { |
||||||
|
Logger.error(error) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -1,85 +0,0 @@ |
|||||||
// |
|
||||||
// LoserGroupStageSettingsView.swift |
|
||||||
// PadelClub |
|
||||||
// |
|
||||||
// Created by Razmig Sarkissian on 29/06/2024. |
|
||||||
// |
|
||||||
|
|
||||||
import SwiftUI |
|
||||||
|
|
||||||
extension Round { |
|
||||||
var isGroupStageLoserBracket: Bool { |
|
||||||
return false |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
extension Tournament { |
|
||||||
func groupStageLoserBrackets() -> [Round] { |
|
||||||
[] |
|
||||||
} |
|
||||||
|
|
||||||
func removeGroupStageLoserBrackets() { |
|
||||||
|
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
struct LoserGroupStageSettingsView: View { |
|
||||||
var tournament: Tournament |
|
||||||
@State private var loserGroupStageBracketType: Int? = nil |
|
||||||
@State private var losers : Set<TeamRegistration> = Set() |
|
||||||
@Environment(\.editMode) private var editMode |
|
||||||
|
|
||||||
var body: some View { |
|
||||||
List(selection: $losers) { |
|
||||||
if tournament.groupStageLoserBrackets().isEmpty == false { |
|
||||||
//for each all rounds without parent and loserGroupStage, ability to delete them |
|
||||||
Section { |
|
||||||
RowButtonView("Effacer", role: .destructive) { |
|
||||||
tournament.removeGroupStageLoserBrackets() |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if self.editMode?.wrappedValue == .active { |
|
||||||
Section { |
|
||||||
//rajouter + toolbar valider / cancel |
|
||||||
ForEach(tournament.groupStageTeams().filter({ $0.qualified == false })) { team in |
|
||||||
TeamRowView(team: team).tag(team) |
|
||||||
} |
|
||||||
} header: { |
|
||||||
Text("Sélection des perdants de poules") |
|
||||||
} |
|
||||||
} else { |
|
||||||
Section { |
|
||||||
RowButtonView("Ajouter un match de perdant") { |
|
||||||
self.editMode?.wrappedValue = .active |
|
||||||
} |
|
||||||
} footer: { |
|
||||||
Text("Permet d'ajouter un match de perdant de poules.") |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
.toolbar { |
|
||||||
if self.editMode?.wrappedValue == .active { |
|
||||||
ToolbarItem(placement: .topBarLeading) { |
|
||||||
Button("Annuler") { |
|
||||||
self.editMode?.wrappedValue = .inactive |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
ToolbarItem(placement: .topBarTrailing) { |
|
||||||
Button("Valider") { |
|
||||||
self.editMode?.wrappedValue = .inactive |
|
||||||
//tournament.createGroupStageLoserBracket() |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
.navigationTitle("Match de perdant de poules") |
|
||||||
.navigationBarBackButtonHidden(self.editMode?.wrappedValue == .active) |
|
||||||
.navigationBarTitleDisplayMode(.inline) |
|
||||||
.toolbar(.visible, for: .navigationBar) |
|
||||||
.headerProminence(.increased) |
|
||||||
.toolbarBackground(.visible, for: .navigationBar) |
|
||||||
} |
|
||||||
} |
|
||||||
@ -0,0 +1,483 @@ |
|||||||
|
// |
||||||
|
// TournamentLookUpView.swift |
||||||
|
// PadelClub |
||||||
|
// |
||||||
|
// Created by razmig on 08/09/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import SwiftUI |
||||||
|
import CoreLocation |
||||||
|
import CoreLocationUI |
||||||
|
|
||||||
|
struct TournamentLookUpView: View { |
||||||
|
@Environment(FederalDataViewModel.self) var federalDataViewModel: FederalDataViewModel |
||||||
|
@StateObject var locationManager = LocationManager() |
||||||
|
@Environment(\.dismiss) private var dismiss |
||||||
|
|
||||||
|
@State private var searchField: String = "" |
||||||
|
@State var page: Int = 0 |
||||||
|
@State var total: Int = 0 |
||||||
|
|
||||||
|
@State private var tournamentCategories = Set<TournamentCategory.ID>() |
||||||
|
@State private var tournamentLevels = Set<TournamentLevel.ID>() |
||||||
|
@State private var tournamentAges = Set<FederalTournamentAge.ID>() |
||||||
|
@State private var tournamentTypes = Set<FederalTournamentType.ID>() |
||||||
|
@State private var searching: Bool = false |
||||||
|
@State private var startDate: Date = Date() |
||||||
|
@State private var endDate: Date = Calendar.current.date(byAdding: .month, value: 3, to: Date())! |
||||||
|
@AppStorage("lastCity") private var city: String = "" |
||||||
|
@State private var ligue: String = "" |
||||||
|
@State private var distance: Double = 30 |
||||||
|
@State private var sortingOption: String = "dateDebut+asc" |
||||||
|
@State private var requestedToGetAllPages: Bool = false |
||||||
|
@State private var nationalCup: Bool = false |
||||||
|
@State private var revealSearchParameters: Bool = true |
||||||
|
@State private var presentAlert: Bool = false |
||||||
|
|
||||||
|
var tournaments: [FederalTournament] { |
||||||
|
federalDataViewModel.searchedFederalTournaments |
||||||
|
} |
||||||
|
|
||||||
|
var body: some View { |
||||||
|
List { |
||||||
|
searchParametersView |
||||||
|
} |
||||||
|
.alert("Attention", isPresented: $presentAlert, actions: { |
||||||
|
Button { |
||||||
|
presentAlert = false |
||||||
|
requestedToGetAllPages = true |
||||||
|
page += 1 |
||||||
|
searching = true |
||||||
|
Task { |
||||||
|
await getNewPage() |
||||||
|
searching = false |
||||||
|
dismiss() |
||||||
|
} |
||||||
|
} label: { |
||||||
|
Label("Tout voir", systemImage: "arrow.down.circle") |
||||||
|
} |
||||||
|
Button("Annuler") { |
||||||
|
revealSearchParameters = true |
||||||
|
presentAlert = false |
||||||
|
} |
||||||
|
}, message: { |
||||||
|
Text("Il y a beacoup de tournois pour cette requête, êtes-vous sûr de vouloir tout récupérer ? Sinon essayez d'affiner votre recherche.") |
||||||
|
}) |
||||||
|
.toolbarBackground(.visible, for: .bottomBar, .navigationBar) |
||||||
|
.navigationTitle("Chercher un tournoi") |
||||||
|
.navigationBarTitleDisplayMode(.inline) |
||||||
|
.onChange(of: locationManager.city) { |
||||||
|
if let newValue = locationManager.city, city.isEmpty { |
||||||
|
city = newValue |
||||||
|
} |
||||||
|
} |
||||||
|
.toolbarTitleDisplayMode(.large) |
||||||
|
.toolbar { |
||||||
|
ToolbarItem(placement: .bottomBar) { |
||||||
|
if revealSearchParameters { |
||||||
|
FooterButtonView("Lancer la recherche") { |
||||||
|
runSearch() |
||||||
|
} |
||||||
|
.disabled(searching) |
||||||
|
} else if searching { |
||||||
|
HStack(spacing: 20) { |
||||||
|
Spacer() |
||||||
|
ProgressView() |
||||||
|
if total > 0 { |
||||||
|
let percent = Double(tournaments.count) / Double(total) |
||||||
|
Text(percent.formatted(.percent.precision(.significantDigits(1...3))) + " en récupération de Tenup") |
||||||
|
.font(.caption) |
||||||
|
} |
||||||
|
Spacer() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
ToolbarItem(placement: .topBarTrailing) { |
||||||
|
Menu { |
||||||
|
#if DEBUG |
||||||
|
if tournaments.isEmpty == false { |
||||||
|
Section { |
||||||
|
ShareLink(item: pastedTournaments) { |
||||||
|
Label("Par texte", systemImage: "square.and.arrow.up") |
||||||
|
.labelStyle(.titleAndIcon) |
||||||
|
} |
||||||
|
ShareLink(item: japList) { |
||||||
|
Label("JAP liste", systemImage: "square.and.arrow.up") |
||||||
|
.labelStyle(.titleAndIcon) |
||||||
|
} |
||||||
|
} header: { |
||||||
|
Text("Partager les résultats") |
||||||
|
} |
||||||
|
} |
||||||
|
Divider() |
||||||
|
#endif |
||||||
|
|
||||||
|
Button(role: .destructive) { |
||||||
|
tournamentLevels = Set() |
||||||
|
tournamentCategories = Set() |
||||||
|
city = "" |
||||||
|
locationManager.location = nil |
||||||
|
locationManager.city = nil |
||||||
|
distance = 30 |
||||||
|
startDate = Date() |
||||||
|
endDate = Calendar.current.date(byAdding: .month, value: 3, to: Date())! |
||||||
|
sortingOption = "dateDebut+asc" |
||||||
|
revealSearchParameters = true |
||||||
|
federalDataViewModel.searchedFederalTournaments = [] |
||||||
|
federalDataViewModel.searchAttemptCount = 0 |
||||||
|
} label: { |
||||||
|
Text("Ré-initialiser la recherche") |
||||||
|
} |
||||||
|
} label: { |
||||||
|
Label("Options", systemImage: "ellipsis.circle") |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
var pastedTournaments: String { |
||||||
|
tournaments.map { $0.shareMessage }.joined() |
||||||
|
} |
||||||
|
|
||||||
|
var japList: String { |
||||||
|
Set(tournaments.map { $0.japMessage }).joined(separator: "\n") |
||||||
|
} |
||||||
|
|
||||||
|
private var clubsFound: [String] { |
||||||
|
Set(tournaments.compactMap { $0.nomClub }).sorted() |
||||||
|
} |
||||||
|
|
||||||
|
private var liguesFound: [String] { |
||||||
|
Set(tournaments.compactMap { $0.nomLigue }).sorted() |
||||||
|
} |
||||||
|
|
||||||
|
private func runSearch() { |
||||||
|
revealSearchParameters = false |
||||||
|
total = 0 |
||||||
|
page = 0 |
||||||
|
federalDataViewModel.searchedFederalTournaments = [] |
||||||
|
searching = true |
||||||
|
requestedToGetAllPages = false |
||||||
|
federalDataViewModel.searchAttemptCount += 1 |
||||||
|
Task { |
||||||
|
await getNewPage() |
||||||
|
searching = false |
||||||
|
if tournaments.isEmpty == false && tournaments.count < total && total >= 200 && requestedToGetAllPages == false { |
||||||
|
presentAlert = true |
||||||
|
} else { |
||||||
|
dismiss() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private var distanceLimit: Measurement<UnitLength> { |
||||||
|
distanceLimit(distance: distance) |
||||||
|
} |
||||||
|
|
||||||
|
private func distanceLimit(distance: Double) -> Measurement<UnitLength> { |
||||||
|
Measurement(value: distance, unit: .kilometers) |
||||||
|
} |
||||||
|
|
||||||
|
private var categories: [TournamentCategory] { |
||||||
|
tournamentCategories.compactMap { TournamentCategory(rawValue: $0) } |
||||||
|
} |
||||||
|
|
||||||
|
private var levels: [TournamentLevel] { |
||||||
|
tournamentLevels.compactMap { TournamentLevel(rawValue: $0) } |
||||||
|
} |
||||||
|
|
||||||
|
private var ages: [FederalTournamentAge] { |
||||||
|
tournamentAges.compactMap { FederalTournamentAge(rawValue: $0) } |
||||||
|
} |
||||||
|
|
||||||
|
private var types: [FederalTournamentType] { |
||||||
|
tournamentTypes.compactMap { FederalTournamentType(rawValue: $0) } |
||||||
|
} |
||||||
|
|
||||||
|
func getNewPage() async { |
||||||
|
do { |
||||||
|
if NetworkFederalService.shared.formId.isEmpty { |
||||||
|
await getNewBuildForm() |
||||||
|
} else { |
||||||
|
let commands = try await NetworkFederalService.shared.getAllFederalTournaments(sortingOption: sortingOption, page: page, startDate: startDate, endDate: endDate, city: city, distance: distance, categories: categories, levels: levels, lat: locationManager.location?.coordinate.latitude.formatted(.number.locale(Locale(identifier: "us"))), lng: locationManager.location?.coordinate.longitude.formatted(.number.locale(Locale(identifier: "us"))), ages: ages, types: types, nationalCup: nationalCup) |
||||||
|
let resultCommand = commands.first(where: { $0.results != nil }) |
||||||
|
if let newTournaments = resultCommand?.results?.items { |
||||||
|
newTournaments.forEach { ft in |
||||||
|
if tournaments.contains(where: { $0.id == ft.id }) == false { |
||||||
|
federalDataViewModel.searchedFederalTournaments.append(ft) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
if let count = resultCommand?.results?.nb_results { |
||||||
|
print("count", count, total, tournaments.count, page) |
||||||
|
total = count |
||||||
|
|
||||||
|
if tournaments.count < count && page < total / 30 { |
||||||
|
if total < 200 || requestedToGetAllPages { |
||||||
|
page += 1 |
||||||
|
await getNewPage() |
||||||
|
} |
||||||
|
} else { |
||||||
|
print("finished") |
||||||
|
} |
||||||
|
} else { |
||||||
|
print("total results not found") |
||||||
|
} |
||||||
|
} |
||||||
|
} catch { |
||||||
|
print("getNewPage", error) |
||||||
|
await getNewBuildForm() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func getNewBuildForm() async { |
||||||
|
do { |
||||||
|
try await NetworkFederalService.shared.getNewBuildForm() |
||||||
|
await getNewPage() |
||||||
|
} catch { |
||||||
|
print("getNewBuildForm", error) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@ViewBuilder |
||||||
|
var searchContollerView: some View { |
||||||
|
Section { |
||||||
|
Button { |
||||||
|
runSearch() |
||||||
|
} label: { |
||||||
|
HStack { |
||||||
|
Label("Chercher un tournoi", systemImage: "magnifyingglass") |
||||||
|
if searching { |
||||||
|
Spacer() |
||||||
|
ProgressView() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
Button { |
||||||
|
tournamentLevels = Set() |
||||||
|
tournamentCategories = Set() |
||||||
|
city = "" |
||||||
|
locationManager.location = nil |
||||||
|
locationManager.city = nil |
||||||
|
distance = 30 |
||||||
|
startDate = Date() |
||||||
|
endDate = Calendar.current.date(byAdding: .month, value: 3, to: Date())! |
||||||
|
sortingOption = "dateDebut+asc" |
||||||
|
revealSearchParameters = true |
||||||
|
} label: { |
||||||
|
Label("Ré-initialiser la recherche", systemImage: "xmark.circle") |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@ViewBuilder |
||||||
|
var searchParametersView: some View { |
||||||
|
@Bindable var federalDataViewModel = federalDataViewModel |
||||||
|
Section { |
||||||
|
DatePicker("Début", selection: $startDate, displayedComponents: .date) |
||||||
|
DatePicker("Fin", selection: $endDate, displayedComponents: .date) |
||||||
|
Picker(selection: $federalDataViewModel.dayDuration) { |
||||||
|
Text("aucune").tag(nil as Int?) |
||||||
|
Text(1.formatted()).tag(1 as Int?) |
||||||
|
Text(2.formatted()).tag(2 as Int?) |
||||||
|
Text(3.formatted()).tag(3 as Int?) |
||||||
|
} label: { |
||||||
|
Text("Durée max (en jours)") |
||||||
|
} |
||||||
|
|
||||||
|
Picker(selection: $federalDataViewModel.dayPeriod) { |
||||||
|
ForEach(DayPeriod.allCases) { |
||||||
|
Text($0.localizedDayPeriodLabel()).tag($0) |
||||||
|
} |
||||||
|
} label: { |
||||||
|
Text("En semaine ou week-end") |
||||||
|
} |
||||||
|
|
||||||
|
HStack { |
||||||
|
TextField("Ville", text: $city) |
||||||
|
if let city = locationManager.city { |
||||||
|
Divider() |
||||||
|
Text(city).italic() |
||||||
|
} |
||||||
|
if locationManager.requestStarted { |
||||||
|
ProgressView() |
||||||
|
} else { |
||||||
|
LocationButton { |
||||||
|
locationManager.requestLocation() |
||||||
|
} |
||||||
|
.symbolVariant(.fill) |
||||||
|
.foregroundColor (Color.white) |
||||||
|
.cornerRadius (20) |
||||||
|
.font(.system(size: 12)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Picker(selection: $distance) { |
||||||
|
Text(distanceLimit(distance:30).formatted()).tag(30.0) |
||||||
|
Text(distanceLimit(distance:50).formatted()).tag(50.0) |
||||||
|
Text(distanceLimit(distance:60).formatted()).tag(60.0) |
||||||
|
Text(distanceLimit(distance:90).formatted()).tag(90.0) |
||||||
|
Text(distanceLimit(distance:150).formatted()).tag(150.0) |
||||||
|
Text(distanceLimit(distance:200).formatted()).tag(200.0) |
||||||
|
Text(distanceLimit(distance:400).formatted()).tag(400.0) |
||||||
|
Text("Aucune").tag(3000.0) |
||||||
|
} label: { |
||||||
|
Text("Distance max") |
||||||
|
} |
||||||
|
|
||||||
|
Picker(selection: $sortingOption) { |
||||||
|
Text("Distance").tag("_DIST_") |
||||||
|
Text("Date de début").tag("dateDebut+asc") |
||||||
|
Text("Date de fin").tag("dateFin+asc") |
||||||
|
} label: { |
||||||
|
Text("Trier par") |
||||||
|
} |
||||||
|
|
||||||
|
NavigationLink { |
||||||
|
List(TournamentCategory.allCases, selection: $tournamentCategories) { type in |
||||||
|
Text(type.localizedLabel()) |
||||||
|
} |
||||||
|
.navigationTitle("Catégories") |
||||||
|
.environment(\.editMode, Binding.constant(EditMode.active)) |
||||||
|
} label: { |
||||||
|
HStack { |
||||||
|
Text("Catégorie") |
||||||
|
Spacer() |
||||||
|
categoriesLabel |
||||||
|
.foregroundStyle(.secondary) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
NavigationLink { |
||||||
|
List(TournamentLevel.allCases, selection: $tournamentLevels) { type in |
||||||
|
Text(type.localizedLabel()) |
||||||
|
} |
||||||
|
.navigationTitle("Niveaux") |
||||||
|
.environment(\.editMode, Binding.constant(EditMode.active)) |
||||||
|
} label: { |
||||||
|
HStack { |
||||||
|
Text("Niveau") |
||||||
|
Spacer() |
||||||
|
levelsLabel |
||||||
|
.foregroundStyle(.secondary) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
NavigationLink { |
||||||
|
List(FederalTournamentAge.allCases, selection: $tournamentAges) { type in |
||||||
|
Text(type.localizedLabel()) |
||||||
|
} |
||||||
|
.navigationTitle("Limites d'âge") |
||||||
|
.environment(\.editMode, Binding.constant(EditMode.active)) |
||||||
|
} label: { |
||||||
|
HStack { |
||||||
|
Text("Limite d'âge") |
||||||
|
Spacer() |
||||||
|
if tournamentAges.isEmpty || tournamentAges.count == FederalTournamentAge.allCases.count { |
||||||
|
Text("Tous les âges") |
||||||
|
.foregroundStyle(.secondary) |
||||||
|
} else { |
||||||
|
Text(ages.map({ $0.localizedLabel()}).joined(separator: ", ")) |
||||||
|
.foregroundStyle(.secondary) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
NavigationLink { |
||||||
|
List(FederalTournamentType.allCases, selection: $tournamentTypes) { type in |
||||||
|
Text(type.localizedLabel()) |
||||||
|
} |
||||||
|
.navigationTitle("Types de tournoi") |
||||||
|
.environment(\.editMode, Binding.constant(EditMode.active)) |
||||||
|
} label: { |
||||||
|
HStack { |
||||||
|
Text("Type de tournoi") |
||||||
|
Spacer() |
||||||
|
if tournamentTypes.isEmpty || tournamentTypes.count == FederalTournamentType.allCases.count { |
||||||
|
Text("Tous les types") |
||||||
|
.foregroundStyle(.secondary) |
||||||
|
} else { |
||||||
|
Text(types.map({ $0.localizedLabel()}).joined(separator: ", ")) |
||||||
|
.foregroundStyle(.secondary) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Picker(selection: $nationalCup) { |
||||||
|
Text("N'importe").tag(false) |
||||||
|
Text("Uniquement").tag(true) |
||||||
|
} label: { |
||||||
|
Text("Circuit National Padel Cup") |
||||||
|
} |
||||||
|
} header: { |
||||||
|
Text("Critères de recherche") |
||||||
|
} |
||||||
|
.headerProminence(.increased) |
||||||
|
.disabled(searching) |
||||||
|
} |
||||||
|
|
||||||
|
var categoriesLabel: some View { |
||||||
|
if tournamentCategories.isEmpty || tournamentCategories.count == TournamentCategory.allCases.count { |
||||||
|
Text("Toutes les catégories") |
||||||
|
} else { |
||||||
|
Text(categories.map({ $0.localizedLabel() }).joined(separator: ", ")) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var levelsLabel: some View { |
||||||
|
if tournamentLevels.isEmpty || tournamentLevels.count == TournamentLevel.allCases.count { |
||||||
|
Text("Tous les niveaux") |
||||||
|
} else { |
||||||
|
Text(levels.map({ $0.localizedLabel() }).joined(separator: ", ")) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@ViewBuilder |
||||||
|
var searchParametersSummaryView: some View { |
||||||
|
VStack(alignment: .leading) { |
||||||
|
HStack { |
||||||
|
Text("Lieu") |
||||||
|
Spacer() |
||||||
|
Text(city) |
||||||
|
if distance >= 3000 { |
||||||
|
Text("sans limite de distance") |
||||||
|
} else { |
||||||
|
Text("à moins de " + distanceLimit.formatted()) |
||||||
|
} |
||||||
|
} |
||||||
|
HStack { |
||||||
|
Text("Période") |
||||||
|
Spacer() |
||||||
|
Text("Du") |
||||||
|
Text(startDate.twoDigitsYearFormatted) |
||||||
|
Text("Au") |
||||||
|
Text(endDate.twoDigitsYearFormatted) |
||||||
|
} |
||||||
|
HStack { |
||||||
|
Text("Niveau") |
||||||
|
Spacer() |
||||||
|
levelsLabel |
||||||
|
} |
||||||
|
HStack { |
||||||
|
Text("Catégorie") |
||||||
|
Spacer() |
||||||
|
categoriesLabel |
||||||
|
} |
||||||
|
|
||||||
|
HStack { |
||||||
|
Text("Tri") |
||||||
|
Spacer() |
||||||
|
Text(sortingOptionLabel) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var sortingOptionLabel: String { |
||||||
|
switch sortingOption { |
||||||
|
case "_DIST_": return "Distance" |
||||||
|
case "dateDebut+asc": return "Date de début" |
||||||
|
case "dateFin+asc": return "Date de fin" |
||||||
|
default: |
||||||
|
return "Distance" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,232 @@ |
|||||||
|
// |
||||||
|
// TournamentSubscriptionView.swift |
||||||
|
// PadelClub |
||||||
|
// |
||||||
|
// Created by Razmig Sarkissian on 09/09/2024. |
||||||
|
// |
||||||
|
|
||||||
|
import SwiftUI |
||||||
|
|
||||||
|
struct TournamentSubscriptionView: View { |
||||||
|
@EnvironmentObject var networkMonitor: NetworkMonitor |
||||||
|
|
||||||
|
let federalTournament: FederalTournament |
||||||
|
let build: any TournamentBuildHolder |
||||||
|
let user: User |
||||||
|
|
||||||
|
@State private var selectedPlayers: [ImportedPlayer] |
||||||
|
@State private var contactType: ContactType? = nil |
||||||
|
@State private var sentError: ContactManagerError? = nil |
||||||
|
|
||||||
|
init(federalTournament: FederalTournament, build: any TournamentBuildHolder, user: User) { |
||||||
|
self.federalTournament = federalTournament |
||||||
|
self.build = build |
||||||
|
self.user = user |
||||||
|
_selectedPlayers = .init(wrappedValue: [user.currentPlayerData()].compactMap({ $0 })) |
||||||
|
} |
||||||
|
|
||||||
|
var body: some View { |
||||||
|
List { |
||||||
|
Section { |
||||||
|
LabeledContent("Tournoi") { |
||||||
|
Text(federalTournament.libelle ?? "Tournoi") |
||||||
|
} |
||||||
|
LabeledContent("Club") { |
||||||
|
Text(federalTournament.clubLabel()) |
||||||
|
} |
||||||
|
LabeledContent("Épreuve") { |
||||||
|
Text(build.buildHolderTitle()) |
||||||
|
} |
||||||
|
|
||||||
|
LabeledContent("JAP") { |
||||||
|
Text(federalTournament.umpireLabel()) |
||||||
|
} |
||||||
|
LabeledContent("Mail") { |
||||||
|
Text(federalTournament.mailLabel()) |
||||||
|
} |
||||||
|
LabeledContent("Téléphone") { |
||||||
|
Text(federalTournament.phoneLabel()) |
||||||
|
} |
||||||
|
} header: { |
||||||
|
Text("Informations") |
||||||
|
} |
||||||
|
|
||||||
|
Section { |
||||||
|
ForEach(selectedPlayers) { teamPlayer in |
||||||
|
NavigationLink { |
||||||
|
SelectablePlayerListView(allowSelection: 1, playerSelectionAction: { players in |
||||||
|
if let player = players.first { |
||||||
|
selectedPlayers.remove(elements: [teamPlayer]) |
||||||
|
selectedPlayers.append(player) |
||||||
|
} |
||||||
|
}) |
||||||
|
} label: { |
||||||
|
ImportedPlayerView(player: teamPlayer) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if selectedPlayers.count < 2 { |
||||||
|
NavigationLink { |
||||||
|
SelectablePlayerListView(allowSelection: 1, playerSelectionAction: { players in |
||||||
|
if let player = players.first { |
||||||
|
selectedPlayers.append(player) |
||||||
|
} |
||||||
|
}) |
||||||
|
} label: { |
||||||
|
Text("Choisir un partenaire") |
||||||
|
} |
||||||
|
} |
||||||
|
} header: { |
||||||
|
if selectedPlayers.isEmpty == false { |
||||||
|
HStack { |
||||||
|
Text("Poids de l'équipe") |
||||||
|
Spacer() |
||||||
|
Text(selectedPlayers.map { $0.rank }.reduce(0, +).formatted()) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if let courrielEngagement = federalTournament.courrielEngagement { |
||||||
|
Section { |
||||||
|
RowButtonView("S'inscrire par email") { |
||||||
|
contactType = .mail(date: nil, recipients: [courrielEngagement], bccRecipients: nil, body: messageBody, subject: messageSubject, tournamentBuild: build as? TournamentBuild) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if let installation = federalTournament.installation, let telephone = installation.telephone { |
||||||
|
if telephone.isMobileNumber() { |
||||||
|
Section { |
||||||
|
RowButtonView("S'inscrire par message") { |
||||||
|
contactType = .message(date: nil, recipients: [telephone], body: messageBodyShort, tournamentBuild: build as? TournamentBuild) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
let number = telephone.replacingOccurrences(of: " ", with: "") |
||||||
|
if let url = URL(string: "tel:\(number)") { |
||||||
|
Link(destination: url) { |
||||||
|
Label("Appeler", systemImage: "phone") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Section { |
||||||
|
Text(messageBody) |
||||||
|
} header: { |
||||||
|
Text("Message preparé par Padel Club") |
||||||
|
} footer: { |
||||||
|
CopyPasteButtonView(pasteValue: messageBody) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
.toolbar(content: { |
||||||
|
Menu { |
||||||
|
Link(destination: URL(string:"https://tenup.fft.fr/tournoi/\(federalTournament.id)")!) { |
||||||
|
Label("Voir sur Tenup", systemImage: "tennisball") |
||||||
|
} |
||||||
|
ShareLink(item: federalTournament.shareMessage) { |
||||||
|
Label("Partager les infos", systemImage: "info") |
||||||
|
} |
||||||
|
} label: { |
||||||
|
LabelOptions() |
||||||
|
} |
||||||
|
}) |
||||||
|
.alert("Un problème est survenu", isPresented: messageSentFailed) { |
||||||
|
Button("OK") { |
||||||
|
} |
||||||
|
} message: { |
||||||
|
Text(_networkErrorMessage) |
||||||
|
} |
||||||
|
.sheet(item: $contactType) { contactType in |
||||||
|
Group { |
||||||
|
switch contactType { |
||||||
|
case .message(_, let recipients, let body, _): |
||||||
|
MessageComposeView(recipients: recipients, body: body) { result in |
||||||
|
switch result { |
||||||
|
case .cancelled: |
||||||
|
break |
||||||
|
case .failed: |
||||||
|
self.sentError = .messageFailed |
||||||
|
case .sent: |
||||||
|
if networkMonitor.connected == false { |
||||||
|
self.sentError = .messageNotSent |
||||||
|
} |
||||||
|
@unknown default: |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
case .mail(_, let recipients, let bccRecipients, let body, let subject, _): |
||||||
|
MailComposeView(recipients: recipients, bccRecipients: bccRecipients, body: body, subject: subject) { result in |
||||||
|
switch result { |
||||||
|
case .cancelled, .saved: |
||||||
|
self.contactType = nil |
||||||
|
case .failed: |
||||||
|
self.contactType = nil |
||||||
|
self.sentError = .mailFailed |
||||||
|
case .sent: |
||||||
|
if networkMonitor.connected == false { |
||||||
|
self.contactType = nil |
||||||
|
self.sentError = .mailNotSent |
||||||
|
} |
||||||
|
@unknown default: |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
.tint(.master) |
||||||
|
} |
||||||
|
.navigationBarTitleDisplayMode(.inline) |
||||||
|
.toolbarBackground(.visible, for: .navigationBar) |
||||||
|
.navigationTitle("Détail du tournoi") |
||||||
|
} |
||||||
|
|
||||||
|
var teamsString: String { |
||||||
|
selectedPlayers.map { $0.pasteData() }.joined(separator: "\n") |
||||||
|
} |
||||||
|
|
||||||
|
var messageBody: String { |
||||||
|
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, "----------------------------------\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" |
||||||
|
return body |
||||||
|
} |
||||||
|
|
||||||
|
var messageBodyShort: String { |
||||||
|
let body = [[build.buildHolderTitle(), federalTournament.clubLabel()].compacted().joined(separator: " "), federalTournament.computedStartDate, teamsString].compacted().joined(separator: "\n") + "\n" |
||||||
|
return body |
||||||
|
} |
||||||
|
|
||||||
|
var messageSubject: String { |
||||||
|
let subject = [build.buildHolderTitle(), federalTournament.clubLabel()].compacted().joined(separator: " ") |
||||||
|
return subject |
||||||
|
} |
||||||
|
|
||||||
|
var messageSentFailed: Binding<Bool> { |
||||||
|
Binding { |
||||||
|
sentError != nil |
||||||
|
} set: { newValue in |
||||||
|
if newValue == false { |
||||||
|
sentError = nil |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private var _networkErrorMessage: String { |
||||||
|
var errors: [String] = [] |
||||||
|
|
||||||
|
if networkMonitor.connected == false { |
||||||
|
errors.append("L'appareil n'est pas connecté à internet.") |
||||||
|
} |
||||||
|
if sentError == .mailNotSent { |
||||||
|
errors.append("Le mail est dans la boîte d'envoi de l'app Mail. Vérifiez son état dans l'app Mail avant d'essayer de le renvoyer.") |
||||||
|
} |
||||||
|
if (sentError == .messageFailed || sentError == .messageNotSent) { |
||||||
|
errors.append("Le SMS n'a pas été envoyé") |
||||||
|
} |
||||||
|
if sentError == .mailFailed { |
||||||
|
errors.append("Le mail n'a pas été envoyé") |
||||||
|
} |
||||||
|
return errors.joined(separator: "\n") |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue