multistore
Razmig Sarkissian 2 years ago
parent 355833d808
commit 26f8383981
  1. 15
      PadelClub/ViewModel/MatchScheduler.swift
  2. 2
      PadelClub/Views/Components/BarButtonView.swift
  3. 16
      PadelClub/Views/Planning/Components/DateUpdateManagerView.swift
  4. 17
      PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift
  5. 9
      PadelClub/Views/Planning/GroupStageScheduleEditorView.swift
  6. 2
      PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift
  7. 10
      PadelClub/Views/Planning/LoserRoundStepScheduleEditorView.swift
  8. 2
      PadelClub/Views/Planning/MatchScheduleEditorView.swift
  9. 9
      PadelClub/Views/Planning/PlanningSettingsView.swift
  10. 11
      PadelClub/Views/Planning/PlanningView.swift
  11. 8
      PadelClub/Views/Planning/RoundScheduleEditorView.swift
  12. 1
      PadelClub/Views/Planning/SchedulerView.swift
  13. 40
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  14. 2
      PadelClub/Views/Tournament/TournamentView.swift

@ -58,6 +58,7 @@ enum MatchSchedulerOption: Hashable {
case randomizeCourts case randomizeCourts
case rotationDifferenceIsImportant case rotationDifferenceIsImportant
case shouldHandleUpperRoundSlice case shouldHandleUpperRoundSlice
case shouldEndRoundBeforeStartingNext
} }
class MatchScheduler { class MatchScheduler {
@ -69,6 +70,10 @@ class MatchScheduler {
var upperBracketRotationDifference: Int = 1 var upperBracketRotationDifference: Int = 1
var courtsUnavailability: [DateInterval]? = nil var courtsUnavailability: [DateInterval]? = nil
func shouldEndRoundBeforeStartingNext() -> Bool {
options.contains(.shouldEndRoundBeforeStartingNext)
}
func shouldHandleUpperRoundSlice() -> Bool { func shouldHandleUpperRoundSlice() -> Bool {
options.contains(.shouldHandleUpperRoundSlice) options.contains(.shouldHandleUpperRoundSlice)
} }
@ -464,11 +469,19 @@ class MatchScheduler {
let upperRounds = tournament.rounds() let upperRounds = tournament.rounds()
let allMatches = tournament.allMatches() let allMatches = tournament.allMatches()
let rounds = upperRounds.map { var rounds = [Round]()
if shouldEndRoundBeforeStartingNext() {
rounds = upperRounds.flatMap {
[$0] + $0.loserRoundsAndChildren()
}
} else {
rounds = upperRounds.map {
$0 $0
} + upperRounds.flatMap { } + upperRounds.flatMap {
$0.loserRoundsAndChildren() $0.loserRoundsAndChildren()
} }
}
var flattenedMatches = rounds.flatMap { round in var flattenedMatches = rounds.flatMap { round in
round._matches().filter({ $0.disabled == false }).sorted(by: \.index) round._matches().filter({ $0.disabled == false }).sorted(by: \.index)

@ -28,7 +28,7 @@ struct BarButtonView: View {
Image(systemName: icon) Image(systemName: icon)
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.frame(minHeight: 28) .frame(minHeight: 36)
} }
.labelStyle(.iconOnly) .labelStyle(.iconOnly)
} }

@ -20,14 +20,24 @@ struct DateUpdateManagerView: View {
@Binding var startDate: Date @Binding var startDate: Date
@State private var dateUpdated: Bool = false @State private var dateUpdated: Bool = false
var duration: Int?
var validateAction: () -> Void var validateAction: () -> Void
var body: some View { var body: some View {
HStack { HStack {
Menu { Menu {
Text("à demain 9h") Button("à demain 9h") {
Text("à la prochaine rotation") startDate = startDate.tomorrowAtNine
Text("à la précédente rotation") }
if let duration {
Button("à la prochaine rotation") {
startDate = startDate.addingTimeInterval(Double(duration) * 60)
}
Button("à la précédente rotation") {
startDate = startDate.addingTimeInterval(Double(duration) * -60)
}
}
} label: { } label: {
Text("décaler") Text("décaler")
.underline() .underline()

@ -17,6 +17,7 @@ struct CourtAvailabilitySettingsView: View {
@State private var courtIndex: Int = 0 @State private var courtIndex: Int = 0
@State private var startDate: Date = Date() @State private var startDate: Date = Date()
@State private var endDate: Date = Date() @State private var endDate: Date = Date()
@State private var editingSlot: DateInterval?
var courtsUnavailability: [Int: [DateInterval]] { var courtsUnavailability: [Int: [DateInterval]] {
let groupedBy = Dictionary(grouping: event.courtsUnavailability, by: { dateInterval in let groupedBy = Dictionary(grouping: event.courtsUnavailability, by: { dateInterval in
@ -55,6 +56,7 @@ struct CourtAvailabilitySettingsView: View {
try? dataStore.dateIntervals.addOrUpdate(instance: duplicatedDateInterval) try? dataStore.dateIntervals.addOrUpdate(instance: duplicatedDateInterval)
} }
Button("éditer") { Button("éditer") {
editingSlot = dateInterval
courtIndex = dateInterval.courtIndex courtIndex = dateInterval.courtIndex
startDate = dateInterval.startDate startDate = dateInterval.startDate
endDate = dateInterval.endDate endDate = dateInterval.endDate
@ -86,8 +88,9 @@ struct CourtAvailabilitySettingsView: View {
} }
} }
.overlay { .overlay {
if courtsUnavailability.isEmpty {
ContentUnavailableView { ContentUnavailableView {
Label("Tous les terrains sont disponibles", systemImage: "checkmark.circle.fill") Label("Tous les terrains sont disponibles", systemImage: "checkmark.circle.fill").tint(.green)
} description: { } description: {
Text("Vous pouvez précisez l'indisponibilité d'une ou plusieurs terrains, que ce soit pour une journée entière ou un créneau précis.") Text("Vous pouvez précisez l'indisponibilité d'une ou plusieurs terrains, que ce soit pour une journée entière ou un créneau précis.")
} actions: { } actions: {
@ -96,9 +99,12 @@ struct CourtAvailabilitySettingsView: View {
} }
} }
} }
}
.toolbar { .toolbar {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
BarButtonView("Ajouter une indisponibilité", icon: "plus.circle.fill") { BarButtonView("Ajouter une indisponibilité", icon: "plus.circle.fill") {
startDate = tournament.startDate
endDate = tournament.startDate.addingTimeInterval(5400)
showingPopover = true showingPopover = true
} }
} }
@ -119,14 +125,21 @@ struct CourtAvailabilitySettingsView: View {
} footer: { } footer: {
FooterButtonView("jour entier") { FooterButtonView("jour entier") {
startDate = startDate.startOfDay startDate = startDate.startOfDay
endDate = endDate.endOfDay() endDate = startDate.endOfDay()
} }
} }
} }
.toolbar { .toolbar {
ButtonValidateView { ButtonValidateView {
if editingSlot == nil {
let dateInterval = DateInterval(event: event.id, courtIndex: courtIndex, startDate: startDate, endDate: endDate) let dateInterval = DateInterval(event: event.id, courtIndex: courtIndex, startDate: startDate, endDate: endDate)
try? dataStore.dateIntervals.addOrUpdate(instance: dateInterval) try? dataStore.dateIntervals.addOrUpdate(instance: dateInterval)
} else {
editingSlot?.courtIndex = courtIndex
editingSlot?.endDate = endDate
editingSlot?.startDate = startDate
try? dataStore.dateIntervals.addOrUpdate(instance: editingSlot!)
}
showingPopover = false showingPopover = false
} }
} }

@ -9,6 +9,7 @@ import SwiftUI
struct GroupStageScheduleEditorView: View { struct GroupStageScheduleEditorView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
var groupStage: GroupStage var groupStage: GroupStage
@State private var startDate: Date @State private var startDate: Date
@State private var dateUpdated: Bool = false @State private var dateUpdated: Bool = false
@ -33,17 +34,11 @@ struct GroupStageScheduleEditorView: View {
} header: { } header: {
Text(groupStage.groupStageTitle()) Text(groupStage.groupStageTitle())
} footer: { } footer: {
DateUpdateManagerView(startDate: $startDate) { DateUpdateManagerView(startDate: $startDate, duration: groupStage.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) {
groupStage.startDate = startDate groupStage.startDate = startDate
_save() _save()
} }
} }
NavigationLink {
GroupStageView(groupStage: groupStage)
} label: {
Text("Voir la poule")
}
} }
.onChange(of: groupStage.matchFormat) { .onChange(of: groupStage.matchFormat) {
_save() _save()

@ -34,7 +34,7 @@ struct LoserRoundScheduleEditorView: View {
} header: { } header: {
Text("Classement " + upperRound.roundTitle()) Text("Classement " + upperRound.roundTitle())
} footer: { } footer: {
DateUpdateManagerView(startDate: $startDate) { DateUpdateManagerView(startDate: $startDate, duration: matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) {
_updateSchedule() _updateSchedule()
} }
} }

@ -51,9 +51,17 @@ struct LoserRoundStepScheduleEditorView: View {
} header: { } header: {
Text(round.selectionLabel()) Text(round.selectionLabel())
} footer: { } footer: {
DateUpdateManagerView(startDate: $startDate) { HStack {
DateUpdateManagerView(startDate: $startDate, duration: round.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) {
_updateSchedule() _updateSchedule()
} }
if round.startDate != nil {
FooterButtonView("retirer l'horaire") {
round.startDate = nil
}
}
}
} }
.headerProminence(.increased) .headerProminence(.increased)
} }

@ -29,7 +29,7 @@ struct MatchScheduleEditorView: View {
Text(match.matchTitle()) Text(match.matchTitle())
} }
} footer: { } footer: {
DateUpdateManagerView(startDate: $startDate) { DateUpdateManagerView(startDate: $startDate, duration: match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) {
_updateSchedule() _updateSchedule()
} }
} }

@ -22,6 +22,7 @@ struct PlanningSettingsView: View {
@State private var isScheduling: Bool = false @State private var isScheduling: Bool = false
@State private var schedulingDone: Bool = false @State private var schedulingDone: Bool = false
@State private var showOptions: Bool = false @State private var showOptions: Bool = false
@State private var shouldEndBeforeStartNext: Bool = true
init(tournament: Tournament) { init(tournament: Tournament) {
self.tournament = tournament self.tournament = tournament
@ -143,6 +144,10 @@ struct PlanningSettingsView: View {
Text("Équilibrer les matchs d'une manche sur plusieurs tours") Text("Équilibrer les matchs d'une manche sur plusieurs tours")
} }
Toggle(isOn: $shouldEndBeforeStartNext) {
Text("Finir une manche et les matchs de classements avant de continuer")
}
Toggle(isOn: $upperBracketBreakTime) { Toggle(isOn: $upperBracketBreakTime) {
Text("Tableau : tenir compte des pauses") Text("Tableau : tenir compte des pauses")
} }
@ -181,6 +186,10 @@ struct PlanningSettingsView: View {
matchScheduler.courtsUnavailability = tournament.eventObject?.courtsUnavailability matchScheduler.courtsUnavailability = tournament.eventObject?.courtsUnavailability
matchScheduler.options.removeAll() matchScheduler.options.removeAll()
if shouldEndBeforeStartNext {
matchScheduler.options.insert(.shouldEndRoundBeforeStartingNext)
}
if randomCourtDistribution { if randomCourtDistribution {
matchScheduler.options.insert(.randomizeCourts) matchScheduler.options.insert(.randomizeCourts)
} }

@ -55,7 +55,12 @@ struct PlanningView: View {
} }
} }
} header: { } header: {
Text(day.formatted(.dateTime.day().weekday().month().year())) HStack {
Text(day.formatted(.dateTime.day().weekday().month()))
Spacer()
let count = _matchesCount(inDayInt: day.dayInt)
Text(count.formatted() + " match" + count.pluralSuffix)
}
} }
.headerProminence(.increased) .headerProminence(.increased)
} }
@ -63,6 +68,10 @@ struct PlanningView: View {
.navigationTitle("Programmation") .navigationTitle("Programmation")
} }
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 { private func _timeSlotView(key: Date, matches: [Match]) -> some View {
LabeledContent { LabeledContent {
Text(matches.count.formatted() + " match" + matches.count.pluralSuffix) Text(matches.count.formatted() + " match" + matches.count.pluralSuffix)

@ -29,18 +29,16 @@ struct RoundScheduleEditorView: View {
} }
} footer: { } footer: {
HStack { HStack {
DateUpdateManagerView(startDate: $startDate) { DateUpdateManagerView(startDate: $startDate, duration: round.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) {
_updateSchedule() _updateSchedule()
} }
Spacer() Spacer()
if let roundStartDate = round.startDate { if round.startDate != nil {
Button("horaire automatique") { FooterButtonView("retirer l'horaire") {
round.startDate = nil round.startDate = nil
} }
.underline()
.buttonStyle(.borderless)
} }
} }
} }

@ -50,6 +50,7 @@ struct SchedulerView: View {
} }
else if let groupStage = schedulable as? GroupStage { else if let groupStage = schedulable as? GroupStage {
GroupStageScheduleEditorView(groupStage: groupStage) GroupStageScheduleEditorView(groupStage: groupStage)
.environment(tournament)
} }
} }
.navigationTitle(schedulable.titleLabel()) .navigationTitle(schedulable.titleLabel())

@ -30,6 +30,7 @@ struct InscriptionManagerView: View {
@State private var currentRankSourceDate: Date? @State private var currentRankSourceDate: Date?
@State private var confirmUpdateRank = false @State private var confirmUpdateRank = false
@State private var selectionSearchField: String? @State private var selectionSearchField: String?
@State private var autoSelect: Bool = false
let slideToDeleteTip = SlideToDeleteTip() let slideToDeleteTip = SlideToDeleteTip()
let inscriptionManagerWomanRankTip = InscriptionManagerWomanRankTip() let inscriptionManagerWomanRankTip = InscriptionManagerWomanRankTip()
@ -279,6 +280,7 @@ struct InscriptionManagerView: View {
await MainActor.run { await MainActor.run {
fetchPlayers.nsPredicate = _pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable) fetchPlayers.nsPredicate = _pastePredicate(pasteField: first, mostRecentDate: SourceFileManager.shared.mostRecentDateAvailable)
pasteString = first pasteString = first
autoSelect = true
} }
} }
} }
@ -563,6 +565,24 @@ struct InscriptionManagerView: View {
private func _buildingTeamView() -> some View { private func _buildingTeamView() -> some View {
List(selection: $createdPlayerIds) { List(selection: $createdPlayerIds) {
if let pasteString {
Section {
Text(pasteString)
} footer: {
HStack {
Text("contenu du presse-papier")
Spacer()
Button("effacer", role: .destructive) {
self.pasteString = nil
self.createdPlayers.removeAll()
self.createdPlayerIds.removeAll()
}
.buttonStyle(.borderless)
}
}
}
Section { Section {
ForEach(createdPlayerIds.sorted(), id: \.self) { id in ForEach(createdPlayerIds.sorted(), id: \.self) { id in
if let p = createdPlayers.first(where: { $0.id == id }) { if let p = createdPlayers.first(where: { $0.id == id }) {
@ -591,22 +611,6 @@ struct InscriptionManagerView: View {
} }
if let pasteString { if let pasteString {
Section {
Text(pasteString)
} footer: {
HStack {
Text("contenu du presse-papier")
Spacer()
Button("effacer", role: .destructive) {
self.pasteString = nil
self.createdPlayers.removeAll()
self.createdPlayerIds.removeAll()
}
.buttonStyle(.borderless)
}
}
if fetchPlayers.isEmpty { if fetchPlayers.isEmpty {
ContentUnavailableView { ContentUnavailableView {
Label("Aucun résultat", systemImage: "person.2.slash") Label("Aucun résultat", systemImage: "person.2.slash")
@ -634,13 +638,13 @@ struct InscriptionManagerView: View {
} }
} }
.onReceive(fetchPlayers.publisher.count()) { _ in // <-- here .onReceive(fetchPlayers.publisher.count()) { _ in // <-- here
if let pasteString, count == 2 { if let pasteString, count == 2, autoSelect == true {
fetchPlayers.filter { $0.hitForSearch(pasteString) >= hitTarget }.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) }).forEach { player in fetchPlayers.filter { $0.hitForSearch(pasteString) >= hitTarget }.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) }).forEach { player in
createdPlayerIds.insert(player.license!) createdPlayerIds.insert(player.license!)
} }
autoSelect = false
} }
} }
.environment(\.editMode, Binding.constant(EditMode.active)) .environment(\.editMode, Binding.constant(EditMode.active))
} }

@ -44,7 +44,7 @@ struct TournamentView: View {
} }
} }
} footer: { } footer: {
if tournament.inscriptionClosed() { if tournament.inscriptionClosed() == false {
Button { Button {
tournament.lockRegistration() tournament.lockRegistration()
_save() _save()

Loading…
Cancel
Save