club_update
Razmig Sarkissian 1 year ago
parent b7dbac40bc
commit 713b85306c
  1. 2
      PadelClub/Data/Match.swift
  2. 20
      PadelClub/Data/TeamRegistration.swift
  3. 18
      PadelClub/Data/Tournament.swift
  4. 34
      PadelClub/Extensions/Color+Extensions.swift
  5. 4
      PadelClub/Extensions/Date+Extensions.swift
  6. 2
      PadelClub/Utils/PadelRule.swift
  7. 2
      PadelClub/Views/Components/FortuneWheelView.swift
  8. 11
      PadelClub/Views/GroupStage/GroupStagesSettingsView.swift
  9. 12
      PadelClub/Views/Team/Components/TeamHeaderView.swift
  10. 4
      PadelClub/Views/Team/EditingTeamView.swift
  11. 10
      PadelClub/Views/Team/TeamRowView.swift
  12. 3
      PadelClub/Views/Tournament/Screen/Components/CloseDatePicker.swift
  13. 272
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  14. 55
      PadelClub/Views/Tournament/TournamentView.swift

@ -608,6 +608,8 @@ defer {
func canBeStarted(inMatches matches: [Match]) -> Bool {
let teams = teamScores
guard teams.count == 2 else { return false }
guard hasEnded() == false else { return false }
guard hasStarted() == false else { return false }
return teams.compactMap({ $0.team }).allSatisfy({ $0.canPlay() && isTeamPlaying($0, inMatches: matches) == false })
}

@ -7,6 +7,7 @@
import Foundation
import LeStorage
import SwiftUI
@Observable
final class TeamRegistration: ModelObject, Storable {
@ -272,6 +273,25 @@ final class TeamRegistration: ModelObject, Storable {
return bracketPosition != nil
}
func positionLabel() -> String? {
if groupStagePosition != nil { return "Poule" }
if let initialRound = initialRound() {
return initialRound.roundTitle()
} else {
return nil
}
}
func initialRoundColor() -> Color? {
if walkOut { return Color.logoRed }
if groupStagePosition != nil { return Color.mint }
if let initialRound = initialRound(), let colorHex = RoundRule.colors[safe: initialRound.index] {
return Color(uiColor: .init(fromHex: colorHex))
} else {
return nil
}
}
func resetGroupeStagePosition() {
if let groupStage {
let matches = self.tournamentStore.matches.filter({ $0.groupStage == groupStage }).map { $0.id }

@ -7,6 +7,7 @@
import Foundation
import LeStorage
import SwiftUI
@Observable
final class Tournament : ModelObject, Storable {
@ -906,7 +907,20 @@ defer {
return "Attente"
}
}
func cutLabelColor(index: Int?, teamCount: Int?) -> Color {
guard let index else { return Color.gray }
let _teamCount = teamCount ?? selectedSortedTeams().count
let bracketCut = bracketCut(teamCount: _teamCount)
if index < bracketCut {
return Color.cyan
} else if index - bracketCut < groupStageCut() && _teamCount > 0 {
return Color.indigo
} else {
return Color.gray
}
}
func unsortedTeamsWithoutWO() -> [TeamRegistration] {
return self.tournamentStore.teamRegistrations.filter { $0.walkOut == false }
// return Store.main.filter { $0.tournament == self.id && $0.walkOut == false }
@ -1410,7 +1424,7 @@ defer {
}
func moreQualifiedToDraw() -> Int {
return max(qualifiedTeams().count - (qualifiedFromGroupStage() + groupStageAdditionalQualified), 0)
return max((qualifiedFromGroupStage() + groupStageAdditionalQualified) - qualifiedTeams().count, 0)
}
func missingQualifiedFromGroupStages() -> [TeamRegistration] {

@ -6,6 +6,7 @@
//
import SwiftUI
import UIKit
extension Color {
@ -33,3 +34,36 @@ extension Color {
}
}
extension UIColor {
/// Initialises NSColor from a hexadecimal string. Color is clear if string is invalid.
/// - Parameter fromHex: supported formats are "#RGB", "#RGBA", "#RRGGBB", "#RRGGBBAA", with or without the # character
public convenience init(fromHex:String) {
var r = 0, g = 0, b = 0, a = 255
let offset = fromHex.hasPrefix("#") ? 1 : 0
let ch = fromHex.map{$0}
switch(ch.count - offset) {
case 8:
a = 16 * (ch[offset+6].hexDigitValue ?? 0) + (ch[offset+7].hexDigitValue ?? 0)
fallthrough
case 6:
r = 16 * (ch[offset+0].hexDigitValue ?? 0) + (ch[offset+1].hexDigitValue ?? 0)
g = 16 * (ch[offset+2].hexDigitValue ?? 0) + (ch[offset+3].hexDigitValue ?? 0)
b = 16 * (ch[offset+4].hexDigitValue ?? 0) + (ch[offset+5].hexDigitValue ?? 0)
break
case 4:
a = 16 * (ch[offset+3].hexDigitValue ?? 0) + (ch[offset+3].hexDigitValue ?? 0)
fallthrough
case 3: // Three digit #0D3 is the same as six digit #00DD33
r = 16 * (ch[offset+0].hexDigitValue ?? 0) + (ch[offset+0].hexDigitValue ?? 0)
g = 16 * (ch[offset+1].hexDigitValue ?? 0) + (ch[offset+1].hexDigitValue ?? 0)
b = 16 * (ch[offset+2].hexDigitValue ?? 0) + (ch[offset+2].hexDigitValue ?? 0)
break
default:
a = 0
break
}
self.init(red: CGFloat(r)/255, green: CGFloat(g)/255, blue: CGFloat(b)/255, alpha: CGFloat(a)/255)
}
}

@ -227,4 +227,8 @@ extension Date {
func localizedDay() -> String {
self.formatted(.dateTime.weekday(.wide).day())
}
func localizedWeekDay() -> String {
self.formatted(.dateTime.weekday(.wide))
}
}

@ -1444,6 +1444,8 @@ enum PlayersCountRange: Int, CaseIterable {
}
enum RoundRule {
static let colors = ["#d4afb9", "#d1cfe2", "#9cadce", "#7ec4cf", "#daeaf6", "#caffbf"]
static func loserBrackets(index: Int) -> [String] {
switch index {
case 1:

@ -336,7 +336,7 @@ struct FortuneWheelView: View {
let strings = segments[index].segmentLabel(.short)
ForEach(strings, id: \.self) { string in
Text(string).bold()
.font(.caption)
.font(.subheadline)
}
}
.offset(x: -20)

@ -20,8 +20,8 @@ struct GroupStagesSettingsView: View {
var body: some View {
List {
if tournament.moreQualifiedToDraw() > 0 {
let missingQualifiedFromGroupStages = tournament.missingQualifiedFromGroupStages()
let missingQualifiedFromGroupStages = tournament.missingQualifiedFromGroupStages()
Section {
let name = "\((tournament.groupStageAdditionalQualified + 1).ordinalFormatted())"
NavigationLink("Tirage au sort d'un \(name)") {
SpinDrawView(drawees: ["Qualification d'un \(name)"], segments: missingQualifiedFromGroupStages) { results in
@ -35,6 +35,13 @@ struct GroupStagesSettingsView: View {
}
}
}
.disabled(tournament.moreQualifiedToDraw() == 0 || missingQualifiedFromGroupStages.isEmpty)
} footer: {
if tournament.moreQualifiedToDraw() == 0 {
Text("Aucune équipe supplémentaire à qualifier. Vous pouvez en rajouter en modifier le paramètre dans structure.")
} else if missingQualifiedFromGroupStages.isEmpty {
Text("Aucune équipe supplémentaire à tirer au sort. Attendez la fin des poules.")
}
}
if tournament.shouldVerifyGroupStage {

@ -42,12 +42,20 @@ struct TeamHeaderView: View {
Text("").font(.caption)
Text("WO")
} else if let teamIndex, let tournament {
let positionLabel = team.positionLabel()
let cutLabel = tournament.cutLabel(index: teamIndex, teamCount: teamCount)
if team.isWildCard() {
Text("wildcard").font(.caption).italic()
Text(positionLabel ?? cutLabel)
} else {
Text("").font(.caption)
if let positionLabel {
Text("placée").font(.caption)
Text(positionLabel)
} else {
Text("estimée").font(.caption)
Text(cutLabel)
}
}
Text(tournament.cutLabel(index: teamIndex, teamCount: teamCount))
}
}
}

@ -50,7 +50,7 @@ struct EditingTeamView: View {
}
Section {
DatePicker(registrationDate.formatted(.dateTime.weekday()), selection: $registrationDate)
DatePicker(registrationDate.localizedWeekDay(), selection: $registrationDate)
} header: {
Text("Date d'inscription")
}
@ -101,7 +101,7 @@ struct EditingTeamView: View {
_save()
}
.headerProminence(.increased)
.navigationTitle("Édition")
.navigationTitle("Statut de l'équipe")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
}

@ -20,8 +20,14 @@ struct TeamRowView: View {
if let name = team.name {
Text(name).foregroundStyle(.secondary)
}
ForEach(team.players()) { player in
Text(player.playerLabel())
if team.players().isEmpty == false {
ForEach(team.players()) { player in
Text(player.playerLabel())
}
} else {
Text("Place réservée")
Text("Place réservée")
}
}
if displayCallDate {

@ -15,7 +15,8 @@ struct CloseDatePicker: View {
var body: some View {
DatePicker(selection: $closedRegistrationDate) {
Text("Date de clôture")
Text("Date de clôture").font(.caption)
Text(closedRegistrationDate.localizedWeekDay().capitalized)
}
.onChange(of: closedRegistrationDate) {
tournament.closedRegistrationDate = closedRegistrationDate

@ -49,7 +49,7 @@ struct InscriptionManagerView: View {
@State private var showSubscriptionView: Bool = false
@State private var confirmDuplicate: Bool = false
@State private var presentAddTeamView: Bool = false
@State private var compactMode: Bool = false
@State private var compactMode: Bool = true
@State private var pasteString: String?
var tournamentStore: TournamentStore {
@ -89,19 +89,63 @@ struct InscriptionManagerView: View {
case waiting
case bracket
case groupStage
case wildcardGroupStage
case wildcardBracket
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 .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."
}
}
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 .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"
}
}
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 / souhaitées" : "Paires inscrites"
return displayStyle == .wide ? "Équipes inscrites" : "inscris"
case .bracket:
return displayStyle == .wide ? "En Tableau" : "Tableau"
return displayStyle == .wide ? "En Tableau" : "tableau"
case .groupStage:
return displayStyle == .wide ? "En Poule" : "Poule"
return displayStyle == .wide ? "En Poule" : "poule"
case .walkOut:
return displayStyle == .wide ? "Forfaits" : "Forfait"
return displayStyle == .wide ? "Forfaits" : "forfait"
case .waiting:
return displayStyle == .wide ? "Liste d'attente" : "Attente"
return displayStyle == .wide ? "Liste d'attente" : "attente"
}
}
}
@ -313,7 +357,7 @@ struct InscriptionManagerView: View {
Divider()
Picker(selection: $filterMode) {
ForEach(FilterMode.allCases) {
Text($0.localizedLabel(.short)).tag($0)
Text($0.localizedLabel()).tag($0)
}
} label: {
}
@ -415,10 +459,14 @@ struct InscriptionManagerView: View {
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() })
teams = teams.filter({ $0.inRound() && $0.inGroupStage() == false })
case .groupStage:
teams = teams.filter({ $0.inGroupStage() })
default:
@ -469,39 +517,53 @@ struct InscriptionManagerView: View {
}
}
}
if compactMode {
Section {
ForEach(teams) { team in
let teamIndex = team.index(in: sortedTeams)
NavigationLink {
_teamCompactTeamEditionView(team)
.environment(tournament)
} label: {
TeamRowView(team: team)
if teams.isEmpty == false {
if compactMode {
Section {
ForEach(teams) { team in
let teamIndex = team.index(in: sortedTeams)
NavigationLink {
_teamCompactTeamEditionView(team)
.environment(tournament)
} label: {
TeamRowView(team: team)
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
_teamDeleteButtonView(team)
}
.listRowView(isActive: true, color: team.initialRoundColor() ?? tournament.cutLabelColor(index: teamIndex, teamCount: selectedSortedTeams.count), hideColorVariation: true)
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
_teamDeleteButtonView(team)
} header: {
if filterMode == .all {
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)")
}
}
} header: {
LabeledContent {
Text(teams.count.formatted())
} label: {
Text("É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)
}
}
.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)
} else if filterMode != .all {
ContentUnavailableView {
Label(filterMode.emptyLocalizedLabelTitle(), systemImage: "person.2.slash")
} description: {
Text(filterMode.emptyLocalizedLabelDescription())
} actions: {
RowButtonView("Supprimer le filtre") {
filterMode = .all
}
.headerProminence(.increased)
}
}
}
@ -526,7 +588,8 @@ struct InscriptionManagerView: View {
EditingTeamView(team: team)
.environment(tournament)
} label: {
Text("Éditer une donnée de l'équipe")
Text("Modifier le statut de l'équipe")
Text("Nom de l'équipe, date d'inscription, présence, position")
}
NavigationLink {
@ -676,10 +739,14 @@ struct InscriptionManagerView: View {
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() + " / " + tournament.teamCount.formatted()
return unsortedTeamsWithoutWO.count.formatted()
case .bracket:
return tournament.selectedSortedTeams().filter({ $0.inRound() }).count.formatted()
return tournament.selectedSortedTeams().filter({ $0.inRound() && $0.inGroupStage() == false }).count.formatted()
case .groupStage:
return tournament.selectedSortedTeams().filter({ $0.inGroupStage() }).count.formatted()
case .walkOut:
@ -695,83 +762,57 @@ struct InscriptionManagerView: View {
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
// 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)
}
VStack(alignment: .leading, spacing: 0) {
Text("Paires souhaitées").font(.caption)
Text(tournament.teamCount.formatted()).font(.largeTitle)
}
.frame(maxWidth: .infinity)
Button {
presentAddTeamView = true
} label: {
Label {
Text("Ajouter une équipe")
} icon: {
Image(systemName: "person.2.fill")
.resizable()
.scaledToFit()
.frame(height:44)
}
.labelStyle(.iconOnly)
.overlay(alignment: .bottomTrailing) {
Image(systemName: "plus.circle.fill")
.foregroundColor(.master)
.background (
Color(.systemBackground)
.clipShape(.circle)
)
}
Image(systemName: "plus.circle.fill")
.resizable()
.scaledToFit()
.frame(maxWidth: .infinity)
}
.buttonBorderShape(.roundedRectangle)
.buttonStyle(.borderedProminent)
.frame(maxWidth: .infinity)
.labelStyle(.iconOnly)
.buttonStyle(.borderless)
.padding(10)
}
.padding(.bottom, -4)
.fixedSize(horizontal: false, vertical: false)
.padding(.horizontal, -8)
.listRowSeparator(.hidden)
HStack {
ForEach([FilterMode.waiting, FilterMode.walkOut, FilterMode.groupStage, FilterMode.bracket]) { filterMode in
Button {
if self.filterMode == filterMode {
self.filterMode = .all
} else {
self.filterMode = filterMode
}
} label: {
VStack(alignment: .leading, spacing: 0) {
Text(filterMode.localizedLabel(.short)).font(.caption)
Text(_teamCountForFilterMode(filterMode: filterMode)).font(.largeTitle)
}
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
}
.buttonBorderShape(.roundedRectangle)
.buttonStyle(.borderedProminent)
.tint(self.filterMode == filterMode ? .master : .beige)
ForEach([FilterMode.groupStage, FilterMode.bracket, FilterMode.wildcardGroupStage, FilterMode.wildcardBracket]) { filterMode in
_filterModeView(filterMode: filterMode)
}
}
.foregroundStyle(.primary)
.padding(.bottom, -4)
.fixedSize(horizontal: false, vertical: false)
NavigationLink {
InscriptionInfoView()
.environment(tournament)
} label: {
LabeledContent {
Text(tournament.registrationIssues().formatted())
.listRowSeparator(.hidden)
let registrationIssues = tournament.registrationIssues()
if registrationIssues > 0 {
NavigationLink {
InscriptionInfoView()
.environment(tournament)
} label: {
Text("Problèmes détéctés")
LabeledContent {
Text(tournament.registrationIssues().formatted())
.foregroundStyle(.logoRed)
.fontWeight(.bold)
} label: {
Text("Problèmes détéctés")
}
}
}
@ -794,7 +835,30 @@ struct InscriptionManagerView: View {
}
}
}
//
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(.primary)
.tint(self.filterMode == filterMode ? .master : .beige)
}
//
// @ViewBuilder
// private func _informationView() -> some View {
// Section {
@ -1012,7 +1076,7 @@ struct InscriptionManagerView: View {
EditingTeamView(team: team)
.environment(tournament)
} label: {
Text("Éditer une donnée de l'équipe")
Text("Modifier une donnée de l'équipe")
}
Divider()
Toggle(isOn: .init(get: {

@ -118,16 +118,18 @@ struct TournamentView: View {
.navigationBarTitleDisplayMode(.inline)
.toolbarTitleMenu {
if let event = tournament.eventObject() {
Picker(selection: selectedTournamentId) {
ForEach(event.tournaments) { tournament in
Text(tournament.tournamentTitle(.title)).tag(tournament.id as String)
if presentationContext == .agenda {
Picker(selection: selectedTournamentId) {
ForEach(event.tournaments) { tournament in
Text(tournament.tournamentTitle(.title)).tag(tournament.id as String)
}
} label: {
}
} label: {
Divider()
}
Divider()
NavigationLink(value: Screen.event) {
Text("Gestion de l'événement")
}
@ -148,7 +150,7 @@ struct TournamentView: View {
}
}
if (presentationContext == .agenda || tournament.state() == .running) && tournament.isCanceled == false {
if tournament.isCanceled == false {
ToolbarItem(placement: .topBarTrailing) {
Menu {
@ -170,34 +172,29 @@ struct TournamentView: View {
} label: {
Label("Voir dans le gestionnaire", systemImage: "line.diagonal.arrow")
}
Divider()
}
Divider()
if tournament.state() == .running || tournament.state() == .finished {
NavigationLink(value: Screen.event) {
Text("Gestion de l'événement")
}
NavigationLink(value: Screen.settings) {
LabelSettings()
}
NavigationLink(value: Screen.structure) {
LabelStructure()
}
NavigationLink(value: Screen.broadcast) {
Label("Publication", systemImage: "airplayvideo")
}
NavigationLink(value: Screen.print) {
Label("Imprimer", systemImage: "printer")
}
NavigationLink(value: Screen.event) {
Text("Gestion de l'événement")
}
NavigationLink(value: Screen.settings) {
LabelSettings()
}
NavigationLink(value: Screen.structure) {
LabelStructure()
}
NavigationLink(value: Screen.broadcast) {
Label("Publication", systemImage: "airplayvideo")
}
NavigationLink(value: Screen.print) {
Label("Imprimer", systemImage: "printer")
}
Divider()
NavigationLink {
TournamentStatusView(tournament: tournament)
} label: {

Loading…
Cancel
Save