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