update match scheduler

multistore
Razmig Sarkissian 2 years ago
parent faaeb416c9
commit 512cad69e5
  1. 40
      PadelClub.xcodeproj/project.pbxproj
  2. 12
      PadelClub/Data/GroupStage.swift
  3. 20
      PadelClub/Data/Match.swift
  4. 6
      PadelClub/Data/MockData.swift
  5. 41
      PadelClub/Data/Round.swift
  6. 14
      PadelClub/Data/Tournament.swift
  7. 251
      PadelClub/ViewModel/MatchScheduler.swift
  8. 2
      PadelClub/Views/GroupStage/GroupStageView.swift
  9. 26
      PadelClub/Views/Match/MatchDetailView.swift
  10. 2
      PadelClub/Views/Match/MatchSummaryView.swift
  11. 48
      PadelClub/Views/Planning/GroupStageScheduleEditorView.swift
  12. 40
      PadelClub/Views/Planning/MatchScheduleEditorView.swift
  13. 159
      PadelClub/Views/Planning/PlanningSettingsView.swift
  14. 149
      PadelClub/Views/Planning/PlanningView.swift
  15. 58
      PadelClub/Views/Planning/RoundScheduleEditorView.swift
  16. 66
      PadelClub/Views/Planning/SchedulerView.swift
  17. 18
      PadelClub/Views/Tournament/Screen/Components/TournamentFieldsManagerView.swift
  18. 1
      PadelClub/Views/Tournament/Screen/Screen.swift
  19. 91
      PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift
  20. 2
      PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift
  21. 10
      PadelClub/Views/Tournament/TournamentRunningView.swift
  22. 20
      PadelClub/Views/Tournament/TournamentView.swift

@ -38,6 +38,7 @@
FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF089EBA2BB0120700F0AEC7 /* PlayerPopoverView.swift */; };
FF089EBD2BB0287D00F0AEC7 /* PlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF089EBC2BB0287D00F0AEC7 /* PlayerView.swift */; };
FF089EBF2BB0B14600F0AEC7 /* FileImportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF089EBE2BB0B14600F0AEC7 /* FileImportView.swift */; };
FF0E0B6D2BC254C6005F00A9 /* TournamentScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF0E0B6C2BC254C6005F00A9 /* TournamentScheduleView.swift */; };
FF0EC5202BB16F680056B6D1 /* SwiftParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF0EC51D2BB16F680056B6D1 /* SwiftParser.swift */; };
FF0EC5222BB173E70056B6D1 /* UpdateSourceRankDateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF0EC5212BB173E70056B6D1 /* UpdateSourceRankDateView.swift */; };
FF0EC54E2BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-02-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5382BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-2-02-2023.csv */; };
@ -97,6 +98,7 @@
FF2BE4882B85E27400592328 /* LeStorage.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C425D4542B6D24E2002A7B48 /* LeStorage.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = FF3795602B9396D0004EA093 /* PadelClubApp.xcdatamodeld */; };
FF3795662B9399AA004EA093 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3795652B9399AA004EA093 /* Persistence.swift */; };
FF3B60A32BC49BBC008C2E66 /* MatchScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3B60A22BC49BBC008C2E66 /* MatchScheduler.swift */; };
FF3F74F62B919E45004CFE0E /* UmpireView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3F74F52B919E45004CFE0E /* UmpireView.swift */; };
FF3F74FF2B91A2D4004CFE0E /* AgendaDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3F74FE2B91A2D4004CFE0E /* AgendaDestination.swift */; };
FF4AB6B52B9248200002987F /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6B42B9248200002987F /* NetworkManager.swift */; };
@ -198,11 +200,17 @@
FFDB1C6D2BB2A02000F1E467 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */; };
FFDB1C732BB2CFE900F1E467 /* MySortDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDB1C722BB2CFE900F1E467 /* MySortDescriptor.swift */; };
FFDDD40C2B93B2BB00C91A49 /* DeferredViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */; };
FFF527D62BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */; };
FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACCC2B92367B008466FA /* FederalPlayer.swift */; };
FFF8ACD42B92392C008466FA /* SourceFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACD32B92392C008466FA /* SourceFileManager.swift */; };
FFF8ACD62B923960008466FA /* URL+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACD52B923960008466FA /* URL+Extensions.swift */; };
FFF8ACD92B923F3C008466FA /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACD82B923F3C008466FA /* String+Extensions.swift */; };
FFF8ACDB2B923F48008466FA /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACDA2B923F48008466FA /* Date+Extensions.swift */; };
FFF964502BC25E3700EEF017 /* PlanningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF9644F2BC25E3700EEF017 /* PlanningView.swift */; };
FFF964532BC262B000EEF017 /* PlanningSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF964522BC262B000EEF017 /* PlanningSettingsView.swift */; };
FFF964552BC266CF00EEF017 /* SchedulerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF964542BC266CF00EEF017 /* SchedulerView.swift */; };
FFF964572BC26B3400EEF017 /* RoundScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF964562BC26B3400EEF017 /* RoundScheduleEditorView.swift */; };
FFF9645B2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF9645A2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -286,6 +294,7 @@
FF089EBA2BB0120700F0AEC7 /* PlayerPopoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerPopoverView.swift; sourceTree = "<group>"; };
FF089EBC2BB0287D00F0AEC7 /* PlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerView.swift; sourceTree = "<group>"; };
FF089EBE2BB0B14600F0AEC7 /* FileImportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileImportView.swift; sourceTree = "<group>"; };
FF0E0B6C2BC254C6005F00A9 /* TournamentScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentScheduleView.swift; sourceTree = "<group>"; };
FF0EC51D2BB16F680056B6D1 /* SwiftParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftParser.swift; sourceTree = "<group>"; };
FF0EC5212BB173E70056B6D1 /* UpdateSourceRankDateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateSourceRankDateView.swift; sourceTree = "<group>"; };
FF0EC5232BB195CA0056B6D1 /* CLASSEMENT PADEL DAMES-07-2023.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT PADEL DAMES-07-2023.csv"; sourceTree = "<group>"; };
@ -343,6 +352,7 @@
FF1DC55A2BAB80C400FD8220 /* DisplayContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayContext.swift; sourceTree = "<group>"; };
FF3795612B9396D0004EA093 /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = "<group>"; };
FF3795652B9399AA004EA093 /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
FF3B60A22BC49BBC008C2E66 /* MatchScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchScheduler.swift; sourceTree = "<group>"; };
FF3F74F52B919E45004CFE0E /* UmpireView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UmpireView.swift; sourceTree = "<group>"; };
FF3F74FE2B91A2D4004CFE0E /* AgendaDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgendaDestination.swift; sourceTree = "<group>"; };
FF4AB6B42B9248200002987F /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = "<group>"; };
@ -445,11 +455,17 @@
FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
FFDB1C722BB2CFE900F1E467 /* MySortDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MySortDescriptor.swift; sourceTree = "<group>"; };
FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeferredViewModifier.swift; sourceTree = "<group>"; };
FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchScheduleEditorView.swift; sourceTree = "<group>"; };
FFF8ACCC2B92367B008466FA /* FederalPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalPlayer.swift; sourceTree = "<group>"; };
FFF8ACD32B92392C008466FA /* SourceFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceFileManager.swift; sourceTree = "<group>"; };
FFF8ACD52B923960008466FA /* URL+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extensions.swift"; sourceTree = "<group>"; };
FFF8ACD82B923F3C008466FA /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
FFF8ACDA2B923F48008466FA /* Date+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = "<group>"; };
FFF9644F2BC25E3700EEF017 /* PlanningView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlanningView.swift; sourceTree = "<group>"; };
FFF964522BC262B000EEF017 /* PlanningSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlanningSettingsView.swift; sourceTree = "<group>"; };
FFF964542BC266CF00EEF017 /* SchedulerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SchedulerView.swift; sourceTree = "<group>"; };
FFF964562BC26B3400EEF017 /* RoundScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundScheduleEditorView.swift; sourceTree = "<group>"; };
FFF9645A2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupStageScheduleEditorView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -597,6 +613,7 @@
FFCFC00B2BBC39A600B82851 /* Score */,
FF967D072BAF3D3000A9A3BD /* Team */,
FF089EB92BB011EE00F0AEC7 /* Player */,
FFF964512BC2628600EEF017 /* Planning */,
FF3F74F72B919F96004CFE0E /* Tournament */,
C4A47D882B7BBB5000ADC637 /* Subscription */,
C4A47D852B7BA33F00ADC637 /* User */,
@ -776,6 +793,7 @@
FF70916D2B9108C600AB08DA /* InscriptionManagerView.swift */,
FF8F26422BADFE5B00650388 /* TournamentSettingsView.swift */,
FF8F26532BAE1E4400650388 /* TableStructureView.swift */,
FF0E0B6C2BC254C6005F00A9 /* TournamentScheduleView.swift */,
FF8F26522BAE0E4E00650388 /* Components */,
);
path = Screen;
@ -821,6 +839,7 @@
FFCFC0132BBC59FC00B82851 /* MatchDescriptor.swift */,
FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */,
FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */,
FF3B60A22BC49BBC008C2E66 /* MatchScheduler.swift */,
);
path = ViewModel;
sourceTree = "<group>";
@ -1005,6 +1024,19 @@
path = Extensions;
sourceTree = "<group>";
};
FFF964512BC2628600EEF017 /* Planning */ = {
isa = PBXGroup;
children = (
FFF9644F2BC25E3700EEF017 /* PlanningView.swift */,
FFF964522BC262B000EEF017 /* PlanningSettingsView.swift */,
FFF964542BC266CF00EEF017 /* SchedulerView.swift */,
FFF964562BC26B3400EEF017 /* RoundScheduleEditorView.swift */,
FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */,
FFF9645A2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift */,
);
path = Planning;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -1239,6 +1271,7 @@
FF6EC8F72B94773200EA7F5A /* RowButtonView.swift in Sources */,
FF8F263B2BAD528600650388 /* EventCreationView.swift in Sources */,
FFC1E1082BAC29FC008D6F59 /* LocationManager.swift in Sources */,
FFF964552BC266CF00EEF017 /* SchedulerView.swift in Sources */,
FFA1B1292BB71773006CE248 /* PadelClubButtonView.swift in Sources */,
FF5DA19B2BB9662200A33061 /* TournamentSeedEditing.swift in Sources */,
FF70916C2B91005400AB08DA /* TournamentView.swift in Sources */,
@ -1262,16 +1295,19 @@
C4A47D5A2B6D383C00ADC637 /* Tournament.swift in Sources */,
C4A47D7B2B73C0F900ADC637 /* TournamentV2.swift in Sources */,
FF3795662B9399AA004EA093 /* Persistence.swift in Sources */,
FFF964502BC25E3700EEF017 /* PlanningView.swift in Sources */,
FF967CEC2BAECB9900A9A3BD /* Match.swift in Sources */,
FF8F264B2BAE0B4100650388 /* TournamentLevelPickerView.swift in Sources */,
FF1CBC222BB53E590036DAAB /* FederalTournamentHolder.swift in Sources */,
C4A47D5E2B6D38EC00ADC637 /* DataStore.swift in Sources */,
FFCFC01C2BBC5AAA00B82851 /* SetDescriptor.swift in Sources */,
FF82CFC52B911F5B00B0CAF2 /* OrganizedTournamentView.swift in Sources */,
FFF964572BC26B3400EEF017 /* RoundScheduleEditorView.swift in Sources */,
FF59FFB32B90EFAC0061EFF9 /* EventListView.swift in Sources */,
C4A47D7D2B73CDC300ADC637 /* ClubV1.swift in Sources */,
FF8F263D2BAD627A00650388 /* TournamentConfiguratorView.swift in Sources */,
FFC1E10C2BAC7FB0008D6F59 /* ClubImportView.swift in Sources */,
FF3B60A32BC49BBC008C2E66 /* MatchScheduler.swift in Sources */,
FF5DA1952BB927E800A33061 /* GenericDestinationPickerView.swift in Sources */,
FF8F26542BAE1E4400650388 /* TableStructureView.swift in Sources */,
FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */,
@ -1315,6 +1351,7 @@
FF4C7F022BBBD7150031B6A3 /* TabItemModifier.swift in Sources */,
FFDDD40C2B93B2BB00C91A49 /* DeferredViewModifier.swift in Sources */,
FFD784042B91C280000F62A6 /* EmptyActivityView.swift in Sources */,
FF0E0B6D2BC254C6005F00A9 /* TournamentScheduleView.swift in Sources */,
FF3F74F62B919E45004CFE0E /* UmpireView.swift in Sources */,
C4A47D772B73789100ADC637 /* TournamentV1.swift in Sources */,
C4A47DAD2B85FCCD00ADC637 /* User.swift in Sources */,
@ -1330,6 +1367,8 @@
FF1DC5592BAB767000FD8220 /* Tips.swift in Sources */,
FF59FFB72B90EFBF0061EFF9 /* MainView.swift in Sources */,
FF967D0D2BAF3EB300A9A3BD /* MatchDateView.swift in Sources */,
FFF964532BC262B000EEF017 /* PlanningSettingsView.swift in Sources */,
FFF527D62BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift in Sources */,
FFD784022B91C1B4000F62A6 /* WelcomeView.swift in Sources */,
FFF8ACD62B923960008466FA /* URL+Extensions.swift in Sources */,
FF089EBD2BB0287D00F0AEC7 /* PlayerView.swift in Sources */,
@ -1344,6 +1383,7 @@
FF8F26512BAE0BAD00650388 /* MatchFormatPickerView.swift in Sources */,
FF5D0D872BB48AFD005CB568 /* NumberFormatter+Extensions.swift in Sources */,
FFCFC0182BBC5A6800B82851 /* SetLabelView.swift in Sources */,
FFF9645B2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift in Sources */,
C4A47DA62B83948E00ADC637 /* LoginView.swift in Sources */,
FF967CF82BAEDF0000A9A3BD /* Labels.swift in Sources */,
FF089EB42BB0020000F0AEC7 /* PlayerSexPickerView.swift in Sources */,

@ -85,7 +85,7 @@ class GroupStage: ModelObject, Storable {
try? DataStore.shared.matches.addOrUpdate(contentOfs: _matches)
}
func matches() -> [Match] {
func playedMatches() -> [Match] {
let ordered = _matches()
if ordered.isEmpty == false && ordered.count == _matchOrder().count {
return _matchOrder().map {
@ -127,19 +127,19 @@ class GroupStage: ModelObject, Storable {
}
func availableToStart() -> [Match] {
matches().filter({ $0.canBeStarted() && $0.isRunning() == false })
playedMatches().filter({ $0.canBeStarted() && $0.isRunning() == false })
}
func runningMatches() -> [Match] {
matches().filter({ $0.isRunning() })
playedMatches().filter({ $0.isRunning() })
}
func readyMatches() -> [Match] {
matches().filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false })
playedMatches().filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false })
}
func finishedMatches() -> [Match] {
matches().filter({ $0.hasEnded() })
playedMatches().filter({ $0.hasEnded() })
}
private func _matchOrder() -> [Int] {
@ -195,7 +195,7 @@ class GroupStage: ModelObject, Storable {
(size * (size - 1)) / 2
}
private func _matches() -> [Match] {
func _matches() -> [Match] {
Store.main.filter { $0.groupStage == self.id }
}

@ -111,7 +111,6 @@ class Match: ModelObject, Storable {
}
let matchIndex = index
let seedRound = RoundRule.roundIndex(fromMatchIndex: matchIndex)
let position = matchIndex * 2 + teamPosition.rawValue
let teamScoreLuckyLoser = TeamScore(match: id, teamRegistration: team.id)
teamScoreLuckyLoser.luckyLoser = position
@ -149,6 +148,9 @@ class Match: ModelObject, Storable {
try? DataStore.shared.matches.addOrUpdate(instance: self)
}
func next() -> Match? {
Store.main.filter(isIncluded: { $0.round == round && $0.index == index + 1 }).first
}
func topPreviousRoundMatchIndex() -> Int {
index * 2 + 1
}
@ -187,6 +189,16 @@ class Match: ModelObject, Storable {
}
}
func upperMatches() -> [Match] {
guard let roundObject else { return [] }
return [roundObject.upperBracketTopMatch(ofMatchIndex: index), roundObject.upperBracketBottomMatch(ofMatchIndex: index)].compactMap({ $0 })
}
var computedOrder: Int {
guard let roundObject else { return index }
return roundObject.isLoserBracket() ? roundObject.index * 100 + indexInRound() : roundObject.index * 1000 + indexInRound()
}
func previousMatches() -> [Match] {
guard let roundObject else { return [] }
return Store.main.filter { match in
@ -327,11 +339,7 @@ class Match: ModelObject, Storable {
func isBracket() -> Bool {
round != nil
}
func isTournamentMatch() -> Bool {
groupStageObject?.tournament != nil
}
func walkoutTeam() -> [TeamRegistration] {
scores().filter({ $0.walkOut != nil }).compactMap { $0.team }
}

@ -17,6 +17,12 @@ extension Club {
}
}
extension GroupStage {
static func mock() -> GroupStage {
GroupStage(tournament: "", index: 0, size: 4)
}
}
extension Round {
static func mock() -> Round {
Round(tournament: "", index: 0)

@ -17,7 +17,8 @@ class Round: ModelObject, Storable {
var index: Int
var loser: String?
var format: Int?
var startDate: Date?
internal init(tournament: String, index: Int, loser: String? = nil, matchFormat: MatchFormat? = nil) {
self.tournament = tournament
self.index = index
@ -47,10 +48,39 @@ class Round: ModelObject, Storable {
Store.main.findById(tournament)
}
private func _matches() -> [Match] {
func _matches() -> [Match] {
Store.main.filter { $0.round == self.id }
}
func upperMatches(ofMatch match: Match) -> [Match] {
if loser != nil, previousRound() == nil, let parentRound {
let matchIndex = match.index
let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex)
return [parentRound.getMatch(atMatchIndexInRound: indexInRound * 2), parentRound.getMatch(atMatchIndexInRound: indexInRound * 2 + 1)].compactMap({ $0 })
}
return []
}
func previousMatches(ofMatch match: Match) -> [Match] {
guard let previousRound = previousRound() else { return [] }
return Store.main.filter {
($0.index == match.topPreviousRoundMatchIndex() || $0.index == match.bottomPreviousRoundMatchIndex()) && $0.round == previousRound.id
}
}
func precedentMatches(ofMatch match: Match) -> [Match] {
let upper = upperMatches(ofMatch: match)
if upper.isEmpty == false {
return upper
}
let previous : [Match] = previousMatches(ofMatch: match)
if previous.isEmpty == false && previous.allSatisfy({ $0.disabled }), let previousRound = previousRound() {
return previous.flatMap({ previousRound.precedentMatches(ofMatch: $0) })
} else {
return previous
}
}
func team(_ team: TeamPosition, inMatch match: Match) -> TeamRegistration? {
switch team {
case .one:
@ -150,10 +180,10 @@ class Round: ModelObject, Storable {
if loser == nil {
Store.main.filter { $0.round == self.id && $0.disabled == false }
} else {
Store.main.filter { $0.round == self.id }
_matches()
}
}
func previousRound() -> Round? {
Store.main.filter(isIncluded: { $0.tournament == tournament && $0.loser == loser && $0.index == index + 1 }).first
}
@ -245,7 +275,7 @@ class Round: ModelObject, Storable {
func roundTitle(_ displayStyle: DisplayStyle = .wide) -> String {
if let parentRound, let initialRound = parentRound.initialRound() {
let parentMatchCount = parentRound.cumulativeMatchCount - initialRound.playedMatches().count
print("initialRound", initialRound.roundTitle())
// print("initialRound", initialRound.roundTitle())
if let initialRoundNextRound = initialRound.nextRound()?.playedMatches() {
return SeedInterval(first: parentMatchCount + initialRoundNextRound.count * 2 + 1, last: parentMatchCount + initialRoundNextRound.count * 2 + (previousRound() ?? parentRound).playedMatches().count).localizedLabel(displayStyle)
}
@ -323,6 +353,7 @@ class Round: ModelObject, Storable {
case _index = "index"
case _loser = "loser"
case _format = "format"
case _startDate = "startDate"
}
}

@ -296,6 +296,12 @@ class Tournament : ModelObject, Storable {
return rounds.filter({ $0.hasStarted() && $0.hasEnded() == false }).sorted(by: \.index).reversed().first ?? rounds.first
}
func allMatches() -> [Match] {
let unsortedGroupStages : [GroupStage] = Store.main.filter { $0.tournament == self.id }
let matches: [Match] = unsortedGroupStages.flatMap { $0._matches() } + allRounds().flatMap { $0._matches() }
return matches.filter({ $0.disabled == false })
}
func allRounds() -> [Round] {
Store.main.filter { $0.tournament == self.id }
}
@ -578,10 +584,14 @@ class Tournament : ModelObject, Storable {
return true
}
return groupStages.allSatisfy({ $0.hasEnded() })
return qualifiedTeams().count == qualifiedFromGroupStage() + groupStageAdditionalQualified
//return qualifiedTeams().count == qualifiedFromGroupStage() + groupStageAdditionalQualified
}
func scheduleStatus() -> String {
//todo
return "todo"
}
func bracketStatus() -> String {
if let round = getActiveRound() {
return [round.roundTitle(), round.roundStatus()].joined(separator: " ")

@ -0,0 +1,251 @@
//
// MatchScheduler.swift
// PadelClub
//
// Created by Razmig Sarkissian on 08/04/2024.
//
import Foundation
struct TimeMatch {
let matchID: String
let rotationIndex: Int
var courtIndex: Int
let groupIndex: Int
}
struct MatchDispatcher {
let timedMatches: [TimeMatch]
let freeCourtPerRotation: [Int: [Int]]
let rotationCount: Int
let groupLastRotation: [Int: Int]
}
extension Match {
func teamIds() -> [String] {
return teams().map { $0.id }
}
func containsTeamId(_ id: String) -> Bool {
teamIds().contains(id)
}
}
class MatchScheduler {
static let shared = MatchScheduler()
func groupStageDispatcher(numberOfCourtsAvailablePerRotation: Int, groupStages: [GroupStage], startingDate: Date?, randomizeCourts: Bool) -> MatchDispatcher {
let _groupStages = groupStages.filter { startingDate == nil || $0.startDate == startingDate }
// Get the maximum count of matches in any group
let maxMatchesCount = _groupStages.map { $0._matches().count }.max() ?? 0
// Use zip and flatMap to flatten matches in the desired order
let flattenedMatches = (0..<maxMatchesCount).flatMap { index in
_groupStages.compactMap { group in
// Use optional subscript to safely access matches
let playedMatches = group.playedMatches()
return playedMatches.indices.contains(index) ? playedMatches[index] : nil
}
}
var slots = [TimeMatch]()
var availableMatchs = flattenedMatches
var rotationIndex = 0
var teamsPerRotation = [Int: [String]]()
var freeCourtPerRotation = [Int: [Int]]()
var groupLastRotation = [Int: Int]()
while slots.count < flattenedMatches.count {
teamsPerRotation[rotationIndex] = []
freeCourtPerRotation[rotationIndex] = []
let previousRotationBracketIndexes = slots.filter { $0.rotationIndex == rotationIndex - 1 }.map { ($0.groupIndex, 1) }
let counts = Dictionary(previousRotationBracketIndexes, uniquingKeysWith: +)
var rotationMatches = Array(availableMatchs.filter({ match in
teamsPerRotation[rotationIndex]!.allSatisfy({ match.containsTeamId($0) == false }) == true
}).prefix(numberOfCourtsAvailablePerRotation))
if rotationIndex > 0 {
rotationMatches = rotationMatches.sorted(by: {
if counts[$0.groupStageObject!.index] ?? 0 == counts[$1.groupStageObject!.index] ?? 0 {
return $0.groupStageObject!.index < $1.groupStageObject!.index
} else {
return counts[$0.groupStageObject!.index] ?? 0 < counts[$1.groupStageObject!.index] ?? 0
}
})
}
(0..<numberOfCourtsAvailablePerRotation).forEach { courtIndex in
//print(mt.map { ($0.bracket!.index.intValue, counts[$0.bracket!.index.intValue]) })
if let first = rotationMatches.first(where: { match in
teamsPerRotation[rotationIndex]!.allSatisfy({ match.containsTeamId($0) == false }) == true
}) {
slots.append(TimeMatch(matchID: first.id, rotationIndex: rotationIndex, courtIndex: courtIndex, groupIndex: first.groupStageObject!.index ))
teamsPerRotation[rotationIndex]!.append(contentsOf: first.teamIds())
rotationMatches.removeAll(where: { $0.id == first.id })
availableMatchs.removeAll(where: { $0.id == first.id })
if let index = first.groupStageObject?.index {
groupLastRotation[index] = rotationIndex
}
} else {
freeCourtPerRotation[rotationIndex]!.append(courtIndex)
}
}
rotationIndex += 1
}
var organizedSlots = [TimeMatch]()
for i in 0..<rotationIndex {
let courtsSorted = slots.filter({ $0.rotationIndex == i }).map { $0.courtIndex }.sorted()
let courts = randomizeCourts ? courtsSorted.shuffled() : courtsSorted
var matches = slots.filter({ $0.rotationIndex == i }).sorted(using: .keyPath(\.groupIndex), .keyPath(\.courtIndex))
for j in 0..<matches.count {
matches[j].courtIndex = courts[j]
organizedSlots.append(matches[j])
}
}
return MatchDispatcher(timedMatches: organizedSlots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex, groupLastRotation: groupLastRotation)
}
func roundMatchCanBePlayed(_ match: Match, roundObject: Round, slots: [TimeMatch], rotationIndex: Int) -> Bool {
print(roundObject.roundTitle(), match.matchTitle())
let previousMatches = roundObject.precedentMatches(ofMatch: match)
if previousMatches.isEmpty { return true }
let previousMatchSlots = slots.filter({ slot in
previousMatches.map { $0.id }.contains(slot.matchID)
})
if previousMatchSlots.isEmpty {
if previousMatches.filter({ $0.disabled == false }).allSatisfy({ $0.startDate != nil }) {
return true
}
return false
}
if previousMatches.filter({ $0.disabled == false }).count > previousMatchSlots.count {
if previousMatches.filter({ $0.disabled == false }).anySatisfy({ $0.startDate != nil }) {
return true
}
return false
}
let previousMatchIsInPreviousRotation = previousMatchSlots.allSatisfy({ $0.rotationIndex + (roundObject.loser == nil ? 1 : 0) < rotationIndex })
return previousMatchIsInPreviousRotation
}
func roundDispatcher(numberOfCourtsAvailablePerRotation: Int, flattenedMatches: [Match], randomizeCourts: Bool, initialOccupiedCourt: Int = 0) -> MatchDispatcher {
var slots = [TimeMatch]()
var availableMatchs = flattenedMatches
var rotationIndex = 0
var freeCourtPerRotation = [Int: [Int]]()
var groupLastRotation = [Int: Int]()
while slots.count < flattenedMatches.count {
freeCourtPerRotation[rotationIndex] = []
var matchPerRound = [Int: Int]()
var availableCourt = numberOfCourtsAvailablePerRotation
if rotationIndex == 0 {
availableCourt = availableCourt - initialOccupiedCourt
}
(0..<availableCourt).forEach { courtIndex in
//print(mt.map { ($0.bracket!.index.intValue, counts[$0.bracket!.index.intValue]) })
if let first = availableMatchs.first(where: { match in
let roundObject = match.roundObject!
let canBePlayed = roundMatchCanBePlayed(match, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex)
if roundObject.loser == nil && roundObject.index > 0, match.indexInRound() == 0, numberOfCourtsAvailablePerRotation > 1, let nextMatch = match.next() {
if canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex) {
return true
} else {
return false
}
}
if (matchPerRound[roundObject.index] ?? 0)%2 == 0 && roundObject.index != 0 && roundObject.loser == nil && courtIndex == numberOfCourtsAvailablePerRotation - 1 {
return false
}
return canBePlayed
}) {
//print(first.roundObject!.roundTitle(), first.matchTitle())
if first.roundObject!.loser == nil {
if let roundIndex = matchPerRound[first.roundObject!.index] {
matchPerRound[first.roundObject!.index] = roundIndex + 1
} else {
matchPerRound[first.roundObject!.index] = 1
}
}
slots.append(TimeMatch(matchID: first.id, rotationIndex: rotationIndex, courtIndex: courtIndex, groupIndex: first.roundObject!.index ))
availableMatchs.removeAll(where: { $0.id == first.id })
if let index = first.roundObject?.index {
groupLastRotation[index] = rotationIndex
}
} else {
freeCourtPerRotation[rotationIndex]!.append(courtIndex)
}
}
rotationIndex += 1
}
var organizedSlots = [TimeMatch]()
for i in 0..<rotationIndex {
let courtsSorted = slots.filter({ $0.rotationIndex == i }).map { $0.courtIndex }.sorted()
let courts = randomizeCourts ? courtsSorted.shuffled() : courtsSorted
var matches = slots.filter({ $0.rotationIndex == i }).sorted(using: .keyPath(\.groupIndex), .keyPath(\.courtIndex))
for j in 0..<matches.count {
matches[j].courtIndex = courts[j]
organizedSlots.append(matches[j])
}
}
return MatchDispatcher(timedMatches: organizedSlots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex, groupLastRotation: groupLastRotation)
}
func updateSchedule(tournament: Tournament, fromRoundId roundId: String?, fromMatchId matchId: String?, randomizeCourts: Bool, startDate: Date) {
let upperRounds = tournament.rounds()
var roundIndex = 0
if let roundId {
roundIndex = upperRounds.firstIndex(where: { $0.id == roundId }) ?? 0
}
let rounds = upperRounds.flatMap {
[$0] + $0.loserRoundsAndChildren()
}
var flattenedMatches = rounds[roundIndex...].flatMap { round in
round._matches().filter({ $0.disabled == false }).sorted(by: \.index)
}
if let matchId, let matchIndex = flattenedMatches.firstIndex(where: { $0.id == matchId }) {
flattenedMatches = Array(flattenedMatches[matchIndex...])
}
flattenedMatches.forEach({ $0.startDate = nil })
let roundDispatch = self.roundDispatcher(numberOfCourtsAvailablePerRotation: tournament.courtCount, flattenedMatches: flattenedMatches, randomizeCourts: randomizeCourts, initialOccupiedCourt: 0)
roundDispatch.timedMatches.forEach { matchSchedule in
if let match = flattenedMatches.first(where: { $0.id == matchSchedule.matchID }) {
let timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(match.matchFormat.estimatedDuration) * 60
match.startDate = startDate.addingTimeInterval(timeIntervalToAdd)
match.setCourt(matchSchedule.courtIndex + 1)
}
}
try? DataStore.shared.matches.addOrUpdate(contentOfs: flattenedMatches)
}
}

@ -122,7 +122,7 @@ struct GroupStageView: View {
private func _groupStageMenuView() -> some View {
Menu {
if groupStage.matches().isEmpty {
if groupStage.playedMatches().isEmpty {
Button {
//groupStage.startGroupStage()
//save()

@ -153,18 +153,14 @@ struct MatchDetailView: View {
MatchSummaryView(match: match, matchViewStyle: .plainStyle)
} header: {
} footer: {
HStack {
if match.isTournamentMatch() {
if match.isEmpty() == false {
Button {
showDetails = true
} label: {
Text("Détails des joueurs")
}
if match.isEmpty() == false {
HStack {
Button {
showDetails = true
} label: {
Text("Détails des joueurs")
}
}
Spacer()
if match.isEmpty() == false && match.isTournamentMatch() {
Spacer()
Menu {
//MenuWarnView(warningSender: match)
} label: {
@ -204,7 +200,7 @@ struct MatchDetailView: View {
// .presentationDetents([.fraction(0.66)])
// }
.sheet(item: $scoreType, onDismiss: {
if match.hasEnded() && match.isTournamentMatch() {
if match.hasEnded() {
dismiss()
}
}) { scoreType in
@ -361,11 +357,7 @@ struct MatchDetailView: View {
DisclosureGroup(isExpanded: $isEditing) {
startingOptionView
} label: {
if match.isTournamentMatch() {
Text("Modifier l'horaire et le terrain")
} else {
Text("Horaires et terrain")
}
Text("Modifier l'horaire et le terrain")
}
}

@ -60,6 +60,8 @@ struct MatchSummaryView: View {
if match.isGroupStage() && matchViewStyle != .feedStyle {
if let groupStage = match.groupStageObject, matchViewStyle == .standardStyle {
Text(groupStage.groupStageTitle())
} else if let round = match.roundObject {
Text(round.roundTitle())
}
Text(match.matchTitle())
} else if let currentTournament = match.currentTournament() {

@ -0,0 +1,48 @@
//
// GroupStageScheduleEditorView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 07/04/2024.
//
import SwiftUI
struct GroupStageScheduleEditorView: View {
@EnvironmentObject var dataStore: DataStore
var groupStage: GroupStage
var body: some View {
@Bindable var groupStage = groupStage
List {
Section {
MatchFormatPickerView(headerLabel: "Format", matchFormat: $groupStage.matchFormat)
}
Section {
Text("Modifier l'horaire")
}
RowButtonView("Convoquer") {
}
NavigationLink {
GroupStageView(groupStage: groupStage)
} label: {
Text("Voir la poule")
}
}
.onChange(of: groupStage.matchFormat) {
_save()
}
}
private func _save() {
try? dataStore.groupStages.addOrUpdate(instance: groupStage)
}
}
#Preview {
GroupStageScheduleEditorView(groupStage: GroupStage.mock())
}

@ -0,0 +1,40 @@
//
// MatchScheduleEditorView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 10/04/2024.
//
import SwiftUI
struct MatchScheduleEditorView: View {
@Environment(Tournament.self) var tournament: Tournament
var match: Match
@State private var startDate: Date
init(match: Match) {
self.match = match
self._startDate = State(wrappedValue: match.startDate ?? Date())
}
var body: some View {
Section {
DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday()))
}
RowButtonView("Modifier") {
_updateSchedule()
}
} header: {
Text(match.matchTitle())
}
}
private func _updateSchedule() {
MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: match.round, fromMatchId: match.id, randomizeCourts: true, startDate: startDate)
}
}
#Preview {
MatchScheduleEditorView(match: Match.mock())
}

@ -0,0 +1,159 @@
//
// PlanningSettingsView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 07/04/2024.
//
import SwiftUI
struct PlanningSettingsView: View {
@EnvironmentObject var dataStore: DataStore
var tournament: Tournament
@State private var scheduleSetup: Bool = false
@State private var randomCourtDistribution: Bool = false
@State private var groupStageCourtCount: Int
init(tournament: Tournament) {
self.tournament = tournament
self._groupStageCourtCount = State(wrappedValue: tournament.groupStageCourtCount ?? 1)
}
var body: some View {
@Bindable var tournament = tournament
List {
Section {
DatePicker(tournament.startDate.formatted(.dateTime.weekday()), selection: $tournament.startDate)
Stepper(value: $tournament.dayDuration, in: 1...1_000) {
HStack {
Text("Durée")
Spacer()
Text("\(tournament.dayDuration) jour" + tournament.dayDuration.pluralSuffix)
}
}
} header: {
Text("Démarrage et durée du tournoi")
} footer: {
Text("todo: Expliquer ce que ca fait")
}
Section {
TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount)
TournamentFieldsManagerView(localizedStringKey: "Terrains par poule", count: $groupStageCourtCount)
NavigationLink {
} label: {
Text("Disponibilité des terrains")
}
}
Section {
Toggle(isOn: $randomCourtDistribution) {
Text("Distribuer les terrains au hasard")
}
RowButtonView("Horaire intelligent", role: .destructive) {
let groupStageCourtCount = tournament.groupStageCourtCount ?? 1
let groupStages = tournament.groupStages()
let numberOfCourtsAvailablePerRotation: Int = min(tournament.courtCount, groupStageCourtCount * groupStages.count)
let matchScheduler = MatchScheduler.shared
let matches = tournament.groupStages().flatMap({ $0._matches() })
matches.forEach({ $0.startDate = nil })
var times = Set(groupStages.compactMap { $0.startDate })
if times.isEmpty {
groupStages.forEach({ $0.startDate = tournament.startDate })
times.insert(tournament.startDate)
try? dataStore.groupStages.addOrUpdate(contentOfs: groupStages)
}
var lastDate : Date? = nil
times.forEach { time in
let dispatch = matchScheduler.groupStageDispatcher(numberOfCourtsAvailablePerRotation: numberOfCourtsAvailablePerRotation, groupStages: groupStages, startingDate: time, randomizeCourts: randomCourtDistribution)
dispatch.timedMatches.forEach { matchSchedule in
if let match = matches.first(where: { $0.id == matchSchedule.matchID }) {
let timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(match.matchFormat.estimatedDuration) * 60
if let startDate = match.groupStageObject?.startDate {
match.startDate = startDate.addingTimeInterval(timeIntervalToAdd)
lastDate = match.startDate?.addingTimeInterval(Double(match.matchFormat.estimatedDuration) * 60)
}
match.setCourt(matchSchedule.courtIndex + 1)
}
}
}
try? dataStore.matches.addOrUpdate(contentOfs: matches)
let upperRounds = tournament.rounds()
let rounds = upperRounds.flatMap {
[$0] + $0.loserRoundsAndChildren()
}
let flattenedMatches = rounds.flatMap { round in
round._matches().filter({ $0.disabled == false }).sorted(by: \.index)
}
flattenedMatches.forEach({ $0.startDate = nil })
let roundDispatch = matchScheduler.roundDispatcher(numberOfCourtsAvailablePerRotation: tournament.courtCount, flattenedMatches: flattenedMatches, randomizeCourts: randomCourtDistribution)
roundDispatch.timedMatches.forEach { matchSchedule in
if let match = flattenedMatches.first(where: { $0.id == matchSchedule.matchID }) {
let timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(match.matchFormat.estimatedDuration) * 60
if let lastDate {
match.startDate = lastDate.addingTimeInterval(timeIntervalToAdd)
}
match.setCourt(matchSchedule.courtIndex + 1)
}
}
try? dataStore.matches.addOrUpdate(contentOfs: flattenedMatches)
scheduleSetup = true
}
if scheduleSetup {
HStack {
Image(systemName: "checkmark")
}
}
}
Section {
NavigationLink {
} label: {
Text("Modifier le message de convocation")
}
}
}
.onChange(of: groupStageCourtCount) {
tournament.groupStageCourtCount = groupStageCourtCount
_save()
}
.onChange(of: tournament.startDate) {
_save()
}
.onChange(of: tournament.courtCount) {
_save()
}
.onChange(of: tournament.groupStageCourtCount) {
_save()
}
.onChange(of: tournament.dayDuration) {
_save()
}
}
private func _save() {
try? dataStore.tournaments.addOrUpdate(instance: tournament)
}
}
#Preview {
PlanningSettingsView(tournament: Tournament.mock())
}

@ -0,0 +1,149 @@
//
// PlanningView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 07/04/2024.
//
import SwiftUI
struct PlanningView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(\.editMode) private var editMode
let matches: [Match]
@State private var timeSlots: [Date:[Match]]
@State private var days: [Date]
@State private var keys: [Date]
init(matches: [Match]) {
self.matches = matches
let timeSlots = Dictionary(grouping: matches) { $0.startDate ?? .distantFuture }
_timeSlots = State(wrappedValue: timeSlots)
_days = State(wrappedValue: Set(timeSlots.keys.map { $0.startOfDay }).sorted())
_keys = State(wrappedValue: timeSlots.keys.sorted())
}
var body: some View {
List {
ForEach(days, id: \.self) { day in
Section {
ForEach(keys.filter({ $0.dayInt == day.dayInt }), id: \.self) { key in
if let _matches = timeSlots[key] {
if editMode?.wrappedValue.isEditing == true {
HStack {
VStack(alignment: .leading) {
let index = keys.firstIndex(of: key)
Button {
let previousKey = keys[index! - 1]
let previousMatches = timeSlots[previousKey]
previousMatches?.forEach { match in
match.startDate = key
}
_matches.forEach { match in
match.startDate = previousKey
}
_update()
} label: {
Image(systemName: "arrow.up")
}
.buttonStyle(.bordered)
.disabled(index == 0)
Button {
let nextKey = keys[index! + 1]
let nextMatches = timeSlots[nextKey]
nextMatches?.forEach { match in
match.startDate = key
}
_matches.forEach { match in
match.startDate = nextKey
}
_update()
} label: {
Image(systemName: "arrow.down")
}
.buttonStyle(.bordered)
.disabled(index == keys.count - 1)
}
VStack(alignment: .leading) {
LabeledContent {
Text(_matches.count.formatted() + " match" + _matches.count.pluralSuffix)
} label: {
Text(key.formatted(date: .omitted, time: .shortened)).font(.largeTitle)
}
ForEach(_matches) { match in
LabeledContent {
} label: {
if let groupStage = match.groupStageObject {
Text(groupStage.groupStageTitle())
} else if let round = match.roundObject {
Text(round.roundTitle())
}
Text(match.matchTitle())
}
}
}
}
} else {
DisclosureGroup {
ForEach(_matches) { match in
NavigationLink {
MatchDetailView(match: match, matchViewStyle: .sectionedStandardStyle)
} label: {
if let groupStage = match.groupStageObject {
Text(groupStage.groupStageTitle())
} else if let round = match.roundObject {
Text(round.roundTitle())
}
Text(match.matchTitle())
}
}
} label: {
_timeSlotView(key: key, matches: _matches)
}
}
}
}
} header: {
Text(day.formatted(.dateTime.day().weekday().month().year()))
}
.headerProminence(.increased)
}
}
.toolbar {
EditButton()
}
.onChange(of: isEditing) { old, new in
if old == true && new == false {
print("save")
try? dataStore.matches.addOrUpdate(contentOfs: matches)
}
}
.navigationTitle("Programmation")
}
private func _update() {
let timeSlots = Dictionary(grouping: matches) { $0.startDate ?? .distantFuture }
self.timeSlots = timeSlots
self.days = Set(timeSlots.keys.map { $0.startOfDay }).sorted()
self.keys = timeSlots.keys.sorted()
}
private var isEditing: Bool {
editMode?.wrappedValue.isEditing == true
}
private func _timeSlotView(key: Date, matches: [Match]) -> some View {
LabeledContent {
Text(matches.count.formatted() + " match" + matches.count.pluralSuffix)
} label: {
Text(key.formatted(date: .omitted, time: .shortened)).font(.largeTitle)
}
}
}
#Preview {
PlanningView(matches: [])
}

@ -0,0 +1,58 @@
//
// RoundScheduleEditorView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 07/04/2024.
//
import SwiftUI
struct RoundScheduleEditorView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
var round: Round
@State private var startDate: Date
init(round: Round) {
self.round = round
self._startDate = State(wrappedValue: round.startDate ?? round.playedMatches().first?.startDate ?? Date())
}
var body: some View {
@Bindable var round = round
List {
Section {
MatchFormatPickerView(headerLabel: "Format", matchFormat: $round.matchFormat)
}
Section {
DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday()))
}
RowButtonView("Modifier") {
_updateSchedule()
}
}
ForEach(round.playedMatches()) { match in
MatchScheduleEditorView(match: match)
}
}
.onChange(of: round.matchFormat) {
_save()
}
}
private func _updateSchedule() {
MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, randomizeCourts: true, startDate: startDate)
}
private func _save() {
try? dataStore.rounds.addOrUpdate(instance: round)
}
}
#Preview {
RoundScheduleEditorView(round: Round.mock())
}

@ -0,0 +1,66 @@
//
// SchedulerView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 07/04/2024.
//
import SwiftUI
extension GroupStage: Schedulable {}
extension Round: Schedulable {}
struct SchedulerView: View {
var tournament: Tournament
var body: some View {
List {
ForEach(tournament.groupStages()) {
_schedulerView($0)
}
ForEach(tournament.rounds()) { round in
_schedulerView(round)
ForEach(round.loserRoundsAndChildren()) { loserRound in
if round.isDisabled() == false {
_schedulerView(loserRound)
}
}
}
}
.headerProminence(.increased)
}
func _schedulerView(_ schedulable: any Schedulable) -> some View {
Section {
NavigationLink {
Group {
if let round = schedulable as? Round {
RoundScheduleEditorView(round: round)
.environment(tournament)
}
else if let groupStage = schedulable as? GroupStage {
GroupStageScheduleEditorView(groupStage: groupStage)
}
}
.navigationTitle(schedulable.selectionLabel())
} label: {
LabeledContent {
Text(schedulable.matchFormat.format).font(.largeTitle)
} label: {
if let startDate = schedulable.startDate {
Text(startDate.formatted(.dateTime.hour().minute())).font(.largeTitle)
Text(startDate.formatted(.dateTime.weekday().day(.twoDigits).month().year()))
} else {
Text("Aucun horaire")
}
}
}
} header: {
Text(schedulable.selectionLabel())
}
}
}
#Preview {
SchedulerView(tournament: Tournament.mock())
}

@ -8,21 +8,21 @@
import SwiftUI
struct TournamentFieldsManagerView: View {
@Environment(Tournament.self) private var tournament: Tournament
let localizedStringKey: String
@Binding var count: Int
var body: some View {
@Bindable var tournament = tournament
Stepper(value: $tournament.courtCount, in: 1...100_000) {
Stepper(value: $count, in: 1...1_000) {
LabeledContent {
Text(tournament.courtCount.formatted())
Text(count.formatted())
} label: {
Text("Nombre de terrains")
Text(localizedStringKey)
}
}
}}
}
}
#Preview {
TournamentFieldsManagerView()
TournamentFieldsManagerView(localizedStringKey: "test", count: .constant(2))
.environment(Tournament.mock())
}

@ -13,4 +13,5 @@ enum Screen: String, Codable {
case round
case settings
case structure
case schedule
}

@ -0,0 +1,91 @@
//
// TournamentScheduleView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 07/04/2024.
//
import SwiftUI
protocol Schedulable: Selectable, Identifiable {
var startDate: Date? { get set }
var matchFormat: MatchFormat { get set }
func playedMatches() -> [Match]
}
enum ScheduleDestination: Identifiable, Selectable {
var id: String {
switch self {
case .groupStage(let groupStage):
return groupStage.id
case .round(let round):
return round.id
default:
return String(describing: self)
}
}
case planning
case schedule
case groupStage(GroupStage)
case round(Round)
func selectionLabel() -> String {
switch self {
case .schedule:
return "Horaires"
case .planning:
return "Progr."
case .groupStage(let groupStage):
return groupStage.selectionLabel()
case .round(let round):
return round.selectionLabel()
}
}
func badgeValue() -> Int? {
nil
}
}
struct TournamentScheduleView: View {
var tournament: Tournament
@State private var selectedScheduleDestination: ScheduleDestination? = nil
let allDestinations: [ScheduleDestination]
let verticalDestinations: [ScheduleDestination]
init(tournament: Tournament) {
self.tournament = tournament
self.verticalDestinations = tournament.groupStages().map({ .groupStage($0) }) + tournament.rounds().map({ .round($0) })
self.allDestinations = [.schedule, .planning]
}
var body: some View {
VStack(spacing: 0) {
GenericDestinationPickerView(selectedDestination: $selectedScheduleDestination, destinations: allDestinations, nilDestinationIsValid: true)
switch selectedScheduleDestination {
case .none:
PlanningSettingsView(tournament: tournament)
.navigationTitle("Réglages")
case .some(let selectedSchedule):
switch selectedSchedule {
case .groupStage(let groupStage):
Text("ok")
case .round(let round):
Text("ok")
case .planning:
PlanningView(matches: tournament.allMatches())
case .schedule:
SchedulerView(tournament: tournament)
}
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Horaires")
}
}
#Preview {
TournamentScheduleView(tournament: Tournament.mock())
}

@ -44,7 +44,7 @@ struct TournamentSettingsView: View {
TournamentLevelPickerView()
TournamentDurationManagerView()
TournamentFieldsManagerView()
TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount)
TournamentDatePickerView()

@ -12,6 +12,16 @@ struct TournamentRunningView: View {
@ViewBuilder
var body: some View {
Section {
NavigationLink(value: Screen.schedule) {
LabeledContent {
Text(tournament.scheduleStatus())
} label: {
Text("Horaires")
}
}
}
Section {
NavigationLink(value: Screen.groupStage) {
LabeledContent {

@ -21,15 +21,15 @@ struct TournamentView: View {
var body: some View {
List {
if tournament.missingUnrankedValue() {
Button("update NC") {
tournament.femaleUnrankedValue = SourceFileManager.shared.getUnrankValue(forMale: false, rankSourceDate: tournament.rankSourceDate)
tournament.maleUnrankedValue = SourceFileManager.shared.getUnrankValue(forMale: true, rankSourceDate: tournament.rankSourceDate)
try? dataStore.tournaments.addOrUpdate(instance: tournament)
}
}
// if tournament.missingUnrankedValue() {
// Button("update NC") {
// tournament.femaleUnrankedValue = SourceFileManager.shared.getUnrankValue(forMale: false, rankSourceDate: tournament.rankSourceDate)
// tournament.maleUnrankedValue = SourceFileManager.shared.getUnrankValue(forMale: true, rankSourceDate: tournament.rankSourceDate)
// try? dataStore.tournaments.addOrUpdate(instance: tournament)
// }
// }
//
//
Section {
NavigationLink(value: Screen.inscription) {
LabeledContent {
@ -78,6 +78,8 @@ struct TournamentView: View {
GroupStagesView(tournament: tournament)
case .round:
RoundsView(tournament: tournament)
case .schedule:
TournamentScheduleView(tournament: tournament)
}
}
.environment(tournament)

Loading…
Cancel
Save