diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 8ad6e82..b0c58b3 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -3129,7 +3129,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.39; + MARKETING_VERSION = 1.2.40; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3175,7 +3175,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.39; + MARKETING_VERSION = 1.2.40; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/PadelClub/Extensions/Tournament+Extensions.swift b/PadelClub/Extensions/Tournament+Extensions.swift index c637a6f..a928ded 100644 --- a/PadelClub/Extensions/Tournament+Extensions.swift +++ b/PadelClub/Extensions/Tournament+Extensions.swift @@ -333,7 +333,7 @@ extension Tournament { } let rankSourceDate = _mostRecentDateAvailable - return Tournament(rankSourceDate: rankSourceDate) + return Tournament(rankSourceDate: rankSourceDate, currencyCode: Locale.defaultCurrency()) } } diff --git a/PadelClub/Views/Cashier/CashierDetailView.swift b/PadelClub/Views/Cashier/CashierDetailView.swift index 22bba64..d1fbec0 100644 --- a/PadelClub/Views/Cashier/CashierDetailView.swift +++ b/PadelClub/Views/Cashier/CashierDetailView.swift @@ -22,13 +22,17 @@ struct CashierDetailView: View { self.tournaments = [tournament] } + func defaultCurrency() -> String { + tournaments.first?.currencyCode ?? Locale.defaultCurrency() + } + var body: some View { List { if tournaments.count > 1 { Section { LabeledContent { if let remainingAmount { - Text(remainingAmount.formatted(.currency(code: Locale.defaultCurrency()).precision(.fractionLength(0)))) + Text(remainingAmount.formatted(.currency(code: defaultCurrency()).precision(.fractionLength(0)))) } else { ProgressView() } @@ -38,7 +42,7 @@ struct CashierDetailView: View { LabeledContent { if let earnings { - Text(earnings.formatted(.currency(code: Locale.defaultCurrency()).precision(.fractionLength(0)))) + Text(earnings.formatted(.currency(code: defaultCurrency()).precision(.fractionLength(0)))) } else { ProgressView() } @@ -116,7 +120,7 @@ struct CashierDetailView: View { Section { LabeledContent { if let remainingAmount { - Text(remainingAmount.formatted(.currency(code: Locale.defaultCurrency()).precision(.fractionLength(0)))) + Text(remainingAmount.formatted(.currency(code: tournament.defaultCurrency()).precision(.fractionLength(0)))) } else { ProgressView() } @@ -126,7 +130,7 @@ struct CashierDetailView: View { LabeledContent { if let earnings { - Text(earnings.formatted(.currency(code: Locale.defaultCurrency()).precision(.fractionLength(0)))) + Text(earnings.formatted(.currency(code: tournament.defaultCurrency()).precision(.fractionLength(0)))) } else { ProgressView() } @@ -176,10 +180,14 @@ struct CashierDetailView: View { @State private var value: Double? + func defaultCurrency() -> String { + tournaments.first?.currencyCode ?? Locale.defaultCurrency() + } + var body: some View { LabeledContent { if let value { - Text(value.formatted(.currency(code: Locale.defaultCurrency()))) + Text(value.formatted(.currency(code: defaultCurrency()))) } else { ProgressView() } @@ -207,7 +215,7 @@ struct CashierDetailView: View { if players.count > 0 { LabeledContent { let sum = players.compactMap({ $0.paidAmount(tournament) }).reduce(0.0, +) - Text(sum.formatted(.currency(code: Locale.defaultCurrency()))) + Text(sum.formatted(.currency(code: tournament.defaultCurrency()))) } label: { Text(type.localizedLabel()) Text(players.count.formatted()) diff --git a/PadelClub/Views/Cashier/CashierSettingsView.swift b/PadelClub/Views/Cashier/CashierSettingsView.swift index 01110d4..2baab1c 100644 --- a/PadelClub/Views/Cashier/CashierSettingsView.swift +++ b/PadelClub/Views/Cashier/CashierSettingsView.swift @@ -29,7 +29,7 @@ struct CashierSettingsView: View { List { Section { LabeledContent { - TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $entryFee, format: .currency(code: Locale.defaultCurrency())) + TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $entryFee, format: .currency(code: tournament.defaultCurrency())) .keyboardType(.decimalPad) .multilineTextAlignment(.trailing) .frame(maxWidth: .infinity) @@ -38,7 +38,7 @@ struct CashierSettingsView: View { Text("Frais d'inscription") } LabeledContent { - TextField("Réduction", value: $clubMemberFeeDeduction, format: .currency(code: Locale.defaultCurrency())) + TextField("Réduction", value: $clubMemberFeeDeduction, format: .currency(code: tournament.defaultCurrency())) .keyboardType(.decimalPad) .multilineTextAlignment(.trailing) .frame(maxWidth: .infinity) @@ -132,7 +132,7 @@ struct CashierSettingsView: View { if focusedField == ._entryFee { if tournament.isFree() { ForEach(priceTags, id: \.self) { priceTag in - Button(priceTag.formatted(.currency(code: Locale.defaultCurrency()))) { + Button(priceTag.formatted(.currency(code: tournament.defaultCurrency()))) { entryFee = priceTag tournament.entryFee = priceTag focusedField = nil @@ -150,7 +150,7 @@ struct CashierSettingsView: View { } } else if focusedField == ._clubMemberFeeDeduction { ForEach(deductionTags, id: \.self) { deductionTag in - Button(deductionTag.formatted(.currency(code: Locale.defaultCurrency()).precision(.fractionLength(0)))) { + Button(deductionTag.formatted(.currency(code: tournament.defaultCurrency()).precision(.fractionLength(0)))) { clubMemberFeeDeduction = deductionTag tournament.clubMemberFeeDeduction = deductionTag focusedField = nil diff --git a/PadelClub/Views/Cashier/Event/EventStatusView.swift b/PadelClub/Views/Cashier/Event/EventStatusView.swift index 5cf37ff..7d6a7f1 100644 --- a/PadelClub/Views/Cashier/Event/EventStatusView.swift +++ b/PadelClub/Views/Cashier/Event/EventStatusView.swift @@ -35,6 +35,10 @@ struct EventStatusView: View { init(tournaments: [Tournament]) { self.tournaments = tournaments } + + func defaultCurrency() -> String { + tournaments.first?.currencyCode ?? Locale.defaultCurrency() + } private func _calculateTeamsCount() async { Task { @@ -55,7 +59,7 @@ struct EventStatusView: View { private func _currencyView(value: Double, value2: Double? = nil) -> some View { let maps = [value, value2].compactMap({ $0 }).map { - $0.formatted(.currency(code: Locale.defaultCurrency()).precision(.fractionLength(0))) + $0.formatted(.currency(code: defaultCurrency()).precision(.fractionLength(0))) } let string = maps.joined(separator: " / ") diff --git a/PadelClub/Views/Score/EditScoreView.swift b/PadelClub/Views/Score/EditScoreView.swift index 624f2c5..9a355aa 100644 --- a/PadelClub/Views/Score/EditScoreView.swift +++ b/PadelClub/Views/Score/EditScoreView.swift @@ -19,6 +19,7 @@ struct EditScoreView: View { @Environment(\.dismiss) private var dismiss @State private var firstTeamIsFirstScoreToEnter: Bool = true @State private var shouldEndMatch: Bool = false + @State private var walkoutPosition: TeamPosition? init(match: Match, confirmScoreEdition: Binding) { let matchDescriptor = MatchDescriptor(match: match) @@ -196,9 +197,20 @@ struct EditScoreView: View { Text("Terminer le match") } + + if shouldEndMatch { + Picker(selection: $walkoutPosition) { + Text("Non").tag(nil as TeamPosition?) + Text(matchDescriptor.teamLabelOne).tag(TeamPosition.one) + Text(matchDescriptor.teamLabelTwo).tag(TeamPosition.two) + } label: { + Text("Abandon") + } + } + RowButtonView("Confirmer") { if shouldEndMatch { - matchDescriptor.match?.setUnfinishedScore(fromMatchDescriptor: matchDescriptor) + matchDescriptor.match?.setUnfinishedScore(fromMatchDescriptor: matchDescriptor, walkoutPosition: walkoutPosition) } else { matchDescriptor.match?.updateScore(fromMatchDescriptor: matchDescriptor) } diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift index 66d7de0..abdb908 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift @@ -22,6 +22,7 @@ struct TournamentGeneralSettingsView: View { @State private var umpireCustomContact: String @State private var umpireCustomMailIsInvalid: Bool = false @State private var umpireCustomPhoneIsInvalid: Bool = false + @State private var showCurrencyPicker: Bool = false // New state for action sheet @FocusState private var focusedField: Tournament.CodingKeys? let priceTags: [Double] = [15.0, 20.0, 25.0] @@ -45,7 +46,7 @@ struct TournamentGeneralSettingsView: View { TournamentDatePickerView() TournamentDurationManagerView() LabeledContent { - TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $entryFee, format: .currency(code: Locale.defaultCurrency())) + TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $entryFee, format: .currency(code: tournament.defaultCurrency())) .keyboardType(.decimalPad) .multilineTextAlignment(.trailing) .frame(maxWidth: .infinity) @@ -60,10 +61,14 @@ struct TournamentGeneralSettingsView: View { } label: { Text("Inscription") + FooterButtonView("modifier la devise") { + showCurrencyPicker = true + } + .font(.footnote) } LabeledContent { - TextField("Réduction", value: $clubMemberFeeDeduction, format: .currency(code: Locale.defaultCurrency())) + TextField("Réduction", value: $clubMemberFeeDeduction, format: .currency(code: tournament.defaultCurrency())) .keyboardType(.decimalPad) .multilineTextAlignment(.trailing) .frame(maxWidth: .infinity) @@ -84,27 +89,62 @@ struct TournamentGeneralSettingsView: View { } if tournament.onlineRegistrationCanBeEnabled() { + let canEnableOnlinePayment = dataStore.user.canEnableOnlinePayment() + Section { - NavigationLink { - RegistrationSetupView(tournament: tournament) + // MARK: - Online Registration Row + LabeledContent { + if tournament.enableOnlineRegistration { + Text("Activée") + .foregroundStyle(.green) + .font(.headline) + } else { + Text("Désactivée") + .foregroundStyle(.logoRed) + .font(.headline) + } } label: { + Text("Inscription en ligne") + Text(tournament.getOnlineRegistrationStatus().statusLocalized()) + } + + // MARK: - Online Payment Row (Conditionally Visible) + if canEnableOnlinePayment { LabeledContent { - if tournament.enableOnlineRegistration { - Text("activée").foregroundStyle(.green) + if tournament.enableOnlinePayment { + Text("Activé") + .foregroundStyle(.green) .font(.headline) } else { - Text("désactivée").foregroundStyle(.logoRed) + Text("Désactivé") + .foregroundStyle(.logoRed) .font(.headline) } } label: { - Text("Accéder aux paramètres") - Text(tournament.getOnlineRegistrationStatus().statusLocalized()) + Text("Paiement en ligne") + Text(tournament.getPaymentStatus().statusLocalized()) } } + + // MARK: - Access Settings Row + NavigationLink { + RegistrationSetupView(tournament: tournament) + } label: { + Text("Accès aux réglages") + } + } header: { - Text("Inscription en ligne") + if canEnableOnlinePayment { + Text("Inscription et paiement en ligne") + } else { + Text("Inscription en ligne") + } } footer: { - Text("Paramétrez les possibilités d'inscription en ligne à votre tournoi via Padel Club") + if canEnableOnlinePayment { + Text("Paramétrez les possibilités d'inscription en ligne à votre tournoi via Padel Club") + } else { + Text("Paramétrez les possibilités d'inscription et paiement en ligne à votre tournoi via Padel Club") + } } } @@ -156,9 +196,14 @@ struct TournamentGeneralSettingsView: View { } footer: { FooterButtonView("Ajouter le prix de l'inscription") { tournamentInformation.append("\n" + tournament.entryFeeMessage) + _save() } } } + .sheet(isPresented: $showCurrencyPicker, content: { + CurrencySelectorView() + .environment(tournament) + }) .navigationBarBackButtonHidden(focusedField != nil) .toolbar(content: { if focusedField != nil { @@ -177,7 +222,7 @@ struct TournamentGeneralSettingsView: View { if focusedField == ._entryFee { if tournament.isFree() { ForEach(priceTags, id: \.self) { priceTag in - Button(priceTag.formatted(.currency(code: Locale.defaultCurrency()).precision(.fractionLength(0)))) { + Button(priceTag.formatted(.currency(code: tournament.defaultCurrency()).precision(.fractionLength(0)))) { entryFee = priceTag tournament.entryFee = priceTag focusedField = nil @@ -195,7 +240,7 @@ struct TournamentGeneralSettingsView: View { } } else if focusedField == ._clubMemberFeeDeduction { ForEach(deductionTags, id: \.self) { deductionTag in - Button(deductionTag.formatted(.currency(code: Locale.defaultCurrency()).precision(.fractionLength(0)))) { + Button(deductionTag.formatted(.currency(code: tournament.defaultCurrency()).precision(.fractionLength(0)))) { clubMemberFeeDeduction = deductionTag tournament.clubMemberFeeDeduction = deductionTag focusedField = nil @@ -291,6 +336,100 @@ struct TournamentGeneralSettingsView: View { } } + struct CurrencySelectorView: View { + @EnvironmentObject var dataStore: DataStore + @Environment(Tournament.self) var tournament + @Environment(\.dismiss) var dismiss + @State private var currencySearchText: String = "" + + func formatter(forCurrencyCode currencyCode: String) -> NumberFormatter { + let formatter = NumberFormatter() + formatter.numberStyle = .currency + formatter.currencyCode = currencyCode + return formatter + } + + struct CurrencyData: Identifiable { + let id: String + let name: String + let symbol: String + + init?(code: String) { + if let name = Locale.current.localizedString(forCurrencyCode: code) { + let formatter = NumberFormatter() + formatter.numberStyle = .currency + formatter.currencyCode = code + self.symbol = formatter.currencySymbol ?? code + self.id = code + self.name = name + } else { + return nil + } + } + } + + let currencies : [CurrencyData] = Locale.Currency.isoCurrencies.sorted(by: { Locale.current.localizedString(forCurrencyCode: $0.identifier) ?? $0.identifier < Locale.current.localizedString(forCurrencyCode: $1.identifier) ?? $1.identifier }).compactMap { currency in + CurrencyData(code: currency.identifier) + } + + var currencyCode: Binding { + Binding { + tournament.defaultCurrency() + } set: { currency in + tournament.currencyCode = currency + dataStore.tournaments.addOrUpdate(instance: tournament) + dismiss() + } + } + + var filteredCurrencies: [CurrencyData] { + if currencySearchText.isEmpty { + return currencies + } else { + return currencies.filter { + $0.name.lowercased().contains(currencySearchText.lowercased()) + } + } + } + + var body: some View { + NavigationStack { + List(selection: currencyCode) { + Section { + LabeledContent { + Text(tournament.defaultCurrency()) + } label: { + Text("Devise utilisée du tournoi") + } + } header: { + Text("") + } + + Section { + ForEach(filteredCurrencies) { currency in + LabeledContent { + Text(currency.symbol) + } label: { + Text(currency.name) + } + .tag(currency.id) + } + } + } + .navigationBarTitle("Choisir une devise") + .searchable(text: $currencySearchText, placement: .navigationBarDrawer(displayMode: .always)) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button("Retour", role: .cancel) { + self.dismiss() + } + } + } + .toolbarBackground(.visible, for: .navigationBar) + } + } + } + private func _confirmUmpireMail() { umpireCustomMailIsInvalid = false if umpireCustomMail.isEmpty { diff --git a/PadelClubTests/ServerDataTests.swift b/PadelClubTests/ServerDataTests.swift index 9e06d95..a0729e0 100644 --- a/PadelClubTests/ServerDataTests.swift +++ b/PadelClubTests/ServerDataTests.swift @@ -115,7 +115,7 @@ final class ServerDataTests: XCTestCase { let tournament = Tournament(event: eventId, name: "RG Homme", startDate: Date(), endDate: nil, creationDate: Date(), isPrivate: false, groupStageFormat: MatchFormat.megaTie, roundFormat: MatchFormat.nineGames, loserRoundFormat: MatchFormat.nineGamesDecisivePoint, groupStageSortMode: GroupStageOrderingMode.snake, groupStageCount: 2, rankSourceDate: Date(), dayDuration: 5, teamCount: 3, teamSorting: TeamSortingType.rank, federalCategory: TournamentCategory.mix, federalLevelCategory: TournamentLevel.p1000, federalAgeCategory: FederalTournamentAge.a45, closedRegistrationDate: Date(), groupStageAdditionalQualified: 4, courtCount: 9, prioritizeClubMembers: true, qualifiedPerGroupStage: 1, teamsPerGroupStage: 2, entryFee: 30.0, additionalEstimationDuration: 5, isDeleted: true, publishTeams: true, publishSummons: true, publishGroupStages: true, publishBrackets: true, shouldVerifyGroupStage: true, shouldVerifyBracket: true, hideTeamsWeight: true, publishTournament: true, hidePointsEarned: true, publishRankings: true, loserBracketMode: .manual, initialSeedRound: 8, initialSeedCount: 4, accountIsRequired: false, licenseIsRequired: false, minimumPlayerPerTeam: 3, maximumPlayerPerTeam: 5, information: "Super", umpireCustomMail: "razmig@padelclub.app", umpireCustomContact: "Raz", umpireCustomPhone: "+33681598193", hideUmpireMail: true, hideUmpirePhone: true, disableRankingFederalRuling: true, teamCountLimit: false, enableOnlinePayment: false, onlinePaymentIsMandatory: false, enableOnlinePaymentRefund: false, refundDateLimit: nil, stripeAccountId: nil, enableTimeToConfirm: false, isCorporateTournament: false, isTemplate: false, publishProg: true, - showTeamsInProg: true + showTeamsInProg: true, currencyCode: "USD") ) @@ -190,6 +190,7 @@ final class ServerDataTests: XCTestCase { assert(t.isTemplate == tournament.isTemplate) assert(t.publishProg == tournament.publishProg) assert(t.showTeamsInProg == tournament.showTeamsInProg) + assert(t.currencyCode == tournament.currencyCode) } else { XCTFail("missing data") }