@ -5,20 +5,20 @@
// C r e a t e d b y R a z m i g S a r k i s s i a n o n 0 7 / 0 4 / 2 0 2 4 .
//
import SwiftUI
import LeStorage
import TipKit
import PadelClubData
import SwiftUI
import TipKit
struct PlanningView : View {
@ EnvironmentObject var dataStore : DataStore
@ Environment ( Tournament . self ) var tournament : Tournament
@ State private var selectedDay : Date ?
@ Binding var selectedScheduleDestination : ScheduleDestination ?
@ State private var filterOption : PlanningFilterOption = . byDefault
@ State private var showFinishedMatches : Bool = false
@ State private var enableMove : Bool = false
@ Environment ( \ . editMode ) private var editMode
let allMatches : [ Match ]
let timeSlotMoveOptionTip = TimeSlotMoveOptionTip ( )
@ -56,18 +56,37 @@ struct PlanningView: View {
}
}
private func _confirmationMode ( ) -> Bool {
enableMove || editMode ? . wrappedValue = = . active
}
private var enableEditionBinding : Binding < Bool > {
Binding {
editMode ? . wrappedValue = = . active
} set : { value in
if value {
editMode ? . wrappedValue = . active
} else {
editMode ? . wrappedValue = . inactive
}
}
}
var body : some View {
let timeSlots = self . timeSlots
let keys = self . keys ( timeSlots : timeSlots )
let days = self . days ( timeSlots : timeSlots )
let matches = matches
let notSlots = matches . allSatisfy ( { $0 . startDate = = nil } )
BySlotView ( days : days , keys : keys , timeSlots : timeSlots , matches : matches , selectedDay : selectedDay )
BySlotView (
days : days , keys : keys , timeSlots : timeSlots , matches : matches , selectedDay : selectedDay
)
. environment ( \ . filterOption , filterOption )
. environment ( \ . showFinishedMatches , showFinishedMatches )
. environment ( \ . enableMove , enableMove )
. navigationTitle ( Text ( _computedTitle ( days : days ) ) )
. navigationBarBackButtonHidden ( enableMove )
. navigationBarBackButtonHidden ( _confirmationMode ( ) )
. toolbar ( content : {
if days . count > 1 {
ToolbarTitleMenu {
@ -77,46 +96,63 @@ struct PlanningView: View {
if day . monthYearFormatted = = Date . distantFuture . monthYearFormatted {
Text ( " Sans horaire " ) . tag ( day as Date ? )
} else {
Text ( day . formatted ( . dateTime . day ( ) . weekday ( ) . month ( ) ) ) . tag ( day as Date ? )
Text ( day . formatted ( . dateTime . day ( ) . weekday ( ) . month ( ) ) ) . tag (
day as Date ? )
}
}
} label : {
Text ( " Jour " )
}
. pickerStyle ( . automatic )
. disabled ( enableMove )
. disabled ( _confirmationMode ( ) )
}
}
if enableMove {
if _confirmationMode ( ) {
ToolbarItem ( placement : . topBarLeading ) {
Button ( " Annuler " ) {
enableMove = false
enableEditionBinding . wrappedValue = false
}
}
ToolbarItem ( placement : . topBarTrailing ) {
if enableMove {
ToolbarItemGroup ( placement : . topBarTrailing ) {
Button ( " Sauver " ) {
do {
try self . tournament . tournamentStore ? . matches . addOrUpdate ( contentOfs : allMatches )
} catch {
Logger . error ( error )
let groupByTournaments = allMatches . grouped { match in
match . currentTournament ( )
}
groupByTournaments . forEach { tournament , matches in
tournament ? . tournamentStore ? . matches . addOrUpdate ( contentOfs : matches )
}
enableMove = false
}
}
}
} else {
ToolbarItemGroup ( placement : . topBarTrailing ) {
if notSlots = = false {
ToolbarItemGroup ( placement : . bottomBar ) {
HStack {
CourtOptionsView ( timeSlots : timeSlots , underlined : false )
Spacer ( )
Toggle ( isOn : $ enableMove ) {
Label ( " Déplacer " , systemImage : " rectangle.2.swap " )
Label {
Text ( " Déplacer " )
} icon : {
Image ( systemName : " rectangle.2.swap " )
}
}
. popoverTip ( timeSlotMoveOptionTip )
. disabled ( _confirmationMode ( ) )
Spacer ( )
Toggle ( isOn : enableEditionBinding ) {
Text ( " Modifier " )
}
. disabled ( _confirmationMode ( ) )
}
}
}
ToolbarItemGroup ( placement : . topBarTrailing ) {
Menu {
Section {
Picker ( selection : $ showFinishedMatches ) {
@ -149,7 +185,8 @@ struct PlanningView: View {
}
} label : {
Label ( " Trier " , systemImage : " line.3.horizontal.decrease.circle " )
. symbolVariant ( filterOption = = . byCourt || showFinishedMatches ? . fill : . none )
. symbolVariant (
filterOption = = . byCourt || showFinishedMatches ? . fill : . none )
}
}
@ -160,7 +197,9 @@ struct PlanningView: View {
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 " )
Text (
" Vous n'avez pas encore défini d'horaire pour les différentes phases du tournoi "
)
} actions : {
RowButtonView ( " Horaire intelligent " ) {
selectedScheduleDestination = nil
@ -171,10 +210,13 @@ struct PlanningView: View {
}
struct BySlotView : View {
@ Environment ( Tournament . self ) var tournament : Tournament
@ Environment ( \ . filterOption ) private var filterOption
@ Environment ( \ . showFinishedMatches ) private var showFinishedMatches
@ Environment ( \ . enableMove ) private var enableMove
@ Environment ( \ . editMode ) private var editMode
@ State private var selectedIds = Set < String > ( )
@ State private var showDateUpdateView : Bool = false
@ State private var dateToUpdate : Date = Date ( )
let days : [ Date ]
let keys : [ Date ]
@ -184,15 +226,15 @@ struct PlanningView: View {
let timeSlotMoveTip = TimeSlotMoveTip ( )
var body : some View {
List {
List ( selection : $ selectedIds ) {
if enableMove {
TipView ( timeSlotMoveTip )
. tipStyle ( tint : . logoYellow , asSection : true )
}
if ! matches . allSatisfy ( { $0 . startDate = = nil } ) {
ForEach ( days . filter ( { selectedDay = = nil || selectedDay = = $0 } ) , id : \ . self ) { day in
ForEach ( days . filter ( { selectedDay = = nil || selectedDay = = $0 } ) , id : \ . self ) {
day in
DaySectionView (
day : day ,
keys : keys . filter ( { $0 . dayInt = = day . dayInt } ) ,
@ -202,15 +244,108 @@ struct PlanningView: View {
}
}
}
. toolbar ( content : {
if editMode ? . wrappedValue = = . active {
ToolbarItem ( placement : . bottomBar ) {
Button {
showDateUpdateView = true
} label : {
Text ( " Modifier la date des matchs sélectionnés " )
}
. disabled ( selectedIds . isEmpty )
}
}
} )
. sheet ( isPresented : $ showDateUpdateView , onDismiss : {
selectedIds . removeAll ( )
} ) {
let selectedMatches = matches . filter ( { selectedIds . contains ( $0 . stringId ) } )
DateUpdateView ( selectedMatches : selectedMatches )
}
}
}
struct DateUpdateView : View {
@ Environment ( \ . dismiss ) var dismiss
let selectedMatches : [ Match ]
let selectedFormats : [ MatchFormat ]
@ State private var dateToUpdate : Date
init ( selectedMatches : [ Match ] ) {
self . selectedMatches = selectedMatches
self . selectedFormats = Array ( Set ( selectedMatches . map ( { match in
match . matchFormat
} ) ) )
_dateToUpdate = . init ( wrappedValue : selectedMatches . first ? . startDate ? ? Date ( ) )
}
var body : some View {
NavigationStack {
List {
Section {
DatePicker ( selection : $ dateToUpdate ) {
Text ( dateToUpdate . formatted ( . dateTime . weekday ( . wide ) ) ) . font ( . headline )
}
}
Section {
DateAdjusterView ( date : $ dateToUpdate )
DateAdjusterView ( date : $ dateToUpdate , time : 10 )
ForEach ( selectedFormats , id : \ . self ) { matchFormat in
DateAdjusterView ( date : $ dateToUpdate , matchFormat : matchFormat )
}
}
Section {
ForEach ( selectedMatches ) { match in
MatchRowView ( match : match )
}
} header : {
Text ( " Matchs à modifier " )
}
}
. navigationTitle ( " Modification de la date " )
. navigationBarTitleDisplayMode ( . inline )
. toolbarBackground ( . visible , for : . navigationBar )
. toolbar ( content : {
ToolbarItem ( placement : . topBarLeading ) {
Button ( " Annuler " , role : . cancel ) {
dismiss ( )
}
}
ToolbarItem ( placement : . topBarTrailing ) {
Button ( " Valider " ) {
_updateDate ( )
}
}
} )
}
}
private func _updateDate ( ) {
selectedMatches . forEach { match in
match . startDate = dateToUpdate
}
let groupByTournaments = selectedMatches . grouped { match in
match . currentTournament ( )
}
groupByTournaments . forEach { tournament , matches in
tournament ? . tournamentStore ? . matches . addOrUpdate ( contentOfs : matches )
}
dismiss ( )
}
}
struct DaySectionView : View {
@ Environment ( Tournament . self ) var tournament : Tournament
@ Environment ( \ . filterOption ) private var filterOption
@ Environment ( \ . showFinishedMatches ) private var showFinishedMatches
@ Environment ( \ . enableMove ) private var enableMove
@ Environment ( \ . editMode ) private var editMode
let day : Date
let keys : [ Date ]
@ -222,15 +357,23 @@ struct PlanningView: View {
ForEach ( keys , id : \ . self ) { key in
TimeSlotSectionView (
key : key ,
matches : timeSlots [ key ] ? . sorted ( by : filterOption = = . byDefault ? \ . computedOrder : \ . courtIndexForSorting ) ? ? [ ]
matches : timeSlots [ key ] ? . sorted (
by : filterOption = = . byDefault
? \ . computedOrder : \ . courtIndexForSorting ) ? ? [ ]
)
}
. onMove ( perform : enableMove ? moveSection : nil )
} header : {
HeaderView ( day : day , timeSlots : timeSlots )
} footer : {
VStack ( alignment : . leading ) {
if day . monthYearFormatted = = Date . distantFuture . monthYearFormatted {
Text ( " Il s'agit des matchs qui n'ont pas réussi à être placé par Padel Club. Peut-être à cause de créneaux indisponibles, d'autres tournois ou des réglages. " )
Text (
" Il s'agit des matchs qui n'ont pas réussi à être placé par Padel Club. Peut-être à cause de créneaux indisponibles, d'autres tournois ou des réglages. "
)
}
CourtOptionsView ( timeSlots : timeSlots , underlined : true )
}
}
}
@ -240,7 +383,8 @@ struct PlanningView: View {
guard let sourceIdx = source . first ,
sourceIdx < daySlots . count ,
destination <= daySlots . count else {
destination <= daySlots . count
else {
return
}
@ -272,22 +416,47 @@ struct PlanningView: View {
}
}
struct TimeSlotSectionView : View {
@ Environment ( \ . enableMove ) private var enableMove
@ Environment ( \ . editMode ) private var editMode
let key : Date
let matches : [ Match ]
@ State private var isExpanded : Bool = false
@ State private var showDateUpdateView : Bool = false
var body : some View {
if ! matches . isEmpty {
if enableMove {
TimeSlotHeaderView ( key : key , matches : matches )
} else {
DisclosureGroup {
DisclosureGroup ( isExpanded : $ isExpanded ) {
MatchListView ( matches : matches )
} label : {
TimeSlotHeaderView ( key : key , matches : matches )
}
. contextMenu {
PlanningView . CourtOptionsView ( timeSlots : [ key : matches ] , underlined : false )
Button {
showDateUpdateView = true
} label : {
Text ( " Modifier la date " )
}
}
. sheet ( isPresented : $ showDateUpdateView , onDismiss : {
} ) {
PlanningView . DateUpdateView ( selectedMatches : matches )
}
// . o n C h a n g e ( o f : e d i t M o d e ? . w r a p p e d V a l u e ) {
// i f e d i t M o d e ? . w r a p p e d V a l u e = = . a c t i v e , i s E x p a n d e d = = f a l s e {
// i s E x p a n d e d = t r u e
// } e l s e i f e d i t M o d e ? . w r a p p e d V a l u e = = . i n a c t i v e , i s E x p a n d e d = = t r u e {
// i s E x p a n d e d = f a l s e
// }
// }
}
}
}
@ -297,7 +466,7 @@ struct PlanningView: View {
let matches : [ Match ]
var body : some View {
ForEach ( matches ) { match in
ForEach ( matches , id : \ . stringId ) { match in
NavigationLink {
MatchDetailView ( match : match )
. matchViewStyle ( . sectionedStandardStyle )
@ -309,6 +478,7 @@ struct PlanningView: View {
}
struct MatchRowView : View {
@ Environment ( \ . matchViewStyle ) private var matchViewStyle
let match : Match
var body : some View {
@ -319,14 +489,19 @@ struct PlanningView: View {
} label : {
if let groupStage = match . groupStageObject {
Text ( groupStage . groupStageTitle ( . title ) )
Text ( match . matchTitle ( ) )
} else if let round = match . roundObject {
Text ( round . roundTitle ( ) )
}
if round . index > 0 {
Text ( match . matchTitle ( ) )
}
}
if matchViewStyle = = . feedStyle , let tournament = match . currentTournament ( ) {
Text ( tournament . tournamentTitle ( ) )
}
}
}
}
struct HeaderView : View {
@ Environment ( \ . filterOption ) private var filterOption
@ -365,7 +540,6 @@ struct PlanningView: View {
struct TimeSlotHeaderView : View {
let key : Date
let matches : [ Match ]
@ Environment ( Tournament . self ) var tournament : Tournament
var body : some View {
LabeledContent {
@ -379,7 +553,6 @@ struct PlanningView: View {
. fontWeight ( . semibold )
}
if matches . count <= tournament . courtCount {
let names = matches . sorted ( by : \ . computedOrder )
. compactMap ( { $0 . roundTitle ( ) } )
. reduce ( into : [ String ] ( ) ) { uniqueNames , name in
@ -388,14 +561,90 @@ struct PlanningView: View {
}
}
Text ( names . joined ( separator : " , " ) ) . lineLimit ( 1 ) . truncationMode ( . tail )
} else {
Text ( matches . count . formatted ( ) . appending ( " matchs " ) )
// i f m a t c h e s . c o u n t < = m a t c h e s . f i r s t ? . c o u r t C o u n t ( ) ? ? {
// } e l s e {
// T e x t ( m a t c h e s . c o u n t . f o r m a t t e d ( ) . a p p e n d i n g ( " m a t c h s " ) )
// }
}
}
}
struct CourtOptionsView : View {
let timeSlots : [ Date : [ Match ] ]
let underlined : Bool
var allMatches : [ Match ] {
timeSlots . flatMap { $0 . value }
}
private func _removeCourts ( ) {
allMatches . forEach { match in
match . courtIndex = nil
}
}
private func _eventCourtCount ( ) -> Int { timeSlots . first ? . value . first ? . currentTournament ( ) ? . eventObject ( ) ? . eventCourtCount ( ) ? ? 2
}
private func _save ( ) {
let groupByTournaments = allMatches . grouped { match in
match . currentTournament ( )
}
groupByTournaments . forEach { tournament , matches in
tournament ? . tournamentStore ? . matches . addOrUpdate ( contentOfs : matches )
}
}
var body : some View {
Menu {
Button ( " Supprimer " ) {
_removeCourts ( )
_save ( )
}
Button ( " Tirer au sort " ) {
_removeCourts ( )
let eventCourtCount = _eventCourtCount ( )
for slot in timeSlots {
var courtsAvailable = Array ( 0. . . eventCourtCount )
let matches = slot . value
matches . forEach { match in
if let rand = courtsAvailable . randomElement ( ) {
match . courtIndex = rand
courtsAvailable . remove ( elements : [ rand ] )
}
}
}
_save ( )
}
Button ( " Fixer par ordre croissant " ) {
_removeCourts ( )
let eventCourtCount = _eventCourtCount ( )
for slot in timeSlots {
var courtsAvailable = Array ( 0. . < eventCourtCount )
let matches = slot . value
for i in 0. . < matches . count {
if ! courtsAvailable . isEmpty {
let court = courtsAvailable . removeFirst ( )
matches [ i ] . courtIndex = court
}
}
}
_save ( )
}
} label : {
Text ( " Terrains " )
. underline ( underlined )
}
}
}
// s t r u c t B y S l o t V i e w : V i e w {
// @ E n v i r o n m e n t ( T o u r n a m e n t . s e l f ) v a r t o u r n a m e n t : T o u r n a m e n t
@ -558,7 +807,6 @@ enum PlanningFilterOption: Int, CaseIterable, Identifiable {
}
}
struct FilterOptionKey : EnvironmentKey {
static let defaultValue : PlanningFilterOption = . byDefault
}