diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index a9d4b62..77b74aa 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -257,6 +257,7 @@ FFF03C942BD91D0C00B516FC /* ButtonValidateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF03C932BD91D0C00B516FC /* ButtonValidateView.swift */; }; FFF116E12BD2A9B600A33B06 /* DateInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF116E02BD2A9B600A33B06 /* DateInterval.swift */; }; FFF116E32BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF116E22BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift */; }; + FFF1D2CB2C4A22B200C8D33D /* ExportFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF1D2CA2C4A22B200C8D33D /* ExportFormat.swift */; }; FFF527D62BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */; }; FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACCC2B92367B008466FA /* FederalPlayer.swift */; }; FFF8ACD42B92392C008466FA /* SourceFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACD32B92392C008466FA /* SourceFileManager.swift */; }; @@ -599,6 +600,7 @@ FFF03C932BD91D0C00B516FC /* ButtonValidateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonValidateView.swift; sourceTree = ""; }; FFF116E02BD2A9B600A33B06 /* DateInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateInterval.swift; sourceTree = ""; }; FFF116E22BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourtAvailabilitySettingsView.swift; sourceTree = ""; }; + FFF1D2CA2C4A22B200C8D33D /* ExportFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportFormat.swift; sourceTree = ""; }; FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchScheduleEditorView.swift; sourceTree = ""; }; FFF8ACCC2B92367B008466FA /* FederalPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalPlayer.swift; sourceTree = ""; }; FFF8ACD32B92392C008466FA /* SourceFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceFileManager.swift; sourceTree = ""; }; @@ -1323,6 +1325,7 @@ FF0EC51D2BB16F680056B6D1 /* SwiftParser.swift */, FF1DC5582BAB767000FD8220 /* Tips.swift */, C49EF01A2BD6A1E80077B5AA /* URLs.swift */, + FFF1D2CA2C4A22B200C8D33D /* ExportFormat.swift */, ); path = Utils; sourceTree = ""; @@ -1720,6 +1723,7 @@ C493B37E2C10AD3600862481 /* LoadingViewModifier.swift in Sources */, FF089EBD2BB0287D00F0AEC7 /* PlayerView.swift in Sources */, FF967D032BAEF0C000A9A3BD /* MatchDetailView.swift in Sources */, + FFF1D2CB2C4A22B200C8D33D /* ExportFormat.swift in Sources */, FF967D0F2BAF63B000A9A3BD /* PlayerBlockView.swift in Sources */, C4A47D922B7BBBEC00ADC637 /* StoreItem.swift in Sources */, FFB9C8712BBADDE200A0EF4F /* Selectable.swift in Sources */, diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index d95532c..8746cb3 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -140,8 +140,13 @@ final class PlayerRegistration: ModelObject, Storable { return nil } - func pasteData() -> String { - [firstName.capitalized, lastName.capitalized, licenceId].compactMap({ $0 }).joined(separator: " ") + func pasteData(_ exportFormat: ExportFormat = .rawText) -> String { + switch exportFormat { + case .rawText: + return [firstName.capitalized, lastName.capitalized, licenceId].compactMap({ $0 }).joined(separator: exportFormat.separator()) + case .csv: + return [lastName.uppercased() + " " + firstName.capitalized].joined(separator: exportFormat.separator()) + } } func isPlaying() -> Bool { diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index 6dd422b..be3ece8 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -327,24 +327,61 @@ final class TeamRegistration: ModelObject, Storable { resetBracketPosition() } - func pasteData() -> String { - [playersPasteData(), formattedInscriptionDate(), name].compactMap({ $0 }).joined(separator: "\n") + func pasteData(_ exportFormat: ExportFormat = .rawText, _ index: Int = 0) -> String { + switch exportFormat { + case .rawText: + return [playersPasteData(exportFormat), formattedInscriptionDate(exportFormat), name].compactMap({ $0 }).joined(separator: exportFormat.newLineSeparator()) + case .csv: + return [index.formatted(), playersPasteData(exportFormat), isWildCard() ? "WC" : weight.formatted()].joined(separator: exportFormat.separator()) + } } var computedRegistrationDate: Date { return registrationDate ?? .distantFuture } - func formattedInscriptionDate() -> String? { - if let registrationDate { - return "Inscrit le " + registrationDate.formatted(.dateTime.weekday().day().month().hour().minute()) - } else { - return nil + func formattedInscriptionDate(_ exportFormat: ExportFormat = .rawText) -> String? { + switch exportFormat { + case .rawText: + if let registrationDate { + return "Inscrit le " + registrationDate.formatted(.dateTime.weekday().day().month().hour().minute()) + } else { + return nil + } + case .csv: + if let registrationDate { + return registrationDate.formatted(.dateTime.weekday().day().month().hour().minute()) + } else { + return nil + } + } + } + + func formattedSummonDate(_ exportFormat: ExportFormat = .rawText) -> String? { + + switch exportFormat { + case .rawText: + if let callDate { + return "Convoqué le " + callDate.formatted(.dateTime.weekday().day().month().hour().minute()) + } else { + return nil + } + case .csv: + if let callDate { + return callDate.formatted(.dateTime.weekday().day().month().hour().minute()) + } else { + return nil + } } } - func playersPasteData() -> String { - return players().map { $0.pasteData() }.joined(separator: "\n") + func playersPasteData(_ exportFormat: ExportFormat = .rawText) -> String { + switch exportFormat { + case .rawText: + return players().map { $0.pasteData(exportFormat) }.joined(separator: exportFormat.newLineSeparator()) + case .csv: + return players().map { [$0.pasteData(exportFormat), isWildCard() ? "WC" : $0.computedRank.formatted() ].joined(separator: exportFormat.separator()) }.joined(separator: exportFormat.separator()) + } } func updatePlayers(_ players: Set, inTournamentCategory tournamentCategory: TournamentCategory) { diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index c31f7b7..f5d9bf4 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -529,9 +529,19 @@ defer { return Store.main.findById(event) } - func pasteDataForImporting() -> String { + func pasteDataForImporting(_ exportFormat: ExportFormat = .rawText) -> String { let selectedSortedTeams = selectedSortedTeams() - return (selectedSortedTeams.compactMap { $0.pasteData() } + ["Liste d'attente"] + waitingListTeams(in: selectedSortedTeams, includingWalkOuts: true).compactMap { $0.pasteData() }).joined(separator: "\n\n") + switch exportFormat { + case .rawText: + return (selectedSortedTeams.compactMap { $0.pasteData(exportFormat) } + ["Liste d'attente"] + waitingListTeams(in: selectedSortedTeams, includingWalkOuts: true).compactMap { $0.pasteData(exportFormat) }).joined(separator: exportFormat.newLineSeparator(2)) + case .csv: + let headers = ["N°", "Nom Prénom", "rang", "Nom Prénom", "rang", "poids"].joined(separator: exportFormat.separator()) + var teamPaste = [headers] + for (index, team) in selectedSortedTeams.enumerated() { + teamPaste.append(team.pasteData(exportFormat, index + 1)) + } + return teamPaste.joined(separator: exportFormat.newLineSeparator()) + } } func club() -> Club? { diff --git a/PadelClub/Extensions/String+Extensions.swift b/PadelClub/Extensions/String+Extensions.swift index a0abca5..1fe097d 100644 --- a/PadelClub/Extensions/String+Extensions.swift +++ b/PadelClub/Extensions/String+Extensions.swift @@ -185,10 +185,10 @@ extension LosslessStringConvertible { } extension String { - func createTxtFile(_ withName: String = "temp") -> URL { + func createFile(_ withName: String = "temp", _ exportedFormat: ExportFormat = .rawText) -> URL { let url = FileManager.default.temporaryDirectory .appendingPathComponent(withName) - .appendingPathExtension("txt") + .appendingPathExtension(exportedFormat.suffix) let string = self try? FileManager.default.removeItem(at: url) try? string.write(to: url, atomically: true, encoding: .utf8) diff --git a/PadelClub/Utils/ExportFormat.swift b/PadelClub/Utils/ExportFormat.swift new file mode 100644 index 0000000..4a2cb61 --- /dev/null +++ b/PadelClub/Utils/ExportFormat.swift @@ -0,0 +1,37 @@ +// +// ExportFormat.swift +// PadelClub +// +// Created by Razmig Sarkissian on 19/07/2024. +// + +import Foundation + +enum ExportFormat: Int, Identifiable, CaseIterable { + var id: Int { self.rawValue } + + case rawText + case csv + + var suffix: String { + switch self { + case .rawText: + return "txt" + case .csv: + return "csv" + } + } + + func separator() -> String { + switch self { + case .rawText: + return " " + case .csv: + return ";" + } + } + + func newLineSeparator(_ count: Int = 1) -> String { + return Array(repeating: "\n", count: count).joined() + } +} diff --git a/PadelClub/Utils/PadelRule.swift b/PadelClub/Utils/PadelRule.swift index 0af54b5..dfddbff 100644 --- a/PadelClub/Utils/PadelRule.swift +++ b/PadelClub/Utils/PadelRule.swift @@ -264,6 +264,15 @@ enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable { } } + func shouldShareTeams() -> Bool { + switch self { + case .p500, .p1000, .p1500, .p2000: + return true + default: + return false + } + } + static func mostRecent(inTournaments tournaments: [Tournament]) -> Self { return tournaments.first?.tournamentLevel ?? .p100 } diff --git a/PadelClub/Utils/Tips.swift b/PadelClub/Utils/Tips.swift index 6add654..ae4c3c8 100644 --- a/PadelClub/Utils/Tips.swift +++ b/PadelClub/Utils/Tips.swift @@ -534,6 +534,21 @@ struct BracketEditTip: Tip { } } +struct TeamsExportTip: Tip { + + var title: Text { + Text("Exporter les paires") + } + + var message: Text? { + Text("Partager les paires comme indiqué dans le guide de la compétition à J-6 avant midi.") + } + + var image: Image? { + Image(systemName: "square.and.arrow.up") + } +} + struct TipStyleModifier: ViewModifier { @Environment(\.colorScheme) var colorScheme var tint: Color? diff --git a/PadelClub/Views/Shared/LearnMoreSheetView.swift b/PadelClub/Views/Shared/LearnMoreSheetView.swift index d055183..a8bf0b9 100644 --- a/PadelClub/Views/Shared/LearnMoreSheetView.swift +++ b/PadelClub/Views/Shared/LearnMoreSheetView.swift @@ -12,36 +12,29 @@ struct LearnMoreSheetView: View { var tournament: Tournament var body: some View { - VStack(spacing: 20) { - Text("Pourquoi cette étape ?") - .font(.title) - Text(""" + List { + ContentUnavailableView { + Text("Pourquoi cette étape ?") + } description: { + Text(""" Pour terminer la préparation de votre tournoi et pouvoir commencer à convoquer vos joueurs, vous devez inscrire les paires que vous avez préparé dans Padel Club sur le site beach-padel.app.fft.fr. Padel Club ne peut pas, pour l'instant, faire cette manipulation automatiquement. Par contre, vous pouvez exporter les paires que vous avez préparé en un simple fichier texte vous permettant ainsi d'accélérer un peu plus la saisie sur le site fédéral. - Une fois vos que vos paires seront inscrites sur beach-padel.app.fft.fr, vous pourrez les importer à nouveau dans Padel Club en un instant, vous donnant accès aux emails et téléphones des joueurs dans le but de les convoquer. + Une fois que vos paires seront inscrites sur beach-padel.app.fft.fr, vous pourrez les importer à nouveau dans Padel Club en un instant, vous donnant accès aux emails et téléphones des joueurs dans le but de les convoquer. """) - .foregroundStyle(.secondary) - - - ShareLink(item: tournament.pasteDataForImporting().createTxtFile(tournament.tournamentTitle(.short))) { - HStack { - Spacer() + } actions: { + ShareLink(item: tournament.pasteDataForImporting().createFile(tournament.tournamentTitle(.short))) { Text("Exporter les inscriptions") - Spacer() } - } - .buttonStyle(.borderedProminent) - - Button("J'ai compris") { - dismiss() + RowButtonView("J'ai compris") { + dismiss() + } } } - .padding([.leading, .trailing], 40) } } diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index 983e0af..3b4824f 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -16,8 +16,9 @@ let inscriptionManagerWomanRankTip = InscriptionManagerWomanRankTip() //let searchTip = InscriptionManagerSearchInputTip() //let createTip = InscriptionManagerCreateInputTip() let rankUpdateTip = InscriptionManagerRankUpdateTip() -//let padelBeachExportTip = PadelBeachExportTip() -//let padelBeachImportTip = PadelBeachImportTip() +let padelBeachExportTip = PadelBeachExportTip() +let padelBeachImportTip = PadelBeachImportTip() +let teamsExportTip = TeamsExportTip() struct InscriptionManagerView: View { @@ -396,11 +397,7 @@ struct InscriptionManagerView: View { Label("Clôturer", systemImage: "lock") } Divider() - if let teamPaste { - ShareLink(item: teamPaste) { - Label("Exporter les paires", systemImage: "square.and.arrow.up") - } - } + _sharingTeamsMenuView() Button { presentImportView = true } label: { @@ -410,6 +407,11 @@ struct InscriptionManagerView: View { Label("beach-padel.app.fft.fr", systemImage: "safari") } } else { + + _sharingTeamsMenuView() + + Divider() + Button { tournament.unlockRegistration() _save() @@ -431,6 +433,23 @@ struct InscriptionManagerView: View { .navigationBarTitleDisplayMode(.inline) } + private func _sharingTeamsMenuView() -> some View { + Menu { + if let teamPaste = teamPaste() { + ShareLink(item: teamPaste) { + Text("En texte") + } + } + if let teamPaste = teamPaste(.csv) { + ShareLink(item: teamPaste) { + Text("En csv") + } + } + } label: { + Label("Exporter les paires", systemImage: "square.and.arrow.up") + } + } + var walkoutTeams: [TeamRegistration] { tournament.walkoutTeams() } @@ -439,8 +458,8 @@ struct InscriptionManagerView: View { tournament.unsortedTeamsWithoutWO() } - var teamPaste: URL? { - tournament.pasteDataForImporting().createTxtFile(self.tournament.tournamentTitle(.short)) + func teamPaste(_ exportFormat: ExportFormat = .rawText) -> URL? { + tournament.pasteDataForImporting(exportFormat).createFile(self.tournament.tournamentTitle(.short), exportFormat) } var unsortedPlayers: [PlayerRegistration] { @@ -535,7 +554,7 @@ struct InscriptionManagerView: View { .listRowView(isActive: true, color: team.initialRoundColor() ?? tournament.cutLabelColor(index: teamIndex, teamCount: filterMode == .waiting ? 0 : selectedSortedTeams.count), hideColorVariation: true) } } header: { - if filterMode == .all { + if filterMode == .all && walkoutTeams.isEmpty == false { Text("\(teams.count.formatted()) équipe\(teams.count.pluralSuffix) dont \(walkoutTeams.count.formatted()) forfait\(walkoutTeams.count.pluralSuffix)") } else { Text("\(teams.count.formatted()) équipe\(teams.count.pluralSuffix)") @@ -823,9 +842,13 @@ struct InscriptionManagerView: View { @ViewBuilder private func _relatedTips() -> some View { -// if pasteString == nil -// && createdPlayerIds.isEmpty -// && tournament.unsortedTeams().count >= tournament.teamCount +// if tournament.inscriptionClosed() && tournament.tournamentLevel.shouldShareTeams() { +// Section { +// TipView(teamsExportTip) +// .tipStyle(tint: nil) +// } +// } +// if tournament.unsortedTeams().count >= tournament.teamCount // && tournament.unsortedPlayers().filter({ $0.source == .beachPadel }).isEmpty { // Section { // TipView(padelBeachExportTip) { action in