Laurent 1 year ago
commit 92b5b91c77
  1. 4
      PadelClub.xcodeproj/project.pbxproj
  2. 4
      PadelClub/Data/Match.swift
  3. 6
      PadelClub/Data/User.swift
  4. 8
      PadelClub/Extensions/String+Extensions.swift
  5. 8
      PadelClub/Views/Club/ClubDetailView.swift
  6. 3
      PadelClub/Views/Club/ClubRowView.swift
  7. 42
      PadelClub/Views/Club/ClubsView.swift
  8. 10
      PadelClub/Views/Navigation/Agenda/ActivityView.swift
  9. 2
      PadelClub/Views/Navigation/Agenda/EventListView.swift
  10. 2
      PadelClub/Views/Navigation/MainView.swift
  11. 28
      PadelClub/Views/Round/RoundSettingsView.swift
  12. 4
      PadelClub/Views/Round/RoundView.swift
  13. 22
      PadelClub/Views/Tournament/FileImportView.swift
  14. 19
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift

@ -1939,7 +1939,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 34; CURRENT_PROJECT_VERSION = 37;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -1977,7 +1977,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 34; CURRENT_PROJECT_VERSION = 37;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;

@ -355,6 +355,10 @@ class Match: ModelObject, Storable {
func followingMatch() -> Match? { func followingMatch() -> Match? {
guard let nextRoundId = roundObject?.nextRound()?.id else { return nil } guard let nextRoundId = roundObject?.nextRound()?.id else { return nil }
return getFollowingMatch(fromNextRoundId: nextRoundId)
}
func getFollowingMatch(fromNextRoundId nextRoundId: String) -> Match? {
return Store.main.filter(isIncluded: { $0.round == nextRoundId && $0.index == (index - 1) / 2 }).first return Store.main.filter(isIncluded: { $0.round == nextRoundId && $0.index == (index - 1) / 2 }).first
} }

@ -73,8 +73,8 @@ class User: ModelObject, UserBase, Storable {
return "Sportivement,\n\(firstName) \(lastName), votre JAP." return "Sportivement,\n\(firstName) \(lastName), votre JAP."
} }
func hasClubs() -> Bool { func hasTenupClubs() -> Bool {
self.clubs.isEmpty == false self.clubsObjects().filter({ $0.code != nil }).isEmpty == false
} }
func hasFavoriteClubsAndCreatedClubs() -> Bool { func hasFavoriteClubsAndCreatedClubs() -> Bool {
@ -90,7 +90,7 @@ class User: ModelObject, UserBase, Storable {
} }
func createdClubsObjectsNotFavorite() -> [Club] { func createdClubsObjectsNotFavorite() -> [Club] {
return Store.main.filter(isIncluded: { ($0.creator == id) || clubs.contains($0.id) == false }) return Store.main.filter(isIncluded: { ($0.creator == id) && clubs.contains($0.id) == false })
} }
func saveMatchFormatsDefaultDuration(_ matchFormat: MatchFormat, estimatedDuration: Int) { func saveMatchFormatsDefaultDuration(_ matchFormat: MatchFormat, estimatedDuration: Int) {

@ -46,16 +46,18 @@ extension String {
func acronym() -> String { func acronym() -> String {
let acronym = canonicalVersion.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines) let acronym = canonicalVersion.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines)
if acronym.count > 10 { if acronym.count > 10 {
return concatenateFirstLetters() return concatenateFirstLetters().uppercased()
} else { } else {
return acronym return acronym.uppercased()
} }
} }
func concatenateFirstLetters() -> String { func concatenateFirstLetters() -> String {
// Split the input into sentences // Split the input into sentences
let sentences = self.components(separatedBy: .whitespacesAndNewlines) let sentences = self.components(separatedBy: .whitespacesAndNewlines)
if sentences.count == 1 {
return String(self.prefix(10))
}
// Extract the first character of each sentence // Extract the first character of each sentence
let firstLetters = sentences.compactMap { sentence -> Character? in let firstLetters = sentences.compactMap { sentence -> Character? in
let trimmedSentence = sentence.trimmingCharacters(in: .whitespacesAndNewlines) let trimmedSentence = sentence.trimmingCharacters(in: .whitespacesAndNewlines)

@ -17,6 +17,7 @@ struct ClubDetailView: View {
@State private var zipCode: String @State private var zipCode: String
@State private var selectedCourt: Court? @State private var selectedCourt: Court?
@Bindable var club: Club @Bindable var club: Club
@State private var clubDeleted: Bool = false
var displayContext: DisplayContext var displayContext: DisplayContext
var selection: ((Club) -> ())? = nil var selection: ((Club) -> ())? = nil
@ -58,7 +59,7 @@ struct ClubDetailView: View {
.submitLabel( displayContext == .addition ? .next : .done) .submitLabel( displayContext == .addition ? .next : .done)
.onSubmit(of: .text) { .onSubmit(of: .text) {
if club.acronym.isEmpty { if club.acronym.isEmpty {
club.acronym = club.name.canonicalVersion.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines) club.acronym = club.name.canonicalVersion.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines).acronym()
} }
if displayContext == .edition && city.isEmpty == true { if displayContext == .edition && city.isEmpty == true {
@ -217,9 +218,10 @@ struct ClubDetailView: View {
Section { Section {
RowButtonView("Supprimer ce club", role: .destructive) { RowButtonView("Supprimer ce club", role: .destructive) {
do { do {
try dataStore.clubs.deleteById(club.id) clubDeleted = true
dataStore.user.clubs.removeAll(where: { $0 == club.id }) dataStore.user.clubs.removeAll(where: { $0 == club.id })
self.dataStore.saveUser() self.dataStore.saveUser()
try dataStore.clubs.deleteById(club.id)
dismiss() dismiss()
} catch { } catch {
Logger.error(error) Logger.error(error)
@ -240,7 +242,7 @@ struct ClubDetailView: View {
CourtView(court: court) CourtView(court: court)
} }
.onDisappear { .onDisappear {
if displayContext == .edition { if displayContext == .edition && clubDeleted == false {
do { do {
try dataStore.clubs.addOrUpdate(instance: club) try dataStore.clubs.addOrUpdate(instance: club)
} catch { } catch {

@ -13,13 +13,14 @@ struct ClubRowView: View {
var body: some View { var body: some View {
LabeledContent { LabeledContent {
Text(club.acronym)
// if displayContext == .edition { // if displayContext == .edition {
// Image(systemName: club.isFavorite() ? "star.fill" : "star") // Image(systemName: club.isFavorite() ? "star.fill" : "star")
// .foregroundStyle(club.isFavorite() ? .green : .logoRed) // .foregroundStyle(club.isFavorite() ? .green : .logoRed)
// } // }
} label: { } label: {
Text("Club")
Text(club.name) Text(club.name)
Text(club.acronym)
} }
} }
} }

@ -28,7 +28,13 @@ struct ClubsView: View {
var body: some View { var body: some View {
List { List {
let clubs : [Club] = dataStore.user.clubsObjects(includeCreated: false) let clubs : [Club] = dataStore.user.clubsObjects(includeCreated: false)
let onlyCreatedClubs : [Club] = dataStore.user.createdClubsObjectsNotFavorite()
if clubs.isEmpty == false || onlyCreatedClubs.isEmpty == false {
Section { Section {
if clubs.isEmpty && onlyCreatedClubs.isEmpty == false {
_contentUnavailable()
}
ForEach(clubs) { club in ForEach(clubs) { club in
if let selection { if let selection {
Button { Button {
@ -51,8 +57,7 @@ struct ClubsView: View {
} header: { } header: {
Text("Clubs favoris") Text("Clubs favoris")
} }
}
let onlyCreatedClubs : [Club] = dataStore.user.createdClubsObjectsNotFavorite()
if onlyCreatedClubs.isEmpty == false { if onlyCreatedClubs.isEmpty == false {
Section { Section {
@ -81,22 +86,11 @@ struct ClubsView: View {
} }
} }
.overlay { .overlay {
if dataStore.user.hasFavoriteClubsAndCreatedClubs() == false { if dataStore.user.clubsObjects(includeCreated: true).isEmpty {
ContentUnavailableView { _contentUnavailable()
Label("Aucun club", systemImage: "house.and.flag.fill")
} description: {
Text("Texte décrivant l'utilité d'un club et les features que cela apporte")
} actions: {
RowButtonView("Créer un nouveau club", systemImage: "plus.circle.fill") {
newClub = Club.newEmptyInstance()
}
RowButtonView("Chercher un club", systemImage: "magnifyingglass.circle.fill") {
presentClubSearchView = true
}
} }
} }
} .navigationTitle(selection == nil ? "Clubs favoris" : "Choisir un club")
.navigationTitle(selection == nil ? "Mes clubs" : "Choisir un club")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.sheet(isPresented: presentClubCreationView) { .sheet(isPresented: presentClubCreationView) {
@ -141,6 +135,22 @@ struct ClubsView: View {
} }
} }
} }
private func _contentUnavailable() -> some View {
ContentUnavailableView {
Label("Aucun club", systemImage: "house.and.flag.fill")
} description: {
Text("Avoir un club en favori permet d'accéder aux tournois enregistrés sur Tenup.")
} actions: {
RowButtonView("Créer un nouveau club", systemImage: "plus.circle.fill") {
newClub = Club.newEmptyInstance()
}
RowButtonView("Chercher un club", systemImage: "magnifyingglass.circle.fill") {
presentClubSearchView = true
}
}
}
} }
#Preview { #Preview {

@ -121,14 +121,14 @@ struct ActivityView: View {
} }
.task { .task {
if navigation.agendaDestination == .tenup if navigation.agendaDestination == .tenup
&& dataStore.user.hasClubs() == true && dataStore.user.hasTenupClubs() == true
&& federalDataViewModel.federalTournaments.isEmpty { && federalDataViewModel.federalTournaments.isEmpty {
_gatherFederalTournaments() _gatherFederalTournaments()
} }
} }
.onChange(of: navigation.agendaDestination) { .onChange(of: navigation.agendaDestination) {
if navigation.agendaDestination == .tenup if navigation.agendaDestination == .tenup
&& dataStore.user.hasClubs() == true && dataStore.user.hasTenupClubs() == true
&& federalDataViewModel.federalTournaments.isEmpty { && federalDataViewModel.federalTournaments.isEmpty {
_gatherFederalTournaments() _gatherFederalTournaments()
} }
@ -192,7 +192,7 @@ struct ActivityView: View {
.tint(.master) .tint(.master)
} }
.sheet(isPresented: $presentClubSearchView, onDismiss: { .sheet(isPresented: $presentClubSearchView, onDismiss: {
if dataStore.user.hasClubs() == true { if dataStore.user.hasTenupClubs() == true {
federalDataViewModel.federalTournaments.removeAll() federalDataViewModel.federalTournaments.removeAll()
navigation.agendaDestination = .tenup navigation.agendaDestination = .tenup
} }
@ -247,7 +247,7 @@ struct ActivityView: View {
RowButtonView("Créer un nouvel événement") { RowButtonView("Créer un nouvel événement") {
newTournament = Tournament.newEmptyInstance() newTournament = Tournament.newEmptyInstance()
} }
if dataStore.user.hasClubs() == false { if dataStore.user.hasTenupClubs() == false {
RowButtonView("Chercher l'un de vos clubs") { RowButtonView("Chercher l'un de vos clubs") {
presentClubSearchView = true presentClubSearchView = true
} }
@ -268,7 +268,7 @@ struct ActivityView: View {
} }
private func _tenupEmptyView() -> some View { private func _tenupEmptyView() -> some View {
if dataStore.user.hasClubs() == false { if dataStore.user.hasTenupClubs() == false {
ContentUnavailableView { ContentUnavailableView {
Label("Aucun tournoi", systemImage: "shield.slash") Label("Aucun tournoi", systemImage: "shield.slash")
} description: { } description: {

@ -53,7 +53,7 @@ struct EventListView: View {
.headerProminence(.increased) .headerProminence(.increased)
.task { .task {
if navigation.agendaDestination == .tenup if navigation.agendaDestination == .tenup
&& dataStore.user.hasClubs() == true && dataStore.user.hasTenupClubs() == true
&& _tournaments.isEmpty { && _tournaments.isEmpty {
_gatherFederalTournaments(startDate: section) _gatherFederalTournaments(startDate: section)
} }

@ -49,7 +49,7 @@ struct MainView: View {
} }
var badgeText: Text? { var badgeText: Text? {
dataStore.user.username.isEmpty ? Text("!").font(.headline) : nil Store.main.userId == nil ? Text("!").font(.headline) : nil
} }
var body: some View { var body: some View {

@ -52,8 +52,30 @@ struct RoundSettingsView: View {
let round = Round(tournament: tournament.id, index: roundIndex, matchFormat: tournament.matchFormat) let round = Round(tournament: tournament.id, index: roundIndex, matchFormat: tournament.matchFormat)
let matchCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex) let matchCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex)
let matchStartIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex) let matchStartIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex)
let matches = (0..<matchCount).map { //0 is final match let nextRound = round.nextRound()
return Match(round: round.id, index: $0 + matchStartIndex, matchFormat: round.matchFormat, name: Match.setServerTitle(upperRound: round, matchIndex: $0)) var currentIndex = 0
let matches = (0..<matchCount).map { index in //0 is final match
let match = Match(round: round.id, index: index + matchStartIndex, matchFormat: round.matchFormat)
if let nextRound, let followingMatch = match.getFollowingMatch(fromNextRoundId: nextRound.id) {
if followingMatch.disabled {
match.disabled = true
} else if index%2 == 1 && followingMatch.team(.one) != nil {
//index du match courant impair = position haut du prochain match
match.disabled = true
} else if index%2 == 0 && followingMatch.team(.two) != nil {
//index du match courant pair = position basse du prochain match
match.disabled = true
} else {
match.name = Match.setServerTitle(upperRound: round, matchIndex: currentIndex)
currentIndex += 1
}
} else {
match.name = Match.setServerTitle(upperRound: round, matchIndex: currentIndex)
currentIndex += 1
}
return match
} }
do { do {
try dataStore.rounds.addOrUpdate(instance: round) try dataStore.rounds.addOrUpdate(instance: round)
@ -66,6 +88,8 @@ struct RoundSettingsView: View {
Logger.error(error) Logger.error(error)
} }
round.buildLoserBracket() round.buildLoserBracket()
matches.filter { $0.disabled }.forEach { $0._toggleLoserMatchDisableState(true)
}
} }
} }

@ -75,7 +75,7 @@ struct RoundView: View {
Section { Section {
RowButtonView("Placer \(availableSeedGroup.localizedLabel())" + ((availableSeedGroup.isFixed() == false) ? " au hasard" : "")) { RowButtonView("Placer \(availableSeedGroup.localizedLabel())" + ((availableSeedGroup.isFixed() == false) ? " au hasard" : "")) {
tournament.setSeeds(inRoundIndex: round.index, inSeedGroup: availableSeedGroup) tournament.setSeeds(inRoundIndex: round.index, inSeedGroup: availableSeedGroup)
//_save() await _save()
if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty { if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty {
self.isEditingTournamentSeed.wrappedValue = false self.isEditingTournamentSeed.wrappedValue = false
} }
@ -245,7 +245,7 @@ struct RoundView: View {
.toolbar { .toolbar {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
Button(isEditingTournamentSeed.wrappedValue == true ? "Valider" : "Modifier") { Button(isEditingTournamentSeed.wrappedValue == true ? "Valider" : "Modifier") {
if isEditingTournamentSeed.wrappedValue { if isEditingTournamentSeed.wrappedValue == true {
Task { Task {
await _save() await _save()
} }

@ -105,16 +105,6 @@ struct FileImportView: View {
} }
} }
if validationInProgress {
Section {
LabeledContent {
ProgressView()
} label: {
Text("Mise à jour des équipes")
}
}
}
if let errorMessage { if let errorMessage {
Section { Section {
Text(errorMessage) Text(errorMessage)
@ -157,7 +147,7 @@ struct FileImportView: View {
Section { Section {
ContentUnavailableView("Aucune équipe détectée", systemImage: "person.2.slash") ContentUnavailableView("Aucune équipe détectée", systemImage: "person.2.slash")
} }
} else if didImport && validationInProgress == false { } else if didImport {
let _filteredTeams = filteredTeams let _filteredTeams = filteredTeams
let previousTeams = tournament.sortedTeams() let previousTeams = tournament.sortedTeams()
@ -232,18 +222,24 @@ struct FileImportView: View {
} }
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
if validationInProgress {
ProgressView()
} else {
ButtonValidateView { ButtonValidateView {
_validate() validationInProgress = true
} }
.disabled(teams.isEmpty) .disabled(teams.isEmpty)
} }
} }
}
.interactiveDismissDisabled(validationInProgress) .interactiveDismissDisabled(validationInProgress)
.disabled(validationInProgress) .disabled(validationInProgress)
.onChange(of: validationInProgress) {
_validate()
}
} }
private func _validate() { private func _validate() {
validationInProgress = true
Task { Task {
let previousTeams = filteredTeams.compactMap({ $0.previousTeam }) let previousTeams = filteredTeams.compactMap({ $0.previousTeam })

@ -152,14 +152,15 @@ struct InscriptionManagerView: View {
} }
private func _handleHashDiff() async { private func _handleHashDiff() async {
let newHash = _simpleHash(ids: tournament.selectedSortedTeams().map { $0.id }) let selectedSortedTeams = tournament.selectedSortedTeams()
if let teamsHash, newHash != teamsHash { let newHash = _simpleHash(ids: selectedSortedTeams.map { $0.id })
if (self.teamsHash != nil && newHash != teamsHash!) || (self.teamsHash == nil && selectedSortedTeams.isEmpty == false) {
self.teamsHash = newHash self.teamsHash = newHash
if self.tournament.shouldVerifyBracket == false || self.tournament.shouldVerifyGroupStage == false { if self.tournament.shouldVerifyBracket == false || self.tournament.shouldVerifyGroupStage == false {
self.tournament.shouldVerifyBracket = true self.tournament.shouldVerifyBracket = true
self.tournament.shouldVerifyGroupStage = true self.tournament.shouldVerifyGroupStage = true
let waitingList = self.tournament.waitingListTeams(in: self.tournament.selectedSortedTeams()) let waitingList = self.tournament.waitingListTeams(in: selectedSortedTeams)
waitingList.forEach { team in waitingList.forEach { team in
if team.bracketPosition != nil || team.groupStagePosition != nil { if team.bracketPosition != nil || team.groupStagePosition != nil {
team.resetPositions() team.resetPositions()
@ -181,10 +182,9 @@ struct InscriptionManagerView: View {
_managementView() _managementView()
if _isEditingTeam() { if _isEditingTeam() {
_buildingTeamView() _buildingTeamView()
} else if tournament.unsortedTeams().isEmpty { } else if unfilteredTeams.isEmpty {
_inscriptionTipsView() _inscriptionTipsView()
} } else {
if _isEditingTeam() == false {
_teamRegisteredView() _teamRegisteredView()
} }
} }
@ -284,7 +284,9 @@ struct InscriptionManagerView: View {
} }
.tint(.master) .tint(.master)
} }
.sheet(isPresented: $presentImportView) { .sheet(isPresented: $presentImportView, onDismiss: {
_getTeams()
}) {
NavigationStack { NavigationStack {
FileImportView() FileImportView()
} }
@ -428,12 +430,13 @@ struct InscriptionManagerView: View {
} }
private func _prepareTeams() { private func _prepareTeams() {
#if DEBUG_TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func _prepareTeams", duration.formatted(.units(allowed: [.seconds, .milliseconds]))) print("func _prepareTeams", duration.formatted(.units(allowed: [.seconds, .milliseconds])))
} }
#endif
sortedTeams = tournament.sortedTeams() sortedTeams = tournament.sortedTeams()
var teams = sortedTeams var teams = sortedTeams

Loading…
Cancel
Save