Laurent 1 year ago
commit 38071b0523
  1. 4
      PadelClub.xcodeproj/project.pbxproj
  2. 7
      PadelClub/Data/TeamRegistration.swift
  3. 7
      PadelClub/Data/Tournament.swift
  4. 4
      PadelClub/Views/Components/ButtonValidateView.swift
  5. 6
      PadelClub/Views/Components/GenericDestinationPickerView.swift
  6. 115
      PadelClub/Views/Player/PlayerDetailView.swift
  7. 2
      PadelClub/Views/Shared/ImportedPlayerView.swift
  8. 26
      PadelClub/Views/Team/EditingTeamView.swift
  9. 1
      PadelClub/Views/Team/TeamDetailView.swift
  10. 77
      PadelClub/Views/Tournament/FileImportView.swift
  11. 22
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  12. 1
      PadelClub/Views/Tournament/TournamentView.swift

@ -1859,7 +1859,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 48;
CURRENT_PROJECT_VERSION = 50;
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -1897,7 +1897,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 48;
CURRENT_PROJECT_VERSION = 50;
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6;

@ -67,6 +67,7 @@ class TeamRegistration: ModelObject, Storable {
override func deleteDependencies() throws {
DataStore.shared.playerRegistrations.deleteDependencies(self.unsortedPlayers())
DataStore.shared.teamScores.deleteDependencies(self.teamScores())
}
func hasArrived() {
@ -199,13 +200,13 @@ class TeamRegistration: ModelObject, Storable {
return ids.hashValue == searchedIds.hashValue
}
func includes(_ players: [PlayerRegistration]) -> Bool {
func includes(players: [PlayerRegistration]) -> Bool {
return players.allSatisfy { player in
includes(player)
includes(player: player)
}
}
func includes(_ player: PlayerRegistration) -> Bool {
func includes(player: PlayerRegistration) -> Bool {
return unsortedPlayers().anySatisfy { _player in
_player.isSameAs(player)
}

@ -463,7 +463,10 @@ class Tournament : ModelObject, Storable {
func shareURL(_ pageLink: PageLink = .matches) -> URL? {
if pageLink == .clubBroadcast {
if let club = club(), let broadcastCode = club.broadcastCode {
let club = club()
print("club", club)
print("club broadcast code", club?.broadcastCode)
if let club, let broadcastCode = club.broadcastCode {
return URLs.main.url.appending(path: "c/\(broadcastCode)")
} else {
return nil
@ -1192,7 +1195,7 @@ class Tournament : ModelObject, Storable {
}
func findTeam(_ players: [PlayerRegistration]) -> TeamRegistration? {
return unsortedTeams().first(where: { $0.includes(players) })
return unsortedTeams().first(where: { $0.includes(players: players) })
}
func tournamentTitle(_ displayStyle: DisplayStyle = .wide) -> String {

@ -9,11 +9,11 @@ import SwiftUI
struct ButtonValidateView: View {
var role: ButtonRole?
var title: String = "Valider"
let action: () -> ()
var body: some View {
Button("Valider", role: role) {
Button(title, role: role) {
action()
}
.clipShape(Capsule())

@ -49,7 +49,7 @@ struct GenericDestinationPickerView<T: Identifiable & Selectable & Equatable >:
.buttonStyle(.plain)
.overlay(alignment: .bottomTrailing) {
if destination.displayImageIfValueZero() {
let count = destination.badgeValue()
let count : Int? = destination.badgeValue()
if let count, count == 0, let badge = destination.badgeImage() {
Image(systemName: badge.systemName())
.foregroundColor(badge.color())
@ -60,7 +60,7 @@ struct GenericDestinationPickerView<T: Identifiable & Selectable & Equatable >:
)
.offset(x: 3, y: 3)
} else if let count, count > 0 {
Image(systemName: count <= 50 ? "\(count).circle.fill" : "plus.circle.fill")
Image(systemName: count <= 50 ? "\(String(count)).circle.fill" : "plus.circle.fill")
.foregroundColor(destination.badgeValueColor() ?? .red)
.imageScale(.medium)
.background (
@ -80,7 +80,7 @@ struct GenericDestinationPickerView<T: Identifiable & Selectable & Equatable >:
)
.offset(x: 3, y: 3)
} else if let count = destination.badgeValue(), count > 0 {
Image(systemName: count <= 50 ? "\(count).circle.fill" : "plus.circle.fill")
Image(systemName: count <= 50 ? "\(String(count)).circle.fill" : "plus.circle.fill")
.foregroundColor(destination.badgeValueColor() ?? .red)
.imageScale(.medium)
.background (

@ -12,7 +12,17 @@ struct PlayerDetailView: View {
@Environment(Tournament.self) var tournament: Tournament
@EnvironmentObject var dataStore: DataStore
@Bindable var player: PlayerRegistration
@FocusState private var textFieldIsFocus: Bool
@State private var licenceId: String
@State private var phoneNumber: String
@State private var email: String
@FocusState var focusedField: PlayerRegistration.CodingKeys?
init(player: PlayerRegistration) {
self.player = player
_licenceId = .init(wrappedValue: player.licenceId ?? "")
_email = .init(wrappedValue: player.email ?? "")
_phoneNumber = .init(wrappedValue: player.phoneNumber ?? "")
}
var body: some View {
Form {
@ -22,6 +32,9 @@ struct PlayerDetailView: View {
.keyboardType(.alphabet)
.multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity)
.onSubmit(of: .text) {
_save()
}
} label: {
Text("Nom")
}
@ -31,6 +44,9 @@ struct PlayerDetailView: View {
.keyboardType(.alphabet)
.multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity)
.onSubmit(of: .text) {
_save()
}
} label: {
Text("Prénom")
}
@ -44,7 +60,7 @@ struct PlayerDetailView: View {
.keyboardType(.decimalPad)
.multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity)
.focused($textFieldIsFocus)
.focused($focusedField, equals: ._rank)
} label: {
Text("Rang")
}
@ -69,7 +85,7 @@ struct PlayerDetailView: View {
.keyboardType(.decimalPad)
.multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity)
.focused($textFieldIsFocus)
.focused($focusedField, equals: ._computedRank)
} label: {
Text("Poids re-calculé")
}
@ -79,17 +95,90 @@ struct PlayerDetailView: View {
Text("Calculé en fonction du sexe")
}
}
Section {
Toggle("Joueur sur place", isOn: $player.hasArrived)
}
Section {
LabeledContent {
TextField("Licence", text: $licenceId)
.keyboardType(.alphabet)
.multilineTextAlignment(.trailing)
.autocorrectionDisabled()
.frame(maxWidth: .infinity)
.onSubmit(of: .text) {
player.licenceId = licenceId
_save()
}
} label: {
Text("Licence")
}
RowButtonView("Copier dans le presse-papier") {
let pasteboard = UIPasteboard.general
pasteboard.string = player.licenceId?.strippedLicense
}
}
Section {
LabeledContent {
TextField("Téléphone", text: $phoneNumber)
.keyboardType(.namePhonePad)
.multilineTextAlignment(.trailing)
.autocorrectionDisabled()
.frame(maxWidth: .infinity)
.onSubmit(of: .text) {
player.phoneNumber = phoneNumber
_save()
}
} label: {
Text("Téléphone")
}
RowButtonView("Copier dans le presse-papier") {
let pasteboard = UIPasteboard.general
pasteboard.string = player.phoneNumber
}
}
Section {
LabeledContent {
TextField("Email", text: $email)
.keyboardType(.emailAddress)
.multilineTextAlignment(.trailing)
.autocorrectionDisabled()
.frame(maxWidth: .infinity)
.onSubmit(of: .text) {
player.email = email
_save()
}
} label: {
Text("Email")
}
RowButtonView("Copier dans le presse-papier") {
let pasteboard = UIPasteboard.general
pasteboard.string = player.email
}
}
}
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
ShareLink(item: player.pasteData()) {
Label("Partagez", systemImage: "square.and.arrow.up")
}
}
}
.scrollDismissesKeyboard(.immediately)
.onChange(of: player.sex) {
.onChange(of: player.hasArrived) {
_save()
}
.onChange(of: player.computedRank) {
player.team()?.updateWeight(inTournamentCategory: tournament.tournamentCategory)
.onChange(of: player.sex) {
_save()
}
.onChange(of: player.rank) {
player.setComputedRank(in: tournament)
.onChange(of: player.computedRank) {
player.team()?.updateWeight(inTournamentCategory: tournament.tournamentCategory)
_save()
}
@ -100,7 +189,15 @@ struct PlayerDetailView: View {
.toolbar {
ToolbarItem(placement: .keyboard) {
Button("Valider") {
textFieldIsFocus = false
if focusedField == ._rank {
player.setComputedRank(in: tournament)
player.team()?.updateWeight(inTournamentCategory: tournament.tournamentCategory)
_save()
} else if focusedField == ._computedRank {
player.team()?.updateWeight(inTournamentCategory: tournament.tournamentCategory)
_save()
}
focusedField = nil
}
}
}

@ -72,9 +72,9 @@ struct ImportedPlayerView: View {
Text("messieurs")
}
.font(.caption)
}
Text(")").font(.title3)
}
}
HStack {
Text(player.formattedLicense())

@ -46,6 +46,17 @@ struct EditingTeamView: View {
Text("Date d'inscription")
}
Section {
Toggle(isOn: hasArrived) {
Text("Équipe sur place")
}
/*
Toggle(isOn: $team.confirmedCall) {
Text("Équipe sur place")
}
*/
}
Section {
RowButtonView("Retirer des poules", role: .destructive) {
team.resetGroupeStagePosition()
@ -72,6 +83,21 @@ struct EditingTeamView: View {
.toolbarBackground(.visible, for: .navigationBar)
}
private var hasArrived: Binding<Bool> {
Binding {
team.unsortedPlayers().allSatisfy({ $0.hasArrived })
} set: { hasArrived in
team.unsortedPlayers().forEach {
$0.hasArrived = hasArrived
}
do {
try dataStore.playerRegistrations.addOrUpdate(contentOfs: team.unsortedPlayers())
} catch {
Logger.error(error)
}
}
}
private func _save() {
do {
try dataStore.teamRegistrations.addOrUpdate(instance: team)

@ -6,6 +6,7 @@
//
import SwiftUI
import LeStorage
struct TeamDetailView: View {
@Environment(Tournament.self) var tournament: Tournament

@ -28,8 +28,9 @@ struct FileImportView: View {
@State private var fileProvider: FileImportManager.FileProvider = .frenchFederation
@State private var validationInProgress: Bool = false
@State private var multiImport: Bool = false
private var filteredTeams: [FileImportManager.TeamHolder] {
private func filteredTeams(tournament: Tournament) -> [FileImportManager.TeamHolder] {
return teams.filter { $0.tournamentCategory == tournament.tournamentCategory }.sorted(by: \.weight)
}
@ -68,6 +69,24 @@ struct FileImportView: View {
Text(.init(footerString))
}
}
if let tournaments = tournament.eventObject()?.tournaments, tournaments.count > 1, fileProvider == .frenchFederation {
Section {
RowButtonView("Importer toutes les épreuves") {
multiImport = true
if let fileContent {
do {
try await _startImport(fileContent: fileContent)
} catch {
errorMessage = error.localizedDescription
}
}
}
} footer: {
Text("Ce tournoi possède plusieurs épreuves. Vous pouvez importer les équipes du fichier pour toutes les épreuves d'un coup.")
}
.disabled(fileContent == nil || convertingFile)
}
}
// if filteredTeams.isEmpty == false && tournament.unsortedTeams().isEmpty == false {
@ -128,7 +147,8 @@ struct FileImportView: View {
// }
// }
if filteredTeams.isEmpty && teams.isEmpty == false {
let filteredTeams = filteredTeams(tournament: tournament)
if filteredTeams.isEmpty && teams.isEmpty == false && multiImport == false {
@Bindable var tournament = tournament
Section {
Text("Aucune équipe \(tournament.tournamentCategory.importingRawValue) détectée mais \(teams.count) équipes sont dans le fichier")
@ -157,6 +177,23 @@ struct FileImportView: View {
.tipStyle(tint: nil)
}
}
if multiImport, fileProvider == .frenchFederation, let tournaments = tournament.eventObject()?.tournaments, tournaments.count > 1 {
ForEach(tournaments) { tournament in
let tournamentFilteredTeams = self.filteredTeams(tournament: tournament)
Section {
RowButtonView("Valider") {
await _validate(tournament: tournament)
}
} header: {
Text("\(tournamentFilteredTeams.count.formatted()) équipe\(tournamentFilteredTeams.count.pluralSuffix)")
} footer: {
Text(tournament.tournamentTitle())
}
}
.headerProminence(.increased)
} else {
Section {
LabeledContent {
Text(_filteredTeams.count.formatted())
@ -174,6 +211,7 @@ struct FileImportView: View {
}
}
}
}
.fileImporter(isPresented: $isShowing, allowedContentTypes: [.spreadsheet, .commaSeparatedText, .text], allowsMultipleSelection: false, onCompletion: { results in
switch results {
@ -215,13 +253,13 @@ struct FileImportView: View {
.navigationTitle("Importation")
.navigationBarTitleDisplayMode(.large)
.toolbar {
ToolbarItem(placement: .bottomBar) {
PasteButton(payloadType: String.self) { strings in
guard let string = strings.first else { return }
fileContent = string
fileProvider = .padelClub
}
}
// ToolbarItem(placement: .bottomBar) {
// PasteButton(payloadType: String.self) { strings in
// guard let string = strings.first else { return }
// fileContent = string
// fileProvider = .padelClub
// }
// }
ToolbarItem(placement: .cancellationAction) {
Button("Annuler", role: .cancel) {
dismiss()
@ -232,8 +270,19 @@ struct FileImportView: View {
if validationInProgress {
ProgressView()
} else {
ButtonValidateView {
ButtonValidateView(title: (multiImport ? "Tout Valider" : "Valider")) {
validationInProgress = true
Task {
if let tournaments = tournament.eventObject()?.tournaments, tournaments.count > 1, multiImport {
for tournament in tournaments {
await _validate(tournament: tournament)
}
dismiss()
} else {
await _validate(tournament: tournament)
}
}
}
.disabled(teams.isEmpty)
}
@ -241,13 +290,10 @@ struct FileImportView: View {
}
.interactiveDismissDisabled(validationInProgress)
.disabled(validationInProgress)
.onChange(of: validationInProgress) {
_validate()
}
}
private func _validate() {
Task {
private func _validate(tournament: Tournament) async {
let filteredTeams = filteredTeams(tournament: tournament)
let previousTeams = filteredTeams.compactMap({ $0.previousTeam })
let unfound = Set(tournament.unsortedTeams()).subtracting(Set(previousTeams))
@ -265,6 +311,7 @@ struct FileImportView: View {
}
tournament.importTeams(filteredTeams)
if multiImport == false {
dismiss()
}
}

@ -215,8 +215,7 @@ struct InscriptionManagerView: View {
Button("OK") {
}
} message: {
let message = [networkMonitor.connected == false ? "L'appareil n'est pas connecté à internet." as String? : nil, sentError == .mailNotSent ? "Le mail est dans la boîte d'envoi de l'app Mail. Vérifiez son état dans l'app Mail avant d'essayer de le renvoyer." as String? : nil, (sentError == .messageFailed || sentError == .messageNotSent) ? "Le SMS n'a pas été envoyé" as String? : nil, sentError == .mailFailed ? "Le mail n'a pas été envoyé" as String? : nil].compacted().joined(separator: "\n")
Text(message)
Text(_getErrorMessage())
}
.sheet(item: $contactType) { contactType in
Group {
@ -534,11 +533,6 @@ struct InscriptionManagerView: View {
let teamIndex = team.index(in: sortedTeams)
Section {
TeamDetailView(team: team)
.contextMenu {
Button("équipe présente") {
team.hasArrived()
}
}
} header: {
TeamHeaderView(team: team, teamIndex: teamIndex, tournament: tournament)
} footer: {
@ -962,7 +956,7 @@ struct InscriptionManagerView: View {
ForEach(createdPlayerIds.sorted(), id: \.self) { id in
if let p = createdPlayers.first(where: { $0.id == id }) {
VStack(alignment: .leading, spacing: 0) {
if unsortedPlayers.first(where: { $0.licenceId == p.licenceId }) != nil {
if let player = unsortedPlayers.first(where: { $0.licenceId == p.licenceId }), editedTeam?.includes(player: player) == false {
Text("Déjà inscrit !").foregroundStyle(.logoRed).bold()
}
PlayerView(player: p).tag(p.id)
@ -991,7 +985,7 @@ struct InscriptionManagerView: View {
}
} else {
RowButtonView("Modifier l'équipe") {
_updateTeam(checkDuplicates: true)
_updateTeam(checkDuplicates: false)
}
}
@ -1231,6 +1225,16 @@ struct InscriptionManagerView: View {
}
}
private func _getErrorMessage() -> String {
let m1 : String? = (networkMonitor.connected == false ? "L'appareil n'est pas connecté à internet." : nil)
let m2 : String? = (sentError == .mailNotSent ? "Le mail est dans la boîte d'envoi de l'app Mail. Vérifiez son état dans l'app Mail avant d'essayer de le renvoyer." : nil)
let m3 : String? = ((sentError == .messageFailed || sentError == .messageNotSent) ? "Le SMS n'a pas été envoyé" : nil)
let m4 : String? = (sentError == .mailFailed ? "Le mail n'a pas été envoyé" : nil)
let message : String = [m1, m2, m3, m4].compacted().joined(separator: "\n")
return message
}
private func _save() {
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)

@ -75,6 +75,7 @@ struct TournamentView: View {
}
}
}
.id(tournament.id)
.toolbarBackground(.visible, for: .navigationBar)
.navigationDestination(for: Screen.self, destination: { screen in
Group {

Loading…
Cancel
Save