diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index c20b8f4..8ad6e82 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3129,7 +3129,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.38; + MARKETING_VERSION = 1.2.39; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3175,7 +3175,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.38; + MARKETING_VERSION = 1.2.39; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/PadelClub/Views/Match/Components/MatchDateView.swift b/PadelClub/Views/Match/Components/MatchDateView.swift index 9b6ba94..df0e586 100644 --- a/PadelClub/Views/Match/Components/MatchDateView.swift +++ b/PadelClub/Views/Match/Components/MatchDateView.swift @@ -46,7 +46,7 @@ struct MatchDateView: View { if let updatedField { match.setCourt(updatedField) } - match.startDate = currentDate + match.updateStartDate(currentDate, keepPlannedStartDate: true) match.endDate = nil match.confirmed = true _save() @@ -55,7 +55,7 @@ struct MatchDateView: View { if let updatedField { match.setCourt(updatedField) } - match.startDate = Calendar.current.date(byAdding: .minute, value: 5, to: currentDate) + match.updateStartDate(Calendar.current.date(byAdding: .minute, value: 5, to: currentDate), keepPlannedStartDate: true) match.endDate = nil match.confirmed = true _save() @@ -64,7 +64,7 @@ struct MatchDateView: View { if let updatedField { match.setCourt(updatedField) } - match.startDate = Calendar.current.date(byAdding: .minute, value: 15, to: currentDate) + match.updateStartDate(Calendar.current.date(byAdding: .minute, value: 15, to: currentDate), keepPlannedStartDate: true) match.endDate = nil match.confirmed = true _save() @@ -73,7 +73,7 @@ struct MatchDateView: View { if let updatedField { match.setCourt(updatedField) } - match.startDate = Calendar.current.date(byAdding: .minute, value: estimatedDuration, to: currentDate) + match.updateStartDate(Calendar.current.date(byAdding: .minute, value: estimatedDuration, to: currentDate), keepPlannedStartDate: true) match.endDate = nil match.confirmed = true _save() @@ -96,7 +96,7 @@ struct MatchDateView: View { } Divider() Button("Retirer l'horaire") { - match.cleanScheduleAndSave() + match.updateStartDate(nil, keepPlannedStartDate: true) } } label: { label diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index 6b0e3c8..c03425d 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -329,10 +329,7 @@ struct MatchDetailView: View { } } Button(role: .destructive) { - match.startDate = nil - match.endDate = nil - match.confirmed = false - save() + match.updateStartDate(nil, keepPlannedStartDate: true) } label: { Text("Supprimer l'horaire") } @@ -348,6 +345,7 @@ struct MatchDetailView: View { } Divider() Button(role: .destructive) { + match.updateStartDate(nil, keepPlannedStartDate: false) match.resetTeamScores(outsideOf: []) match.resetMatch() match.confirmed = false diff --git a/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift b/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift index 5ef5247..3f68a1f 100644 --- a/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift +++ b/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift @@ -262,20 +262,29 @@ struct DateAdjusterView: View { var body: some View { HStack(spacing: 4) { if let matchFormat { - _createButton(label: "-\(matchFormat.defaultEstimatedDuration)m", timeOffset: -matchFormat.defaultEstimatedDuration, component: .minute) - _createButton(label: "+\(matchFormat.defaultEstimatedDuration)m", timeOffset: +matchFormat.defaultEstimatedDuration, component: .minute) + _createButton(label: "-\(matchFormat.getEstimatedDuration())m", timeOffset: -matchFormat.getEstimatedDuration(), component: .minute) + Divider() + _createButton(label: "+\(matchFormat.getEstimatedDuration())m", timeOffset: +matchFormat.getEstimatedDuration(), component: .minute) + Divider() _createButton(label: "-\(matchFormat.estimatedTimeWithBreak)m", timeOffset: -matchFormat.estimatedTimeWithBreak, component: .minute) + Divider() _createButton(label: "+\(matchFormat.estimatedTimeWithBreak)m", timeOffset: +matchFormat.estimatedTimeWithBreak, component: .minute) } else if let time { _createButton(label: "-\(time)m", timeOffset: -time, component: .minute) + Divider() _createButton(label: "-\(time/2)m", timeOffset: -time/2, component: .minute) + Divider() _createButton(label: "+\(time/2)m", timeOffset: time/2, component: .minute) + Divider() _createButton(label: "+\(time)m", timeOffset: time, component: .minute) } else { - _createButton(label: "-1h", timeOffset: -1, component: .hour) + _createButton(label: "-1h", timeOffset: -60, component: .minute) + Divider() _createButton(label: "-30m", timeOffset: -30, component: .minute) + Divider() _createButton(label: "+30m", timeOffset: 30, component: .minute) - _createButton(label: "+1h", timeOffset: 1, component: .hour) + Divider() + _createButton(label: "+1h", timeOffset: 60, component: .minute) } } .font(.headline) @@ -287,11 +296,58 @@ struct DateAdjusterView: View { }) { Text(label) .lineLimit(1) - .font(.footnote) - .underline() .frame(maxWidth: .infinity) // Make buttons take equal space } - .buttonStyle(.borderedProminent) + .buttonStyle(.borderless) + .tint(.master) + } +} + +struct StepAdjusterView: View { + @Binding var step: Int + var time: Int? + var matchFormat: MatchFormat? + + var body: some View { + HStack(spacing: 4) { + if let matchFormat { + _createButton(label: "-\(matchFormat.getEstimatedDuration())m", timeOffset: -matchFormat.getEstimatedDuration(), component: .minute) + Divider() + _createButton(label: "+\(matchFormat.getEstimatedDuration())m", timeOffset: +matchFormat.getEstimatedDuration(), component: .minute) + Divider() + _createButton(label: "-\(matchFormat.estimatedTimeWithBreak)m", timeOffset: -matchFormat.estimatedTimeWithBreak, component: .minute) + Divider() + _createButton(label: "+\(matchFormat.estimatedTimeWithBreak)m", timeOffset: +matchFormat.estimatedTimeWithBreak, component: .minute) + } else if let time { + _createButton(label: "-\(time)m", timeOffset: -time, component: .minute) + Divider() + _createButton(label: "-\(time/2)m", timeOffset: -time/2, component: .minute) + Divider() + _createButton(label: "+\(time/2)m", timeOffset: time/2, component: .minute) + Divider() + _createButton(label: "+\(time)m", timeOffset: time, component: .minute) + } else { + _createButton(label: "-1h", timeOffset: -60, component: .minute) + Divider() + _createButton(label: "-30m", timeOffset: -30, component: .minute) + Divider() + _createButton(label: "+30m", timeOffset: 30, component: .minute) + Divider() + _createButton(label: "+1h", timeOffset: 60, component: .minute) + } + } + .font(.headline) + } + + private func _createButton(label: String, timeOffset: Int, component: Calendar.Component) -> some View { + Button(action: { + step += timeOffset + }) { + Text(label) + .lineLimit(1) + .frame(maxWidth: .infinity) // Make buttons take equal space + } + .buttonStyle(.borderless) .tint(.master) } } diff --git a/PadelClub/Views/Planning/PlanningView.swift b/PadelClub/Views/Planning/PlanningView.swift index f670f48..33137e2 100644 --- a/PadelClub/Views/Planning/PlanningView.swift +++ b/PadelClub/Views/Planning/PlanningView.swift @@ -110,7 +110,7 @@ struct PlanningView: View { if _confirmationMode() { ToolbarItem(placement: .topBarLeading) { - Button("Annuler") { + Button(enableMove ? "Annuler" : "Terminer") { enableMove = false enableEditionBinding.wrappedValue = false } @@ -130,82 +130,86 @@ struct PlanningView: View { } } } else { - if notSlots == false { - ToolbarItemGroup(placement: .bottomBar) { - HStack { + ToolbarItemGroup(placement: .topBarTrailing) { + Menu { + if notSlots == false { CourtOptionsView(timeSlots: timeSlots, underlined: false) - Spacer() Toggle(isOn: $enableMove) { Label { - Text("Déplacer") + Text("Déplacer un créneau") } icon: { Image(systemName: "rectangle.2.swap") } } .popoverTip(timeSlotMoveOptionTip) .disabled(_confirmationMode()) - Spacer() Toggle(isOn: enableEditionBinding) { - Text("Modifier") + Text("Modifier un horaire") } .disabled(_confirmationMode()) } - } - } - ToolbarItemGroup(placement: .topBarTrailing) { - Menu { - Section { - Picker(selection: $showFinishedMatches) { - Text("Afficher tous les matchs").tag(true) - Text("Masquer les matchs terminés").tag(false) - } label: { + + Divider() + + Menu { + Section { + Picker(selection: $showFinishedMatches) { + Text("Afficher tous les matchs").tag(true) + Text("Masquer les matchs terminés").tag(false) + } label: { + Text("Option de filtrage") + } + .labelsHidden() + .pickerStyle(.inline) + } header: { Text("Option de filtrage") } - .labelsHidden() - .pickerStyle(.inline) - } header: { - Text("Option de filtrage") - } - Divider() + Divider() - Section { - Picker(selection: $filterOption) { - ForEach(PlanningFilterOption.allCases) { - Text($0.localizedPlanningLabel()).tag($0) + Section { + Picker(selection: $filterOption) { + ForEach(PlanningFilterOption.allCases) { + Text($0.localizedPlanningLabel()).tag($0) + } + } label: { + Text("Option de triage") } - } label: { + .labelsHidden() + .pickerStyle(.inline) + } header: { Text("Option de triage") - } - .labelsHidden() - .pickerStyle(.inline) - } header: { - Text("Option de triage") + } + } label: { + Label("Trier", systemImage: "line.3.horizontal.decrease.circle") + .symbolVariant( + filterOption == .byCourt || showFinishedMatches ? .fill : .none) } - } label: { - Label("Trier", systemImage: "line.3.horizontal.decrease.circle") - .symbolVariant( - filterOption == .byCourt || showFinishedMatches ? .fill : .none) - } - - Button("Mettre à jour", systemImage: "arrow.trianglehead.2.clockwise.rotate.90.circle") { - let now = Date() - matches.forEach { - if let startDate = $0.startDate, startDate > now { - $0.plannedStartDate = $0.startDate + Divider() + + Button("Mettre à jour", systemImage: "arrow.trianglehead.2.clockwise.rotate.90.circle") { + let now = Date() + matches.forEach { + if let startDate = $0.startDate, startDate > now { + $0.plannedStartDate = $0.startDate + } + } + + let groupByTournaments = matches.grouped { match in + match.currentTournament() + } + groupByTournaments.forEach { tournament, matches in + tournament?.tournamentStore?.matches.addOrUpdate(contentOfs: matches) } } - - let groupByTournaments = matches.grouped { match in - match.currentTournament() - } - groupByTournaments.forEach { tournament, matches in - tournament?.tournamentStore?.matches.addOrUpdate(contentOfs: matches) - } + .popoverTip(updatePlannedDatesTip) + + + } label: { + LabelOptions() } - .popoverTip(updatePlannedDatesTip) } } }) @@ -235,8 +239,8 @@ struct PlanningView: View { @Environment(\.editMode) private var editMode @State private var selectedIds = Set() @State private var showDateUpdateView: Bool = false - @State private var dateToUpdate: Date = Date() - + @State private var matchesToUpdate: [Match] = [] + let days: [Date] let keys: [Date] let timeSlots: [Date: [Match]] @@ -258,28 +262,34 @@ struct PlanningView: View { day: day, keys: keys.filter({ $0.dayInt == day.dayInt }), timeSlots: timeSlots, - selectedDay: selectedDay + selectedDay: selectedDay, + selectedIds: $selectedIds, + matchesForUpdateSheet: $matchesToUpdate ) } } } .toolbar(content: { if editMode?.wrappedValue == .active { - ToolbarItem(placement: .bottomBar) { + ToolbarItem(placement: .topBarTrailing) { Button { - showDateUpdateView = true + matchesToUpdate = matches.filter({ selectedIds.contains($0.stringId) }) } label: { - Text("Modifier la date des matchs sélectionnés") + Text("Modifier") } + .buttonStyle(.borderless) .disabled(selectedIds.isEmpty) } } }) + .onChange(of: matchesToUpdate, { oldValue, newValue in + showDateUpdateView = matchesToUpdate.count > 0 + }) .sheet(isPresented: $showDateUpdateView, onDismiss: { selectedIds.removeAll() + matchesToUpdate = [] }) { - let selectedMatches = matches.filter({ selectedIds.contains($0.stringId) }) - DateUpdateView(selectedMatches: selectedMatches) + DateUpdateView(selectedMatches: matchesToUpdate) } } } @@ -290,6 +300,7 @@ struct PlanningView: View { let selectedMatches: [Match] let selectedFormats: [MatchFormat] @State private var dateToUpdate: Date + @State private var updateStep: Int = 0 init(selectedMatches: [Match]) { self.selectedMatches = selectedMatches @@ -306,13 +317,37 @@ struct PlanningView: View { DatePicker(selection: $dateToUpdate) { Text(dateToUpdate.formatted(.dateTime.weekday(.wide))).font(.headline) } + + RowButtonView("Définir le nouvel horaire", role: .destructive) { + _setDate() + } + } footer: { + Text("Choisir un nouvel horaire pour tous les matchs sélectionnés") } - + Section { - DateAdjusterView(date: $dateToUpdate) - DateAdjusterView(date: $dateToUpdate, time: 10) + LabeledContent { + StepperView(title: "minutes", count: $updateStep, step: 5) + } label: { + Text("Décalage") + } + + RowButtonView("Décaler les horaires", role: .destructive) { + _updateDate() + } + + } footer: { + Text("décale CHAQUE horaire du nombre de minutes indiqué") + .foregroundStyle(.logoRed) + } + + VStack { + StepAdjusterView(step: $updateStep) + Divider() + StepAdjusterView(step: $updateStep, time: 10) ForEach(selectedFormats, id: \.self) { matchFormat in - DateAdjusterView(date: $dateToUpdate, matchFormat: matchFormat) + Divider() + StepAdjusterView(step: $updateStep, matchFormat: matchFormat) } } @@ -325,7 +360,10 @@ struct PlanningView: View { } } - .navigationTitle("Modification de la date") + .onChange(of: updateStep, { oldValue, newValue in + dateToUpdate.addTimeInterval(TimeInterval((newValue - oldValue) * 60)) + }) + .navigationTitle("Modifier l'horaire") .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) .toolbar(content: { @@ -334,25 +372,34 @@ struct PlanningView: View { dismiss() } } - ToolbarItem(placement: .topBarTrailing) { - Button("Valider") { - _updateDate() - } - } }) } } private func _updateDate() { + selectedMatches.forEach { match in + if match.hasStarted() || match.hasEnded() { + match.plannedStartDate?.addTimeInterval(TimeInterval(updateStep * 60)) + } else { + match.startDate?.addTimeInterval(TimeInterval(updateStep * 60)) + } + } + + let groupByTournaments = selectedMatches.grouped { match in + match.currentTournament() + } + groupByTournaments.forEach { tournament, matches in + tournament?.tournamentStore?.matches.addOrUpdate(contentOfs: matches) + } + dismiss() + } + + private func _setDate() { selectedMatches.forEach { match in if match.hasStarted() || match.hasEnded() { match.plannedStartDate = dateToUpdate } else { - let hasStarted = match.currentTournament()?.hasStarted() == true match.startDate = dateToUpdate - if hasStarted { - match.plannedStartDate = dateToUpdate - } } } @@ -378,7 +425,10 @@ struct PlanningView: View { let keys: [Date] let timeSlots: [Date: [Match]] let selectedDay: Date? - + @Binding var selectedIds: Set + @State private var selectAll: Bool = false + @Binding var matchesForUpdateSheet: [Match] + var body: some View { Section { ForEach(keys, id: \.self) { key in @@ -386,12 +436,22 @@ struct PlanningView: View { key: key, matches: timeSlots[key]?.sorted( by: filterOption == .byDefault - ? \.computedOrder : \.courtIndexForSorting) ?? [] + ? \.computedOrder : \.courtIndexForSorting) ?? [], matchesForUpdateSheet: $matchesForUpdateSheet ) } .onMove(perform: enableMove ? moveSection : nil) } header: { - HeaderView(day: day, timeSlots: timeSlots) + if editMode?.wrappedValue == .active { + HStack { + Spacer() + FooterButtonView(selectAll ? "Tout desélectionner" : "Tout sélectionner") { + selectAll.toggle() + } + .textCase(nil) + } + } else { + HeaderView(day: day, timeSlots: timeSlots) + } } footer: { VStack(alignment: .leading) { if day.monthYearFormatted == Date.distantFuture.monthYearFormatted { @@ -403,6 +463,16 @@ struct PlanningView: View { CourtOptionsView(timeSlots: timeSlots, underlined: true) } } + .onChange(of: selectAll, { oldValue, newValue in + if oldValue == false, newValue == true { + selectedIds = Set(timeSlots.filter({ keys.contains($0.key) }).values.flatMap({ values in values.compactMap({ match in match.stringId }) })) + } else if oldValue == true, newValue == false { + selectedIds.removeAll() + } + }) + .onChange(of: editMode?.wrappedValue) { oldValue, newValue in + selectAll = false + } } func moveSection(from source: IndexSet, to destination: Int) { @@ -450,7 +520,7 @@ struct PlanningView: View { let matches: [Match] @State private var isExpanded: Bool = false - @State private var showDateUpdateView: Bool = false + @Binding var matchesForUpdateSheet: [Match] var body: some View { if !matches.isEmpty { @@ -462,21 +532,23 @@ struct PlanningView: View { } label: { TimeSlotHeaderView(key: key, matches: matches) } + .onChange(of: editMode?.wrappedValue, { oldValue, newValue in + if oldValue == .inactive, newValue == .active, isExpanded == false { + isExpanded = true + } else if oldValue == .active, newValue == .inactive, isExpanded == true { + isExpanded = false + } + }) .contextMenu { PlanningView.CourtOptionsView(timeSlots: [key: matches], underlined: false) Button { - showDateUpdateView = true + matchesForUpdateSheet = matches } label: { Text("Modifier la date") } } - .sheet(isPresented: $showDateUpdateView, onDismiss: { - }) { - PlanningView.DateUpdateView(selectedMatches: matches) - } - // .onChange(of: editMode?.wrappedValue) { // if editMode?.wrappedValue == .active, isExpanded == false { // isExpanded = true @@ -500,6 +572,7 @@ struct PlanningView: View { } label: { MatchRowView(match: match) } + .listRowView(isActive: match.hasStarted(), color: .green, hideColorVariation: true) } } }