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.
1152 lines
42 KiB
1152 lines
42 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?
|
|
|
|
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 {
|
|
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)
|
|
}
|
|
|
|
if tournament.groupStageCount > 0 {
|
|
Button("+1 en poules") {
|
|
tournament.addWildCard(1, .groupStage)
|
|
}
|
|
}
|
|
} header: {
|
|
Text("Ajout de wildcards")
|
|
}
|
|
|
|
Button("Bloquer une place") {
|
|
tournament.addEmptyTeamRegistration(1)
|
|
}
|
|
|
|
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 _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
|
|
}
|
|
}
|
|
}
|
|
|
|
if teams.isEmpty == false {
|
|
if compactMode {
|
|
Section {
|
|
ForEach(teams) { team in
|
|
let teamIndex = team.index(in: sortedTeams)
|
|
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)
|
|
}
|
|
} 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)")
|
|
}
|
|
}
|
|
.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()
|
|
}
|
|
|
|
@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)
|
|
} 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)
|
|
}
|
|
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()
|
|
}
|
|
}
|
|
}
|
|
|