multistore
Razmig Sarkissian 2 years ago
parent 355833d808
commit 26f8383981
  1. 21
      PadelClub/ViewModel/MatchScheduler.swift
  2. 2
      PadelClub/Views/Components/BarButtonView.swift
  3. 16
      PadelClub/Views/Planning/Components/DateUpdateManagerView.swift
  4. 33
      PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift
  5. 9
      PadelClub/Views/Planning/GroupStageScheduleEditorView.swift
  6. 2
      PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift
  7. 12
      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,10 +469,18 @@ 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]()
$0
} + upperRounds.flatMap { if shouldEndRoundBeforeStartingNext() {
$0.loserRoundsAndChildren() rounds = upperRounds.flatMap {
[$0] + $0.loserRoundsAndChildren()
}
} else {
rounds = upperRounds.map {
$0
} + upperRounds.flatMap {
$0.loserRoundsAndChildren()
}
} }
var flattenedMatches = rounds.flatMap { round in var flattenedMatches = rounds.flatMap { round in

@ -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,19 +88,23 @@ struct CourtAvailabilitySettingsView: View {
} }
} }
.overlay { .overlay {
ContentUnavailableView { if courtsUnavailability.isEmpty {
Label("Tous les terrains sont disponibles", systemImage: "checkmark.circle.fill") ContentUnavailableView {
} description: { Label("Tous les terrains sont disponibles", systemImage: "checkmark.circle.fill").tint(.green)
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.") } description: {
} actions: { 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.")
RowButtonView("Ajouter une indisponibilité", systemImage: "plus.circle.fill") { } actions: {
showingPopover = true RowButtonView("Ajouter une indisponibilité", systemImage: "plus.circle.fill") {
showingPopover = true
}
} }
} }
} }
.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 {
let dateInterval = DateInterval(event: event.id, courtIndex: courtIndex, startDate: startDate, endDate: endDate) if editingSlot == nil {
try? dataStore.dateIntervals.addOrUpdate(instance: dateInterval) let dateInterval = DateInterval(event: event.id, courtIndex: courtIndex, startDate: startDate, endDate: endDate)
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,8 +51,16 @@ struct LoserRoundStepScheduleEditorView: View {
} header: { } header: {
Text(round.selectionLabel()) Text(round.selectionLabel())
} footer: { } footer: {
DateUpdateManagerView(startDate: $startDate) { HStack {
_updateSchedule() DateUpdateManagerView(startDate: $startDate, duration: round.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) {
_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