club_update
Razmig Sarkissian 1 year ago
parent fc8c69d224
commit 3647d20b1f
  1. 6
      PadelClub/Data/Club.swift
  2. 7
      PadelClub/Data/Event.swift
  3. 17
      PadelClub/Data/TeamRegistration.swift
  4. 24
      PadelClub/Data/Tournament.swift
  5. 2
      PadelClub/Utils/PadelRule.swift
  6. 105
      PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift
  7. 16
      PadelClub/Views/GroupStage/GroupStageSettingsView.swift
  8. 4
      PadelClub/Views/GroupStage/Shared/GroupStageTeamReplacementView.swift
  9. 1
      PadelClub/Views/Match/Components/MatchTeamDetailView.swift
  10. 2
      PadelClub/Views/Match/MatchDetailView.swift
  11. 6
      PadelClub/Views/Player/Components/EditablePlayerView.swift
  12. 6
      PadelClub/Views/Player/Components/PlayerPayView.swift
  13. 8
      PadelClub/Views/Tournament/FileImportView.swift
  14. 2
      PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift
  15. 162
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift

@ -81,7 +81,11 @@ final class Club : ModelObject, Storable, Hashable {
}
override func deleteDependencies() throws {
DataStore.shared.courts.deleteDependencies(self.customizedCourts)
let customizedCourts = self.customizedCourts
for customizedCourt in customizedCourts {
try customizedCourt.deleteDependencies()
}
DataStore.shared.courts.deleteDependencies(customizedCourts)
}
enum CodingKeys: String, CodingKey {

@ -38,7 +38,12 @@ final class Event: ModelObject, Storable {
}
DataStore.shared.tournaments.deleteDependencies(tournaments)
DataStore.shared.dateIntervals.deleteDependencies(self.courtsUnavailability)
let courtsUnavailabilities = self.courtsUnavailability
for courtsUnavailability in courtsUnavailabilities {
try courtsUnavailability.deleteDependencies()
}
DataStore.shared.dateIntervals.deleteDependencies(courtsUnavailabilities)
}
// MARK: - Computed dependencies

@ -72,8 +72,17 @@ final class TeamRegistration: ModelObject, Storable {
// MARK: -
override func deleteDependencies() throws {
self.tournamentStore.playerRegistrations.deleteDependencies(self.unsortedPlayers())
self.tournamentStore.teamScores.deleteDependencies(self.teamScores())
let unsortedPlayers = unsortedPlayers()
for player in unsortedPlayers {
try player.deleteDependencies()
}
self.tournamentStore.playerRegistrations.deleteDependencies(unsortedPlayers)
let teamScores = teamScores()
for teamScore in teamScores {
try teamScore.deleteDependencies()
}
self.tournamentStore.teamScores.deleteDependencies(teamScores)
}
func hasArrived(isHere: Bool = false) {
@ -309,10 +318,6 @@ final class TeamRegistration: ModelObject, Storable {
player.teamRegistration = id
}
}
func qualifiedFromGroupStage() -> Bool {
groupStagePosition != nil && bracketPosition != nil
}
typealias TeamRange = (left: TeamRegistration?, right: TeamRegistration?)

@ -530,7 +530,7 @@ defer {
func pasteDataForImporting() -> String {
let selectedSortedTeams = selectedSortedTeams()
return (selectedSortedTeams.compactMap { $0.pasteData() } + ["Liste d'attente"] + waitingListTeams(in: selectedSortedTeams).compactMap { $0.pasteData() }).joined(separator: "\n\n")
return (selectedSortedTeams.compactMap { $0.pasteData() } + ["Liste d'attente"] + waitingListTeams(in: selectedSortedTeams, includingWalkOuts: true).compactMap { $0.pasteData() }).joined(separator: "\n\n")
}
func club() -> Club? {
@ -804,12 +804,12 @@ defer {
func sortedTeams() -> [TeamRegistration] {
let teams = selectedSortedTeams()
return teams + waitingListTeams(in: teams)
return teams + waitingListTeams(in: teams, includingWalkOuts: true)
}
func waitingListSortedTeams() -> [TeamRegistration] {
let teams = selectedSortedTeams()
return waitingListTeams(in: teams)
return waitingListTeams(in: teams, includingWalkOuts: false)
}
@ -876,9 +876,15 @@ defer {
return _sortedTeams
}
func waitingListTeams(in teams: [TeamRegistration]) -> [TeamRegistration] {
func waitingListTeams(in teams: [TeamRegistration], includingWalkOuts: Bool) -> [TeamRegistration] {
let waitingList = Set(unsortedTeams()).subtracting(teams)
return waitingList.filter { $0.walkOut == false }.sorted(using: _defaultSorting(), order: .ascending) + waitingList.filter { $0.walkOut == true }.sorted(using: _defaultSorting(), order: .ascending)
let waitings = waitingList.filter { $0.walkOut == false }.sorted(using: _defaultSorting(), order: .ascending)
let walkOuts = waitingList.filter { $0.walkOut == true }.sorted(using: _defaultSorting(), order: .ascending)
if includingWalkOuts {
return waitings + walkOuts
} else {
return waitings
}
}
func bracketCut(teamCount: Int) -> Int {
@ -1062,7 +1068,7 @@ defer {
let inadequatePlayers : [PlayerRegistration] = inadequatePlayers(in: players)
let playersWithoutValidLicense : [PlayerRegistration] = playersWithoutValidLicense(in: players)
let playersMissing : [TeamRegistration] = selectedTeams.filter({ $0.unsortedPlayers().count < 2 })
let waitingList : [TeamRegistration] = waitingListTeams(in: selectedTeams)
let waitingList : [TeamRegistration] = waitingListTeams(in: selectedTeams, includingWalkOuts: true)
let waitingListInBracket = waitingList.filter({ $0.bracketPosition != nil })
let waitingListInGroupStage = waitingList.filter({ $0.groupStage != nil })
@ -1382,7 +1388,7 @@ defer {
}
func qualifiedTeams() -> [TeamRegistration] {
return unsortedTeams().filter({ $0.qualifiedFromGroupStage() })
return unsortedTeams().filter({ $0.qualified })
}
func moreQualifiedToDraw() -> Int {
@ -1392,9 +1398,9 @@ defer {
func missingQualifiedFromGroupStages() -> [TeamRegistration] {
if groupStageAdditionalQualified > 0 {
return groupStages().filter { $0.hasEnded() }.compactMap { groupStage in
groupStage.teams()[qualifiedPerGroupStage]
groupStage.teams(true)[safe: qualifiedPerGroupStage]
}
.filter({ $0.qualifiedFromGroupStage() == false })
.filter({ $0.qualified == false })
} else {
return []
}

@ -753,7 +753,7 @@ enum TournamentCategory: Int, Hashable, Codable, CaseIterable, Identifiable {
var importingRawValue: String {
switch self {
case .unlisted:
return ""
return "messieurs"
case .men:
return "messieurs"
case .women:

@ -12,8 +12,14 @@ struct GroupStageTeamView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
@EnvironmentObject var networkMonitor: NetworkMonitor
@Environment(\.dismiss) private var dismiss
@State private var contactType: ContactType? = nil
@State private var sentError: ContactManagerError? = nil
@State private var showSubscriptionView: Bool = false
let groupStage: GroupStage
var team: TeamRegistration
@ -21,6 +27,16 @@ struct GroupStageTeamView: View {
return self.tournament.tournamentStore
}
var messageSentFailed: Binding<Bool> {
Binding {
sentError != nil
} set: { newValue in
if newValue == false {
sentError = nil
}
}
}
var body: some View {
List {
Section {
@ -30,23 +46,6 @@ struct GroupStageTeamView: View {
}
if groupStage.tournamentObject()?.hasEnded() == false {
// if team.qualified && team.bracketPosition == nil, let tournament = team.tournamentObject() {
// Section {
// NavigationLink {
// SpinDrawView(drawees: [team], segments: tournament.matchesWithSpace()) { results in
//
// }
// } label: {
// Text("Tirage au sort visuel")
// }
// }
//
// Section {
// RowButtonView("Tirage au sort automatique", role: .destructive) {
// }
// }
// }
if team.qualified == false {
Section {
NavigationLink {
@ -86,10 +85,82 @@ struct GroupStageTeamView: View {
}
}
}
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
MenuWarningView(tournament: tournament, teams: [team], contactType: $contactType)
}
}
.alert("Un problème est survenu", isPresented: messageSentFailed) {
Button("OK") {
}
} message: {
Text(_getErrorMessage())
}
.sheet(item: $contactType) { contactType in
Group {
switch contactType {
case .message(_, let recipients, let body, _):
if Guard.main.paymentForNewTournament() != nil {
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
}
}
} else {
SubscriptionView(isPresented: self.$showSubscriptionView, showLackOfPlanMessage: true)
.environment(\.colorScheme, .light)
}
case .mail(_, let recipients, let bccRecipients, let body, let subject, _):
if Guard.main.paymentForNewTournament() != nil {
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
}
}
} else {
SubscriptionView(isPresented: self.$showSubscriptionView, showLackOfPlanMessage: true)
.environment(\.colorScheme, .light)
}
}
}
.tint(.master)
}
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Détail de l'équipe")
}
private func _getErrorMessage() -> String {
let m1 : String? = (networkMonitor.connected == false ? "L'appareil n'est pas connecté à internet." : nil)
let m2 : String? = (sentError == .mailNotSent ? "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." : nil)
let m3 : String? = ((sentError == .messageFailed || sentError == .messageNotSent) ? "Le SMS n'a pas été envoyé" : nil)
let m4 : String? = (sentError == .mailFailed ? "Le mail n'a pas été envoyé" : nil)
let message : String = [m1, m2, m3, m4].compacted().joined(separator: "\n")
return message
}
private func _save() {
do {
try tournamentStore.teamRegistrations.addOrUpdate(instance: team)

@ -20,6 +20,22 @@ struct GroupStageSettingsView: View {
var body: some View {
List {
if tournament.moreQualifiedToDraw() > 0 {
let missingQualifiedFromGroupStages = tournament.missingQualifiedFromGroupStages()
let name = "\((tournament.groupStageAdditionalQualified + 1).ordinalFormatted())"
NavigationLink("Tirage au sort d'un \(name)") {
SpinDrawView(drawees: ["Qualification d'un \(name)"], segments: missingQualifiedFromGroupStages) { results in
results.forEach { drawResult in
missingQualifiedFromGroupStages[drawResult.drawIndex].qualified = true
do {
try self.tournamentStore.teamRegistrations.addOrUpdate(instance: missingQualifiedFromGroupStages[drawResult.drawIndex])
} catch {
Logger.error(error)
}
}
}
}
}
if tournament.shouldVerifyGroupStage {
Section {

@ -89,9 +89,9 @@ struct GroupStageTeamReplacementView: View {
}
}
} header: {
Text("Même position en poule")
Text("Même rang dans la liste")
} footer: {
Text("Intervalle de poids d'équipe pour que le remplacement n'affecte pas les poules")
Text("Intervalle de poids d'équipe pour que le remplacement n'affecte rien")
}
}
if let teamRangeExtended {

@ -8,6 +8,7 @@
import SwiftUI
struct MatchTeamDetailView: View {
@EnvironmentObject var tournamentStore: TournamentStore
let match: Match
var body: some View {

@ -116,6 +116,7 @@ struct MatchDetailView: View {
ForEach(unpaid) { player in
LabeledContent {
PlayerPayView(player: player)
.environmentObject(tournamentStore)
} label: {
Text(player.playerLabel())
}
@ -134,6 +135,7 @@ struct MatchDetailView: View {
}
.sheet(isPresented: $showDetails) {
MatchTeamDetailView(match: match).tint(.master)
.environmentObject(tournamentStore)
}
.sheet(isPresented: self.$showSubscriptionView, content: {
NavigationStack {

@ -17,7 +17,7 @@ struct EditablePlayerView: View {
}
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
@EnvironmentObject var tournamentStore: TournamentStore
@Bindable var player: PlayerRegistration
var editingOptions: [PlayerEditingOption]
@ -25,10 +25,6 @@ struct EditablePlayerView: View {
@State private var shouldPresentLicenceIdEdition: Bool = false
@State private var presentLastNameUpdate: Bool = false
@State private var presentFirstNameUpdate: Bool = false
var tournamentStore: TournamentStore {
return self.tournament.tournamentStore
}
var body: some View {
computedPlayerView(player)

@ -11,12 +11,8 @@ import LeStorage
struct PlayerPayView: View {
@Bindable var player: PlayerRegistration
@Environment(Tournament.self) var tournament: Tournament
@EnvironmentObject var tournamentStore: TournamentStore
var tournamentStore: TournamentStore {
return self.tournament.tournamentStore
}
var body: some View {
Picker(selection: $player.paymentType) {
Text("Non réglé").tag(nil as PlayerRegistration.PlayerPaymentType?)

@ -232,11 +232,11 @@ struct FileImportView: View {
Section {
Text("Aucune équipe \(tournament.tournamentCategory.importingRawValue) détectée mais \(teams.count) équipes sont dans le fichier")
Picker(selection: $tournament.tournamentCategory) {
ForEach(TournamentCategory.allCases) { category in
ForEach([TournamentCategory.men, TournamentCategory.women, TournamentCategory.mix]) { category in
Text(category.importingRawValue).tag(category)
}
} label: {
Text("Modifier la catégorie du tournoi ?")
Text("Modifier la catégorie")
}
.onChange(of: tournament.tournamentCategory) {
_save()
@ -271,13 +271,11 @@ struct FileImportView: View {
let tournamentFilteredTeams = self.filteredTeams(tournament: tournament)
Section {
RowButtonView("Valider") {
RowButtonView("Valider les \(tournamentFilteredTeams.count.formatted()) équipe\(tournamentFilteredTeams.count.pluralSuffix)") {
await _validate(tournament: tournament)
}
.disabled(validatedTournamentIds.contains(tournament.id))
} header: {
Text("\(tournamentFilteredTeams.count.formatted()) équipe\(tournamentFilteredTeams.count.pluralSuffix)")
} footer: {
Text(tournament.tournamentTitle())
}
}

@ -205,7 +205,7 @@ struct InscriptionInfoView: View {
players = tournament.unsortedPlayers()
selectedTeams = tournament.selectedSortedTeams()
callDateIssue = selectedTeams.filter { $0.callDate != nil && tournament.isStartDateIsDifferentThanCallDate($0) }
waitingList = tournament.waitingListTeams(in: selectedTeams)
waitingList = tournament.waitingListTeams(in: selectedTeams, includingWalkOuts: true)
duplicates = tournament.duplicates(in: players)
problematicPlayers = players.filter({ $0.sex == nil })
inadequatePlayers = tournament.inadequatePlayers(in: players)

@ -166,7 +166,7 @@ struct InscriptionManagerView: View {
self.tournament.shouldVerifyBracket = true
self.tournament.shouldVerifyGroupStage = true
let waitingList = self.tournament.waitingListTeams(in: selectedSortedTeams)
let waitingList = self.tournament.waitingListTeams(in: selectedSortedTeams, includingWalkOuts: true)
waitingList.forEach { team in
if team.bracketPosition != nil || team.groupStagePosition != nil {
team.resetPositions()
@ -185,7 +185,6 @@ struct InscriptionManagerView: View {
var body: some View {
VStack(spacing: 0) {
_managementView()
if _isEditingTeam() {
_buildingTeamView()
} else if sortedTeams.isEmpty {
@ -574,7 +573,7 @@ struct InscriptionManagerView: View {
Section {
TeamDetailView(team: team)
} header: {
TeamHeaderView(team: team, teamIndex: teamIndex, tournament: tournament, teamCount: filterMode == .waiting ? 0 : selectedSortedTeams.count)
TeamHeaderView(team: team, teamIndex: filterMode == .waiting ? nil : teamIndex, tournament: tournament, teamCount: filterMode == .waiting ? 0 : selectedSortedTeams.count)
} footer: {
_teamFooterView(team)
}
@ -586,53 +585,31 @@ struct InscriptionManagerView: View {
.autocorrectionDisabled()
}
@MainActor
@ViewBuilder
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 = Self._pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption())
fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)]
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)
Button {
presentPlayerSearch = true
} label: {
Text("Rechercher dans la base fédérale")
}
Button {
presentPlayerCreation = true
} label: {
Text("Créer un non classé / non licencié")
}
PasteButton(payloadType: String.self) { strings in
guard let first = strings.first else { return }
Task {
await MainActor.run {
fetchPlayers.nsPredicate = Self._pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable, filterOption: _filterOption())
fetchPlayers.nsSortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)]
pasteString = first
autoSelect = true
}
.frame(maxWidth: .infinity)
}
}
.buttonStyle(.borderedProminent)
.tint(.master)
.fixedSize(horizontal: false, vertical: true)
.padding(16)
}
@ -690,6 +667,8 @@ struct InscriptionManagerView: View {
}
}
_informationView()
Section {
TipView(fileTip) { action in
@ -758,33 +737,44 @@ struct InscriptionManagerView: View {
}
}
@ViewBuilder
private func _informationView() -> some View {
Section {
LabeledContent {
Text(unsortedTeamsWithoutWO.count.formatted() + "/" + tournament.teamCount.formatted()).font(.largeTitle)
Button {
filterMode = .all
} label: {
Text("Paire\(unsortedTeamsWithoutWO.count.pluralSuffix) inscrite\(unsortedTeamsWithoutWO.count.pluralSuffix)")
LabeledContent {
Text(unsortedTeamsWithoutWO.count.formatted() + "/" + tournament.teamCount.formatted())
} label: {
Text("Paire\(unsortedTeamsWithoutWO.count.pluralSuffix) inscrite\(unsortedTeamsWithoutWO.count.pluralSuffix)")
}
}
.buttonStyle(.plain)
LabeledContent {
Text(walkoutTeams.count.formatted()).font(.largeTitle)
} label: {
Text("Forfait\(walkoutTeams.count.pluralSuffix)")
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)
LabeledContent {
Text(max(0, unsortedTeamsWithoutWO.count - tournament.teamCount).formatted()).font(.largeTitle)
} label: {
Text("Attente")
}
NavigationLink {
InscriptionInfoView()
.environment(tournament)
} label: {
LabeledContent {
if let registrationIssues {
Text(registrationIssues.formatted()).font(.largeTitle)
Text(registrationIssues.formatted())
} else {
ProgressView()
}
@ -792,7 +782,28 @@ struct InscriptionManagerView: View {
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
@ -1016,6 +1027,31 @@ struct InscriptionManagerView: View {
}
}
}
} header: {
let _currentSelection = _currentSelection()
let selectedSortedTeams = tournament.selectedSortedTeams()
let rank = _currentSelection.map {
$0.computedRank
}.reduce(0, +)
let teamIndex = selectedSortedTeams.firstIndex(where: { $0.weight >= rank }) ?? selectedSortedTeams.count
if _currentSelection.isEmpty == false, tournament.hideWeight() == false, rank > 0 {
HStack(spacing: 16.0) {
VStack(alignment: .leading, spacing: 0) {
Text("Rang").font(.caption)
Text("#" + (teamIndex + 1).formatted())
}
VStack(alignment: .leading, spacing: 0) {
Text("Poids").font(.caption)
Text(rank.formatted())
}
Spacer()
VStack(alignment: .trailing, spacing: 0) {
Text("").font(.caption)
Text(tournament.cutLabel(index: teamIndex, teamCount: selectedSortedTeams.count))
}
}
}
}
if editedTeam == nil {
@ -1163,6 +1199,12 @@ struct InscriptionManagerView: View {
private func _teamMenuOptionView(_ team: TeamRegistration) -> some View {
Menu {
Section {
NavigationLink {
GroupStageTeamReplacementView(team: team)
} label: {
Text("Chercher à remplacer")
}
MenuWarningView(tournament: tournament, teams: [team], contactType: $contactType)
//Divider()
Button("Copier") {

Loading…
Cancel
Save