compil
Laurent 1 year ago
commit 55b93f94a8
  1. 12
      PadelClub.xcodeproj/project.pbxproj
  2. 8
      PadelClub/Data/MatchScheduler.swift
  3. 6
      PadelClub/ViewModel/SeedInterval.swift
  4. 2
      PadelClub/Views/Match/Components/MatchDateView.swift
  5. 7
      PadelClub/Views/Match/MatchSummaryView.swift
  6. 144
      PadelClub/Views/Planning/PlanningByCourtView.swift
  7. 45
      PadelClub/Views/Planning/PlanningSettingsView.swift
  8. 35
      PadelClub/Views/Planning/PlanningView.swift
  9. 11
      PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift

@ -107,6 +107,7 @@
FF1F4B8A2BFA02A4000B4573 /* groupstage-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B772BFA0105000B4573 /* groupstage-template.html */; };
FF1F4B8B2BFA02A4000B4573 /* groupstageentrant-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B792BFA0105000B4573 /* groupstageentrant-template.html */; };
FF1F4B8C2BFA02A4000B4573 /* match-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B7D2BFA0105000B4573 /* match-template.html */; };
FF2B51552C7A4DAF00FFF126 /* PlanningByCourtView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2B51542C7A4DAF00FFF126 /* PlanningByCourtView.swift */; };
FF2B6F5E2C036A1500835EE7 /* EventLinksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2B6F5D2C036A1400835EE7 /* EventLinksView.swift */; };
FF2EFBF02BDE295E0049CE3B /* SendToAllView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2EFBEF2BDE295E0049CE3B /* SendToAllView.swift */; };
FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = FF3795602B9396D0004EA093 /* PadelClubApp.xcdatamodeld */; };
@ -451,6 +452,7 @@
FF1F4B7E2BFA0105000B4573 /* player-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "player-template.html"; sourceTree = "<group>"; };
FF1F4B7F2BFA0105000B4573 /* tournament-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "tournament-template.html"; sourceTree = "<group>"; };
FF1F4B812BFA0124000B4573 /* PrintSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrintSettingsView.swift; sourceTree = "<group>"; };
FF2B51542C7A4DAF00FFF126 /* PlanningByCourtView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlanningByCourtView.swift; sourceTree = "<group>"; };
FF2B6F5D2C036A1400835EE7 /* EventLinksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventLinksView.swift; sourceTree = "<group>"; };
FF2EFBEF2BDE295E0049CE3B /* SendToAllView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendToAllView.swift; sourceTree = "<group>"; };
FF3795612B9396D0004EA093 /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = "<group>"; };
@ -1367,6 +1369,7 @@
isa = PBXGroup;
children = (
FFF9644F2BC25E3700EEF017 /* PlanningView.swift */,
FF2B51542C7A4DAF00FFF126 /* PlanningByCourtView.swift */,
FFF964522BC262B000EEF017 /* PlanningSettingsView.swift */,
FFF964542BC266CF00EEF017 /* SchedulerView.swift */,
FFF964562BC26B3400EEF017 /* RoundScheduleEditorView.swift */,
@ -1675,6 +1678,7 @@
FFC2DCB22BBE75D40046DB9F /* LoserRoundView.swift in Sources */,
FF90FC1D2C44FB3E009339B2 /* AddTeamView.swift in Sources */,
FF9267FC2BCE84870080F940 /* PlayerPayView.swift in Sources */,
FF2B51552C7A4DAF00FFF126 /* PlanningByCourtView.swift in Sources */,
FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */,
FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */,
FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */,
@ -1951,7 +1955,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 2;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
@ -1976,7 +1980,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.2;
MARKETING_VERSION = 1.0.3;
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
OTHER_SWIFT_FLAGS = "-Onone";
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
@ -2001,7 +2005,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 2;
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -2024,7 +2028,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.2;
MARKETING_VERSION = 1.0.3;
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-function-bodies=5 -Xfrontend -warn-long-expression-type-checking=20 -Xfrontend -warn-long-function-bodies=50";
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;

@ -30,6 +30,7 @@ final class MatchScheduler : ModelObject, Storable {
var shouldEndRoundBeforeStartingNext: Bool
var groupStageChunkCount: Int?
var overrideCourtsUnavailability: Bool = false
var shouldTryToFillUpCourtsAvailable: Bool = false
init(tournament: String,
timeDifferenceLimit: Int = 5,
@ -41,7 +42,7 @@ final class MatchScheduler : ModelObject, Storable {
rotationDifferenceIsImportant: Bool = false,
shouldHandleUpperRoundSlice: Bool = true,
shouldEndRoundBeforeStartingNext: Bool = true,
groupStageChunkCount: Int? = nil, overrideCourtsUnavailability: Bool = false) {
groupStageChunkCount: Int? = nil, overrideCourtsUnavailability: Bool = false, shouldTryToFillUpCourtsAvailable: Bool = false) {
self.tournament = tournament
self.timeDifferenceLimit = timeDifferenceLimit
self.loserBracketRotationDifference = loserBracketRotationDifference
@ -54,6 +55,7 @@ final class MatchScheduler : ModelObject, Storable {
self.shouldEndRoundBeforeStartingNext = shouldEndRoundBeforeStartingNext
self.groupStageChunkCount = groupStageChunkCount
self.overrideCourtsUnavailability = overrideCourtsUnavailability
self.shouldTryToFillUpCourtsAvailable = shouldTryToFillUpCourtsAvailable
}
enum CodingKeys: String, CodingKey {
@ -70,6 +72,7 @@ final class MatchScheduler : ModelObject, Storable {
case _shouldEndRoundBeforeStartingNext = "shouldEndRoundBeforeStartingNext"
case _groupStageChunkCount = "groupStageChunkCount"
case _overrideCourtsUnavailability = "overrideCourtsUnavailability"
case _shouldTryToFillUpCourtsAvailable = "shouldTryToFillUpCourtsAvailable"
}
var courtsUnavailability: [DateInterval]? {
@ -521,10 +524,13 @@ final class MatchScheduler : ModelObject, Storable {
//not adding a last match of a 4-match round (final not included obviously)
print("\(currentRotationSameRoundMatches) modulo \(currentRotationSameRoundMatches%2) same round match is even, index of round is not 0 and upper bracket. If it's not the last court available \(courtIndex) == \(courts.count - 1)")
if shouldTryToFillUpCourtsAvailable == false {
if roundMatchesCount <= 4 && currentRotationSameRoundMatches%2 == 0 && roundObject.index != 0 && roundObject.parent == nil && ((courts.count > 1 && courtPosition >= courts.count - 1) || courts.count == 1 && availableCourts > 1) {
print("we return false")
return false
}
}
return canBePlayed

@ -51,10 +51,10 @@ struct SeedInterval: Hashable, Comparable {
}
func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
if dimension < 2 {
return "#\(first) / #\(last)"
if dimension < 3 {
return "\(first)\(first.ordinalFormattedSuffix()) place"
} else {
return "#\(first) à #\(last)"
return "Place \(first) à \(last)"
}
}
}

@ -93,7 +93,7 @@ struct MatchDateView: View {
.foregroundStyle(Color.master)
.underline()
} else {
Text("en retard")
Text("en attente")
.foregroundStyle(Color.master)
.underline()
}

@ -29,7 +29,7 @@ struct MatchSummaryView: View {
if let groupStage = match.groupStageObject {
self.roundTitle = groupStage.groupStageTitle()
} else if let round = match.roundObject {
self.roundTitle = round.roundTitle(.short)
self.roundTitle = round.roundTitle(matchViewStyle == .feedStyle ? .wide : .short)
} else {
self.roundTitle = nil
}
@ -46,9 +46,6 @@ struct MatchSummaryView: View {
var body: some View {
VStack(alignment: .leading) {
if matchViewStyle != .plainStyle {
if matchViewStyle == .feedStyle, let tournament = match.currentTournament() {
Text(tournament.tournamentTitle())
}
HStack {
if matchViewStyle != .sectionedStandardStyle {
if let roundTitle {
@ -59,7 +56,7 @@ struct MatchSummaryView: View {
}
}
Spacer()
if let courtName {
if let courtName, matchViewStyle != .feedStyle {
Spacer()
Text(courtName)
.foregroundStyle(.gray)

@ -0,0 +1,144 @@
//
// PlanningByCourtView.swift
// PadelClub
//
// Created by razmig on 24/08/2024.
//
import SwiftUI
struct PlanningByCourtView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
let matches: [Match]
@Binding var selectedScheduleDestination: ScheduleDestination?
@State private var timeSlots: [Date:[Match]]
@State private var days: [Date]
@State private var keys: [Date]
@State private var courts: [Int]
@State private var viewByCourt: Bool = false
@State private var courtSlots: [Int:[Match]]
@State private var selectedDay: Date
@State private var selectedCourt: Int = 0
init(matches: [Match], selectedScheduleDestination: Binding<ScheduleDestination?>, startDate: Date) {
self.matches = matches
_selectedScheduleDestination = selectedScheduleDestination
let timeSlots = Dictionary(grouping: matches) { $0.startDate ?? .distantFuture }
let courtSlots = Dictionary(grouping: matches) { $0.courtIndex ?? Int.max}
_timeSlots = State(wrappedValue: timeSlots)
_courtSlots = State(wrappedValue: courtSlots)
_days = State(wrappedValue: Set(timeSlots.keys.map { $0.startOfDay }).sorted())
_keys = State(wrappedValue: timeSlots.keys.sorted())
_courts = State(wrappedValue: courtSlots.keys.sorted())
_selectedDay = State(wrappedValue: startDate)
}
var body: some View {
List {
_byCourtView()
}
.overlay {
if matches.allSatisfy({ $0.startDate == nil }) {
ContentUnavailableView {
Label("Aucun horaire défini", systemImage: "clock.badge.questionmark")
} description: {
Text("Vous n'avez pas encore défini d'horaire pour les différentes phases du tournoi")
} actions: {
RowButtonView("Horaire intelligent") {
selectedScheduleDestination = nil
}
.padding(.horizontal)
}
}
}
.navigationTitle(Text(selectedDay.formatted(.dateTime.day().weekday().month())))
.toolbar {
if days.count > 1 {
ToolbarTitleMenu {
Picker(selection: $selectedDay) {
ForEach(days, id: \.self) { day in
Text(day.formatted(.dateTime.day().weekday().month())).tag(day as Date?)
}
} label: {
Text("Jour")
}
.pickerStyle(.automatic)
}
}
ToolbarItemGroup(placement: .topBarTrailing) {
if courts.count > 1 {
Picker(selection: $selectedCourt) {
ForEach(courts, id: \.self) { courtIndex in
if courtIndex == Int.max {
Image(systemName: "rectangle.slash").tag(Int.max)
} else {
Text(tournament.courtName(atIndex: courtIndex)).tag(courtIndex)
}
}
} label: {
Text("Terrain")
}
.pickerStyle(.automatic)
}
}
}
}
@ViewBuilder
func _byCourtView() -> some View {
if let _matches = courtSlots[selectedCourt]?.filter({ $0.startDate?.dayInt == selectedDay.dayInt }) {
let _sortedMatches = _matches.sorted(by: \.computedStartDateForSorting)
ForEach(_sortedMatches.indices, id: \.self) { index in
let match = _sortedMatches[index]
Section {
MatchRowView(match: match, matchViewStyle: .feedStyle)
} header: {
if let startDate = match.startDate {
if index > 0 {
if match.confirmed {
Text("Pas avant \(startDate.formatted(date: .omitted, time: .shortened))")
} else {
Text("Suivi de")
}
} else {
Text("Démarrage à \(startDate.formatted(date: .omitted, time: .shortened))")
}
} else {
Text("Aucun horaire")
}
}
.headerProminence(.increased)
}
} else if courtSlots.isEmpty == false {
ContentUnavailableView {
Label("Aucun match plannifié", systemImage: "clock.badge.questionmark")
} description: {
Text("Aucun match n'a été plannifié sur ce terrain et au jour sélectionné")
} actions: {
}
}
}
private func _matchesCount(inDayInt dayInt: Int) -> Int {
timeSlots.filter { $0.key.dayInt == dayInt }.flatMap({ $0.value }).count
}
private func _timeSlotView(key: Date, matches: [Match]) -> some View {
LabeledContent {
Text(self._formattedMatchCount(matches.count))
} label: {
Text(key.formatted(date: .omitted, time: .shortened)).font(.title).fontWeight(.semibold)
Text(Set(matches.compactMap { $0.roundTitle() }).joined(separator: ", "))
}
}
fileprivate func _formattedMatchCount(_ count: Int) -> String {
return "\(count.formatted()) match\(count.pluralSuffix)"
}
}

@ -248,18 +248,36 @@ struct PlanningSettingsView: View {
}
Section {
Toggle(isOn: $matchScheduler.overrideCourtsUnavailability) {
Text("Ne pas tenir compte des autres tournois")
}
Toggle(isOn: $matchScheduler.randomizeCourts) {
Text("Distribuer les terrains au hasard")
}
}
Section {
Toggle(isOn: $matchScheduler.shouldTryToFillUpCourtsAvailable) {
Text("Remplir au maximum les terrains d'une rotation")
}
} footer: {
Text("Tout en tenant compte de l'option ci-dessous, Padel Club essaiera de remplir les créneaux à chaque rotation.")
}
Section {
Toggle(isOn: $matchScheduler.shouldHandleUpperRoundSlice) {
Text("Équilibrer les matchs d'une manche sur plusieurs tours")
Text("Équilibrer les matchs d'une manche")
}
} footer: {
Text("Cette option permet de programmer une manche sur plusieurs rotation de manière équilibrée dans le cas où il y a plus de matchs à jouer dans cette manche que de terrains.")
}
Section {
Toggle(isOn: $matchScheduler.overrideCourtsUnavailability) {
Text("Ne pas tenir compte des autres tournois")
}
} footer: {
Text("Cette option fait en sorte qu'un terrain pris par un match d'un autre tournoi est toujours considéré comme libre.")
}
Section {
Toggle(isOn: $matchScheduler.shouldEndRoundBeforeStartingNext) {
Text("Finir une manche, classement inclus avant de continuer")
}
@ -267,19 +285,23 @@ struct PlanningSettingsView: View {
Section {
Toggle(isOn: $matchScheduler.accountUpperBracketBreakTime) {
Text("Tenir compte des pauses")
Text("Tenir compte des temps de pause réglementaires")
}
} header: {
Text("Tableau")
}
Section {
Toggle(isOn: $matchScheduler.accountLoserBracketBreakTime) {
Text("Tenir compte des pauses")
Text("Classement")
Text("Tenir compte des temps de pause réglementaires")
}
} header: {
Text("Classement")
}
Section {
Toggle(isOn: $matchScheduler.rotationDifferenceIsImportant) {
Text("Forcer un créneau supplémentaire entre 2 phases")
Text("Forcer une rotation d'attente supplémentaire entre 2 phases")
}
LabeledContent {
@ -295,6 +317,8 @@ struct PlanningSettingsView: View {
Text("Classement")
}
.disabled(matchScheduler.rotationDifferenceIsImportant == false)
} footer: {
Text("Cette option ajoute du temps entre 2 rotations, permettant ainsi de mieux configurer plusieurs tournois se déroulant en même temps.")
}
Section {
@ -304,6 +328,9 @@ struct PlanningSettingsView: View {
Text("Optimisation des créneaux")
Text("Si libre plus de \(matchScheduler.timeDifferenceLimit) minutes")
}
} footer: {
Text("Cette option essaie d'optimiser les créneaux disponibles à partir du moment où ils sont à priori libre plus de \(matchScheduler.timeDifferenceLimit) minutes.")
}
}

@ -28,6 +28,26 @@ struct PlanningView: View {
var body: some View {
List {
_bySlotView()
}
.overlay {
if matches.allSatisfy({ $0.startDate == nil }) {
ContentUnavailableView {
Label("Aucun horaire défini", systemImage: "clock.badge.questionmark")
} description: {
Text("Vous n'avez pas encore défini d'horaire pour les différentes phases du tournoi")
} actions: {
RowButtonView("Horaire intelligent") {
selectedScheduleDestination = nil
}
.padding(.horizontal)
}
}
}
}
@ViewBuilder
func _bySlotView() -> some View {
if matches.allSatisfy({ $0.startDate == nil }) == false {
ForEach(days, id: \.self) { day in
Section {
@ -69,21 +89,6 @@ struct PlanningView: View {
}
}
}
.overlay {
if matches.allSatisfy({ $0.startDate == nil }) {
ContentUnavailableView {
Label("Aucun horaire défini", systemImage: "clock.badge.questionmark")
} description: {
Text("Vous n'avez pas encore défini d'horaire pour les différentes phases du tournoi")
} actions: {
RowButtonView("Horaire intelligent") {
selectedScheduleDestination = nil
}
.padding(.horizontal)
}
}
}
}
private func _matchesCount(inDayInt dayInt: Int) -> Int {
timeSlots.filter { $0.key.dayInt == dayInt }.flatMap({ $0.value }).count

@ -28,6 +28,7 @@ enum ScheduleDestination: String, Identifiable, Selectable, Equatable {
var id: String { self.rawValue }
case planning
case planningByCourt
case scheduleGroupStage
case scheduleBracket
@ -38,6 +39,8 @@ enum ScheduleDestination: String, Identifiable, Selectable, Equatable {
case .scheduleBracket:
return "Tableau"
case .planning:
return "Horaires"
case .planningByCourt:
return "Prog."
}
}
@ -63,7 +66,7 @@ struct TournamentScheduleView: View {
init(tournament: Tournament) {
self.tournament = tournament
var destinations = [ScheduleDestination.planning]
var destinations = [ScheduleDestination.planning, ScheduleDestination.planningByCourt]
if tournament.groupStages().isEmpty == false {
destinations.append(.scheduleGroupStage)
}
@ -76,6 +79,7 @@ struct TournamentScheduleView: View {
var body: some View {
VStack(spacing: 0) {
GenericDestinationPickerView(selectedDestination: $selectedScheduleDestination, destinations: allDestinations, nilDestinationIsValid: true)
let allMatches = tournament.allMatches()
switch selectedScheduleDestination {
case .none:
PlanningSettingsView(tournament: tournament)
@ -86,11 +90,14 @@ struct TournamentScheduleView: View {
case .scheduleBracket:
SchedulerView(tournament: tournament, destination: selectedSchedule)
case .planning:
PlanningView(matches: tournament.allMatches(), selectedScheduleDestination: $selectedScheduleDestination)
PlanningView(matches: allMatches, selectedScheduleDestination: $selectedScheduleDestination)
case .planningByCourt:
PlanningByCourtView(matches: allMatches, selectedScheduleDestination: $selectedScheduleDestination, startDate: allMatches.filter({ $0.isRunning() }).sorted(by: \.computedStartDateForSorting).first?.startDate ?? tournament.startDate)
}
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbarRole(.editor)
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Horaires et formats")
}

Loading…
Cancel
Save