@ -9,12 +9,22 @@ import SwiftUI
import TipKit
import LeStorage
let slideToDeleteTip = SlideToDeleteTip ( )
let inscriptionManagerWomanRankTip = InscriptionManagerWomanRankTip ( )
let fileTip = InscriptionManagerFileInputTip ( )
let pasteTip = InscriptionManagerPasteInputTip ( )
let searchTip = InscriptionManagerSearchInputTip ( )
let createTip = InscriptionManagerCreateInputTip ( )
let rankUpdateTip = InscriptionManagerRankUpdateTip ( )
let padelBeachExportTip = PadelBeachExportTip ( )
let padelBeachImportTip = PadelBeachImportTip ( )
struct InscriptionManagerView : View {
@ EnvironmentObject var dataStore : DataStore
@ EnvironmentObject var networkMonitor : NetworkMonitor
@ FetchRequest (
sortDescriptors : [ NSSortDescriptor ( keyPath : \ ImportedPlayer . rank , ascending : true ) ] ,
sortDescriptors : [ ] ,
animation : . default )
private var fetchPlayers : FetchedResults < ImportedPlayer >
@ -41,6 +51,13 @@ struct InscriptionManagerView: View {
@ State private var contactType : ContactType ? = nil
@ State private var sentError : ContactManagerError ? = nil
@ State private var showSubscriptionView : Bool = false
@ State private var registrationIssues : Int ? = nil
@ State private var sortedTeams : [ TeamRegistration ] = [ ]
@ State private var unfilteredTeams : [ TeamRegistration ] = [ ]
@ State private var walkoutTeams : [ TeamRegistration ] = [ ]
@ State private var unsortedTeamsWithoutWO : [ TeamRegistration ] = [ ]
@ State private var unsortedPlayers : [ PlayerRegistration ] = [ ]
@ State private var teamPaste : URL ?
var messageSentFailed : Binding < Bool > {
Binding {
@ -83,19 +100,8 @@ struct InscriptionManagerView: View {
}
}
let slideToDeleteTip = SlideToDeleteTip ( )
let inscriptionManagerWomanRankTip = InscriptionManagerWomanRankTip ( )
let fileTip = InscriptionManagerFileInputTip ( )
let pasteTip = InscriptionManagerPasteInputTip ( )
let searchTip = InscriptionManagerSearchInputTip ( )
let createTip = InscriptionManagerCreateInputTip ( )
let rankUpdateTip = InscriptionManagerRankUpdateTip ( )
let padelBeachExportTip = PadelBeachExportTip ( )
let padelBeachImportTip = PadelBeachImportTip ( )
let categoryOption : PlayerFilterOption
let filterable : Bool
let dates = Set ( SourceFileManager . shared . allFilesSortedByDate ( true ) . map ( { $0 . dateFromPath } ) ) . sorted ( ) . reversed ( )
init ( tournament : Tournament ) {
self . tournament = tournament
@ -110,6 +116,16 @@ struct InscriptionManagerView: View {
}
}
private func _clearScreen ( ) {
teamPaste = nil
unsortedPlayers . removeAll ( )
unfilteredTeams . removeAll ( )
walkoutTeams . removeAll ( )
unsortedTeamsWithoutWO . removeAll ( )
sortedTeams . removeAll ( )
registrationIssues = nil
}
// F u n c t i o n t o c r e a t e a s i m p l e h a s h f r o m a l i s t o f I D s
private func _simpleHash ( ids : [ String ] ) -> Int {
// C o m b i n e t h e h a s h v a l u e s o f e a c h s t r i n g
@ -121,7 +137,44 @@ struct InscriptionManagerView: View {
return _simpleHash ( ids : ids1 ) != _simpleHash ( ids : ids2 )
}
private func _setHash ( ) async {
#if DEBUG_TIME
let start = Date ( )
defer {
let duration = Duration . milliseconds ( Date ( ) . timeIntervalSince ( start ) * 1_000 )
print ( " func _setHash " , duration . formatted ( . units ( allowed : [ . seconds , . milliseconds ] ) ) )
}
#endif
let selectedSortedTeams = tournament . selectedSortedTeams ( )
if self . teamsHash = = nil , selectedSortedTeams . isEmpty = = false {
self . teamsHash = _simpleHash ( ids : selectedSortedTeams . map { $0 . id } )
}
}
private func _handleHashDiff ( ) async {
let newHash = _simpleHash ( ids : tournament . selectedSortedTeams ( ) . map { $0 . id } )
if let teamsHash , newHash != teamsHash {
self . teamsHash = newHash
if self . tournament . shouldVerifyBracket = = false || self . tournament . shouldVerifyGroupStage = = false {
self . tournament . shouldVerifyBracket = true
self . tournament . shouldVerifyGroupStage = true
let waitingList = self . tournament . waitingListTeams ( in : self . tournament . selectedSortedTeams ( ) )
waitingList . forEach { team in
if team . bracketPosition != nil || team . groupStagePosition != nil {
team . resetPositions ( )
}
}
do {
try dataStore . teamRegistrations . addOrUpdate ( contentOfs : waitingList )
try dataStore . tournaments . addOrUpdate ( instance : tournament )
} catch {
Logger . error ( error )
}
}
}
}
var body : some View {
VStack ( spacing : 0 ) {
@ -130,38 +183,18 @@ struct InscriptionManagerView: View {
_buildingTeamView ( )
} else if tournament . unsortedTeams ( ) . isEmpty {
_inscriptionTipsView ( )
} else {
}
if _isEditingTeam ( ) = = false {
_teamRegisteredView ( )
}
}
. onAppear {
let selectedSortedTeams = tournament . selectedSortedTeams ( )
if self . teamsHash = = nil , selectedSortedTeams . isEmpty = = false {
self . teamsHash = _simpleHash ( ids : selectedSortedTeams . map { $0 . id } )
}
_getTeams ( )
}
. onDisappear {
let newHash = _simpleHash ( ids : tournament . selectedSortedTeams ( ) . map { $0 . id } )
if let teamsHash , newHash != teamsHash {
self . teamsHash = newHash
if self . tournament . shouldVerifyBracket = = false || self . tournament . shouldVerifyGroupStage = = false {
self . tournament . shouldVerifyBracket = true
self . tournament . shouldVerifyGroupStage = true
let waitingList = self . tournament . waitingListTeams ( in : self . tournament . selectedSortedTeams ( ) )
waitingList . forEach { team in
if team . bracketPosition != nil || team . groupStagePosition != nil {
team . resetPositions ( )
}
}
Task {
await _handleHashDiff ( )
do {
try dataStore . teamRegistrations . addOrUpdate ( contentOfs : waitingList )
try dataStore . tournaments . addOrUpdate ( instance : tournament )
} catch {
Logger . error ( error )
}
}
}
}
. alert ( " Un problème est survenu " , isPresented : messageSentFailed ) {
@ -258,10 +291,14 @@ struct InscriptionManagerView: View {
. tint ( . master )
}
. onChange ( of : tournament . prioritizeClubMembers ) {
_clearScreen ( )
_save ( )
_getTeams ( )
}
. onChange ( of : tournament . teamSorting ) {
_clearScreen ( )
_save ( )
_getTeams ( )
}
. onChange ( of : currentRankSourceDate ) {
if let currentRankSourceDate , tournament . rankSourceDate != currentRankSourceDate {
@ -334,8 +371,10 @@ struct InscriptionManagerView: View {
Label ( " Clôturer " , systemImage : " lock " )
}
Divider ( )
ShareLink ( item : tournament . pasteDataForImporting ( ) . createTxtFile ( self . tournament . tournamentTitle ( . short ) ) ) {
Label ( " Exporter les paires " , systemImage : " square.and.arrow.up " )
if let teamPaste {
ShareLink ( item : teamPaste ) {
Label ( " Exporter les paires " , systemImage : " square.and.arrow.up " )
}
}
Button {
presentImportView = true
@ -373,7 +412,30 @@ struct InscriptionManagerView: View {
createdPlayerIds . isEmpty = = false || editedTeam != nil || pasteString != nil
}
private func _getTeams ( from sortedTeams : [ TeamRegistration ] ) -> [ TeamRegistration ] {
private func _prepareStats ( ) async {
#if DEBUG_TIME
let start = Date ( )
defer {
let duration = Duration . milliseconds ( Date ( ) . timeIntervalSince ( start ) * 1_000 )
print ( " func _prepareStats " , duration . formatted ( . units ( allowed : [ . seconds , . milliseconds ] ) ) )
}
#endif
unsortedPlayers = tournament . unsortedPlayers ( )
walkoutTeams = tournament . walkoutTeams ( )
unsortedTeamsWithoutWO = tournament . unsortedTeamsWithoutWO ( )
teamPaste = tournament . pasteDataForImporting ( ) . createTxtFile ( self . tournament . tournamentTitle ( . short ) )
}
private func _prepareTeams ( ) {
let start = Date ( )
defer {
let duration = Duration . milliseconds ( Date ( ) . timeIntervalSince ( start ) * 1_000 )
print ( " func _prepareTeams " , duration . formatted ( . units ( allowed : [ . seconds , . milliseconds ] ) ) )
}
sortedTeams = tournament . sortedTeams ( )
var teams = sortedTeams
if filterMode = = . walkOut {
teams = teams . filter ( { $0 . walkOut } )
@ -384,17 +446,34 @@ struct InscriptionManagerView: View {
}
if byDecreasingOrdering {
return teams . reversed ( )
self . unfilteredTeams = teams . reversed ( )
} else {
return teams
self . unfilteredTeams = teams
}
}
private func _getIssues ( ) async {
#if DEBUG_TIME
let start = Date ( )
defer {
let duration = Duration . milliseconds ( Date ( ) . timeIntervalSince ( start ) * 1_000 )
print ( " func _getIssues " , duration . formatted ( . units ( allowed : [ . seconds , . milliseconds ] ) ) )
}
#endif
await registrationIssues = tournament . registrationIssues ( )
}
private func _getTeams ( ) {
_prepareTeams ( )
Task {
await _prepareStats ( )
await _getIssues ( )
await _setHash ( )
}
}
private func _teamRegisteredView ( ) -> some View {
List {
let sortedTeams = tournament . sortedTeams ( )
let unfilteredTeams = _getTeams ( from : sortedTeams )
if presentSearch = = false {
_rankHandlerView ( )
_relatedTips ( )
@ -418,6 +497,7 @@ struct InscriptionManagerView: View {
Task {
await MainActor . run ( ) {
fetchPlayers . nsPredicate = _pastePredicate ( pasteField : searchField , mostRecentDate : SourceFileManager . shared . mostRecentDateAvailable )
fetchPlayers . nsSortDescriptors = [ NSSortDescriptor ( keyPath : \ ImportedPlayer . rank , ascending : true ) ]
pasteString = searchField
}
}
@ -474,6 +554,7 @@ struct InscriptionManagerView: View {
Task {
await MainActor . run {
fetchPlayers . nsPredicate = _pastePredicate ( pasteField : first , mostRecentDate : SourceFileManager . shared . mostRecentDateAvailable )
fetchPlayers . nsSortDescriptors = [ NSSortDescriptor ( keyPath : \ ImportedPlayer . rank , ascending : true ) ]
pasteString = first
autoSelect = true
}
@ -504,6 +585,8 @@ struct InscriptionManagerView: View {
@ ViewBuilder
func rankingDateSourcePickerView ( showDateInLabel : Bool ) -> some View {
Section {
let dates = Set ( SourceFileManager . shared . allFilesSortedByDate ( true ) . map ( { $0 . dateFromPath } ) ) . sorted ( ) . reversed ( )
Picker ( selection : $ currentRankSourceDate ) {
if currentRankSourceDate = = nil {
Text ( " inconnu " ) . tag ( nil as Date ? )
@ -571,6 +654,7 @@ struct InscriptionManagerView: View {
Task {
await MainActor . run {
fetchPlayers . nsPredicate = _pastePredicate ( pasteField : paste , mostRecentDate : SourceFileManager . shared . mostRecentDateAvailable )
fetchPlayers . nsSortDescriptors = [ NSSortDescriptor ( keyPath : \ ImportedPlayer . rank , ascending : true ) ]
pasteString = paste
autoSelect = true
}
@ -621,9 +705,6 @@ struct InscriptionManagerView: View {
private func _informationView ( count : Int ) -> some View {
Section {
let walkoutTeams = tournament . walkoutTeams ( )
let unsortedTeamsWithoutWO = tournament . unsortedTeamsWithoutWO ( )
LabeledContent {
Text ( unsortedTeamsWithoutWO . count . formatted ( ) + " / " + tournament . teamCount . formatted ( ) ) . font ( . largeTitle )
} label : {
@ -647,7 +728,11 @@ struct InscriptionManagerView: View {
. environment ( tournament )
} label : {
LabeledContent {
Text ( tournament . registrationIssues ( ) . formatted ( ) ) . font ( . largeTitle )
if let registrationIssues {
Text ( registrationIssues . formatted ( ) ) . font ( . largeTitle )
} else {
ProgressView ( )
}
} label : {
Text ( " Problèmes détéctés " )
if let closedRegistrationDate = tournament . closedRegistrationDate {
@ -660,43 +745,43 @@ struct InscriptionManagerView: View {
@ ViewBuilder
private func _relatedTips ( ) -> some View {
if pasteString = = ni l
&& createdPlayerIds . isEmpt y
&& tournament . unsortedTeams ( ) . count >= tournament . teamCoun t
&& tournament . unsortedPlayers ( ) . filter ( { $0 . source = = . beachPadel } ) . isEmpty {
Sec tion {
TipView ( padelBeachExportTip ) { action i n
if action . id = = " more-info-export " {
is Lea rn in gMore = true
}
if action . id = = " padel-beach " {
UIApplication . shared . open ( URLs . beachPadel . url )
}
}
. tipStyle ( tint : nil )
}
Sec tion {
TipView ( padelBeachImportTip ) { action i n
if action . id = = " more-info-import " {
p res en tImp ortView = true
}
}
. tipStyle ( tint : nil )
}
}
// i f p a s t e S t r i n g = = n i l
// & & c r e a t e d P l a y e r I d s . i s E m p t y
// & & t o u r n a m e n t . u n s o r t e d T e a m s ( ) . c o u n t > = t o u r n a m e n t . t e a m C o u n t
// & & t o u r n a m e n t . u n s o r t e d P l a y e r s ( ) . f i l t e r ( { $ 0 . s o u r c e = = . b e a c h P a d e l } ) . i s E m p t y {
// S ec t i on {
// T i p V i e w ( p a d e l B e a c h E x p o r t T i p ) { a c t i o n i n
// i f a c t i o n . i d = = " m o r e - i n f o - e x p o r t " {
// is L ea rn in g M o re = t r u e
// }
// i f a c t i o n . i d = = " p a d e l - b e a c h " {
// U I A p p l i c a t i o n . s h a r e d . o p e n ( U R L s . b e a c h P a d e l . u r l )
// }
// }
// . t i p S t y l e ( t i n t : n i l )
// }
// S ec t i on {
// T i p V i e w ( p a d e l B e a c h I m p o r t T i p ) { a c t i o n i n
// i f a c t i o n . i d = = " m o r e - i n f o - i m p o r t " {
// p r es en t I mp o r t V i ew = t r u e
// }
// }
// . t i p S t y l e ( t i n t : n i l )
// }
// }
//
if tournament . tournamentCategory = = . men && tournament . femalePlayers ( ) . isEmpty = = false {
if tournament . tournamentCategory = = . men && unsortedPlayers . filter ( { $0 . isMalePlayer ( ) = = false } ) . isEmpty = = false {
Section {
TipView ( inscriptionManagerWomanRankTip )
. tipStyle ( tint : nil )
}
}
Sec tion {
TipView ( slideToDeleteTip )
. tipStyle ( tint : nil )
}
//
// S ec t i o n {
// T i p V i e w ( s l i d e T o D e l e t e T i p )
// . t i p S t y l e ( t i n t : n i l )
// }
}
private func _searchSource ( ) -> String ? {
@ -777,6 +862,9 @@ struct InscriptionManagerView: View {
createdPlayers . removeAll ( )
createdPlayerIds . removeAll ( )
pasteString = nil
_clearScreen ( )
_getTeams ( )
}
private func _updateTeam ( ) {
@ -797,6 +885,8 @@ struct InscriptionManagerView: View {
createdPlayerIds . removeAll ( )
pasteString = nil
self . editedTeam = nil
_clearScreen ( )
_getTeams ( )
}
private func _buildingTeamView ( ) -> some View {
@ -873,6 +963,7 @@ struct InscriptionManagerView: View {
}
}
}
. headerProminence ( . increased )
. onReceive ( fetchPlayers . publisher . count ( ) ) { _ in // < - - h e r e
if let pasteString , count = = 2 , autoSelect = = true {
fetchPlayers . filter { $0 . hitForSearch ( pasteString ) >= hitTarget } . sorted ( by : { $0 . hitForSearch ( pasteString ) > $1 . hitForSearch ( pasteString ) } ) . forEach { player in
@ -999,14 +1090,19 @@ struct InscriptionManagerView: View {
Toggle ( isOn : . init ( get : {
return team . wildCardBracket
} , set : { value in
team . resetPositions ( )
team . wildCardGroupStage = false
team . walkOut = false
team . wildCardBracket = value
do {
try dataStore . teamRegistrations . addOrUpdate ( instance : team )
} catch {
Logger . error ( error )
_clearScreen ( )
Task {
team . resetPositions ( )
team . wildCardGroupStage = false
team . walkOut = false
team . wildCardBracket = value
do {
try dataStore . teamRegistrations . addOrUpdate ( instance : team )
} catch {
Logger . error ( error )
}
_getTeams ( )
}
} ) ) {
Label ( " Wildcard Tableau " , systemImage : team . wildCardBracket ? " circle.inset.filled " : " circle " )
@ -1015,14 +1111,19 @@ struct InscriptionManagerView: View {
Toggle ( isOn : . init ( get : {
return team . wildCardGroupStage
} , set : { value in
team . resetPositions ( )
team . wildCardBracket = false
team . walkOut = false
team . wildCardGroupStage = value
do {
try dataStore . teamRegistrations . addOrUpdate ( instance : team )
} catch {
Logger . error ( error )
_clearScreen ( )
Task {
team . resetPositions ( )
team . wildCardBracket = false
team . walkOut = false
team . wildCardGroupStage = value
do {
try dataStore . teamRegistrations . addOrUpdate ( instance : team )
} catch {
Logger . error ( error )
}
_getTeams ( )
}
} ) ) {
Label ( " Wildcard Poule " , systemImage : team . wildCardGroupStage ? " circle.inset.filled " : " circle " )
@ -1032,24 +1133,32 @@ struct InscriptionManagerView: View {
Toggle ( isOn : . init ( get : {
return team . walkOut
} , set : { value in
team . resetPositions ( )
team . wildCardBracket = false
team . wildCardGroupStage = false
team . walkOut = value
do {
try dataStore . teamRegistrations . addOrUpdate ( instance : team )
} catch {
Logger . error ( error )
_clearScreen ( )
Task {
team . resetPositions ( )
team . wildCardBracket = false
team . wildCardGroupStage = false
team . walkOut = value
do {
try dataStore . teamRegistrations . addOrUpdate ( instance : team )
} catch {
Logger . error ( error )
}
_getTeams ( )
}
} ) ) {
Label ( " WO " , systemImage : team . walkOut ? " circle.inset.filled " : " circle " )
}
Divider ( )
Button ( role : . destructive ) {
do {
try dataStore . teamRegistrations . delete ( instance : team )
} catch {
Logger . error ( error )
_clearScreen ( )
Task {
do {
try dataStore . teamRegistrations . delete ( instance : team )
} catch {
Logger . error ( error )
}
_getTeams ( )
}
} label : {
LabelDelete ( )