You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
PadelClub/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift

1186 lines
44 KiB

//
// InscriptionManagerView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 29/02/2024.
//
import SwiftUI
import TipKit
import LeStorage
//let slideToDeleteTip = SlideToDeleteTip()
let inscriptionManagerWomanRankTip = InscriptionManagerWomanRankTip()
//let fileTip = InscriptionManagerFileInputTip()
//let pasteTip = InscriptionManagerPasteInputTip()
//let searchTip = InscriptionManagerSearchInputTip()
//let createTip = InscriptionManagerCreateInputTip()
let rankUpdateTip = InscriptionManagerRankUpdateTip()
let padelBeachExportTip = PadelBeachExportTip()
let padelBeachImportTip = PadelBeachImportTip()
let teamsExportTip = TeamsExportTip()
struct InscriptionManagerView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(NavigationViewModel.self) var navigation: NavigationViewModel
@Environment(\.dismiss) var dismiss
@Bindable var tournament: Tournament
var cancelShouldDismiss: Bool = false
@State private var searchField: String = ""
@State private var presentSearch: Bool = false
@State private var presentPlayerSearch: Bool = false
@State private var presentPlayerCreation: Bool = false
@State private var presentImportView: Bool = false
@State private var isLearningMore: Bool = false
@State private var editedTeam: TeamRegistration?
@State private var currentRankSourceDate: Date?
@State private var confirmUpdateRank = false
@State private var selectionSearchField: String?
@State private var teamsHash: Int?
@State private var presentationCount: Int = 0
@State private var filterMode: FilterMode = .all
@State private var sortingMode: SortingMode = .teamWeight
@State private var byDecreasingOrdering: Bool = false
@State private var confirmDuplicate: Bool = false
@State private var presentAddTeamView: Bool = false
@State private var compactMode: Bool = true
@State private var pasteString: String?
@State private var registrationIssues: Int? = nil
@State private var refreshResult: String? = nil
@State private var refreshInProgress: Bool = false
@State private var refreshStatus: Bool?
@State private var showLegendView: Bool = false
var tournamentStore: TournamentStore {
return self.tournament.tournamentStore
}
enum SortingMode: Int, Identifiable, CaseIterable {
var id: Int { self.rawValue }
case registrationDate
case teamWeight
func localizedLabel() -> String {
switch self {
case .registrationDate:
return "Date d'inscription"
case .teamWeight:
return "Poids d'équipe"
}
}
}
enum FilterMode: Int, Identifiable, CaseIterable {
var id: Int { self.rawValue }
case all
case registeredLocally
case registeredOnline
case walkOut
case waiting
case bracket
case groupStage
case wildcardGroupStage
case wildcardBracket
case notImported
func emptyLocalizedLabelDescription() -> String {
switch self {
case .wildcardBracket:
return "Vous n'avez aucune wildcard en tableau."
case .wildcardGroupStage:
return "Vous n'avez aucune wildcard en poule."
case .all:
return "Vous n'avez encore aucune équipe inscrite."
case .registeredOnline:
return "Aucune équipe inscrite en ligne."
case .registeredLocally:
return "Aucune équipe inscrite par vous-même."
case .walkOut:
return "Vous n'avez aucune équipe forfait."
case .waiting:
return "Vous n'avez aucune équipe en liste d'attente."
case .bracket:
return "Vous n'avez placé aucune équipe dans le tableau."
case .groupStage:
return "Vous n'avez placé aucune équipe en poule."
case .notImported:
return "Vous n'avez aucune équipe non importé. Elles proviennent toutes du fichier."
}
}
func emptyLocalizedLabelTitle() -> String {
switch self {
case .wildcardBracket:
return "Aucune wildcard en tableau"
case .wildcardGroupStage:
return "Aucune wildcard en poule"
case .all:
return "Aucune équipe inscrite"
case .registeredLocally:
return "Aucune équipe inscrite par vous-même"
case .registeredOnline:
return "Aucune équipe inscrite en ligne"
case .walkOut:
return "Aucune équipe forfait"
case .waiting:
return "Aucune équipe en attente"
case .bracket:
return "Aucune équipe dans le tableau"
case .groupStage:
return "Aucune équipe en poule"
case .notImported:
return "Aucune équipe non importée"
}
}
func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
switch self {
case .wildcardBracket:
return displayStyle == .wide ? "Wildcard Tableau" : "wc tableau"
case .wildcardGroupStage:
return displayStyle == .wide ? "Wildcard Poule" : "wc poule"
case .all:
return displayStyle == .wide ? "Équipes inscrites" : "inscris"
case .registeredLocally:
return displayStyle == .wide ? "Inscrites par vous-même" : "par vous-même"
case .registeredOnline:
return displayStyle == .wide ? "Inscrites en ligne" : "en ligne"
case .bracket:
return displayStyle == .wide ? "En Tableau" : "tableau"
case .groupStage:
return displayStyle == .wide ? "En Poule" : "poule"
case .walkOut:
return displayStyle == .wide ? "Forfaits" : "forfait"
case .waiting:
return displayStyle == .wide ? "Liste d'attente" : "attente"
case .notImported:
return "Non importées"
}
}
}
init(tournament: Tournament) {
self.tournament = tournament
_currentRankSourceDate = State(wrappedValue: tournament.rankSourceDate)
}
// Function to create a simple hash from a list of IDs
private func _simpleHash(ids: [String]) -> Int {
// Combine the hash values of each string
return ids.reduce(0) { $0 ^ $1.hashValue }
}
// Function to check if two lists of IDs produce different hashes
private func _areDifferent(ids1: [String], ids2: [String]) -> Bool {
return _simpleHash(ids: ids1) != _simpleHash(ids: ids2)
}
private func _setHash() {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func _setHash", duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
let selectedSortedTeams = tournament.selectedSortedTeams()
if self.teamsHash == nil, selectedSortedTeams.isEmpty == false {
self.teamsHash = _simpleHash(ids: selectedSortedTeams.map { $0.id })
}
self.registrationIssues = nil
DispatchQueue.main.async {
self.registrationIssues = tournament.registrationIssues()
}
}
private func _handleHashDiff() {
let selectedSortedTeams = tournament.selectedSortedTeams()
let newHash = _simpleHash(ids: selectedSortedTeams.map { $0.id })
if (self.teamsHash != nil && newHash != teamsHash!) || (self.teamsHash == nil && selectedSortedTeams.isEmpty == false) {
self.teamsHash = newHash
if self.tournament.shouldVerifyBracket == false || self.tournament.shouldVerifyGroupStage == false {
self.tournament.shouldVerifyBracket = true
self.tournament.shouldVerifyGroupStage = true
let waitingList = self.tournament.waitingListTeams(in: selectedSortedTeams, includingWalkOuts: true)
waitingList.forEach { team in
if team.bracketPosition != nil || team.groupStagePosition != nil {
team.resetPositions()
}
}
do {
try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: waitingList)
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
}
}
}
var body: some View {
Group {
if tournament.unsortedTeams().isEmpty == false {
_teamRegisteredView()
} else {
List {
}
.overlay {
ContentUnavailableView {
Label("Aucune équipe", systemImage: "person.2.slash")
} description: {
Text("Vous n'avez aucune équipe dans votre liste. Complétez là ou importer un fichier.")
} actions: {
RowButtonView("Ajouter une équipe") {
presentAddTeamView = true
}
RowButtonView("Importer un fichier") {
presentImportView = true
}
if tournament.enableOnlineRegistration {
RowButtonView("Rafraîchir la liste") {
await _refreshList()
}
} else if tournament.onlineRegistrationCanBeEnabled() {
RowButtonView("Inscription en ligne") {
navigation.path.append(Screen.settings)
}
}
}
}
}
}
.refreshable {
await _refreshList()
}
.onAppear {
_setHash()
}
.onDisappear {
_handleHashDiff()
}
.sheet(isPresented: $isLearningMore) {
LearnMoreSheetView(tournament: tournament)
.tint(.master)
}
.sheet(isPresented: $presentImportView, onDismiss: {
_setHash()
}) {
NavigationStack {
FileImportView(defaultFileProvider: tournament.isAnimation() ? .custom : .frenchFederation)
}
.tint(.master)
}
.onChange(of: tournament.prioritizeClubMembers) {
_save()
_setHash()
}
.onChange(of: tournament.teamSorting) {
_save()
_setHash()
}
.onChange(of: currentRankSourceDate) {
if let currentRankSourceDate, tournament.rankSourceDate != currentRankSourceDate {
confirmUpdateRank = true
}
}
.sheet(isPresented: $confirmUpdateRank, onDismiss: {
currentRankSourceDate = tournament.rankSourceDate
}) {
UpdateSourceRankDateView(currentRankSourceDate: $currentRankSourceDate, confirmUpdateRank: $confirmUpdateRank, tournament: tournament)
.tint(.master)
}
.fullScreenCover(isPresented: $presentAddTeamView, onDismiss: {
_setHash()
}) {
NavigationStack {
AddTeamView(tournament: tournament)
}
.tint(.master)
}
.fullScreenCover(item: $editedTeam, onDismiss: {
_setHash()
}) { editedTeam in
NavigationStack {
AddTeamView(tournament: tournament, editedTeam: editedTeam)
}
.tint(.master)
}
.fullScreenCover(item: $pasteString, onDismiss: {
_setHash()
}) { pasteString in
NavigationStack {
AddTeamView(tournament: tournament, pasteString: pasteString)
}
.tint(.master)
}
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Menu {
Toggle(isOn: $compactMode) {
Text("Vue compact")
}
Divider()
Picker(selection: $filterMode) {
ForEach(FilterMode.allCases) {
Text($0.localizedLabel()).tag($0)
}
} label: {
}
Picker(selection: $sortingMode) {
ForEach(SortingMode.allCases) {
Text($0.localizedLabel()).tag($0)
}
} label: {
}
Picker(selection: $byDecreasingOrdering) {
Text("Croissant").tag(false)
Text("Décroissant").tag(true)
} label: {
}
} label: {
LabelFilter()
.symbolVariant(filterMode == .all ? .none : .fill)
}
Menu {
if tournament.inscriptionClosed() == false {
Menu {
_sortingTypePickerView()
} label: {
Text("Méthode de sélection")
Text(tournament.teamSorting.localizedLabel())
}
Divider()
rankingDateSourcePickerView(showDateInLabel: true)
Divider()
Button {
tournament.lockRegistration()
_save()
} label: {
Label("Clôturer", systemImage: "lock")
}
}
if tournament.isAnimation() == false {
if tournament.inscriptionClosed() == false {
Divider()
Section {
Button("+1 en tableau") {
tournament.addWildCard(1, .bracket)
_setHash()
}
if tournament.groupStageCount > 0 {
Button("+1 en poules") {
tournament.addWildCard(1, .groupStage)
_setHash()
}
}
} header: {
Text("Ajout de wildcards")
}
Button("Bloquer une place") {
tournament.addEmptyTeamRegistration(1)
_setHash()
}
Divider()
_sharingTeamsMenuView()
Button {
presentImportView = true
} label: {
Label("Importer beach-padel", systemImage: "square.and.arrow.down")
}
Link(destination: URLs.beachPadel.url) {
Label("beach-padel.app.fft.fr", systemImage: "safari")
}
} else {
_sharingTeamsMenuView()
Divider()
Button {
tournament.unlockRegistration()
_save()
} label: {
Label("Ré-ouvrir", systemImage: "lock.open")
}
}
} else {
Button("Bloquer une place") {
tournament.addEmptyTeamRegistration(1)
}
Toggle(isOn: $tournament.hideTeamsWeight) {
Text("Masquer les poids des équipes")
}
//rankingDateSourcePickerView(showDateInLabel: true)
Divider()
_sharingTeamsMenuView()
Divider()
Button {
presentImportView = true
} label: {
Label("Importer un fichier", systemImage: "square.and.arrow.down")
}
}
} label: {
if tournament.inscriptionClosed() == false {
LabelOptions()
} else {
Label("Clôturé", systemImage: "lock")
}
}
}
}
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Inscriptions")
.navigationBarTitleDisplayMode(.inline)
.onChange(of: tournament.hideTeamsWeight) {
_save()
}
}
private func _sharingTeamsMenuView() -> some View {
Menu {
ShareLink(item: teamPaste(), preview: .init("Inscriptions")) {
Text("En texte")
}
ShareLink(item: teamPaste(.csv), preview: .init("Inscriptions")) {
Text("En csv")
}
} label: {
Label("Exporter les paires", systemImage: "square.and.arrow.up")
}
}
var walkoutTeams: [TeamRegistration] {
tournament.walkoutTeams()
}
var unsortedTeamsWithoutWO: [TeamRegistration] {
tournament.unsortedTeamsWithoutWO()
}
func teamPaste(_ exportFormat: ExportFormat = .rawText) -> TournamentShareFile {
TournamentShareFile(tournament: tournament, exportFormat: exportFormat)
}
var unsortedPlayers: [PlayerRegistration] {
tournament.unsortedPlayers()
}
var sortedTeams: [TeamRegistration] {
if filterMode == .waiting {
return tournament.waitingListSortedTeams()
} else {
return tournament.sortedTeams()
}
}
var filteredTeams: [TeamRegistration] {
var teams = sortedTeams
switch filterMode {
case .wildcardBracket:
teams = teams.filter({ $0.wildCardBracket })
case .wildcardGroupStage:
teams = teams.filter({ $0.wildCardGroupStage })
case .walkOut:
teams = teams.filter({ $0.walkOut })
case .bracket:
teams = teams.filter({ $0.inRound() && $0.inGroupStage() == false })
case .groupStage:
teams = teams.filter({ $0.inGroupStage() })
case .notImported:
teams = teams.filter({ $0.isImported() == false })
case .registeredLocally:
teams = teams.filter({ $0.hasRegisteredOnline() == false })
case .registeredOnline:
teams = teams.filter({ $0.hasRegisteredOnline() == true })
default:
break
}
if sortingMode == .registrationDate {
teams = teams.sorted(by: \.computedRegistrationDate)
}
if byDecreasingOrdering {
return teams.reversed()
} else {
return teams
}
}
// private func _fixModel() {
// let players = tournament.players()
//
// players.forEach { player in
// if player.source == .onlineRegistration {
// player.source = .frenchFederation
// player.registeredOnline = true
// }
// }
//
// try? tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players)
// }
//
private func _refreshList() async {
if refreshInProgress { return }
refreshResult = nil
refreshStatus = nil
refreshInProgress = true
do {
try await self.tournamentStore.playerRegistrations.loadDataFromServerIfAllowed(clear: true)
try await self.tournamentStore.teamScores.loadDataFromServerIfAllowed(clear: true)
try await self.tournamentStore.teamRegistrations.loadDataFromServerIfAllowed(clear: true)
_setHash()
self.refreshResult = "la synchronization a réussi"
self.refreshStatus = true
refreshInProgress = false
} catch {
Logger.error(error)
self.refreshResult = "la synchronization a échoué"
self.refreshStatus = false
refreshInProgress = false
}
}
private func _teamRegisteredView() -> some View {
List {
let selectedSortedTeams = tournament.selectedSortedTeams()
if presentSearch == false {
_informationView()
if tournament.isAnimation() == false {
_rankHandlerView()
_relatedTips()
}
}
let teams = searchField.isEmpty ? filteredTeams : filteredTeams.filter({ $0.contains(searchField.canonicalVersion) })
if teams.isEmpty && searchField.isEmpty == false {
ContentUnavailableView {
Label("Aucun résultat", systemImage: "person.2.slash")
} description: {
Text("\(searchField) est introuvable dans les équipes inscrites.")
} actions: {
RowButtonView("Modifier la recherche") {
searchField = ""
presentSearch = true
}
RowButtonView("Créer une équipe") {
pasteString = searchField
}
RowButtonView("D'accord") {
searchField = ""
presentSearch = false
}
}
}
let isImported = teams.anySatisfy({ $0.isImported() })
if teams.isEmpty == false {
if compactMode {
Section {
ForEach(teams) { team in
let teamIndex = team.index(in: sortedTeams)
let color: Color? = isImported ? (team.unrankedOrUnknown() ? .logoRed : (team.isImported() == false ? .beige : nil)) : nil
NavigationLink {
EditingTeamView(team: team)
.environment(tournament)
} label: {
TeamRowView(team: team)
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
if tournament.enableOnlineRegistration == false {
_teamDeleteButtonView(team)
}
}
.listRowView(isActive: true, color: team.initialRoundColor() ?? tournament.cutLabelColor(index: teamIndex, teamCount: filterMode == .waiting ? 0 : selectedSortedTeams.count), hideColorVariation: true, backgroundColor: color, alignment: .leading)
}
} header: {
if filterMode == .all && walkoutTeams.isEmpty == false {
Text("\(teams.count.formatted()) équipe\(teams.count.pluralSuffix) dont \(walkoutTeams.count.formatted()) forfait\(walkoutTeams.count.pluralSuffix)")
} else {
Text("\(teams.count.formatted()) équipe\(teams.count.pluralSuffix)")
}
} footer: {
FooterButtonView("Légende des codes couleurs") {
showLegendView = true
}
}
.headerProminence(.increased)
} else {
ForEach(teams) { team in
let teamIndex = team.index(in: sortedTeams)
Section {
TeamDetailView(team: team)
} header: {
TeamHeaderView(team: team, teamIndex: filterMode == .waiting ? nil : teamIndex, tournament: tournament, teamCount: filterMode == .waiting ? 0 : selectedSortedTeams.count)
} footer: {
_teamFooterView(team)
}
.headerProminence(.increased)
}
}
} else if filterMode != .all {
ContentUnavailableView {
Label(filterMode.emptyLocalizedLabelTitle(), systemImage: "person.2.slash")
} description: {
Text(filterMode.emptyLocalizedLabelDescription())
} actions: {
RowButtonView("Supprimer le filtre") {
filterMode = .all
}
}
}
}
.searchable(text: $searchField, isPresented: $presentSearch, prompt: Text("Chercher parmi les équipes inscrites"))
.keyboardType(.alphabet)
.autocorrectionDisabled()
.sheet(isPresented: $showLegendView) {
InscriptionLegendView()
}
}
@ViewBuilder
func rankingDateSourcePickerView(showDateInLabel: Bool) -> some View {
Section {
let dates = Set(SourceFileManager.shared.allFilesSortedByDate(true).map({ $0.dateFromPath })).sorted().reversed()
Picker(selection: $currentRankSourceDate) {
if currentRankSourceDate == nil {
Text("inconnu").tag(nil as Date?)
}
ForEach(dates, id: \.self) { date in
Text(date.monthYearFormatted).tag(date as Date?)
}
} label: {
Text("Classement utilisé")
if showDateInLabel {
if let currentRankSourceDate {
Text(currentRankSourceDate.monthYearFormatted)
} else {
Text("Choisir le mois")
}
}
}
.pickerStyle(.menu)
}
}
private func _addPlayerSex() -> Int {
switch tournament.tournamentCategory {
case .men, .unlisted:
return 1
case .women:
return 0
case .mix:
return 1
}
}
private func _filterOption() -> PlayerFilterOption {
return tournament.tournamentCategory.playerFilterOption
}
@ViewBuilder
private func _rankHandlerView() -> some View {
if let mostRecentDate = SourceFileManager.shared.lastDataSourceDate(), let currentRankSourceDate, currentRankSourceDate < mostRecentDate, tournament.hasEnded() == false {
Section {
TipView(rankUpdateTip) { action in
self.currentRankSourceDate = mostRecentDate
}
.tipStyle(tint: nil)
}
rankingDateSourcePickerView(showDateInLabel: false)
} else if tournament.rankSourceDate == nil {
rankingDateSourcePickerView(showDateInLabel: false)
}
}
private func _teamCountForFilterMode(filterMode: FilterMode) -> String {
switch filterMode {
case .wildcardBracket:
return tournament.selectedSortedTeams().filter({ $0.wildCardBracket }).count.formatted()
case .wildcardGroupStage:
return tournament.selectedSortedTeams().filter({ $0.wildCardGroupStage }).count.formatted()
case .all:
return unsortedTeamsWithoutWO.count.formatted()
case .bracket:
return tournament.selectedSortedTeams().filter({ $0.inRound() && $0.inGroupStage() == false }).count.formatted()
case .groupStage:
return tournament.selectedSortedTeams().filter({ $0.inGroupStage() }).count.formatted()
case .walkOut:
let wo = walkoutTeams.count.formatted()
return wo
case .waiting:
let waiting: Int = max(0, unsortedTeamsWithoutWO.count - tournament.teamCount)
return waiting.formatted()
case .notImported:
let notImported: Int = max(0, sortedTeams.filter({ $0.isImported() == false }).count)
return notImported.formatted()
case .registeredLocally:
let registeredLocally: Int = max(0, sortedTeams.filter({ $0.hasRegisteredOnline() == false }).count)
return registeredLocally.formatted()
case .registeredOnline:
let registeredOnline: Int = max(0, sortedTeams.filter({ $0.hasRegisteredOnline() }).count)
return registeredOnline.formatted()
}
}
@ViewBuilder
private func _informationView() -> some View {
Section {
HStack {
// VStack(alignment: .leading, spacing: 0) {
// Text("Inscriptions").font(.caption)
// Text(unsortedTeamsWithoutWO.count.formatted()).font(.largeTitle)
// }
// .frame(maxWidth: .infinity)
// .contentShape(Rectangle())
// .onTapGesture {
// self.filterMode = .all
// }
//
ForEach([FilterMode.all, FilterMode.waiting, FilterMode.walkOut]) { filterMode in
_filterModeView(filterMode: filterMode)
}
Button {
presentAddTeamView = true
} label: {
VStack(alignment: .center, spacing: -2) {
Text(" ").font(.caption).padding(.horizontal, -8)
Text(" ").font(.largeTitle)
}
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
.hidden()
.overlay {
Image(systemName: "plus.circle.fill")
.resizable()
.scaledToFit()
.padding(8)
}
}
.buttonBorderShape(.roundedRectangle)
.buttonStyle(.borderedProminent)
}
.padding(.bottom, -4)
.fixedSize(horizontal: false, vertical: false)
.listRowSeparator(.hidden)
HStack {
ForEach([FilterMode.groupStage, FilterMode.bracket, FilterMode.wildcardGroupStage, FilterMode.wildcardBracket]) { filterMode in
_filterModeView(filterMode: filterMode)
}
}
.padding(.bottom, -4)
.fixedSize(horizontal: false, vertical: false)
.listRowSeparator(.hidden)
if tournament.isAnimation() == false {
NavigationLink {
InscriptionInfoView(tournament: tournament)
.environment(tournament)
} label: {
LabeledContent {
if let registrationIssues {
Text(registrationIssues.formatted())
.foregroundStyle(.logoRed)
.fontWeight(.bold)
} else {
ProgressView()
}
} label: {
Text("Problèmes détectés")
}
}
}
if let closedRegistrationDate = tournament.closedRegistrationDate {
CloseDatePicker(closedRegistrationDate: closedRegistrationDate)
}
// Button("bug fix") {
// _fixModel()
// }
if tournament.enableOnlineRegistration {
Button {
Task {
await _refreshList()
}
} label: {
LabeledContent {
if refreshInProgress {
ProgressView()
} else if let refreshStatus {
if refreshStatus {
Image(systemName: "checkmark").foregroundStyle(.green).font(.headline)
} else {
Image(systemName: "xmark").foregroundStyle(.logoRed).font(.headline)
}
}
} label: {
Text("Récupérer les inscriptions en ligne")
if let refreshResult {
Text(refreshResult)
}
}
}
}
} header: {
HStack {
Spacer()
FooterButtonView(compactMode ? "passer en affichage détaillée" : "passer en affichage compact") {
compactMode.toggle()
}
Spacer()
}
.textCase(nil)
} footer: {
if tournament.closedRegistrationDate != nil {
Text("Toutes les équipes ayant été inscrites après la date de clôture seront en liste d'attente.")
}
}
}
private func _filterModeView(filterMode: FilterMode) -> some View {
Button {
if self.filterMode == filterMode {
self.filterMode = .all
} else {
self.filterMode = filterMode
}
} label: {
VStack(alignment: .center, spacing: -2) {
Text(filterMode.localizedLabel(.short)).font(.caption).padding(.horizontal, -8)
Text(_teamCountForFilterMode(filterMode: filterMode)).font(.largeTitle)
}
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
}
.buttonBorderShape(.roundedRectangle)
.buttonStyle(.borderedProminent)
.foregroundStyle(self.filterMode == filterMode ? Color.white : Color.black)
.tint(self.filterMode == filterMode ? .master : .beige)
}
//
// @ViewBuilder
// private func _informationView() -> some View {
// Section {
// Button {
// filterMode = .all
// } label: {
// LabeledContent {
// Text(unsortedTeamsWithoutWO.count.formatted() + "/" + tournament.teamCount.formatted())
// } label: {
// Text("Paire\(unsortedTeamsWithoutWO.count.pluralSuffix) inscrite\(unsortedTeamsWithoutWO.count.pluralSuffix)")
// }
// }
// .buttonStyle(.plain)
//
// HStack {
// let waiting: Int = max(0, unsortedTeamsWithoutWO.count - tournament.teamCount)
// FooterButtonView("\(waiting.formatted()) équipes en attente\(waiting.pluralSuffix)") {
// filterMode = .waiting
// }
// .disabled(filterMode == .waiting)
//
// Divider()
//
// let wo = walkoutTeams.count
// FooterButtonView("\(wo.formatted()) équipes forfait\(wo.pluralSuffix)") {
// filterMode = .walkOut
// }
// .disabled(filterMode == .walkOut)
// }
// .fixedSize(horizontal: true, vertical: true)
//
// NavigationLink {
// InscriptionInfoView()
// .environment(tournament)
// } label: {
// LabeledContent {
// if let registrationIssues {
// Text(registrationIssues.formatted())
// } else {
// ProgressView()
// }
// } label: {
// Text("Problèmes détéctés")
// }
// }
// } header: {
// Text("Statut des inscriptions")
// } footer: {
// HStack {
// Menu {
// _managementView()
// } label: {
// Text("Complétez votre liste")
// }
// Text("ou")
//
// FooterButtonView("Importez un fichier") {
// presentImportView = true
// }
// }
//// if filterMode != .all {
//// FooterButtonView("tout afficher") {
//// filterMode = .all
//// }
//// }
// }
// .headerProminence(.increased)
// }
@ViewBuilder
private func _relatedTips() -> some View {
// if tournament.inscriptionClosed() && tournament.tournamentLevel.shouldShareTeams() {
// Section {
// TipView(teamsExportTip)
// .tipStyle(tint: nil)
// }
// }
// if tournament.unsortedTeams().count >= tournament.teamCount
// && tournament.unsortedPlayers().filter({ $0.source == .beachPadel }).isEmpty {
// Section {
// TipView(padelBeachExportTip) { action in
// if action.id == "more-info-export" {
// isLearningMore = true
// }
// if action.id == "padel-beach" {
// UIApplication.shared.open(URLs.beachPadel.url)
// }
// }
// .tipStyle(tint: nil)
// }
// Section {
// TipView(padelBeachImportTip) { action in
// if action.id == "more-info-import" {
// presentImportView = true
// }
// }
// .tipStyle(tint: nil)
// }
// }
//
if tournament.tournamentCategory == .men && unsortedPlayers.filter({ $0.isMalePlayer() == false }).isEmpty == false {
Section {
TipView(inscriptionManagerWomanRankTip)
.tipStyle(tint: nil)
}
}
//
// Section {
// TipView(slideToDeleteTip)
// .tipStyle(tint: nil)
// }
}
private func _searchSource() -> String? {
selectionSearchField
}
@ViewBuilder
private func _sortingTypePickerView() -> some View {
@Bindable var tournament = tournament
Picker(selection: $tournament.teamSorting) {
ForEach(TeamSortingType.allCases) {
Text($0.localizedLabel()).tag($0)
}
} label: {
}
}
@ViewBuilder
private func _prioritizeClubMembersButton() -> some View {
@Bindable var tournament = tournament
if let federalClub = tournament.club() {
Menu {
Picker(selection: $tournament.prioritizeClubMembers) {
Text("Oui").tag(true)
Text("Non").tag(false)
} label: {
}
.labelsHidden()
Divider()
NavigationLink {
ClubsView() { club in
if let event = tournament.eventObject() {
event.club = club.id
do {
try dataStore.events.addOrUpdate(instance: event)
} catch {
Logger.error(error)
}
}
_save()
}
} label: {
Text("Changer de club")
}
} label: {
Text("Membres prioritaires")
Text(federalClub.acronym)
}
Divider()
} else {
NavigationLink {
ClubsView() { club in
if let event = tournament.eventObject() {
event.club = club.id
do {
try dataStore.events.addOrUpdate(instance: event)
} catch {
Logger.error(error)
}
}
_save()
}
} label: {
Text("Identifier le club")
}
Divider()
}
}
private func _teamFooterView(_ team: TeamRegistration) -> some View {
HStack {
if let formattedRegistrationDate = team.formattedInscriptionDate() {
Text(formattedRegistrationDate)
}
Spacer()
NavigationLink {
EditingTeamView(team: team)
.environment(tournament)
} label: {
LabelOptions().labelStyle(.titleOnly)
}
}
}
private func _teamDeleteButtonView(_ team: TeamRegistration) -> some View {
Button(role: .destructive) {
team.deleteTeamScores()
do {
try tournamentStore.teamRegistrations.delete(instance: team)
} catch {
Logger.error(error)
}
_setHash()
} label: {
LabelDelete()
}
}
private func _save() {
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
}
}
//#Preview {
// NavigationStack {
// InscriptionManagerView(tournament: Tournament.mock())
// .environment(Tournament.mock())
// }
//}
struct TournamentRoundShareContent: Transferable {
let tournament: Tournament
func shareContent() -> String {
print("Generating URL...")
let content = tournament.rounds().compactMap { $0.pasteData() }.joined(separator: "\n\n")
return content
}
static var transferRepresentation: some TransferRepresentation {
ProxyRepresentation { transferable in
return transferable.shareContent()
}
}
}
struct TournamentGroupStageShareContent: Transferable {
let tournament: Tournament
func shareContent() -> String {
print("Generating URL...")
let content = tournament.groupStages().compactMap { $0.pasteData() }.joined(separator: "\n\n")
return content
}
static var transferRepresentation: some TransferRepresentation {
ProxyRepresentation { transferable in
return transferable.shareContent()
}
}
}
struct TournamentShareFile: Transferable {
let tournament: Tournament
let exportFormat: ExportFormat
func shareFile() -> URL {
print("Generating URL...")
return tournament.pasteDataForImporting(exportFormat).createFile(self.tournament.tournamentTitle()+"-inscriptions", exportFormat)
}
static var transferRepresentation: some TransferRepresentation {
FileRepresentation(exportedContentType: .utf8PlainText) { transferable in
return SentTransferredFile(transferable.shareFile())
}
ProxyRepresentation { transferable in
return transferable.shareFile()
}
}
}