fix call view

fix schedule view
clean up
multistore
Razmig Sarkissian 2 years ago
parent d34967df33
commit b1db99f2a0
  1. 8
      PadelClub.xcodeproj/project.pbxproj
  2. 17
      PadelClub/Data/GroupStage.swift
  3. 6
      PadelClub/Data/PlayerRegistration.swift
  4. 4
      PadelClub/Data/Round.swift
  5. 4
      PadelClub/Data/TeamRegistration.swift
  6. 24
      PadelClub/Data/Tournament.swift
  7. 4
      PadelClub/ViewModel/AgendaDestination.swift
  8. 1
      PadelClub/ViewModel/Selectable.swift
  9. 23
      PadelClub/Views/Calling/GroupStageCallingView.swift
  10. 13
      PadelClub/Views/Components/GenericDestinationPickerView.swift
  11. 81
      PadelClub/Views/Components/RowButtonView.swift
  12. 4
      PadelClub/Views/GroupStage/GroupStagesView.swift
  13. 10
      PadelClub/Views/Navigation/MainView.swift
  14. 55
      PadelClub/Views/Planning/GroupStageScheduleEditorView.swift
  15. 94
      PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift
  16. 31
      PadelClub/Views/Planning/MatchScheduleEditorView.swift
  17. 51
      PadelClub/Views/Planning/PlanningSettingsView.swift
  18. 39
      PadelClub/Views/Planning/RoundScheduleEditorView.swift
  19. 114
      PadelClub/Views/Player/PlayerDetailView.swift
  20. 6
      PadelClub/Views/Round/LoserRoundsView.swift
  21. 4
      PadelClub/Views/Round/RoundView.swift
  22. 13
      PadelClub/Views/Shared/ImportedPlayerView.swift
  23. 45
      PadelClub/Views/Team/EditingTeamView.swift
  24. 4
      PadelClub/Views/Team/TeamDetailView.swift
  25. 7
      PadelClub/Views/Tournament/Screen/Components/TournamentFieldsManagerView.swift
  26. 18
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  27. 44
      PadelClub/Views/Tournament/Screen/TournamentCallView.swift
  28. 33
      PadelClub/Views/Tournament/Screen/TournamentCashierView.swift
  29. 5
      PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift
  30. 19
      PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift
  31. 2
      PadelClub/Views/Tournament/TournamentRunningView.swift
  32. 2
      PadelClub/Views/Tournament/TournamentView.swift
  33. 13
      PadelClub/Views/ViewModifiers/DeferredViewModifier.swift

@ -90,6 +90,8 @@
FF11627F2BCF9432000C4809 /* PlayerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF11627E2BCF9432000C4809 /* PlayerListView.swift */; };
FF1162812BCF945C000C4809 /* TournamentCashierView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162802BCF945C000C4809 /* TournamentCashierView.swift */; };
FF1162832BCFBE4E000C4809 /* EditablePlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162822BCFBE4E000C4809 /* EditablePlayerView.swift */; };
FF1162852BD00279000C4809 /* PlayerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162842BD00279000C4809 /* PlayerDetailView.swift */; };
FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162862BD004AD000C4809 /* EditingTeamView.swift */; };
FF1CBC1B2BB53D1F0036DAAB /* FederalTournament.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */; };
FF1CBC1D2BB53DC10036DAAB /* Calendar+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */; };
FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */; };
@ -364,6 +366,8 @@
FF11627E2BCF9432000C4809 /* PlayerListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerListView.swift; sourceTree = "<group>"; };
FF1162802BCF945C000C4809 /* TournamentCashierView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentCashierView.swift; sourceTree = "<group>"; };
FF1162822BCFBE4E000C4809 /* EditablePlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditablePlayerView.swift; sourceTree = "<group>"; };
FF1162842BD00279000C4809 /* PlayerDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerDetailView.swift; sourceTree = "<group>"; };
FF1162862BD004AD000C4809 /* EditingTeamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditingTeamView.swift; sourceTree = "<group>"; };
FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournament.swift; sourceTree = "<group>"; };
FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Calendar+Extensions.swift"; sourceTree = "<group>"; };
FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournamentSearchScope.swift; sourceTree = "<group>"; };
@ -724,6 +728,7 @@
isa = PBXGroup;
children = (
FF089EBC2BB0287D00F0AEC7 /* PlayerView.swift */,
FF1162842BD00279000C4809 /* PlayerDetailView.swift */,
FF089EB02BB001EA00F0AEC7 /* Components */,
);
path = Player;
@ -1007,6 +1012,7 @@
FF967D082BAF3D4000A9A3BD /* TeamDetailView.swift */,
FF967D0A2BAF3D4C00A9A3BD /* TeamPickerView.swift */,
FF089EB52BB00A3800F0AEC7 /* TeamRowView.swift */,
FF1162862BD004AD000C4809 /* EditingTeamView.swift */,
);
path = Team;
sourceTree = "<group>";
@ -1338,6 +1344,7 @@
FF1CBC1D2BB53DC10036DAAB /* Calendar+Extensions.swift in Sources */,
FF967CF22BAECC0B00A9A3BD /* TeamScore.swift in Sources */,
FF1162832BCFBE4E000C4809 /* EditablePlayerView.swift in Sources */,
FF1162852BD00279000C4809 /* PlayerDetailView.swift in Sources */,
FF5D0D762BB428B2005CB568 /* ListRowViewModifier.swift in Sources */,
FF6EC9002B94794700EA7F5A /* PresentationContext.swift in Sources */,
FFDB1C6D2BB2A02000F1E467 /* AppSettings.swift in Sources */,
@ -1371,6 +1378,7 @@
FF4AB6BB2B9256D50002987F /* SearchViewModel.swift in Sources */,
FF967CF32BAECC0B00A9A3BD /* PlayerRegistration.swift in Sources */,
FF4AB6BF2B92577A0002987F /* ImportedPlayerView.swift in Sources */,
FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */,
FF6EC9062B947A1000EA7F5A /* NetworkManagerError.swift in Sources */,
C4A47D5A2B6D383C00ADC637 /* Tournament.swift in Sources */,
C4A47D7B2B73C0F900ADC637 /* TournamentV2.swift in Sources */,

@ -208,6 +208,10 @@ class GroupStage: ModelObject, Storable {
Store.main.filter { $0.groupStage == self.id }
}
func unsortedPlayers() -> [PlayerRegistration] {
unsortedTeams().flatMap({ $0.unsortedPlayers() })
}
fileprivate typealias TeamScoreAreInIncreasingOrder = (TeamGroupStageScore, TeamGroupStageScore) -> Bool
fileprivate typealias TeamGroupStageScore = (team: TeamRegistration, wins: Int, loses: Int, setDifference: Int, gameDifference: Int)
@ -221,10 +225,13 @@ class GroupStage: ModelObject, Storable {
}
}
func unsortedTeams() -> [TeamRegistration] {
Store.main.filter { $0.groupStage == self.id && $0.groupStagePosition != nil }
}
func teams(_ sortedByScore: Bool = false) -> [TeamRegistration] {
let teams: [TeamRegistration] = Store.main.filter { $0.groupStage == self.id && $0.groupStagePosition != nil }
if sortedByScore {
return teams.compactMap({ _score(forGroupStagePosition: $0.groupStagePosition!) }).sorted { (lhs, rhs) in
return unsortedTeams().compactMap({ _score(forGroupStagePosition: $0.groupStagePosition!) }).sorted { (lhs, rhs) in
let predicates: [TeamScoreAreInIncreasingOrder] = [
{ $0.wins < $1.wins },
{ $0.setDifference < $1.setDifference },
@ -244,7 +251,7 @@ class GroupStage: ModelObject, Storable {
return false
}.map({ $0.team }).reversed()
} else {
return teams.sorted(by: \TeamRegistration.groupStagePosition!)
return unsortedTeams().sorted(by: \TeamRegistration.groupStagePosition!)
}
}
@ -272,4 +279,8 @@ extension GroupStage: Selectable {
func badgeValue() -> Int? {
runningMatches().count
}
func badgeImage() -> String? {
hasEnded() ? "checkmark.circle.fill" : nil
}
}

@ -165,7 +165,11 @@ class PlayerRegistration: ModelObject, Storable {
func rankLabel(_ displayStyle: DisplayStyle = .wide) -> String {
if let rank, rank > 0 {
return rank.formatted()
if rank != weight {
return weight.formatted() + " (" + rank.formatted() + ")"
} else {
return rank.formatted()
}
} else {
return "non classé" + (isMalePlayer() ? "" : "e")
}

@ -418,4 +418,8 @@ extension Round: Selectable {
return playedMatches().filter({ $0.isRunning() }).count
}
}
func badgeImage() -> String? {
hasEnded() ? "checkmark.circle.fill" : nil
}
}

@ -285,7 +285,6 @@ class TeamRegistration: ModelObject, Storable {
func initialRound() -> Round? {
guard let bracketPosition else { return nil }
let matchIndex = RoundRule.matchIndex(fromBracketPosition: bracketPosition)
let roundIndex = RoundRule.roundIndex(fromMatchIndex: bracketPosition / 2)
return Store.main.filter(isIncluded: { $0.tournament == tournament && $0.index == roundIndex }).first
}
@ -293,8 +292,7 @@ class TeamRegistration: ModelObject, Storable {
func initialMatch() -> Match? {
guard let bracketPosition else { return nil }
guard let initialRoundObject = initialRound() else { return nil }
let matchIndex = RoundRule.matchIndex(fromBracketPosition: bracketPosition)
return Store.main.filter(isIncluded: { $0.round == initialRoundObject.id && $0.index == matchIndex }).first
return Store.main.filter(isIncluded: { $0.round == initialRoundObject.id && $0.index == bracketPosition / 2 }).first
}

@ -528,6 +528,30 @@ class Tournament : ModelObject, Storable {
}
func maximumCourtsPerGroupSage() -> Int {
if teamsPerGroupStage > 1 {
return min(teamsPerGroupStage / 2, courtCount)
} else {
return max(1, courtCount)
}
}
func registrationIssues() -> Int {
let players : [PlayerRegistration] = unsortedPlayers()
let selectedTeams : [TeamRegistration] = selectedSortedTeams()
let callDateIssue : [TeamRegistration] = selectedTeams.filter { isStartDateIsDifferentThanCallDate($0) }
let duplicates : [PlayerRegistration] = duplicates(in: players)
let problematicPlayers : [PlayerRegistration] = players.filter({ $0.sex == -1 })
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 waitingListInBracket = waitingList.filter({ $0.bracketPosition != nil })
let waitingListInGroupStage = waitingList.filter({ $0.groupStage != nil })
return callDateIssue.count + duplicates.count + problematicPlayers.count + inadequatePlayers.count + playersWithoutValidLicense.count + playersMissing.count + waitingListInBracket.count + waitingListInGroupStage.count
}
func isStartDateIsDifferentThanCallDate(_ team: TeamRegistration) -> Bool {
guard let callDate = team.callDate else { return false }
if let groupStageStartDate = team.groupStageObject()?.startDate {

@ -55,4 +55,8 @@ enum AgendaDestination: CaseIterable, Identifiable, Selectable {
nil
}
}
func badgeImage() -> String? {
nil
}
}

@ -10,4 +10,5 @@ import Foundation
protocol Selectable {
func selectionLabel() -> String
func badgeValue() -> Int?
func badgeImage() -> String?
}

@ -48,16 +48,19 @@ struct GroupStageCallingView: View {
groupStage.startDate
}
let keys = times.keys.compactMap { $0 }.sorted()
ForEach(keys, id: \.self) { key in
if let _groupStages = times[key], _groupStages.count > 1 {
let teams = _groupStages.flatMap { $0.teams() }
let callSeeds = teams.filter({ tournament.isStartDateIsDifferentThanCallDate($0) == false })
Section {
CallView.CallStatusView(count: callSeeds.count, total: teams.count, startDate: key)
} header: {
Text(groupStages.map { $0.groupStageTitle() }.joined(separator: ", "))
} footer: {
CallView(teams: teams, callDate: key, matchFormat: tournament.groupStageMatchFormat, roundLabel: "poule")
if keys.count != groupStages.count {
ForEach(keys, id: \.self) { key in
if let _groupStages = times[key] {
let teams = _groupStages.flatMap { $0.teams() }
let callSeeds = teams.filter({ tournament.isStartDateIsDifferentThanCallDate($0) == false })
Section {
CallView.CallStatusView(count: callSeeds.count, total: teams.count, startDate: key)
} header: {
Text(_groupStages.map { $0.groupStageTitle() }.joined(separator: ", "))
} footer: {
CallView(teams: teams, callDate: key, matchFormat: tournament.groupStageMatchFormat, roundLabel: "poule")
}
}
}
}

@ -45,9 +45,18 @@ struct GenericDestinationPickerView<T: Identifiable & Selectable>: View {
}
.buttonStyle(.plain)
.overlay(alignment: .bottomTrailing) {
if let count = destination.badgeValue(), count > 0 {
if let image = destination.badgeImage() {
Image(systemName: image)
.foregroundColor(.green)
.imageScale(.medium)
.background (
Color(.systemBackground)
.clipShape(.circle)
)
.offset(x: 5, y: 5)
} else if let count = destination.badgeValue(), count > 0 {
Image(systemName: count <= 50 ? "\(count).circle.fill" : "plus.circle.fill")
.foregroundColor(.secondary)
.foregroundColor(.red)
.imageScale(.medium)
.background (
Color(.systemBackground)

@ -14,56 +14,73 @@ struct RowButtonView: View {
let title: String
var systemImage: String? = nil
var image: String? = nil
var animatedProgress: Bool = false
let confirmationMessage: String
let action: () -> ()
var action: (() -> ())? = nil
var asyncAction: (() async -> ())? = nil
@State private var askConfirmation: Bool = false
@State private var isLoading = false
init(_ title: String, role: ButtonRole? = nil, systemImage: String? = nil, image: String? = nil, animatedProgress: Bool = false, confirmationMessage: String? = nil, action: @escaping () -> Void) {
init(_ title: String, role: ButtonRole? = nil, systemImage: String? = nil, image: String? = nil, confirmationMessage: String? = nil, action: @escaping (() -> ())) {
self.role = role
self.title = title
self.systemImage = systemImage
self.image = image
self.animatedProgress = animatedProgress
self.confirmationMessage = confirmationMessage ?? defaultConfirmationMessage
self.action = action
}
init(_ title: String, role: ButtonRole? = nil, systemImage: String? = nil, image: String? = nil, confirmationMessage: String? = nil, asyncAction: @escaping (() async -> ())) {
self.role = role
self.title = title
self.systemImage = systemImage
self.image = image
self.confirmationMessage = confirmationMessage ?? defaultConfirmationMessage
self.asyncAction = asyncAction
}
var body: some View {
Button(role: role) {
if role == .destructive {
askConfirmation = true
} else {
} else if let action {
action()
} else if let asyncAction {
isLoading = true
Task {
await asyncAction()
isLoading = false
}
}
} label: {
HStack {
if animatedProgress {
Spacer()
ProgressView()
} else {
if let systemImage {
Image(systemName: systemImage)
.resizable()
.scaledToFit()
.frame(height: 24)
}
if let image {
Image(image)
.resizable()
.scaledToFit()
.frame(width: 32, height: 32)
}
Spacer()
Text(title)
.foregroundColor(.white)
.frame(height: 32)
if let systemImage {
Image(systemName: systemImage)
.resizable()
.scaledToFit()
.frame(height: 24)
}
if let image {
Image(image)
.resizable()
.scaledToFit()
.frame(width: 32, height: 32)
}
Spacer()
Text(title)
.opacity(isLoading ? 0.0 : 1.0)
.foregroundColor(.white)
.frame(height: 32)
Spacer()
}
.font(.headline)
}
.disabled(animatedProgress)
.overlay {
if isLoading {
ProgressView()
}
}
.disabled(isLoading)
.frame(maxWidth: .infinity)
.buttonStyle(.borderedProminent)
.tint(role == .destructive ? Color.red : Color.master)
@ -73,7 +90,15 @@ struct RowButtonView: View {
isPresented: $askConfirmation,
titleVisibility: .visible) {
Button("OK") {
action()
if let action {
action()
} else if let asyncAction {
isLoading = true
Task {
await asyncAction()
isLoading = false
}
}
}
Button("Annuler", role: .cancel) {}
} message: {

@ -41,6 +41,10 @@ struct GroupStagesView: View {
return groupStage.badgeValue()
}
}
func badgeImage() -> String? {
nil
}
}
init(tournament: Tournament) {

@ -61,15 +61,7 @@ struct MainView: View {
func _activityStatusBoxView() -> some View {
_activityStatus()
.font(.title3)
.frame(height: 28)
.padding()
.background {
RoundedRectangle(cornerRadius: 20, style: .continuous)
.fill(.white)
}
.shadow(radius: 2)
.offset(y: -64)
.toastFormatted()
}
@ViewBuilder

@ -10,20 +10,54 @@ import SwiftUI
struct GroupStageScheduleEditorView: View {
@EnvironmentObject var dataStore: DataStore
var groupStage: GroupStage
@State private var startDate: Date
@State private var dateUpdated: Bool = false
init(groupStage: GroupStage) {
self.groupStage = groupStage
self._startDate = State(wrappedValue: groupStage.startDate ?? Date())
}
var body: some View {
@Bindable var groupStage = groupStage
List {
Section {
MatchFormatPickerView(headerLabel: "Format", matchFormat: $groupStage.matchFormat)
}
Section {
Text("Modifier l'horaire")
}
RowButtonView("Convoquer") {
DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline)
}
.onChange(of: startDate) {
dateUpdated = true
}
} header: {
Text(groupStage.groupStageTitle())
} footer: {
HStack {
Menu {
Text("à demain 9h")
Text("à la prochaine rotation")
Text("à la précédente rotation")
} label: {
Text("décaler")
.underline()
}
Spacer()
if dateUpdated {
Button {
//todo, faut-il tout décaler ?
groupStage.startDate = startDate
_save()
dateUpdated = false
} label: {
Text("valider la modification")
.underline()
}
}
}
.font(.subheadline)
.buttonStyle(.borderless)
}
NavigationLink {
@ -35,10 +69,15 @@ struct GroupStageScheduleEditorView: View {
.onChange(of: groupStage.matchFormat) {
_save()
}
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
}
private func _save() {
let matches = groupStage._matches()
matches.forEach({ $0.matchFormat = groupStage.matchFormat })
try? dataStore.matches.addOrUpdate(contentOfs: matches)
try? dataStore.groupStages.addOrUpdate(instance: groupStage)
}
}

@ -15,7 +15,8 @@ struct LoserRoundStepScheduleEditorView: View {
var matches: [Match]
@State private var startDate: Date
@State private var matchFormat: MatchFormat
@State private var dateUpdated: Bool = false
init(round: Round, upperRound: Round) {
self.upperRound = upperRound
self.round = round
@ -30,16 +31,14 @@ struct LoserRoundStepScheduleEditorView: View {
Section {
MatchFormatPickerView(headerLabel: "Format", matchFormat: $round.matchFormat)
DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday()))
Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline)
}
RowButtonView("Valider la modification") {
_updateSchedule()
.onChange(of: round.matchFormat) {
dateUpdated = true
}
.onChange(of: startDate) {
dateUpdated = true
}
} header: {
Text(round.selectionLabel())
} footer: {
NavigationLink {
List {
ForEach(matches) { match in
@ -52,8 +51,36 @@ struct LoserRoundStepScheduleEditorView: View {
.navigationTitle(round.selectionLabel())
.environment(tournament)
} label: {
Text("voir tous les matchs")
Text("Voir tous les matchs")
}
} header: {
Text(round.selectionLabel())
} footer: {
HStack {
Menu {
Text("à demain 9h")
Text("à la prochaine rotation")
Text("à la précédente rotation")
} label: {
Text("décaler")
.underline()
}
Spacer()
if dateUpdated {
Button {
_updateSchedule()
dateUpdated = false
} label: {
Text("valider la modification")
.underline()
}
}
}
.font(.subheadline)
.buttonStyle(.borderless)
}
.headerProminence(.increased)
}
@ -67,6 +94,9 @@ struct LoserRoundStepScheduleEditorView: View {
_save()
MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate)
upperRound.loserRounds(forRoundIndex: round.index).forEach({ round in
round.startDate = startDate
})
_save()
}
@ -83,7 +113,8 @@ struct LoserRoundScheduleEditorView: View {
var loserRounds: [Round]
@State private var startDate: Date
@State private var matchFormat: MatchFormat
@State private var dateUpdated: Bool = false
init(upperRound: Round) {
self.upperRound = upperRound
let _loserRounds = upperRound.loserRounds()
@ -97,13 +128,34 @@ struct LoserRoundScheduleEditorView: View {
Section {
MatchFormatPickerView(headerLabel: "Format", matchFormat: $matchFormat)
DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday()))
}
RowButtonView("Valider la modification") {
_updateSchedule()
Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline)
}
} header: {
Text("Classement " + upperRound.roundTitle())
} footer: {
HStack {
Menu {
Text("à demain 9h")
Text("à la prochaine rotation")
Text("à la précédente rotation")
} label: {
Text("décaler")
.underline()
}
Spacer()
if dateUpdated {
Button {
_updateSchedule()
dateUpdated = false
} label: {
Text("valider la modification")
.underline()
}
}
}
.font(.subheadline)
.buttonStyle(.borderless)
}
@ -113,6 +165,12 @@ struct LoserRoundScheduleEditorView: View {
}
}
}
.onChange(of: startDate) {
dateUpdated = true
}
.onChange(of: matchFormat) {
dateUpdated = true
}
.headerProminence(.increased)
.navigationTitle("Réglages")
.toolbarBackground(.visible, for: .navigationBar)
@ -126,12 +184,14 @@ struct LoserRoundScheduleEditorView: View {
upperRound.loserRounds().forEach({ round in
round.resetRound(updateMatchFormat: matchFormat)
})
try? dataStore.matches.addOrUpdate(contentOfs: matches)
_save()
MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: upperRound.loserRounds().first?.id, fromMatchId: nil, startDate: startDate)
_save()
upperRound.loserRounds().first?.startDate = startDate
}
private func _save() {

@ -11,6 +11,7 @@ struct MatchScheduleEditorView: View {
@Environment(Tournament.self) var tournament: Tournament
var match: Match
@State private var startDate: Date
@State private var dateUpdated: Bool = false
init(match: Match) {
self.match = match
@ -20,10 +21,10 @@ struct MatchScheduleEditorView: View {
var body: some View {
Section {
DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday()))
Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline)
}
RowButtonView("Valider la modification") {
_updateSchedule()
.onChange(of: startDate) {
dateUpdated = true
}
} header: {
if let round = match.roundObject {
@ -31,6 +32,30 @@ struct MatchScheduleEditorView: View {
} else {
Text(match.matchTitle())
}
} footer: {
HStack {
Menu {
Text("à demain 9h")
Text("à la prochaine rotation")
Text("à la précédente rotation")
} label: {
Text("décaler")
.underline()
}
Spacer()
if dateUpdated {
Button {
_updateSchedule()
dateUpdated = false
} label: {
Text("valider la modification")
.underline()
}
}
}
.font(.subheadline)
.buttonStyle(.borderless)
}
.headerProminence(.increased)
}

@ -10,7 +10,6 @@ import SwiftUI
struct PlanningSettingsView: View {
@EnvironmentObject var dataStore: DataStore
var tournament: Tournament
@State private var scheduleSetup: Bool = false
@State private var randomCourtDistribution: Bool
@State private var groupStageCourtCount: Int
@State private var upperBracketBreakTime: Bool
@ -20,6 +19,8 @@ struct PlanningSettingsView: View {
@State private var upperBracketRotationDifference: Int
@State private var timeDifferenceLimit: Double
@State private var shouldHandleUpperRoundSlice: Bool
@State private var isScheduling: Bool = false
@State private var schedulingDone: Bool = false
init(tournament: Tournament) {
self.tournament = tournament
@ -52,10 +53,10 @@ struct PlanningSettingsView: View {
}
Section {
TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount)
TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount, max: 100)
if tournament.groupStages().isEmpty == false {
TournamentFieldsManagerView(localizedStringKey: "Terrains par poule", count: $groupStageCourtCount)
TournamentFieldsManagerView(localizedStringKey: "Terrains par poule", count: $groupStageCourtCount, max: tournament.maximumCourtsPerGroupSage())
}
NavigationLink {
@ -102,18 +103,36 @@ struct PlanningSettingsView: View {
.disabled(rotationDifferenceIsImportant == false)
//timeDifferenceLimit
RowButtonView("Horaire intelligent", role: .destructive) {
_setupSchedule()
schedulingDone = false
await _setupSchedule()
schedulingDone = true
}
if scheduleSetup {
HStack {
Image(systemName: "checkmark")
}
}
Section {
RowButtonView("Supprimer tous les horaires", role: .destructive) {
let allMatches = tournament.allMatches()
allMatches.forEach({ $0.startDate = nil })
try? dataStore.matches.addOrUpdate(contentOfs: allMatches)
let allGroupStages = tournament.groupStages()
allGroupStages.forEach({ $0.startDate = nil })
try? dataStore.groupStages.addOrUpdate(contentOfs: allGroupStages)
let allRounds = tournament.allRounds()
allRounds.forEach({ $0.startDate = nil })
try? dataStore.rounds.addOrUpdate(contentOfs: allRounds)
}
}
}
.overlay(alignment: .bottom) {
if schedulingDone {
Label("Horaires mis à jour", systemImage: "checkmark.circle.fill")
.toastFormatted()
.deferredRendering(for: .seconds(2))
}
}
.onChange(of: groupStageCourtCount) {
tournament.groupStageCourtCount = groupStageCourtCount
_save()
@ -132,7 +151,7 @@ struct PlanningSettingsView: View {
}
}
private func _setupSchedule() {
private func _setupSchedule() async {
let groupStageCourtCount = tournament.groupStageCourtCount ?? 1
let groupStages = tournament.groupStages()
let numberOfCourtsAvailablePerRotation: Int = tournament.courtCount
@ -166,13 +185,6 @@ struct PlanningSettingsView: View {
let matches = tournament.groupStages().flatMap({ $0._matches() })
matches.forEach({ $0.startDate = nil })
// var times = Set(groupStages.compactMap { $0.startDate }.filter { $0 >= tournament.startDate } )
// if times.isEmpty {
// groupStages.forEach({ $0.startDate = tournament.startDate })
// times.insert(tournament.startDate)
// try? dataStore.groupStages.addOrUpdate(contentOfs: groupStages)
// }
var lastDate : Date = tournament.startDate
groupStages.chunked(into: groupStageCourtCount).forEach { groups in
groups.forEach({ $0.startDate = lastDate })
@ -195,9 +207,6 @@ struct PlanningSettingsView: View {
try? dataStore.matches.addOrUpdate(contentOfs: matches)
matchScheduler.updateSchedule(tournament: tournament, fromRoundId: nil, fromMatchId: nil, startDate: lastDate)
scheduleSetup = true
}
private func _save() {

@ -13,7 +13,8 @@ struct RoundScheduleEditorView: View {
var round: Round
@State private var startDate: Date
@State private var dateUpdated: Bool = false
init(round: Round) {
self.round = round
self._startDate = State(wrappedValue: round.startDate ?? round.playedMatches().first?.startDate ?? Date())
@ -25,17 +26,46 @@ struct RoundScheduleEditorView: View {
Section {
MatchFormatPickerView(headerLabel: "Format", matchFormat: $round.matchFormat)
DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday()))
Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline)
}
.onChange(of: round.matchFormat) {
dateUpdated = true
}
.onChange(of: startDate) {
dateUpdated = true
}
RowButtonView("Valider la modification") {
_updateSchedule()
} footer: {
HStack {
Menu {
Text("à demain 9h")
Text("à la prochaine rotation")
Text("à la précédente rotation")
} label: {
Text("décaler")
.underline()
}
Spacer()
if dateUpdated {
Button {
_updateSchedule()
dateUpdated = false
} label: {
Text("valider la modification")
.underline()
}
}
}
.font(.subheadline)
.buttonStyle(.borderless)
}
ForEach(round.playedMatches()) { match in
MatchScheduleEditorView(match: match)
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
}
private func _updateSchedule() {
@ -47,6 +77,7 @@ struct RoundScheduleEditorView: View {
_save()
MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate)
round.startDate = startDate
_save()
}

@ -0,0 +1,114 @@
//
// PlayerDetailView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 17/04/2024.
//
import SwiftUI
struct PlayerDetailView: View {
@Environment(Tournament.self) var tournament: Tournament
@EnvironmentObject var dataStore: DataStore
@Bindable var player: PlayerRegistration
@FocusState private var textFieldIsFocus: Bool
var body: some View {
Form {
Section {
LabeledContent {
TextField("Nom", text: $player.lastName)
.keyboardType(.alphabet)
.multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity)
} label: {
Text("Nom")
}
LabeledContent {
TextField("Prénom", text: $player.firstName)
.keyboardType(.alphabet)
.multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity)
} label: {
Text("Prénom")
}
PlayerSexPickerView(player: player)
}
Section {
LabeledContent {
TextField("Rang", value: $player.rank, format: .number)
.keyboardType(.decimalPad)
.multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity)
.focused($textFieldIsFocus)
} label: {
Text("Rang")
}
} header: {
Text("Classement actuel")
}
if player.isMalePlayer() == false && tournament.tournamentCategory == .men, let rank = player.rank {
Section {
let value = PlayerRegistration.addon(for: rank, manMax: tournament.maleUnrankedValue ?? 0, womanMax: tournament.femaleUnrankedValue ?? 0)
LabeledContent {
Text(value.formatted())
} label: {
Text("Valeur à rajouter")
}
LabeledContent {
TextField("Rang", value: $player.weight, format: .number)
.keyboardType(.decimalPad)
.multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity)
.focused($textFieldIsFocus)
} label: {
Text("Poids re-calculé")
}
} header: {
Text("Ré-assimilation")
} footer: {
Text("Calculé en fonction du sexe")
}
}
}
.scrollDismissesKeyboard(.immediately)
.onChange(of: player.sex) {
_save()
}
.onChange(of: player.weight) {
player.team()?.updateWeight()
_save()
}
.onChange(of: player.rank) {
player.setWeight(in: tournament)
player.team()?.updateWeight()
_save()
}
.headerProminence(.increased)
.navigationTitle("Édition")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.toolbar {
ToolbarItem(placement: .keyboard) {
Button("Valider") {
textFieldIsFocus = false
}
}
}
}
private func _save() {
try? dataStore.playerRegistrations.addOrUpdate(instance: player)
if let team = player.team() {
try? dataStore.teamRegistrations.addOrUpdate(instance: team)
}
}
}
#Preview {
PlayerDetailView(player: PlayerRegistration.mock())
}

@ -73,8 +73,10 @@ struct LoserRoundView: View {
}
.headerProminence(.increased)
.toolbar {
Button(isEditingTournamentSeed.wrappedValue == true ? "Valider" : "Modifier") {
isEditingTournamentSeed.wrappedValue.toggle()
ToolbarItem(placement: .topBarTrailing) {
Button(isEditingTournamentSeed.wrappedValue == true ? "Valider" : "Modifier") {
isEditingTournamentSeed.wrappedValue.toggle()
}
}
}
}

@ -53,7 +53,9 @@ struct RoundView: View {
.headerProminence(.increased)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
EditButton()
Button(isEditingTournamentSeed.wrappedValue == true ? "Valider" : "Modifier") {
isEditingTournamentSeed.wrappedValue.toggle()
}
}
}
}

@ -29,9 +29,6 @@ struct ImportedPlayerView: View {
.foregroundStyle(.secondary)
.font(.caption)
}
} else if let computedAge = player.computedAge {
Text(computedAge.formatted() + " ans")
.foregroundStyle(.secondary)
}
}
.font(.title3)
@ -61,9 +58,13 @@ struct ImportedPlayerView: View {
}
}
Text(player.formattedLicense())
.font(.caption)
HStack {
Text(player.formattedLicense())
if let computedAge = player.computedAge {
Text(computedAge.formatted() + " ans")
}
}
.font(.caption)
if let clubName = player.clubName {
Text(clubName)
.font(.caption)

@ -0,0 +1,45 @@
//
// EditingTeamView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 17/04/2024.
//
import SwiftUI
struct EditingTeamView: View {
@EnvironmentObject var dataStore: DataStore
var team: TeamRegistration
@State private var registrationDate : Date
init(team: TeamRegistration) {
self.team = team
_registrationDate = State(wrappedValue: team.registrationDate ?? Date())
}
var body: some View {
List {
Section {
DatePicker(registrationDate.formatted(.dateTime.weekday()), selection: $registrationDate)
} header: {
Text("Date d'inscription")
}
}
.onChange(of: registrationDate) {
team.registrationDate = registrationDate
_save()
}
.headerProminence(.increased)
.navigationTitle("Édition")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
}
private func _save() {
try? dataStore.teamRegistrations.addOrUpdate(instance: team)
}
}
#Preview {
EditingTeamView(team: TeamRegistration.mock())
}

@ -8,6 +8,7 @@
import SwiftUI
struct TeamDetailView: View {
@Environment(Tournament.self) var tournament: Tournament
@EnvironmentObject var dataStore: DataStore
var team: TeamRegistration
@ -17,7 +18,8 @@ struct TeamDetailView: View {
} else {
ForEach(team.players()) { player in
NavigationLink {
Text("Hello wolrd")
PlayerDetailView(player: player)
.environment(tournament)
} label: {
PlayerView(player: player)
}

@ -10,18 +10,19 @@ import SwiftUI
struct TournamentFieldsManagerView: View {
let localizedStringKey: String
@Binding var count: Int
let max: Int
var body: some View {
LabeledContent {
StepperView(count: $count, minimum: 1, maximum: 1_000)
StepperView(count: $count, minimum: 1, maximum: max)
} label: {
Text(localizedStringKey)
Text(count.formatted())
// Text(count.formatted())
}
}
}
#Preview {
TournamentFieldsManagerView(localizedStringKey: "test", count: .constant(2))
TournamentFieldsManagerView(localizedStringKey: "test", count: .constant(2), max: 10)
.environment(Tournament.mock())
}

@ -418,14 +418,16 @@ struct InscriptionManagerView: View {
.environment(tournament)
} label: {
LabeledContent {
Text(count.formatted() + "/" + tournament.teamCount.formatted())
Text(tournament.registrationIssues().formatted()).font(.largeTitle)
} label: {
Text("Analyse des inscriptions")
Text("Problèmes détéctés")
if let closedRegistrationDate = tournament.closedRegistrationDate {
Text("clôturé le " + closedRegistrationDate.formatted())
}
}
}
} header: {
Text(count.formatted() + "/" + tournament.teamCount.formatted() + " paires inscrites")
}
}
@ -739,7 +741,7 @@ struct InscriptionManagerView: View {
private func _teamFooterView(_ team: TeamRegistration) -> some View {
HStack {
if let formattedRegistrationDate = team.formattedInscriptionDate() {
Text(formattedRegistrationDate).foregroundStyle(.secondary)
Text(formattedRegistrationDate)
}
Spacer()
_teamMenuOptionView(team)
@ -748,7 +750,7 @@ struct InscriptionManagerView: View {
private func _teamMenuOptionView(_ team: TeamRegistration) -> some View {
Menu {
Section {
Button("Modifier l'équipe") {
Button("Changer les joueurs") {
editedTeam = team
team.unsortedPlayers().forEach { player in
createdPlayers.insert(player)
@ -756,7 +758,13 @@ struct InscriptionManagerView: View {
}
}
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

@ -7,11 +7,18 @@
import SwiftUI
enum CallDestination: String, Identifiable, Selectable {
case seeds
case groupStages
enum CallDestination: Identifiable, Selectable {
case seeds(Tournament)
case groupStages(Tournament)
var id: String { self.rawValue }
var id: String {
switch self {
case .seeds:
return "seed"
case .groupStages:
return "groupStage"
}
}
func selectionLabel() -> String {
switch self {
@ -23,8 +30,27 @@ enum CallDestination: String, Identifiable, Selectable {
}
func badgeValue() -> Int? {
nil
switch self {
case .seeds(let tournament):
let allSeedCalled = tournament.seeds().filter({ tournament.isStartDateIsDifferentThanCallDate($0) || $0.callDate == nil })
return allSeedCalled.count
case .groupStages(let tournament):
let allSeedCalled = tournament.groupStageTeams().filter({ tournament.isStartDateIsDifferentThanCallDate($0) || $0.callDate == nil })
return allSeedCalled.count
}
}
func badgeImage() -> String? {
switch self {
case .seeds(let tournament):
let allSeedCalled = tournament.seeds().allSatisfy({ tournament.isStartDateIsDifferentThanCallDate($0) == false })
return allSeedCalled ? "checkmark.circle.fill" : nil
case .groupStages(let tournament):
let allSeedCalled = tournament.groupStageTeams().allSatisfy({ tournament.isStartDateIsDifferentThanCallDate($0) == false })
return allSeedCalled ? "checkmark.circle.fill" : nil
}
}
}
@ -38,13 +64,13 @@ struct TournamentCallView: View {
var destinations = [CallDestination]()
let groupStageTeams = tournament.groupStageTeams()
if groupStageTeams.isEmpty == false {
destinations.append(.groupStages)
self._selectedDestination = State(wrappedValue: .groupStages)
destinations.append(.groupStages(tournament))
self._selectedDestination = State(wrappedValue: .groupStages(tournament))
}
if tournament.seededTeams().isEmpty == false {
destinations.append(.seeds)
destinations.append(.seeds(tournament))
if groupStageTeams.isEmpty {
self._selectedDestination = State(wrappedValue: .seeds)
self._selectedDestination = State(wrappedValue: .seeds(tournament))
}
}
self.allDestinations = destinations

@ -11,7 +11,7 @@ enum CashierDestination: Identifiable, Selectable {
case summary
case groupStage(GroupStage)
case bracket(Round)
case all
case all(Tournament)
var id: String {
switch self {
@ -38,8 +38,31 @@ enum CashierDestination: Identifiable, Selectable {
}
func badgeValue() -> Int? {
nil
switch self {
case .summary:
return nil
case .groupStage(let groupStage):
return groupStage.unsortedPlayers().filter({ $0.hasPaid() == false }).count
case .bracket(let round):
return round.seeds().flatMap { $0.unsortedPlayers() }.filter({ $0.hasPaid() == false }).count
case .all(let tournament):
return tournament.selectedPlayers().filter({ $0.hasPaid() == false }).count
}
}
func badgeImage() -> String? {
switch self {
case .summary:
return nil
case .groupStage(let groupStage):
return groupStage.unsortedPlayers().allSatisfy({ $0.hasPaid() }) ? "checkmark" : nil
case .bracket(let round):
return round.seeds().flatMap { $0.unsortedPlayers() }.allSatisfy({ $0.hasPaid() }) ? "checkmark" : nil
case .all(let tournament):
return tournament.selectedPlayers().allSatisfy({ $0.hasPaid() }) ? "checkmark" : nil
}
}
}
struct TournamentCashierView: View {
@ -47,7 +70,7 @@ struct TournamentCashierView: View {
@State private var selectedDestination: CashierDestination?
func allDestinations() -> [CashierDestination] {
var allDestinations : [CashierDestination] = [.summary, .all]
var allDestinations : [CashierDestination] = []
let destinations : [CashierDestination] = tournament.groupStages().map { CashierDestination.groupStage($0) }
allDestinations.append(contentsOf: destinations)
tournament.rounds().forEach { round in
@ -55,6 +78,8 @@ struct TournamentCashierView: View {
allDestinations.append(CashierDestination.bracket(round))
}
}
allDestinations.append(.all(tournament))
allDestinations.append(.summary)
return allDestinations
}
@ -83,7 +108,7 @@ struct TournamentCashierView: View {
CashierView(tournament: tournament, teams: groupStage.teams())
case .bracket(let round):
CashierView(tournament: tournament, teams: round.seeds())
case .all:
case .all(let tournament):
CashierView(tournament: tournament, teams: tournament.selectedSortedTeams())
}
}

@ -41,6 +41,11 @@ enum ScheduleDestination: String, Identifiable, Selectable {
func badgeValue() -> Int? {
nil
}
func badgeImage() -> String? {
nil
}
}
struct TournamentScheduleView: View {

@ -12,15 +12,16 @@ struct TournamentSettingsView: View {
@EnvironmentObject var dataStore: DataStore
@State private var tournamentName: String = ""
@FocusState private var textFieldIsFocus: Bool
var body: some View {
@Bindable var tournament = tournament
Form {
LabeledContent {
TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $tournament.entryFee, format: .currency(code: Locale.current.currency?.identifier ?? "EUR"))
.keyboardType(.decimalPad)
.fixedSize()
.multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity)
} label: {
Text("Inscription")
}
@ -28,7 +29,7 @@ struct TournamentSettingsView: View {
LabeledContent {
TextField("Nom", text: $tournamentName)
.multilineTextAlignment(.trailing)
.fixedSize()
.frame(maxWidth: .infinity)
.keyboardType(.alphabet)
.autocorrectionDisabled()
.onSubmit {
@ -44,7 +45,7 @@ struct TournamentSettingsView: View {
TournamentLevelPickerView()
TournamentDurationManagerView()
TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount)
TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount, max: 100)
TournamentDatePickerView()
@ -83,7 +84,6 @@ struct TournamentSettingsView: View {
event.club = nil
try? dataStore.events.addOrUpdate(instance: event)
}
.font(.caption)
}
}
}
@ -91,8 +91,17 @@ struct TournamentSettingsView: View {
TournamentFormatSelectionView()
}
.focused($textFieldIsFocus)
.scrollDismissesKeyboard(.immediately)
.navigationTitle("Réglages")
.toolbarBackground(.visible, for: .navigationBar)
.toolbar {
ToolbarItem(placement: .keyboard) {
Button("Valider") {
textFieldIsFocus = false
}
}
}
.onDisappear {
try? dataStore.tournaments.addOrUpdate(instance: tournament)
}

@ -49,6 +49,7 @@ struct TournamentRunningView: View {
NavigationLink(value: Screen.groupStage) {
LabeledContent {
Text(tournament.groupStageStatus())
.foregroundStyle(.master)
} label: {
Text("Poules")
}
@ -60,6 +61,7 @@ struct TournamentRunningView: View {
NavigationLink(value: Screen.round) {
LabeledContent {
Text(tournament.bracketStatus())
.foregroundStyle(.master)
} label: {
Text("Tableau")
}

@ -34,6 +34,7 @@ struct TournamentView: View {
NavigationLink(value: Screen.inscription) {
LabeledContent {
Text(tournament.unsortedTeams().count.formatted() + "/" + tournament.teamCount.formatted())
.foregroundStyle(.master)
} label: {
Text("Gestion des inscriptions")
if let closedRegistrationDate = tournament.closedRegistrationDate {
@ -44,6 +45,7 @@ struct TournamentView: View {
if let endOfInscriptionDate = tournament.mandatoryRegistrationCloseDate(), tournament.inscriptionClosed() == false && tournament.hasStarted() == false {
LabeledContent {
Text(endOfInscriptionDate.formatted(date: .abbreviated, time: .shortened))
.foregroundStyle(.master)
} label: {
Text("Date limite")
}

@ -49,6 +49,19 @@ private struct DeferredViewModifier: ViewModifier {
}
extension View {
func toastFormatted() -> some View {
self
.font(.title3)
.frame(height: 28)
.padding()
.background {
RoundedRectangle(cornerRadius: 20, style: .continuous)
.fill(.white)
}
.shadow(radius: 2)
.offset(y: -64)
}
func deferredRendering(for delay: DispatchTimeInterval) -> some View {
modifier(DeferredViewModifier(delay: delay))
}

Loading…
Cancel
Save