diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index acd4aa9..094c04d 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -798,6 +798,12 @@ FFBF41822BF73EB3001B24CB /* EventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF41812BF73EB3001B24CB /* EventView.swift */; }; FFBF41842BF75ED7001B24CB /* EventTournamentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF41832BF75ED7001B24CB /* EventTournamentsView.swift */; }; FFBF41862BF75FDA001B24CB /* EventSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF41852BF75FDA001B24CB /* EventSettingsView.swift */; }; + FFBFC3912CEE3A0E000EBD8D /* RegisrationSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBFC3902CEE3A0E000EBD8D /* RegisrationSetupView.swift */; }; + FFBFC3922CEE3A0E000EBD8D /* RegisrationSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBFC3902CEE3A0E000EBD8D /* RegisrationSetupView.swift */; }; + FFBFC3932CEE3A0E000EBD8D /* RegisrationSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBFC3902CEE3A0E000EBD8D /* RegisrationSetupView.swift */; }; + FFBFC3952CF05CBB000EBD8D /* DateMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBFC3942CF05CBB000EBD8D /* DateMenuView.swift */; }; + FFBFC3962CF05CBB000EBD8D /* DateMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBFC3942CF05CBB000EBD8D /* DateMenuView.swift */; }; + FFBFC3972CF05CBB000EBD8D /* DateMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBFC3942CF05CBB000EBD8D /* DateMenuView.swift */; }; FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1032BAC28C6008D6F59 /* ClubSearchView.swift */; }; FFC1E1082BAC29FC008D6F59 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */; }; FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */; }; @@ -1188,6 +1194,8 @@ FFBF41812BF73EB3001B24CB /* EventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventView.swift; sourceTree = ""; }; FFBF41832BF75ED7001B24CB /* EventTournamentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTournamentsView.swift; sourceTree = ""; }; FFBF41852BF75FDA001B24CB /* EventSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventSettingsView.swift; sourceTree = ""; }; + FFBFC3902CEE3A0E000EBD8D /* RegisrationSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisrationSetupView.swift; sourceTree = ""; }; + FFBFC3942CF05CBB000EBD8D /* DateMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateMenuView.swift; sourceTree = ""; }; FFC1E1032BAC28C6008D6F59 /* ClubSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubSearchView.swift; sourceTree = ""; }; FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = ""; }; FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkFederalService.swift; sourceTree = ""; }; @@ -1672,6 +1680,7 @@ isa = PBXGroup; children = ( FF70916D2B9108C600AB08DA /* InscriptionManagerView.swift */, + FFBFC3902CEE3A0E000EBD8D /* RegisrationSetupView.swift */, FF90FC1C2C44FB3E009339B2 /* AddTeamView.swift */, FF8F26422BADFE5B00650388 /* TournamentSettingsView.swift */, FF8F26532BAE1E4400650388 /* TableStructureView.swift */, @@ -1772,6 +1781,7 @@ FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */, FFE2D2E12C231BEE00D0C7BE /* SupportButtonView.swift */, FFE103112C366E5900684FC9 /* ImagePickerView.swift */, + FFBFC3942CF05CBB000EBD8D /* DateMenuView.swift */, ); path = Shared; sourceTree = ""; @@ -2426,6 +2436,7 @@ C45BAE442BCA753E002EEC8A /* Purchase.swift in Sources */, FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */, FF967CEE2BAECBD700A9A3BD /* Round.swift in Sources */, + FFBFC3922CEE3A0E000EBD8D /* RegisrationSetupView.swift in Sources */, FF5BAF6E2BE0B3C8008B4B7E /* FederalDataViewModel.swift in Sources */, FF3F74FF2B91A2D4004CFE0E /* AgendaDestination.swift in Sources */, FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */, @@ -2458,6 +2469,7 @@ FF6525C32C8C61B400B9498E /* LoserBracketFromGroupStageView.swift in Sources */, FF5D30512BD94E1000F2B93D /* ImportedPlayer+Extensions.swift in Sources */, FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */, + FFBFC3962CF05CBB000EBD8D /* DateMenuView.swift in Sources */, FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */, FF70916E2B9108C600AB08DA /* InscriptionManagerView.swift in Sources */, FF77CE542CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */, @@ -2711,6 +2723,7 @@ FF4CBFB92C996C0600151637 /* Purchase.swift in Sources */, FF4CBFBA2C996C0600151637 /* Screen.swift in Sources */, FF4CBFBB2C996C0600151637 /* Round.swift in Sources */, + FFBFC3912CEE3A0E000EBD8D /* RegisrationSetupView.swift in Sources */, FF4CBFBC2C996C0600151637 /* FederalDataViewModel.swift in Sources */, FF4CBFBD2C996C0600151637 /* AgendaDestination.swift in Sources */, FF4CBFBE2C996C0600151637 /* PadelClubApp.xcdatamodeld in Sources */, @@ -2743,6 +2756,7 @@ FF4CBFD72C996C0600151637 /* LoserBracketFromGroupStageView.swift in Sources */, FF4CBFD82C996C0600151637 /* ImportedPlayer+Extensions.swift in Sources */, FF4CBFD92C996C0600151637 /* ClubSearchView.swift in Sources */, + FFBFC3972CF05CBB000EBD8D /* DateMenuView.swift in Sources */, FF4CBFDA2C996C0600151637 /* PlayerPopoverView.swift in Sources */, FF4CBFDB2C996C0600151637 /* InscriptionManagerView.swift in Sources */, FF77CE522CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */, @@ -2975,6 +2989,7 @@ FF70FB382C90584900129CC2 /* Purchase.swift in Sources */, FF70FB392C90584900129CC2 /* Screen.swift in Sources */, FF70FB3A2C90584900129CC2 /* Round.swift in Sources */, + FFBFC3932CEE3A0E000EBD8D /* RegisrationSetupView.swift in Sources */, FF70FB3B2C90584900129CC2 /* FederalDataViewModel.swift in Sources */, FF70FB3C2C90584900129CC2 /* AgendaDestination.swift in Sources */, FF70FB3D2C90584900129CC2 /* PadelClubApp.xcdatamodeld in Sources */, @@ -3007,6 +3022,7 @@ FF70FB562C90584900129CC2 /* LoserBracketFromGroupStageView.swift in Sources */, FF70FB572C90584900129CC2 /* ImportedPlayer+Extensions.swift in Sources */, FF70FB582C90584900129CC2 /* ClubSearchView.swift in Sources */, + FFBFC3952CF05CBB000EBD8D /* DateMenuView.swift in Sources */, FF70FB592C90584900129CC2 /* PlayerPopoverView.swift in Sources */, FF70FB5A2C90584900129CC2 /* InscriptionManagerView.swift in Sources */, FF77CE532CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */, diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index 4dbc043..fe6b270 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -38,6 +38,22 @@ final class PlayerRegistration: ModelObject, Storable { var source: PlayerDataSource? var hasArrived: Bool = false + var coach: Bool = false + var captain: Bool = false + + func localizedSourceLabel() -> String { + switch source { + case .frenchFederation: + return "Via la base fédérale" + case .beachPadel: + return "Via le fichier beach-padel" + case .onlineRegistration: + return "Via un inscription en ligne" + case nil: + return "Manuellement" + } + } + init(teamRegistration: String? = nil, firstName: String, lastName: String, licenceId: String? = nil, rank: Int? = nil, paymentType: PlayerPaymentType? = nil, sex: PlayerSexType? = nil, tournamentPlayed: Int? = nil, points: Double? = nil, clubName: String? = nil, ligueName: String? = nil, assimilation: String? = nil, phoneNumber: String? = nil, email: String? = nil, birthdate: String? = nil, computedRank: Int = 0, source: PlayerDataSource? = nil, hasArrived: Bool = false) { self.teamRegistration = teamRegistration @@ -391,6 +407,7 @@ final class PlayerRegistration: ModelObject, Storable { enum PlayerDataSource: Int, Codable { case frenchFederation = 0 case beachPadel = 1 + case onlineRegistration = 2 } enum PlayerSexType: Int, Hashable, CaseIterable, Identifiable, Codable { diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index 14909a2..94cd724 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -39,6 +39,21 @@ final class TeamRegistration: ModelObject, Storable { var finalRanking: Int? var pointsEarned: Int? + var unregistered: Bool = false + var unregistrationDate: Date? = nil + + func hasUnregistered() -> Bool { + unregistered + } + + func hasRegisteredOnline() -> Bool { + players().anySatisfy({ $0.source == .onlineRegistration }) + } + + func isOutOfTournament() -> Bool { + walkOut || unregistered + } + init(tournament: String, groupStage: String? = nil, registrationDate: Date? = nil, callDate: Date? = nil, bracketPosition: Int? = nil, groupStagePosition: Int? = nil, comment: String? = nil, source: String? = nil, sourceValue: String? = nil, logo: String? = nil, name: String? = nil, walkOut: Bool = false, wildCardBracket: Bool = false, wildCardGroupStage: Bool = false, weight: Int = 0, lockedWeight: Int? = nil, confirmationDate: Date? = nil, qualified: Bool = false) { self.tournament = tournament self.groupStage = groupStage @@ -67,7 +82,7 @@ final class TeamRegistration: ModelObject, Storable { // MARK: - Computed dependencies func unsortedPlayers() -> [PlayerRegistration] { - return self.tournamentStore.playerRegistrations.filter { $0.teamRegistration == self.id } + return self.tournamentStore.playerRegistrations.filter { $0.teamRegistration == self.id && $0.coach == false } } // MARK: - @@ -314,6 +329,7 @@ final class TeamRegistration: ModelObject, Storable { } func initialRoundColor() -> Color? { + if unregistered { return Color.black } if walkOut { return Color.logoRed } if groupStagePosition != nil { return Color.blue } if let initialRound = initialRound(), let colorHex = RoundRule.colors[safe: initialRound.index] { @@ -463,7 +479,7 @@ final class TeamRegistration: ModelObject, Storable { func players() -> [PlayerRegistration] { - self.tournamentStore.playerRegistrations.filter { $0.teamRegistration == self.id }.sorted { (lhs, rhs) in + self.unsortedPlayers().sorted { (lhs, rhs) in let predicates: [AreInIncreasingOrder] = [ { $0.sex?.rawValue ?? 0 < $1.sex?.rawValue ?? 0 }, { $0.rank ?? 0 < $1.rank ?? 0 }, @@ -483,6 +499,10 @@ final class TeamRegistration: ModelObject, Storable { } } + func coaches() -> [PlayerRegistration] { + tournamentStore.playerRegistrations.filter({ $0.coach }) + } + func setWeight(from players: [PlayerRegistration], inTournamentCategory tournamentCategory: TournamentCategory) { let significantPlayerCount = significantPlayerCount() weight = (players.prefix(significantPlayerCount).map { $0.computedRank } + missingPlayerType(inTournamentCategory: tournamentCategory).map { unrankValue(for: $0 == 1 ? true : false ) }).prefix(significantPlayerCount).reduce(0,+) diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 132db89..025212a 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -60,7 +60,12 @@ final class Tournament : ModelObject, Storable { var loserBracketMode: LoserBracketMode = .automatic var initialSeedRound: Int = 0 var initialSeedCount: Int = 0 - + var enableOnlineRegistration: Bool = false + var registrationDateLimit: Date? = nil + var openingRegistrationDate: Date? = nil + var targetTeamCount: Int? = nil + var waitingListLimit: Int? = nil + @ObservationIgnored var navigationPath: [Screen] = [] @@ -851,7 +856,7 @@ defer { } #endif var _sortedTeams : [TeamRegistration] = [] - var _teams = unsortedTeams().filter({ $0.walkOut == false }) + var _teams = unsortedTeams().filter({ $0.isOutOfTournament() == false }) if let closedRegistrationDate { _teams = _teams.filter({ team in @@ -907,7 +912,7 @@ defer { func waitingListTeams(in teams: [TeamRegistration], includingWalkOuts: Bool) -> [TeamRegistration] { let waitingList = Set(unsortedTeams()).subtracting(teams) - let waitings = waitingList.filter { $0.walkOut == false }.sorted(using: _defaultSorting(), order: .ascending) + let waitings = waitingList.filter { $0.isOutOfTournament() == false }.sorted(using: _defaultSorting(), order: .ascending) let walkOuts = waitingList.filter { $0.walkOut == true }.sorted(using: _defaultSorting(), order: .ascending) if includingWalkOuts { return waitings + walkOuts @@ -950,7 +955,7 @@ defer { } func unsortedTeamsWithoutWO() -> [TeamRegistration] { - return self.tournamentStore.teamRegistrations.filter { $0.walkOut == false } + return self.tournamentStore.teamRegistrations.filter { $0.isOutOfTournament() == false } // return Store.main.filter { $0.tournament == self.id && $0.walkOut == false } } diff --git a/PadelClub/Extensions/Date+Extensions.swift b/PadelClub/Extensions/Date+Extensions.swift index de590b3..8214bb7 100644 --- a/PadelClub/Extensions/Date+Extensions.swift +++ b/PadelClub/Extensions/Date+Extensions.swift @@ -254,4 +254,9 @@ extension Date { formatter.unitsStyle = .abbreviated // You can choose .abbreviated or .short return formatter }() + + func truncateMinutesAndSeconds() -> Date { + let calendar = Calendar.current + return calendar.date(bySetting: .minute, value: 0, of: self)!.withoutSeconds() + } } diff --git a/PadelClub/Views/Player/PlayerDetailView.swift b/PadelClub/Views/Player/PlayerDetailView.swift index 69c153a..fe4a275 100644 --- a/PadelClub/Views/Player/PlayerDetailView.swift +++ b/PadelClub/Views/Player/PlayerDetailView.swift @@ -34,7 +34,11 @@ struct PlayerDetailView: View { Form { Section { Toggle("Joueur sur place", isOn: $player.hasArrived) - + Toggle("Capitaine", isOn: $player.captain) + Toggle("Coach", isOn: $player.coach) + } + + Section { LabeledContent { TextField("Nom", text: $player.lastName) .keyboardType(.alphabet) @@ -66,6 +70,8 @@ struct PlayerDetailView: View { if let birthdate = player.birthdate { Text(birthdate) } + } footer: { + Text(player.localizedSourceLabel()) } Section { @@ -223,7 +229,7 @@ struct PlayerDetailView: View { } } } - .onChange(of: player.hasArrived) { + .onChange(of: [player.hasArrived, player.captain, player.coach]) { _save() } .onChange(of: player.sex) { diff --git a/PadelClub/Views/Shared/DateMenuView.swift b/PadelClub/Views/Shared/DateMenuView.swift new file mode 100644 index 0000000..cfcce39 --- /dev/null +++ b/PadelClub/Views/Shared/DateMenuView.swift @@ -0,0 +1,33 @@ +// +// DateMenuView.swift +// PadelClub +// +// Created by razmig on 22/11/2024. +// + + +import SwiftUI + +struct DateMenuView: View { + @Binding var date: Date + + private func adjustDate(byDays days: Int) { + let calendar = Calendar.current + if let newDate = calendar.date(byAdding: .day, value: days, to: date) { + date = newDate.truncateMinutesAndSeconds() + } + } + + var body: some View { + Menu { + Button("24h avant") { adjustDate(byDays: -1) } + Button("48h avant") { adjustDate(byDays: -2) } + Divider() + Button("24h après") { adjustDate(byDays: 1) } + Button("48h après") { adjustDate(byDays: 2) } + } label: { + Text("Ajuster la date") + .underline() + } + } +} diff --git a/PadelClub/Views/Team/EditingTeamView.swift b/PadelClub/Views/Team/EditingTeamView.swift index f8cc5d5..9432a3e 100644 --- a/PadelClub/Views/Team/EditingTeamView.swift +++ b/PadelClub/Views/Team/EditingTeamView.swift @@ -94,6 +94,34 @@ struct EditingTeamView: View { } } + Toggle(isOn: .init(get: { + return team.unregistered + }, set: { value in + team.resetPositions() + team.wildCardGroupStage = false + team.walkOut = false + team.wildCardBracket = false + team.unregistered = value + if value { + team.unregistrationDate = Date() + } else { + team.unregistered = false + team.unregistrationDate = nil + team.registrationDate = Date() + } + do { + try tournamentStore.teamRegistrations.addOrUpdate(instance: team) + } catch { + Logger.error(error) + } + })) { + Text("Annulation") + if let unregisterDate = team.unregistrationDate { + Text(unregisterDate.localizedDate()) + } + } + + Toggle(isOn: .init(get: { return team.wildCardBracket }, set: { value in diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index 7dd6dc2..8a6e2f4 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -72,8 +72,10 @@ struct InscriptionManagerView: View { enum FilterMode: Int, Identifiable, CaseIterable { var id: Int { self.rawValue } case all + case registeredOnline case walkOut case waiting + case unregistered case bracket case groupStage case wildcardGroupStage @@ -88,6 +90,8 @@ struct InscriptionManagerView: View { return "Vous n'avez aucune wildcard en poule." case .all: return "Vous n'avez encore aucune équipe inscrite." + case .registeredOnline: + return "Aucune équipe inscrite en ligne." case .walkOut: return "Vous n'avez aucune équipe forfait." case .waiting: @@ -98,6 +102,8 @@ struct InscriptionManagerView: View { return "Vous n'avez placé aucune équipe en poule." case .notImported: return "Vous n'avez aucune équipe non importé. Elles proviennent toutes du fichier." + case .unregistered: + return "Vous n'avez aucune équipe ayant annulé une inscription." } } @@ -109,6 +115,8 @@ struct InscriptionManagerView: View { return "Aucune wildcard en poule" case .all: return "Aucune équipe inscrite" + case .registeredOnline: + return "Aucune équipe inscrite en ligne" case .walkOut: return "Aucune équipe forfait" case .waiting: @@ -119,6 +127,8 @@ struct InscriptionManagerView: View { return "Aucune équipe en poule" case .notImported: return "Aucune équipe non importée" + case .unregistered: + return "Aucune équipe ayant annulé" } } @@ -130,6 +140,8 @@ struct InscriptionManagerView: View { return displayStyle == .wide ? "Wildcard Poule" : "wc poule" case .all: return displayStyle == .wide ? "Équipes inscrites" : "inscris" + case .registeredOnline: + return displayStyle == .wide ? "Inscrites en ligne" : "en ligne" case .bracket: return displayStyle == .wide ? "En Tableau" : "tableau" case .groupStage: @@ -140,10 +152,13 @@ struct InscriptionManagerView: View { return displayStyle == .wide ? "Liste d'attente" : "attente" case .notImported: return "Non importées" + case .unregistered: + return displayStyle == .wide ? "Retirées" : "retirées" } } } + init(tournament: Tournament) { self.tournament = tournament _currentRankSourceDate = State(wrappedValue: tournament.rankSourceDate) @@ -229,6 +244,16 @@ struct InscriptionManagerView: View { } } } + .refreshable { + do { + self.tournament.tournamentStore.teamRegistrations.reset() + try await self.tournament.tournamentStore.teamRegistrations.loadDataFromServerIfAllowed() + self.tournament.tournamentStore.playerRegistrations.reset() + try await self.tournament.tournamentStore.playerRegistrations.loadDataFromServerIfAllowed() + } catch { + Logger.error(error) + } + } .onAppear { _setHash() } @@ -320,6 +345,12 @@ struct InscriptionManagerView: View { .symbolVariant(filterMode == .all ? .none : .fill) } Menu { + NavigationLink { + RegisrationSetupView(tournament: tournament) + } label: { + Text("Inscription en ligne") + } + if tournament.isAnimation() == false { if tournament.inscriptionClosed() == false { Menu { @@ -664,6 +695,12 @@ struct InscriptionManagerView: View { case .notImported: let notImported: Int = max(0, sortedTeams.filter({ $0.isImported() == false }).count) return notImported.formatted() + case .unregistered: + let unregistered: Int = max(0, sortedTeams.filter({ $0.hasUnregistered() }).count) + return unregistered.formatted() + case .registeredOnline: + let registeredOnline: Int = max(0, sortedTeams.filter({ $0.hasRegisteredOnline() }).count) + return registeredOnline.formatted() } } diff --git a/PadelClub/Views/Tournament/Screen/RegisrationSetupView.swift b/PadelClub/Views/Tournament/Screen/RegisrationSetupView.swift new file mode 100644 index 0000000..aa2e266 --- /dev/null +++ b/PadelClub/Views/Tournament/Screen/RegisrationSetupView.swift @@ -0,0 +1,244 @@ +// +// RegisrationSetupView.swift +// PadelClub +// +// Created by razmig on 20/11/2024. +// + +import SwiftUI +import LeStorage + +struct RegisrationSetupView: View { + @EnvironmentObject var dataStore: DataStore + @Bindable var tournament: Tournament + @State private var enableOnlineRegistration: Bool + @State private var registrationDateLimit: Date + @State private var openingRegistrationDate: Date + @State private var targetTeamCount: Int + @State private var waitingListLimit: Int + @State private var registrationDateLimitEnabled: Bool + @State private var targetTeamCountEnabled: Bool + @State private var waitingListLimitEnabled: Bool + @State private var openingRegistrationDateEnabled: Bool + @State private var hasChanges: Bool = false + + @Environment(\.dismiss) private var dismiss + + init(tournament: Tournament) { + self.tournament = tournament + _enableOnlineRegistration = .init(wrappedValue: tournament.enableOnlineRegistration) + + // Registration Date Limit + if let registrationDateLimit = tournament.registrationDateLimit { + _registrationDateLimit = .init(wrappedValue: registrationDateLimit) + _registrationDateLimitEnabled = .init(wrappedValue: true) + } else { + _registrationDateLimit = .init(wrappedValue: tournament.startDate.truncateMinutesAndSeconds()) + _registrationDateLimitEnabled = .init(wrappedValue: false) + } + + // Opening Registration Date + if let openingRegistrationDate = tournament.openingRegistrationDate { + _openingRegistrationDate = .init(wrappedValue: openingRegistrationDate) + _openingRegistrationDateEnabled = .init(wrappedValue: true) + } else { + _openingRegistrationDate = .init(wrappedValue: tournament.creationDate.truncateMinutesAndSeconds()) + _openingRegistrationDateEnabled = .init(wrappedValue: false) + } + + // Target Team Count + if let targetTeamCount = tournament.targetTeamCount { + _targetTeamCount = .init(wrappedValue: targetTeamCount) + _targetTeamCountEnabled = .init(wrappedValue: true) + } else { + _targetTeamCount = .init(wrappedValue: tournament.teamCount) // Default value + _targetTeamCountEnabled = .init(wrappedValue: false) + } + + // Waiting List Limit + if let waitingListLimit = tournament.waitingListLimit { + _waitingListLimit = .init(wrappedValue: waitingListLimit) + _waitingListLimitEnabled = .init(wrappedValue: true) + } else { + _waitingListLimit = .init(wrappedValue: 0) // Default value + _waitingListLimitEnabled = .init(wrappedValue: false) + } + } + + var body: some View { + List { + Section { + Toggle(isOn: $enableOnlineRegistration) { + Text("Inscription en ligne") + } + } + + if enableOnlineRegistration { + + Section { + Toggle(isOn: $openingRegistrationDateEnabled) { + Text("Date d'ouverture des inscriptions") + } + + if openingRegistrationDateEnabled { + DatePicker(selection: $openingRegistrationDate, in: tournament.creationDate.truncateMinutesAndSeconds()...tournament.startDate.truncateMinutesAndSeconds()) { + DateMenuView(date: $openingRegistrationDate) + } + } + } header: { + Text("Date d'ouverture des inscriptions") + } footer: { + Text("Activez et définissez une date d'ouverture pour les inscriptions au tournoi.") + } + + Section { + Toggle(isOn: $registrationDateLimitEnabled) { + Text("Activer une limite") + } + + if registrationDateLimitEnabled { + DatePicker(selection: $registrationDateLimit, in: tournament.creationDate.truncateMinutesAndSeconds()...tournament.startDate.truncateMinutesAndSeconds()) { + DateMenuView(date: $registrationDateLimit) + } + } + } header: { + Text("Date de fermeture des inscriptions") + } footer: { + Text("Activez et définissez une date limite pour l'inscription en ligne.") + } + + Section { + Toggle(isOn: $targetTeamCountEnabled) { + Text("Nombre d'équipes cible") + } + + if targetTeamCountEnabled { + StepperView(count: $targetTeamCount, minimum: 4) + } + } header: { + Text("Nombre d'équipes cible") + } footer: { + Text("Activez et définissez le nombre d'équipes cible pour le tournoi.") + } + + Section { + Toggle(isOn: $waitingListLimitEnabled) { + Text("Limite de la liste d'attente") + } + + if waitingListLimitEnabled { + StepperView(count: $waitingListLimit, minimum: 0) + } + } header: { + Text("Limite de la liste d'attente") + } footer: { + Text("Activez et définissez une limite pour la liste d'attente des équipes.") + } + } else { + ContentUnavailableView( + "Activez les inscriptions en ligne", + systemImage: "person.2.crop.square.stack.fill", + description: Text("Permettez aux joueurs de s'inscrire eux-mêmes à ce tournoi. Les équipes inscrites apparaîtront automatiquement dans la liste de l'arbitre. L'inscription en ligne requiert un email de contact et une licence FFT.") + ) + } + } + .toolbar(content: { + if hasChanges { + ToolbarItem(placement: .topBarLeading) { + Button("Annuler", role: .cancel) { + dismiss() + } + } + + ToolbarItem(placement: .topBarTrailing) { + ButtonValidateView(role: .destructive) { + _save() + dismiss() + } + } + } + }) + .toolbarRole(.editor) + .headerProminence(.increased) + .navigationTitle("Inscription en ligne") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .navigationBarBackButtonHidden(hasChanges) + .onChange(of: enableOnlineRegistration, { + _hasChanged() + }) + .onChange(of: openingRegistrationDateEnabled) { + _hasChanged() + } + + .onChange(of: openingRegistrationDate) { + _hasChanged() + } + + .onChange(of: registrationDateLimitEnabled) { + _hasChanged() + } + + .onChange(of: registrationDateLimit) { + _hasChanged() + } + + .onChange(of: targetTeamCountEnabled) { + _hasChanged() + } + + .onChange(of: targetTeamCount) { + _hasChanged() + } + + .onChange(of: waitingListLimitEnabled) { + _hasChanged() + } + + .onChange(of: waitingListLimit) { + _hasChanged() + } + } + + private func _hasChanged() { + hasChanges = true + } + + private func _save() { + hasChanges = false + + tournament.enableOnlineRegistration = enableOnlineRegistration + + if openingRegistrationDateEnabled == false { + tournament.openingRegistrationDate = nil + } else { + tournament.openingRegistrationDate = openingRegistrationDate + } + + if registrationDateLimitEnabled == false { + tournament.registrationDateLimit = nil + } else { + tournament.registrationDateLimit = registrationDateLimit + } + + if targetTeamCountEnabled == false { + tournament.targetTeamCount = nil + } else { + tournament.targetTeamCount = targetTeamCount + } + + if waitingListLimitEnabled == false { + tournament.waitingListLimit = nil + } else { + tournament.waitingListLimit = waitingListLimit + } + + do { + try self.dataStore.tournaments.addOrUpdate(instance: tournament) + } catch { + Logger.error(error) + } + + dismiss() + } +}