Merge branch 'main' into sync3

sync3
Laurent 5 months ago
commit 892db419fe
  1. 4
      PadelClub.xcodeproj/project.pbxproj
  2. 2
      PadelClub/Extensions/Tournament+Extensions.swift
  3. 20
      PadelClub/Views/Cashier/CashierDetailView.swift
  4. 8
      PadelClub/Views/Cashier/CashierSettingsView.swift
  5. 6
      PadelClub/Views/Cashier/Event/EventStatusView.swift
  6. 10
      PadelClub/Views/Match/Components/MatchDateView.swift
  7. 115
      PadelClub/Views/Match/EditSharingView.swift
  8. 6
      PadelClub/Views/Match/MatchDetailView.swift
  9. 11
      PadelClub/Views/Navigation/Agenda/EventListView.swift
  10. 70
      PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift
  11. 241
      PadelClub/Views/Planning/PlanningView.swift
  12. 14
      PadelClub/Views/Score/EditScoreView.swift
  13. 165
      PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift
  14. 3
      PadelClubTests/ServerDataTests.swift

@ -3128,7 +3128,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.37;
MARKETING_VERSION = 1.2.40;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -3174,7 +3174,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.37;
MARKETING_VERSION = 1.2.40;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

@ -333,7 +333,7 @@ extension Tournament {
}
let rankSourceDate = _mostRecentDateAvailable
return Tournament(rankSourceDate: rankSourceDate)
return Tournament(rankSourceDate: rankSourceDate, currencyCode: Locale.defaultCurrency())
}
}

@ -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())

@ -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

@ -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: " / ")

@ -46,7 +46,7 @@ struct MatchDateView: View {
if let updatedField {
match.setCourt(updatedField)
}
match.startDate = currentDate
match.updateStartDate(currentDate, keepPlannedStartDate: true)
match.endDate = nil
match.confirmed = true
_save()
@ -55,7 +55,7 @@ struct MatchDateView: View {
if let updatedField {
match.setCourt(updatedField)
}
match.startDate = Calendar.current.date(byAdding: .minute, value: 5, to: currentDate)
match.updateStartDate(Calendar.current.date(byAdding: .minute, value: 5, to: currentDate), keepPlannedStartDate: true)
match.endDate = nil
match.confirmed = true
_save()
@ -64,7 +64,7 @@ struct MatchDateView: View {
if let updatedField {
match.setCourt(updatedField)
}
match.startDate = Calendar.current.date(byAdding: .minute, value: 15, to: currentDate)
match.updateStartDate(Calendar.current.date(byAdding: .minute, value: 15, to: currentDate), keepPlannedStartDate: true)
match.endDate = nil
match.confirmed = true
_save()
@ -73,7 +73,7 @@ struct MatchDateView: View {
if let updatedField {
match.setCourt(updatedField)
}
match.startDate = Calendar.current.date(byAdding: .minute, value: estimatedDuration, to: currentDate)
match.updateStartDate(Calendar.current.date(byAdding: .minute, value: estimatedDuration, to: currentDate), keepPlannedStartDate: true)
match.endDate = nil
match.confirmed = true
_save()
@ -96,7 +96,7 @@ struct MatchDateView: View {
}
Divider()
Button("Retirer l'horaire") {
match.cleanScheduleAndSave()
match.updateStartDate(nil, keepPlannedStartDate: true)
}
} label: {
label

@ -5,10 +5,11 @@
// Created by Razmig Sarkissian on 03/02/2024.
//
import SwiftUI
import TipKit
import CoreTransferable
import PadelClubData
import SwiftUI
import TipKit
import AVFoundation
struct EditSharingView: View {
var match: Match
@ -17,50 +18,61 @@ struct EditSharingView: View {
@State private var showCamera: Bool = false
@State private var newImage: UIImage?
@State private var copied: Bool = false
@State private var cameraAuthorizationStatus: AVAuthorizationStatus = .notDetermined
@State private var showCameraAccessAlert: Bool = false
var shareMessage: String {
shareMessage(displayRank: displayRank, displayTeamName: displayTeamTitle)
}
func shareMessage(displayRank: Bool, displayTeamName: Bool) -> String {
var messageData: [String] = []
if match.hasEnded() == false {
var locAndTime: String?
if let courtName = match.courtName() {
locAndTime = "\(courtName)"
}
if let startDate = match.startDate {
locAndTime = [locAndTime, startDate.formattedAsHourMinute()].compactMap({ $0 }).joined(separator: " à ")
locAndTime = [locAndTime, startDate.formattedAsHourMinute()].compactMap({ $0 })
.joined(separator: " à ")
}
if let locAndTime, locAndTime.isEmpty == false {
messageData.append(locAndTime)
}
}
if let tournament = match.currentTournament() {
messageData.append(tournament.tournamentTitle())
}
let message = [match.isLoserBracket ? "Classement" : nil, match.roundTitle(), match.isLoserBracket ? nil : ((match.index > 0 || match.isGroupStage()) ? match.matchTitle(.short) : nil)].compactMap({ $0 }).joined(separator: " ")
let message = [
match.isLoserBracket ? "Classement" : nil, match.roundTitle(),
match.isLoserBracket
? nil
: ((match.index > 0 || match.isGroupStage()) ? match.matchTitle(.short) : nil),
].compactMap({ $0 }).joined(separator: " ")
messageData.append(message)
guard let labelOne = match.team(.one)?.teamLabelRanked(displayRank: displayRank, displayTeamName: displayTeamName), let labelTwo = match.team(.two)?.teamLabelRanked(displayRank: displayRank, displayTeamName: displayTeamName) else {
guard
let labelOne = match.team(.one)?.teamLabelRanked(
displayRank: displayRank, displayTeamName: displayTeamName),
let labelTwo = match.team(.two)?.teamLabelRanked(
displayRank: displayRank, displayTeamName: displayTeamName)
else {
return messageData.joined(separator: "\n")
}
let players = "\(labelOne)\ncontre\n\(labelTwo)"
messageData.append(players)
messageData.append(match.scoreLabel())
return messageData.joined(separator: "\n")
}
var body: some View {
List {
@ -70,7 +82,7 @@ struct EditSharingView: View {
TipView(tip)
.tipStyle(tint: .green)
}
Section {
ZStack {
Color.black
@ -93,16 +105,16 @@ struct EditSharingView: View {
} else {
Section {
RowButtonView("Prendre une photo", systemImage: "camera") {
showCamera = true
checkCameraAuthorization()
}
}
}
}
Section {
Toggle(isOn: $displayRank) {
Text("Afficher leurs rangs dans ce tournoi")
}
Toggle(isOn: $displayTeamTitle) {
Text("Afficher plutôt le nom de l'équipe")
}
@ -124,23 +136,40 @@ struct EditSharingView: View {
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
if let newImage {
let photo = Photo(image: Image(uiImage:newImage), caption: shareMessage)
let photo = Photo(image: Image(uiImage: newImage), caption: shareMessage)
ShareLink(
item: photo,
preview: SharePreview(
photo.caption,
image: photo.image)) {
Text("Partager")
}
.onAppear {
UIPasteboard.general.string = shareMessage
copied = true
}
image: photo.image)
) {
Text("Partager")
}
.onAppear {
UIPasteboard.general.string = shareMessage
copied = true
}
} else {
ShareLink("Partager", item: shareMessage)
}
}
}
.onChange(of: displayTeamTitle) {
copied = false
}
.alert("Accès à l'appareil photo requis", isPresented: $showCameraAccessAlert) {
Button("Annuler", role: .cancel) {}
Button("Paramètres") {
if let settingsURL = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(settingsURL)
}
}
} message: {
Text(
"Pour prendre des photos, autorisez l'accès à l'appareil photo dans les paramètres de l'application."
)
}
.navigationTitle("Préparation")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
@ -155,6 +184,34 @@ struct EditSharingView: View {
copied = false
}
}
func checkCameraAuthorization() {
let status = AVCaptureDevice.authorizationStatus(for: .video)
self.cameraAuthorizationStatus = status
switch status {
case .authorized:
// Camera access already granted, show camera
self.showCamera = true
case .notDetermined:
// Request camera access
AVCaptureDevice.requestAccess(for: .video) { granted in
DispatchQueue.main.async {
if granted {
self.showCamera = true
} else {
self.showCameraAccessAlert = true
}
}
}
case .denied, .restricted:
// Camera access was previously denied or restricted
self.showCameraAccessAlert = true
@unknown default:
// Handle future cases
self.showCameraAccessAlert = true
}
}
}
struct Photo: Transferable {

@ -336,10 +336,7 @@ struct MatchDetailView: View {
}
}
Button(role: .destructive) {
match.startDate = nil
match.endDate = nil
match.confirmed = false
save()
match.updateStartDate(nil, keepPlannedStartDate: true)
} label: {
Text("Supprimer l'horaire")
}
@ -355,6 +352,7 @@ struct MatchDetailView: View {
}
Divider()
Button(role: .destructive) {
match.updateStartDate(nil, keepPlannedStartDate: false)
match.resetTeamScores(outsideOf: [])
match.resetMatch()
match.confirmed = false

@ -427,12 +427,11 @@ struct EventListView: View {
LabelDelete()
}
}
// Button() {
// self.showUserSearch = true
// } label: {
// ShareLabel().tint(.orange)
// }
Button() {
dataStore.deleteTournament(tournament, noSync: true)
} label: {
Label("Soft delete", systemImage: "trash")
}
}
#endif
}

@ -262,20 +262,29 @@ struct DateAdjusterView: View {
var body: some View {
HStack(spacing: 4) {
if let matchFormat {
_createButton(label: "-\(matchFormat.defaultEstimatedDuration)m", timeOffset: -matchFormat.defaultEstimatedDuration, component: .minute)
_createButton(label: "+\(matchFormat.defaultEstimatedDuration)m", timeOffset: +matchFormat.defaultEstimatedDuration, component: .minute)
_createButton(label: "-\(matchFormat.getEstimatedDuration())m", timeOffset: -matchFormat.getEstimatedDuration(), component: .minute)
Divider()
_createButton(label: "+\(matchFormat.getEstimatedDuration())m", timeOffset: +matchFormat.getEstimatedDuration(), component: .minute)
Divider()
_createButton(label: "-\(matchFormat.estimatedTimeWithBreak)m", timeOffset: -matchFormat.estimatedTimeWithBreak, component: .minute)
Divider()
_createButton(label: "+\(matchFormat.estimatedTimeWithBreak)m", timeOffset: +matchFormat.estimatedTimeWithBreak, component: .minute)
} else if let time {
_createButton(label: "-\(time)m", timeOffset: -time, component: .minute)
Divider()
_createButton(label: "-\(time/2)m", timeOffset: -time/2, component: .minute)
Divider()
_createButton(label: "+\(time/2)m", timeOffset: time/2, component: .minute)
Divider()
_createButton(label: "+\(time)m", timeOffset: time, component: .minute)
} else {
_createButton(label: "-1h", timeOffset: -1, component: .hour)
_createButton(label: "-1h", timeOffset: -60, component: .minute)
Divider()
_createButton(label: "-30m", timeOffset: -30, component: .minute)
Divider()
_createButton(label: "+30m", timeOffset: 30, component: .minute)
_createButton(label: "+1h", timeOffset: 1, component: .hour)
Divider()
_createButton(label: "+1h", timeOffset: 60, component: .minute)
}
}
.font(.headline)
@ -287,11 +296,58 @@ struct DateAdjusterView: View {
}) {
Text(label)
.lineLimit(1)
.font(.footnote)
.underline()
.frame(maxWidth: .infinity) // Make buttons take equal space
}
.buttonStyle(.borderedProminent)
.buttonStyle(.borderless)
.tint(.master)
}
}
struct StepAdjusterView: View {
@Binding var step: Int
var time: Int?
var matchFormat: MatchFormat?
var body: some View {
HStack(spacing: 4) {
if let matchFormat {
_createButton(label: "-\(matchFormat.getEstimatedDuration())m", timeOffset: -matchFormat.getEstimatedDuration(), component: .minute)
Divider()
_createButton(label: "+\(matchFormat.getEstimatedDuration())m", timeOffset: +matchFormat.getEstimatedDuration(), component: .minute)
Divider()
_createButton(label: "-\(matchFormat.estimatedTimeWithBreak)m", timeOffset: -matchFormat.estimatedTimeWithBreak, component: .minute)
Divider()
_createButton(label: "+\(matchFormat.estimatedTimeWithBreak)m", timeOffset: +matchFormat.estimatedTimeWithBreak, component: .minute)
} else if let time {
_createButton(label: "-\(time)m", timeOffset: -time, component: .minute)
Divider()
_createButton(label: "-\(time/2)m", timeOffset: -time/2, component: .minute)
Divider()
_createButton(label: "+\(time/2)m", timeOffset: time/2, component: .minute)
Divider()
_createButton(label: "+\(time)m", timeOffset: time, component: .minute)
} else {
_createButton(label: "-1h", timeOffset: -60, component: .minute)
Divider()
_createButton(label: "-30m", timeOffset: -30, component: .minute)
Divider()
_createButton(label: "+30m", timeOffset: 30, component: .minute)
Divider()
_createButton(label: "+1h", timeOffset: 60, component: .minute)
}
}
.font(.headline)
}
private func _createButton(label: String, timeOffset: Int, component: Calendar.Component) -> some View {
Button(action: {
step += timeOffset
}) {
Text(label)
.lineLimit(1)
.frame(maxWidth: .infinity) // Make buttons take equal space
}
.buttonStyle(.borderless)
.tint(.master)
}
}

@ -110,7 +110,7 @@ struct PlanningView: View {
if _confirmationMode() {
ToolbarItem(placement: .topBarLeading) {
Button("Annuler") {
Button(enableMove ? "Annuler" : "Terminer") {
enableMove = false
enableEditionBinding.wrappedValue = false
}
@ -130,82 +130,86 @@ struct PlanningView: View {
}
}
} else {
if notSlots == false {
ToolbarItemGroup(placement: .bottomBar) {
HStack {
ToolbarItemGroup(placement: .topBarTrailing) {
Menu {
if notSlots == false {
CourtOptionsView(timeSlots: timeSlots, underlined: false)
Spacer()
Toggle(isOn: $enableMove) {
Label {
Text("Déplacer")
Text("Déplacer un créneau")
} icon: {
Image(systemName: "rectangle.2.swap")
}
}
.popoverTip(timeSlotMoveOptionTip)
.disabled(_confirmationMode())
Spacer()
Toggle(isOn: enableEditionBinding) {
Text("Modifier")
Text("Modifier un horaire")
}
.disabled(_confirmationMode())
}
}
}
ToolbarItemGroup(placement: .topBarTrailing) {
Menu {
Section {
Picker(selection: $showFinishedMatches) {
Text("Afficher tous les matchs").tag(true)
Text("Masquer les matchs terminés").tag(false)
} label: {
Divider()
Menu {
Section {
Picker(selection: $showFinishedMatches) {
Text("Afficher tous les matchs").tag(true)
Text("Masquer les matchs terminés").tag(false)
} label: {
Text("Option de filtrage")
}
.labelsHidden()
.pickerStyle(.inline)
} header: {
Text("Option de filtrage")
}
.labelsHidden()
.pickerStyle(.inline)
} header: {
Text("Option de filtrage")
}
Divider()
Divider()
Section {
Picker(selection: $filterOption) {
ForEach(PlanningFilterOption.allCases) {
Text($0.localizedPlanningLabel()).tag($0)
Section {
Picker(selection: $filterOption) {
ForEach(PlanningFilterOption.allCases) {
Text($0.localizedPlanningLabel()).tag($0)
}
} label: {
Text("Option de triage")
}
} label: {
.labelsHidden()
.pickerStyle(.inline)
} header: {
Text("Option de triage")
}
.labelsHidden()
.pickerStyle(.inline)
} header: {
Text("Option de triage")
}
} label: {
Label("Trier", systemImage: "line.3.horizontal.decrease.circle")
.symbolVariant(
filterOption == .byCourt || showFinishedMatches ? .fill : .none)
}
} label: {
Label("Trier", systemImage: "line.3.horizontal.decrease.circle")
.symbolVariant(
filterOption == .byCourt || showFinishedMatches ? .fill : .none)
}
Button("Mettre à jour", systemImage: "arrow.trianglehead.2.clockwise.rotate.90.circle") {
let now = Date()
matches.forEach {
if let startDate = $0.startDate, startDate > now {
$0.plannedStartDate = $0.startDate
Divider()
Button("Mettre à jour", systemImage: "arrow.trianglehead.2.clockwise.rotate.90.circle") {
let now = Date()
matches.forEach {
if let startDate = $0.startDate, startDate > now {
$0.plannedStartDate = $0.startDate
}
}
let groupByTournaments = matches.grouped { match in
match.currentTournament()
}
groupByTournaments.forEach { tournament, matches in
tournament?.tournamentStore?.matches.addOrUpdate(contentOfs: matches)
}
}
let groupByTournaments = matches.grouped { match in
match.currentTournament()
}
groupByTournaments.forEach { tournament, matches in
tournament?.tournamentStore?.matches.addOrUpdate(contentOfs: matches)
}
.popoverTip(updatePlannedDatesTip)
} label: {
LabelOptions()
}
.popoverTip(updatePlannedDatesTip)
}
}
})
@ -235,8 +239,8 @@ struct PlanningView: View {
@Environment(\.editMode) private var editMode
@State private var selectedIds = Set<String>()
@State private var showDateUpdateView: Bool = false
@State private var dateToUpdate: Date = Date()
@State private var matchesToUpdate: [Match] = []
let days: [Date]
let keys: [Date]
let timeSlots: [Date: [Match]]
@ -258,28 +262,34 @@ struct PlanningView: View {
day: day,
keys: keys.filter({ $0.dayInt == day.dayInt }),
timeSlots: timeSlots,
selectedDay: selectedDay
selectedDay: selectedDay,
selectedIds: $selectedIds,
matchesForUpdateSheet: $matchesToUpdate
)
}
}
}
.toolbar(content: {
if editMode?.wrappedValue == .active {
ToolbarItem(placement: .bottomBar) {
ToolbarItem(placement: .topBarTrailing) {
Button {
showDateUpdateView = true
matchesToUpdate = matches.filter({ selectedIds.contains($0.stringId) })
} label: {
Text("Modifier la date des matchs sélectionnés")
Text("Modifier")
}
.buttonStyle(.borderless)
.disabled(selectedIds.isEmpty)
}
}
})
.onChange(of: matchesToUpdate, { oldValue, newValue in
showDateUpdateView = matchesToUpdate.count > 0
})
.sheet(isPresented: $showDateUpdateView, onDismiss: {
selectedIds.removeAll()
matchesToUpdate = []
}) {
let selectedMatches = matches.filter({ selectedIds.contains($0.stringId) })
DateUpdateView(selectedMatches: selectedMatches)
DateUpdateView(selectedMatches: matchesToUpdate)
}
}
}
@ -290,6 +300,7 @@ struct PlanningView: View {
let selectedMatches: [Match]
let selectedFormats: [MatchFormat]
@State private var dateToUpdate: Date
@State private var updateStep: Int = 0
init(selectedMatches: [Match]) {
self.selectedMatches = selectedMatches
@ -306,13 +317,37 @@ struct PlanningView: View {
DatePicker(selection: $dateToUpdate) {
Text(dateToUpdate.formatted(.dateTime.weekday(.wide))).font(.headline)
}
RowButtonView("Définir le nouvel horaire", role: .destructive) {
_setDate()
}
} footer: {
Text("Choisir un nouvel horaire pour tous les matchs sélectionnés")
}
Section {
DateAdjusterView(date: $dateToUpdate)
DateAdjusterView(date: $dateToUpdate, time: 10)
LabeledContent {
StepperView(title: "minutes", count: $updateStep, step: 5)
} label: {
Text("Décalage")
}
RowButtonView("Décaler les horaires", role: .destructive) {
_updateDate()
}
} footer: {
Text("décale CHAQUE horaire du nombre de minutes indiqué")
.foregroundStyle(.logoRed)
}
VStack {
StepAdjusterView(step: $updateStep)
Divider()
StepAdjusterView(step: $updateStep, time: 10)
ForEach(selectedFormats, id: \.self) { matchFormat in
DateAdjusterView(date: $dateToUpdate, matchFormat: matchFormat)
Divider()
StepAdjusterView(step: $updateStep, matchFormat: matchFormat)
}
}
@ -325,7 +360,10 @@ struct PlanningView: View {
}
}
.navigationTitle("Modification de la date")
.onChange(of: updateStep, { oldValue, newValue in
dateToUpdate.addTimeInterval(TimeInterval((newValue - oldValue) * 60))
})
.navigationTitle("Modifier l'horaire")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.toolbar(content: {
@ -334,25 +372,34 @@ struct PlanningView: View {
dismiss()
}
}
ToolbarItem(placement: .topBarTrailing) {
Button("Valider") {
_updateDate()
}
}
})
}
}
private func _updateDate() {
selectedMatches.forEach { match in
if match.hasStarted() || match.hasEnded() {
match.plannedStartDate?.addTimeInterval(TimeInterval(updateStep * 60))
} else {
match.startDate?.addTimeInterval(TimeInterval(updateStep * 60))
}
}
let groupByTournaments = selectedMatches.grouped { match in
match.currentTournament()
}
groupByTournaments.forEach { tournament, matches in
tournament?.tournamentStore?.matches.addOrUpdate(contentOfs: matches)
}
dismiss()
}
private func _setDate() {
selectedMatches.forEach { match in
if match.hasStarted() || match.hasEnded() {
match.plannedStartDate = dateToUpdate
} else {
let hasStarted = match.currentTournament()?.hasStarted() == true
match.startDate = dateToUpdate
if hasStarted {
match.plannedStartDate = dateToUpdate
}
}
}
@ -378,7 +425,10 @@ struct PlanningView: View {
let keys: [Date]
let timeSlots: [Date: [Match]]
let selectedDay: Date?
@Binding var selectedIds: Set<String>
@State private var selectAll: Bool = false
@Binding var matchesForUpdateSheet: [Match]
var body: some View {
Section {
ForEach(keys, id: \.self) { key in
@ -386,12 +436,22 @@ struct PlanningView: View {
key: key,
matches: timeSlots[key]?.sorted(
by: filterOption == .byDefault
? \.computedOrder : \.courtIndexForSorting) ?? []
? \.computedOrder : \.courtIndexForSorting) ?? [], matchesForUpdateSheet: $matchesForUpdateSheet
)
}
.onMove(perform: enableMove ? moveSection : nil)
} header: {
HeaderView(day: day, timeSlots: timeSlots)
if editMode?.wrappedValue == .active {
HStack {
Spacer()
FooterButtonView(selectAll ? "Tout desélectionner" : "Tout sélectionner") {
selectAll.toggle()
}
.textCase(nil)
}
} else {
HeaderView(day: day, timeSlots: timeSlots)
}
} footer: {
VStack(alignment: .leading) {
if day.monthYearFormatted == Date.distantFuture.monthYearFormatted {
@ -403,6 +463,16 @@ struct PlanningView: View {
CourtOptionsView(timeSlots: timeSlots, underlined: true)
}
}
.onChange(of: selectAll, { oldValue, newValue in
if oldValue == false, newValue == true {
selectedIds = Set(timeSlots.filter({ keys.contains($0.key) }).values.flatMap({ values in values.compactMap({ match in match.stringId }) }))
} else if oldValue == true, newValue == false {
selectedIds.removeAll()
}
})
.onChange(of: editMode?.wrappedValue) { oldValue, newValue in
selectAll = false
}
}
func moveSection(from source: IndexSet, to destination: Int) {
@ -450,7 +520,7 @@ struct PlanningView: View {
let matches: [Match]
@State private var isExpanded: Bool = false
@State private var showDateUpdateView: Bool = false
@Binding var matchesForUpdateSheet: [Match]
var body: some View {
if !matches.isEmpty {
@ -462,21 +532,23 @@ struct PlanningView: View {
} label: {
TimeSlotHeaderView(key: key, matches: matches)
}
.onChange(of: editMode?.wrappedValue, { oldValue, newValue in
if oldValue == .inactive, newValue == .active, isExpanded == false {
isExpanded = true
} else if oldValue == .active, newValue == .inactive, isExpanded == true {
isExpanded = false
}
})
.contextMenu {
PlanningView.CourtOptionsView(timeSlots: [key: matches], underlined: false)
Button {
showDateUpdateView = true
matchesForUpdateSheet = matches
} label: {
Text("Modifier la date")
}
}
.sheet(isPresented: $showDateUpdateView, onDismiss: {
}) {
PlanningView.DateUpdateView(selectedMatches: matches)
}
// .onChange(of: editMode?.wrappedValue) {
// if editMode?.wrappedValue == .active, isExpanded == false {
// isExpanded = true
@ -500,6 +572,7 @@ struct PlanningView: View {
} label: {
MatchRowView(match: match)
}
.listRowView(isActive: match.hasStarted(), color: .green, hideColorVariation: true)
}
}
}

@ -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<Bool>) {
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)
}

@ -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<String?> {
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 {

@ -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")
}

Loading…
Cancel
Save