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

952 lines
35 KiB

//
// InscriptionManagerView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 29/02/2024.
//
import SwiftUI
import TipKit
import LeStorage
struct InscriptionManagerView: View {
@EnvironmentObject var dataStore: DataStore
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)],
animation: .default)
private var fetchPlayers: FetchedResults<ImportedPlayer>
var tournament: Tournament
@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 createdPlayers: Set<PlayerRegistration> = Set()
@State private var createdPlayerIds: Set<String> = Set()
@State private var editedTeam: TeamRegistration?
@State private var pasteString: String?
@State private var currentRankSourceDate: Date?
@State private var confirmUpdateRank = false
@State private var selectionSearchField: String?
@State private var autoSelect: Bool = false
@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
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 walkOut
func localizedLabel() -> String {
switch self {
case .all:
return "Toutes les équipes"
case .walkOut:
return "Voir les WOs"
}
}
}
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 categoryOption: PlayerFilterOption
let filterable: Bool
let dates = Set(SourceFileManager.shared.allFilesSortedByDate(true).map({ $0.dateFromPath })).sorted().reversed()
init(tournament: Tournament) {
self.tournament = tournament
_currentRankSourceDate = State(wrappedValue: tournament.rankSourceDate)
switch tournament.tournamentCategory {
case .women:
categoryOption = .female
filterable = false
default:
categoryOption = .all
filterable = true
}
}
// 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)
}
var body: some View {
VStack(spacing: 0) {
_managementView()
if _isEditingTeam() {
_buildingTeamView()
} else if tournament.unsortedTeams().isEmpty {
_inscriptionTipsView()
} else {
_teamRegisteredView()
}
}
.onAppear {
self.presentationCount += 1
if self.teamsHash == nil {
self.teamsHash = _simpleHash(ids: tournament.selectedSortedTeams().map { $0.id })
}
}
.onDisappear {
self.presentationCount -= 1
if self.presentationCount == 0 {
let newHash = _simpleHash(ids: tournament.selectedSortedTeams().map { $0.id })
if let teamsHash {
self.tournament.shouldVerifyBracket = newHash != teamsHash
self.tournament.shouldVerifyGroupStage = newHash != teamsHash
}
}
}
.sheet(isPresented: $isLearningMore) {
LearnMoreSheetView(tournament: tournament)
.tint(.master)
}
.sheet(isPresented: $presentPlayerSearch, onDismiss: {
selectionSearchField = nil
}) {
NavigationStack {
SelectablePlayerListView(allowSelection: -1, filterOption: _filterOption(), showFemaleInMaleAssimilation: tournament.tournamentCategory.showFemaleInMaleAssimilation) { players in
selectionSearchField = nil
players.forEach { player in
let newPlayer = PlayerRegistration(importedPlayer: player)
newPlayer.setComputedRank(in: tournament)
createdPlayers.insert(newPlayer)
createdPlayerIds.insert(newPlayer.id)
}
} contentUnavailableAction: { searchViewModel in
selectionSearchField = searchViewModel.searchText
presentPlayerSearch = false
presentPlayerCreation = true
}
}
.tint(.master)
}
.sheet(isPresented: $presentPlayerCreation) {
PlayerPopoverView(source: _searchSource(), sex: _addPlayerSex()) { p in
createdPlayers.insert(p)
createdPlayerIds.insert(p.id)
}
.tint(.master)
}
.sheet(isPresented: $presentImportView) {
NavigationStack {
FileImportView()
}
.tint(.master)
}
.onChange(of: tournament.prioritizeClubMembers) {
_save()
}
.onChange(of: tournament.teamSorting) {
_save()
}
.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)
}
.toolbar {
if _isEditingTeam() {
ToolbarItem(placement: .cancellationAction) {
Button("Annuler", role: .cancel) {
pasteString = nil
editedTeam = nil
createdPlayers.removeAll()
createdPlayerIds.removeAll()
}
}
} else {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Menu {
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()
}
Menu {
if tournament.inscriptionClosed() == false {
Menu {
_sortingTypePickerView()
} label: {
Text("Méthode de sélection")
Text(tournament.teamSorting.localizedLabel())
}
Divider()
rankingDateSourcePickerView(showDateInLabel: true)
if tournament.teamSorting == .inscriptionDate {
Divider()
//_prioritizeClubMembersButton()
Button("Bloquer une place") {
_createTeam()
}
}
Divider()
Button {
tournament.lockRegistration()
_save()
} label: {
Label("Clôturer", systemImage: "lock")
}
Divider()
ShareLink(item: tournament.pasteDataForImporting().createTxtFile(self.tournament.tournamentTitle(.short))) {
Label("Exporter les paires", systemImage: "square.and.arrow.up")
}
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 {
Button {
tournament.closedRegistrationDate = nil
_save()
} label: {
Label("Ré-ouvrir", systemImage: "lock.open")
}
}
} label: {
if tournament.inscriptionClosed() == false {
LabelOptions()
} else {
Label("Clôturer", systemImage: "lock")
}
}
}
}
}
.navigationBarBackButtonHidden(_isEditingTeam())
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Inscriptions")
.navigationBarTitleDisplayMode(.inline)
}
private func _isEditingTeam() -> Bool {
createdPlayerIds.isEmpty == false || editedTeam != nil || pasteString != nil
}
private func _getTeams(from sortedTeams: [TeamRegistration]) -> [TeamRegistration] {
var teams = sortedTeams
if filterMode == .walkOut {
teams = teams.filter({ $0.walkOut })
}
if sortingMode == .registrationDate {
teams = teams.sorted(by: \.computedRegistrationDate)
}
if byDecreasingOrdering {
return teams.reversed()
} else {
return teams
}
}
private func _teamRegisteredView() -> some View {
List {
let sortedTeams = tournament.sortedTeams()
let unfilteredTeams = _getTeams(from: sortedTeams)
if presentSearch == false {
_rankHandlerView()
_relatedTips()
_informationView(count: unfilteredTeams.count)
}
let teams = searchField.isEmpty ? unfilteredTeams : unfilteredTeams.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") {
Task {
await MainActor.run() {
fetchPlayers.nsPredicate = _pastePredicate(pasteField: searchField, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable)
pasteString = searchField
}
}
}
RowButtonView("D'accord") {
searchField = ""
presentSearch = false
}
}
}
ForEach(teams) { team in
let teamIndex = team.index(in: sortedTeams)
Section {
TeamDetailView(team: team)
} header: {
TeamHeaderView(team: team, teamIndex: teamIndex, tournament: tournament)
} footer: {
_teamFooterView(team)
}
.headerProminence(.increased)
}
}
.searchable(text: $searchField, isPresented: $presentSearch, prompt: Text("Chercher parmi les équipes inscrites"))
.keyboardType(.alphabet)
.autocorrectionDisabled()
}
@MainActor
private func _managementView() -> some View {
HStack {
Button {
presentPlayerCreation = true
} label: {
HStack(spacing: 4) {
Image(systemName: "person.fill.badge.plus")
.resizable()
.scaledToFit()
.frame(width: 20)
Text("Créer")
.font(.headline)
}
.frame(maxWidth: .infinity)
}
PasteButton(payloadType: String.self) { strings in
guard let first = strings.first else { return }
Task {
await MainActor.run {
fetchPlayers.nsPredicate = _pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable)
pasteString = first
autoSelect = true
}
}
}
Button {
presentPlayerSearch = true
} label: {
HStack(spacing: 4) {
Image(systemName: "person.fill.viewfinder")
.resizable()
.scaledToFit()
.frame(width: 20)
Text("FFT")
.font(.headline)
}
.frame(maxWidth: .infinity)
}
}
.buttonStyle(.borderedProminent)
.tint(.logoBackground)
.fixedSize(horizontal: false, vertical: true)
.padding(16)
}
@ViewBuilder
func rankingDateSourcePickerView(showDateInLabel: Bool) -> some View {
Section {
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:
return 1
case .women:
return 0
case .mix:
return 1
}
}
private func _filterOption() -> PlayerFilterOption {
switch tournament.tournamentCategory {
case .men:
return .male
case .women:
return .female
case .mix:
return .all
}
}
@ViewBuilder
private func _inscriptionTipsView() -> some View {
List {
Section {
TipView(fileTip) { action in
if action.id == "website" {
UIApplication.shared.open(URLs.beachPadel.url)
} else if action.id == "add-team-file" {
presentImportView = true
}
}
.tipStyle(tint: nil)
}
Section {
TipView(pasteTip) { action in
if let paste = UIPasteboard.general.string {
Task {
await MainActor.run {
fetchPlayers.nsPredicate = _pastePredicate(pasteField: paste, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable)
pasteString = paste
autoSelect = true
}
}
}
}
.tipStyle(tint: nil)
}
Section {
TipView(searchTip) { action in
presentPlayerCreation = true
}
.tipStyle(tint: nil)
}
Section {
TipView(createTip) { action in
presentPlayerSearch = true
}
.tipStyle(tint: nil)
}
Section {
ContentUnavailableView("Aucune équipe", systemImage: "person.2.slash", description: Text("Vous n'avez encore aucune équipe dans votre liste d'attente."))
}
_rankHandlerView()
}
}
@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 _informationView(count: Int) -> some View {
Section {
let unsortedTeams = tournament.unsortedTeams()
let walkoutTeams = tournament.walkoutTeams()
LabeledContent {
Text(unsortedTeams.count.formatted() + "/" + tournament.teamCount.formatted()).font(.largeTitle)
} label: {
Text("Paire\(unsortedTeams.count.pluralSuffix) inscrite\(unsortedTeams.count.pluralSuffix)")
Text("dont \(walkoutTeams.count) forfait\(walkoutTeams.count.pluralSuffix)")
}
let unsortedTeamsWithoutWO = tournament.unsortedTeamsWithoutWO()
LabeledContent {
Text(max(0, unsortedTeamsWithoutWO.count - tournament.teamCount).formatted()).font(.largeTitle)
} label: {
Text("Liste d'attente")
}
NavigationLink {
InscriptionInfoView()
.environment(tournament)
} label: {
LabeledContent {
Text(tournament.registrationIssues().formatted()).font(.largeTitle)
} label: {
Text("Problèmes détéctés")
if let closedRegistrationDate = tournament.closedRegistrationDate {
Text("clôturé le " + closedRegistrationDate.formatted())
}
}
}
}
}
@ViewBuilder
private func _relatedTips() -> some View {
if pasteString == nil
&& createdPlayerIds.isEmpty
&& 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 && tournament.femalePlayers().isEmpty == false {
Section {
TipView(inscriptionManagerWomanRankTip)
.tipStyle(tint: nil)
}
}
Section {
TipView(slideToDeleteTip)
.tipStyle(tint: nil)
}
}
private func _searchSource() -> String? {
selectionSearchField ?? pasteString
}
private func _pastePredicate(pasteField: String, mostRecentDate: Date?) -> NSPredicate? {
let text = pasteField.canonicalVersion
let nameComponents = text.components(separatedBy: .whitespacesAndNewlines).compactMap { $0.isEmpty ? nil : $0 }.filter({ $0 != "de" && $0 != "la" && $0 != "le" && $0.count > 1 })
var andPredicates = [NSPredicate]()
var orPredicates = [NSPredicate]()
//self.wordsCount = nameComponents.count
if _filterOption() == .male {
andPredicates.append(NSPredicate(format: "male == YES"))
} else if _filterOption() == .female {
andPredicates.append(NSPredicate(format: "male == NO"))
}
if let mostRecentDate {
andPredicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg))
}
if nameComponents.count > 1 {
orPredicates = nameComponents.pairs().map {
return NSPredicate(format: "(firstName contains[cd] %@ AND lastName contains[cd] %@) OR (firstName contains[cd] %@ AND lastName contains[cd] %@)", $0, $1, $1, $0) }
} else {
orPredicates = nameComponents.map { NSPredicate(format: "firstName contains[cd] %@ OR lastName contains[cd] %@", $0,$0) }
}
let matches = text.licencesFound()
let licensesPredicates = matches.map { NSPredicate(format: "license contains[cd] %@", $0) }
orPredicates = orPredicates + licensesPredicates
var predicate = NSCompoundPredicate(andPredicateWithSubpredicates: andPredicates)
if orPredicates.isEmpty == false {
predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, NSCompoundPredicate(orPredicateWithSubpredicates: orPredicates)])
}
return predicate
}
private func _currentSelection() -> Set<PlayerRegistration> {
var currentSelection = Set<PlayerRegistration>()
createdPlayerIds.compactMap { id in
fetchPlayers.first(where: { id == $0.license })
}.forEach { player in
let player = PlayerRegistration(importedPlayer: player)
player.setComputedRank(in: tournament)
currentSelection.insert(player)
}
createdPlayerIds.compactMap { id in
createdPlayers.first(where: { id == $0.id })
}.forEach {
currentSelection.insert($0)
}
return currentSelection
}
private func _createTeam() {
let players = _currentSelection()
let team = tournament.addTeam(players)
try? dataStore.teamRegistrations.addOrUpdate(instance: team)
try? dataStore.playerRegistrations.addOrUpdate(contentOfs: players)
createdPlayers.removeAll()
createdPlayerIds.removeAll()
pasteString = nil
}
private func _updateTeam() {
guard let editedTeam else { return }
let players = _currentSelection()
editedTeam.updatePlayers(players, inTournamentCategory: tournament.tournamentCategory)
try? dataStore.teamRegistrations.addOrUpdate(instance: editedTeam)
try? dataStore.playerRegistrations.addOrUpdate(contentOfs: players)
createdPlayers.removeAll()
createdPlayerIds.removeAll()
pasteString = nil
self.editedTeam = nil
}
private func _buildingTeamView() -> some View {
List(selection: $createdPlayerIds) {
if let pasteString {
Section {
Text(pasteString)
} footer: {
HStack {
Text("contenu du presse-papier")
Spacer()
Button("effacer", role: .destructive) {
self.pasteString = nil
self.createdPlayers.removeAll()
self.createdPlayerIds.removeAll()
}
.buttonStyle(.borderless)
}
}
}
Section {
ForEach(createdPlayerIds.sorted(), id: \.self) { id in
if let p = createdPlayers.first(where: { $0.id == id }) {
PlayerView(player: p).tag(p.id)
}
if let p = fetchPlayers.first(where: { $0.license == id }) {
ImportedPlayerView(player: p).tag(p.license!)
}
}
}
if editedTeam == nil {
if createdPlayerIds.isEmpty {
RowButtonView("Bloquer une place") {
_createTeam()
}
} else {
RowButtonView("Ajouter l'équipe") {
_createTeam()
}
}
} else {
RowButtonView("Modifier l'équipe") {
_updateTeam()
}
}
if let pasteString {
if fetchPlayers.isEmpty {
ContentUnavailableView {
Label("Aucun résultat", systemImage: "person.2.slash")
} description: {
Text("Aucun joueur classé n'a été trouvé dans ce message.")
} actions: {
RowButtonView("Créer un joueur non classé") {
presentPlayerCreation = true
}
RowButtonView("Effacer cette recherche") {
self.pasteString = nil
}
}
} else {
Section {
ForEach(fetchPlayers.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) })) { player in
ImportedPlayerView(player: player).tag(player.license!)
}
} header: {
Text(fetchPlayers.count.formatted() + " résultat" + fetchPlayers.count.pluralSuffix)
}
}
}
}
.onReceive(fetchPlayers.publisher.count()) { _ in // <-- here
if let pasteString, count == 2, autoSelect == true {
fetchPlayers.filter { $0.hitForSearch(pasteString) >= hitTarget }.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) }).forEach { player in
createdPlayerIds.insert(player.license!)
}
autoSelect = false
}
}
.environment(\.editMode, Binding.constant(EditMode.active))
}
private var count: Int {
return fetchPlayers.filter { $0.hitForSearch(pasteString ?? "") >= hitTarget }.count
}
private var hitTarget: Int {
if (pasteString?.matches(of: /[1-9][0-9]{5,7}/).count ?? 0) > 1 {
if fetchPlayers.filter({ $0.hitForSearch(pasteString ?? "") == 100 }).count == 2 { return 100 }
} else {
return 2
}
return 1
}
@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()
_teamMenuOptionView(team)
}
}
private func _teamMenuOptionView(_ team: TeamRegistration) -> some View {
Menu {
Section {
Button("Copier") {
let pasteboard = UIPasteboard.general
pasteboard.string = team.playersPasteData()
}
Divider()
Button("Changer les joueurs") {
editedTeam = team
team.unsortedPlayers().forEach { player in
createdPlayers.insert(player)
createdPlayerIds.insert(player.id)
}
}
Divider()
NavigationLink {
EditingTeamView(team: team)
.environment(tournament)
} label: {
Text("Éditer une donnée de l'équipe")
}
Divider()
Toggle(isOn: .init(get: {
return team.wildCardBracket
}, set: { value in
team.resetPositions()
team.wildCardGroupStage = false
team.walkOut = false
team.wildCardBracket = value
try? dataStore.teamRegistrations.addOrUpdate(instance: team)
})) {
Label("Wildcard Tableau", systemImage: team.wildCardBracket ? "circle.inset.filled" : "circle")
}
Toggle(isOn: .init(get: {
return team.wildCardGroupStage
}, set: { value in
team.resetPositions()
team.wildCardBracket = false
team.walkOut = false
team.wildCardGroupStage = value
try? dataStore.teamRegistrations.addOrUpdate(instance: team)
})) {
Label("Wildcard Poule", systemImage: team.wildCardGroupStage ? "circle.inset.filled" : "circle")
}
Divider()
Toggle(isOn: .init(get: {
return team.walkOut
}, set: { value in
team.resetPositions()
team.wildCardBracket = false
team.wildCardGroupStage = false
team.walkOut = value
try? dataStore.teamRegistrations.addOrUpdate(instance: team)
})) {
Label("WO", systemImage: team.walkOut ? "circle.inset.filled" : "circle")
}
Divider()
Button(role: .destructive) {
try? dataStore.teamRegistrations.delete(instance: team)
} label: {
LabelDelete()
}
// } header: {
// Text(team.teamLabel(.short))
}
} label: {
LabelOptions().labelStyle(.titleOnly)
}
}
private func _save() {
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
}
}
#Preview {
NavigationStack {
InscriptionManagerView(tournament: Tournament.mock())
.environment(Tournament.mock())
}
}