@ -11,7 +11,8 @@ import LeStorage
struct TournamentRankView : View {
@ Environment ( Tournament . self ) var tournament : Tournament
@ EnvironmentObject var dataStore : DataStore
@ Environment ( \ . editMode ) private var editMode
@ State private var rankings : [ Int : [ TeamRegistration ] ] = [ : ]
@ State private var calculating = false
@ State private var selectedTeam : TeamRegistration ?
@ -33,97 +34,71 @@ struct TournamentRankView: View {
var body : some View {
List {
@ Bindable var tournament = tournament
let rankingPublished = tournament . selectedSortedTeams ( ) . allSatisfy ( { $0 . finalRanking != nil } )
Section {
LabeledContent {
if let matchesLeft {
Text ( matchesLeft . count . formatted ( ) )
} else {
ProgressView ( )
let rankingsCalculated = tournament . selectedSortedTeams ( ) . anySatisfy ( { $0 . finalRanking != nil } )
if editMode ? . wrappedValue . isEditing = = false {
Section {
MatchListView ( section : " Matchs restant " , matches : matchesLeft , hideWhenEmpty : false , isExpanded : false )
MatchListView ( section : " Matchs en cours " , matches : runningMatches , hideWhenEmpty : false , isExpanded : false )
Toggle ( isOn : $ tournament . hidePointsEarned ) {
Text ( " Masquer les points gagnés " )
}
} label : {
Text ( " Matchs restant " )
}
LabeledContent {
if let runningMatches {
Text ( runningMatches . count . formatted ( ) )
} else {
ProgressView ( )
. onChange ( of : tournament . hidePointsEarned ) {
do {
try dataStore . tournaments . addOrUpdate ( instance : tournament )
} catch {
Logger . error ( error )
}
}
} label : {
Text ( " Matchs en cours " )
}
LabeledContent {
if rankingPublished {
Image ( systemName : " checkmark " )
. foregroundStyle ( . green )
} else {
Image ( systemName : " xmark " )
. foregroundStyle ( . logoRed )
Toggle ( isOn : $ tournament . publishRankings ) {
Text ( " Publier sur Padel Club " )
if let url = tournament . shareURL ( . rankings ) {
Link ( destination : url ) {
Text ( " Accéder à la page " )
}
}
}
} label : {
Text ( " Classement publié " )
}
Toggle ( isOn : $ tournament . hidePointsEarned ) {
Text ( " Masquer les points gagnés " )
}
. onChange ( of : tournament . hidePointsEarned ) {
do {
try dataStore . tournaments . addOrUpdate ( instance : tournament )
} catch {
Logger . error ( error )
. onChange ( of : tournament . publishRankings ) {
do {
try dataStore . tournaments . addOrUpdate ( instance : tournament )
} catch {
Logger . error ( error )
}
}
}
}
if rankingPublished = = false {
RowButtonView ( " Publier le classement " , role : . destructive ) {
_publishRankings ( )
if ( editMode ? . wrappedValue . isEditing = = true || rankingsCalculated = = false ) && calculating = = false {
Section {
RowButtonView ( rankingsCalculated ? " Re-calculer le classement " : " Calculer " , role : . destructive ) {
await _calculateRankings ( )
}
} else {
RowButtonView ( " Re-publier le classement " , role : . destructive ) {
_publishRankings ( )
} footer : {
if rankingsCalculated {
Text ( " Vos éditions seront perdus. " )
}
}
}
if rankingPublished {
Section {
RowButtonView ( " Supprimer le classement " , role : . destructive ) {
tournament . unsortedTeams ( ) . forEach { team in
team . finalRanking = nil
team . pointsEarned = nil
if rankingsCalculated {
Section {
RowButtonView ( " Supprimer le classement " , role : . destructive ) {
tournament . unsortedTeams ( ) . forEach { team in
team . finalRanking = nil
team . pointsEarned = nil
}
_save ( )
}
_save ( )
}
} footer : {
Text ( . init ( " Masque également le classement sur le site [Padel Club]( \( URLs . main . rawValue ) ) " ) )
}
}
if rankingPublished {
let teamsRanked = tournament . teamsRanked ( )
if calculating = = false && rankingsCalculated && teamsRanked . isEmpty = = false {
Section {
ForEach ( tournament . teamsRanked ( ) ) { team in
let key = team . finalRanking ? ? 0
Button {
selectedTeam = team
} label : {
TeamRankCellView ( team : team , key : key )
. frame ( maxWidth : . infinity )
}
. contentShape ( Rectangle ( ) )
. buttonStyle ( . plain )
}
} footer : {
Text ( " Vous pouvez appuyer sur une ligne pour éditer manuellement le classement calculé par Padel Club. " )
}
} else {
let keys = rankings . keys . sorted ( )
ForEach ( keys , id : \ . self ) { key in
if let rankedTeams = rankings [ key ] {
ForEach ( rankedTeams ) { team in
ForEach ( teamsRanked ) { team in
if let key = team . finalRanking {
TeamRankCellView ( team : team , key : key )
}
}
@ -165,12 +140,10 @@ struct TournamentRankView: View {
}
} )
. onAppear {
let rankingPublished = tournament . selectedSortedTeams ( ) . all Satisfy ( { $0 . finalRanking != nil } )
let rankingPublished = tournament . selectedSortedTeams ( ) . any Satisfy ( { $0 . finalRanking != nil } )
if rankingPublished = = false {
calculating = true
Task {
await _calculateRankings ( )
calculating = false
}
}
}
@ -179,103 +152,157 @@ struct TournamentRankView: View {
. toolbarBackground ( . visible , for : . navigationBar )
. toolbar {
ToolbarItem ( placement : . topBarTrailing ) {
if let url = tournament . shareURL ( . rankings ) {
_actionForURL ( url )
}
EditButton ( )
}
}
}
struct TeamRankCellView : View {
@ Environment ( \ . editMode ) private var editMode
@ Environment ( Tournament . self ) var tournament : Tournament
let team : TeamRegistration
let key : Int
@ EnvironmentObject var dataStore : DataStore
@ State private var isEditingTeam : Bool = false
@ Bindable var team : TeamRegistration
@ State var key : Int
var body : some View {
HStack {
VStack ( alignment : . trailing ) {
VStack ( alignment : . trailing , spacing : - 8.0 ) {
ZStack ( alignment : . trailing ) {
Text ( tournament . teamCount . formatted ( ) ) . hidden ( )
Text ( key . formatted ( ) )
}
. monospacedDigit ( )
. font ( . largeTitle )
. fontWeight ( . bold )
Text ( key . ordinalFormattedSuffix ( ) ) . font ( . caption )
}
if let index = tournament . indexOf ( team : team ) {
let rankingDifference = index - ( key - 1 )
if rankingDifference > 0 {
HStack ( spacing : 0.0 ) {
Text ( rankingDifference . formatted ( . number . sign ( strategy : . always ( ) ) ) )
. monospacedDigit ( )
Image ( systemName : " arrowtriangle.up.fill " )
. imageScale ( . small )
}
. foregroundColor ( . green )
} else if rankingDifference < 0 {
HStack ( spacing : 0.0 ) {
Text ( rankingDifference . formatted ( . number . sign ( strategy : . always ( ) ) ) )
. monospacedDigit ( )
Image ( systemName : " arrowtriangle.down.fill " )
. imageScale ( . small )
VStack ( spacing : 0 ) {
if editMode ? . wrappedValue . isEditing = = true {
if key > 1 {
Button {
key -= 1
team . finalRanking = key
do {
try dataStore . teamRegistrations . addOrUpdate ( instance : team )
} catch {
Logger . error ( error )
}
. foregroundColor ( . red )
} else {
Text ( " -- " )
} label : {
Label ( " descendre " , systemImage : " chevron.compact.up " ) . labelStyle ( . iconOnly )
}
. buttonStyle ( . bordered )
}
}
Divider ( )
VStack ( alignment : . leading ) {
if let name = team . name {
Text ( name ) . foregroundStyle ( . secondary )
}
ForEach ( team . players ( ) ) { player in
VStack ( alignment : . leading , spacing : - 4.0 ) {
Text ( player . playerLabel ( ) ) . bold ( )
HStack ( alignment : . firstTextBaseline , spacing : 0.0 ) {
Text ( player . rankLabel ( ) )
if let rank = player . getRank ( ) {
Text ( rank . ordinalFormattedSuffix ( ) )
. font ( . caption )
Button {
isEditingTeam = true
} label : {
HStack {
VStack ( alignment : . trailing ) {
VStack ( alignment : . trailing , spacing : - 8.0 ) {
ZStack ( alignment : . trailing ) {
Text ( tournament . teamCount . formatted ( ) ) . hidden ( )
Text ( key . formatted ( ) )
}
. monospacedDigit ( )
. font ( . largeTitle )
. fontWeight ( . bold )
Text ( key . ordinalFormattedSuffix ( ) ) . font ( . caption )
}
if let index = tournament . indexOf ( team : team ) {
let rankingDifference = index - ( key - 1 )
if rankingDifference > 0 {
HStack ( spacing : 0.0 ) {
Text ( rankingDifference . formatted ( . number . sign ( strategy : . always ( ) ) ) )
. monospacedDigit ( )
Image ( systemName : " arrowtriangle.up.fill " )
. imageScale ( . small )
}
. foregroundColor ( . green )
} else if rankingDifference < 0 {
HStack ( spacing : 0.0 ) {
Text ( rankingDifference . formatted ( . number . sign ( strategy : . always ( ) ) ) )
. monospacedDigit ( )
Image ( systemName : " arrowtriangle.down.fill " )
. imageScale ( . small )
}
. foregroundColor ( . red )
} else {
Text ( " -- " )
}
}
}
Divider ( )
VStack ( alignment : . leading ) {
if let name = team . name {
Text ( name ) . foregroundStyle ( . secondary )
}
ForEach ( team . players ( ) ) { player in
VStack ( alignment : . leading , spacing : - 4.0 ) {
Text ( player . playerLabel ( ) ) . bold ( )
HStack ( alignment : . firstTextBaseline , spacing : 0.0 ) {
Text ( player . rankLabel ( ) )
if let rank = player . getRank ( ) {
Text ( rank . ordinalFormattedSuffix ( ) )
. font ( . caption )
}
}
}
}
}
if tournament . isAnimation ( ) = = false && key > 0 {
Spacer ( )
VStack ( alignment : . trailing ) {
HStack ( alignment : . lastTextBaseline , spacing : 0.0 ) {
Text ( tournament . tournamentLevel . points ( for : key - 1 , count : tournament . teamCount ) . formatted ( . number . sign ( strategy : . always ( ) ) ) )
Text ( " pts " ) . font ( . caption )
}
}
}
}
. frame ( maxWidth : . infinity )
}
if tournament . isAnimation ( ) = = false && key > 0 {
Spacer ( )
VStack ( alignment : . trailing ) {
HStack ( alignment : . lastTextBaseline , spacing : 0.0 ) {
Text ( tournament . tournamentLevel . points ( for : key - 1 , count : tournament . teamCount ) . formatted ( . number . sign ( strategy : . always ( ) ) ) )
Text ( " pts " ) . font ( . caption )
. contentShape ( Rectangle ( ) )
. buttonStyle ( . plain )
if editMode ? . wrappedValue . isEditing = = true {
Button {
key += 1
team . finalRanking = key
do {
try dataStore . teamRegistrations . addOrUpdate ( instance : team )
} catch {
Logger . error ( error )
}
} label : {
Label ( " descendre " , systemImage : " chevron.compact.down " ) . labelStyle ( . iconOnly )
}
. buttonStyle ( . bordered )
}
}
}
}
private func _publishRankings ( ) {
rankings . keys . sorted ( ) . forEach { rank in
if let rankedTeams = rankings [ rank ] {
rankedTeams . forEach { team in
team . finalRanking = rank
team . pointsEarned = tournament . isAnimation ( ) ? nil : tournament . tournamentLevel . points ( for : rank - 1 , count : tournament . teamCount )
. alert ( " Position " , isPresented : $ isEditingTeam ) {
TextField ( " Position " , value : $ team . finalRanking , format : . number )
. keyboardType ( . numberPad )
. multilineTextAlignment ( . trailing )
. frame ( maxWidth : . infinity )
Button ( " Valider " ) {
team . pointsEarned = tournament . isAnimation ( ) ? nil : tournament . tournamentLevel . points ( for : key - 1 , count : tournament . teamCount )
do {
try dataStore . teamRegistrations . addOrUpdate ( instance : team )
} catch {
Logger . error ( error )
}
isEditingTeam = false
}
Button ( " Annuler " , role : . cancel ) {
isEditingTeam = false
}
}
}
_save ( )
}
private func _calculateRankings ( ) async {
await MainActor . run {
calculating = true
}
let finalRanks = await tournament . finalRanking ( )
finalRanks . keys . sorted ( ) . forEach { rank in
if let rankedTeamIds = finalRanks [ rank ] {
@ -283,27 +310,21 @@ struct TournamentRankView: View {
self . rankings [ rank ] = teams
}
}
}
@ ViewBuilder
private func _actionForURL ( _ url : URL , removeSource : Bool = false ) -> some View {
Menu {
Button {
UIApplication . shared . open ( url )
} label : {
Label ( " Voir " , systemImage : " safari " )
await MainActor . run {
rankings . keys . sorted ( ) . forEach { rank in
if let rankedTeams = rankings [ rank ] {
rankedTeams . forEach { team in
team . finalRanking = rank
team . pointsEarned = tournament . isAnimation ( ) ? nil : tournament . tournamentLevel . points ( for : rank - 1 , count : tournament . teamCount )
}
}
}
_save ( )
ShareLink ( item : url ) {
Label ( " Partager le lien " , systemImage : " link " )
}
} label : {
Image ( systemName : " square.and.arrow.up " )
calculating = false
}
. frame ( maxWidth : . infinity )
. buttonStyle ( . borderless )
}
private func _save ( ) {
do {