Laurent 3 weeks ago
commit f6cf835ebf
  1. 8
      CLAUDE.md
  2. 12
      PadelClub.xcodeproj/project.pbxproj
  3. 40
      PadelClub/Utils/Network/PaymentService.swift
  4. 2
      PadelClub/Views/Calling/BracketCallingView.swift
  5. 9
      PadelClub/Views/Calling/CallMessageCustomizationView.swift
  6. 4
      PadelClub/Views/Calling/Components/PlayersWithoutContactView.swift
  7. 4
      PadelClub/Views/Calling/SendToAllView.swift
  8. 2
      PadelClub/Views/Cashier/CashierView.swift
  9. 1
      PadelClub/Views/GroupStage/GroupStageView.swift
  10. 1
      PadelClub/Views/Match/MatchSetupView.swift
  11. 19
      PadelClub/Views/Navigation/Toolbox/ToolboxView.swift
  12. 52
      PadelClub/Views/Planning/PlanningView.swift
  13. 2
      PadelClub/Views/Player/PlayerDetailView.swift
  14. 9
      PadelClub/Views/Round/RoundSettingsView.swift
  15. 2
      PadelClub/Views/Shared/LearnMoreSheetView.swift
  16. 24
      PadelClub/Views/Team/EditingTeamView.swift
  17. 268
      PadelClub/Views/Team/PaymentLinkManagerView.swift
  18. 2
      PadelClub/Views/Team/PaymentRequestButton.swift
  19. 55
      PadelClub/Views/Tournament/Screen/Components/HeadManagerView.swift
  20. 8
      PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift
  21. 99
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  22. 274
      PadelClub/Views/Tournament/Screen/TableStructureView.swift

@ -0,0 +1,8 @@
## Padel Club
This is the main directory of a Swift app that helps padel tournament organizers.
The project is structured around three projects linked in the PadelClub.xcworkspace:
- PadelClub: this one, which mostly contains the UI for the project
- PadelClubData: the business logic for the app
- LeStorage: a local storage with a synchronization layer

@ -151,6 +151,9 @@
FF1F4B8A2BFA02A4000B4573 /* groupstage-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B772BFA0105000B4573 /* groupstage-template.html */; };
FF1F4B8B2BFA02A4000B4573 /* groupstageentrant-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B792BFA0105000B4573 /* groupstageentrant-template.html */; };
FF1F4B8C2BFA02A4000B4573 /* match-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B7D2BFA0105000B4573 /* match-template.html */; };
FF2099042EA0D99A003CE880 /* PaymentLinkManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2099032EA0D99A003CE880 /* PaymentLinkManagerView.swift */; };
FF2099052EA0D99A003CE880 /* PaymentLinkManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2099032EA0D99A003CE880 /* PaymentLinkManagerView.swift */; };
FF2099062EA0D99A003CE880 /* PaymentLinkManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2099032EA0D99A003CE880 /* PaymentLinkManagerView.swift */; };
FF2B51552C7A4DAF00FFF126 /* PlanningByCourtView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2B51542C7A4DAF00FFF126 /* PlanningByCourtView.swift */; };
FF2B51612C7E302C00FFF126 /* local.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = FF2B51602C7E302C00FFF126 /* local.sqlite */; };
FF2B6F5E2C036A1500835EE7 /* EventLinksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2B6F5D2C036A1400835EE7 /* EventLinksView.swift */; };
@ -1038,6 +1041,7 @@
FF1F4B7E2BFA0105000B4573 /* player-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "player-template.html"; sourceTree = "<group>"; };
FF1F4B7F2BFA0105000B4573 /* tournament-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "tournament-template.html"; sourceTree = "<group>"; };
FF1F4B812BFA0124000B4573 /* PrintSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrintSettingsView.swift; sourceTree = "<group>"; };
FF2099032EA0D99A003CE880 /* PaymentLinkManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentLinkManagerView.swift; sourceTree = "<group>"; };
FF2B51542C7A4DAF00FFF126 /* PlanningByCourtView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlanningByCourtView.swift; sourceTree = "<group>"; };
FF2B51602C7E302C00FFF126 /* local.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = local.sqlite; sourceTree = "<group>"; };
FF2B51622C7F073100FFF126 /* Model_1_1.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_1_1.xcdatamodel; sourceTree = "<group>"; };
@ -1890,6 +1894,7 @@
FF17CA562CC02FEA003C7323 /* CoachListView.swift */,
FF7DCD382CC330260041110C /* TeamRestingView.swift */,
FF30ACF02E8D7078008B6006 /* PaymentRequestButton.swift */,
FF2099032EA0D99A003CE880 /* PaymentLinkManagerView.swift */,
FF025AD62BD0C0FB00A86CF8 /* Components */,
);
path = Team;
@ -2502,6 +2507,7 @@
FFA97C8D2E8A59D00089EA22 /* TournamentPickerView.swift in Sources */,
C493B37E2C10AD3600862481 /* LoadingViewModifier.swift in Sources */,
FF089EBD2BB0287D00F0AEC7 /* PlayerView.swift in Sources */,
FF2099052EA0D99A003CE880 /* PaymentLinkManagerView.swift in Sources */,
FF967D032BAEF0C000A9A3BD /* MatchDetailView.swift in Sources */,
FFE8B5B32DA848D300BDE966 /* OnlineWaitingListFaqSheetView.swift in Sources */,
FF967D0F2BAF63B000A9A3BD /* PlayerBlockView.swift in Sources */,
@ -2779,6 +2785,7 @@
FFA97C8F2E8A59D00089EA22 /* TournamentPickerView.swift in Sources */,
FF4CC0142C996C0600151637 /* PlayerBlockView.swift in Sources */,
FF4CC0172C996C0600151637 /* PointView.swift in Sources */,
FF2099042EA0D99A003CE880 /* PaymentLinkManagerView.swift in Sources */,
FF4CC0182C996C0600151637 /* ClubHolder.swift in Sources */,
FF4CC0192C996C0600151637 /* EventSettingsView.swift in Sources */,
C49771E72DC25F04005CD239 /* Color+Extensions.swift in Sources */,
@ -3034,6 +3041,7 @@
FFA97C8E2E8A59D00089EA22 /* TournamentPickerView.swift in Sources */,
FF70FB932C90584900129CC2 /* PlayerBlockView.swift in Sources */,
FF70FB962C90584900129CC2 /* PointView.swift in Sources */,
FF2099062EA0D99A003CE880 /* PaymentLinkManagerView.swift in Sources */,
FF70FB972C90584900129CC2 /* ClubHolder.swift in Sources */,
FF70FB982C90584900129CC2 /* EventSettingsView.swift in Sources */,
C49771E42DC25F04005CD239 /* Color+Extensions.swift in Sources */,
@ -3221,7 +3229,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 2;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
@ -3269,7 +3277,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 2;
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6;

@ -13,8 +13,8 @@ class PaymentService {
static func resendPaymentEmail(teamRegistrationId: String) async throws -> SimpleResponse {
let service = try StoreCenter.main.service()
let urlRequest = try service._baseRequest(
servicePath: "resend-payment-email/\(teamRegistrationId)/",
method: .post,
servicePath: "resend-payment-email/\(teamRegistrationId)/",
method: .post,
requiresToken: true
)
@ -27,6 +27,42 @@ class PaymentService {
return try JSON.decoder.decode(SimpleResponse.self, from: data)
}
static func getPaymentLink(teamRegistrationId: String) async throws -> PaymentLinkResponse {
let service = try StoreCenter.main.service()
let urlRequest = try service._baseRequest(
servicePath: "payment-link/\(teamRegistrationId)/",
method: .get,
requiresToken: true
)
let (data, response) = try await URLSession.shared.data(for: urlRequest)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw PaymentError.requestFailed
}
// // Debug: Print the raw JSON response
// if let jsonString = String(data: data, encoding: .utf8) {
// print("Raw JSON Response: \(jsonString)")
// }
return try JSON.decoder.decode(PaymentLinkResponse.self, from: data)
}
}
struct PaymentLinkResponse: Codable {
let success: Bool
let paymentLink: String?
let message: String?
enum CodingKeys: String, CodingKey {
case success
case paymentLink
case message
}
}
enum PaymentError: Error {

@ -106,7 +106,7 @@ struct BracketCallingView: View {
ForEach(filteredRounds()) { round in
let seeds = seeds(forRoundIndex: round.index)
let startDate = round.startDate ?? round.playedMatches().first?.startDate
let startDate = ([round.startDate] + round.playedMatches().map { $0.startDate }).compacted().min()
let callSeeds = seeds.filter({ tournament.isStartDateIsDifferentThanCallDate($0, expectedSummonDate: startDate) == false })
if seeds.isEmpty == false {
Section {

@ -31,7 +31,7 @@ struct CallMessageCustomizationView: View {
self.tournament = tournament
_customCallMessageBody = State(wrappedValue: DataStore.shared.user.summonsMessageBody ?? (DataStore.shared.user.summonsUseFullCustomMessage ? "" : ContactType.defaultCustomMessage))
_customCallMessageSignature = State(wrappedValue: DataStore.shared.user.getSummonsMessageSignature() ?? DataStore.shared.user.defaultSignature(tournament))
_customClubName = State(wrappedValue: tournament.clubName ?? "Lieu du tournoi")
_customClubName = State(wrappedValue: tournament.customClubName ?? tournament.clubName ?? "Lieu du tournoi")
_summonsAvailablePaymentMethods = State(wrappedValue: DataStore.shared.user.summonsAvailablePaymentMethods ?? ContactType.defaultAvailablePaymentMethods)
}
@ -235,14 +235,13 @@ struct CallMessageCustomizationView: View {
if let eventClub = tournament.eventObject()?.clubObject() {
let hasBeenCreated: Bool = eventClub.hasBeenCreated(by: StoreCenter.main.userId)
Section {
TextField("Nom du club", text: $customClubName, axis: .vertical)
.lineLimit(2)
TextField("Nom du club", text: $customClubName)
.autocorrectionDisabled()
.focused($focusedField, equals: .clubName)
.onSubmit {
eventClub.name = customClubName
tournament.customClubName = customClubName.prefixTrimmed(100)
do {
try dataStore.clubs.addOrUpdate(instance: eventClub)
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}

@ -14,7 +14,7 @@ struct PlayersWithoutContactView: View {
var body: some View {
Section {
let withoutEmails = players.filter({ $0.email?.isEmpty == true || $0.email == nil })
let withoutEmails = players.filter({ $0.hasMail() == false })
DisclosureGroup {
ForEach(withoutEmails) { player in
NavigationLink {
@ -32,7 +32,7 @@ struct PlayersWithoutContactView: View {
}
}
let withoutPhones = players.filter({ $0.phoneNumber?.isEmpty == true || $0.phoneNumber == nil || $0.phoneNumber?.isMobileNumber() == false })
let withoutPhones = players.filter({ $0.hasMobilePhone() == false })
DisclosureGroup {
ForEach(withoutPhones) { player in
NavigationLink {

@ -273,9 +273,9 @@ struct SendToAllView: View {
self._verifyUser {
if contactMethod == 0 {
contactType = .message(date: nil, recipients: _teams().flatMap { $0.unsortedPlayers() }.compactMap { $0.phoneNumber }, body: finalMessage(), tournamentBuild: nil)
contactType = .message(date: nil, recipients: _teams().flatMap { $0.unsortedPlayers() }.flatMap { [$0.phoneNumber, $0.contactPhoneNumber] }.compactMap({ $0 }), body: finalMessage(), tournamentBuild: nil)
} else {
contactType = .mail(date: nil, recipients: tournament.umpireMail(), bccRecipients: _teams().flatMap { $0.unsortedPlayers() }.compactMap { $0.email }, body: finalMessage(), subject: tournament.mailSubject(), tournamentBuild: nil)
contactType = .mail(date: nil, recipients: tournament.umpireMail(), bccRecipients: _teams().flatMap { $0.unsortedPlayers() }.flatMap { [$0.email, $0.contactEmail] }.compactMap({ $0 }), body: finalMessage(), subject: tournament.mailSubject(), tournamentBuild: nil)
}
}

@ -18,7 +18,7 @@ struct ShareableObject {
func sharedData() async -> Data? {
let _players = players.filter({ cashierViewModel._shouldDisplayPlayer($0) })
.map {
[$0.pasteData()]
[$0.pasteData(type: .payment)]
.compacted()
.joined(separator: "\n")
}

@ -245,7 +245,6 @@ struct GroupStageView: View {
Text("#\(index + 1)")
.font(.caption)
TeamPickerView(groupStagePosition: index, pickTypeContext: .groupStage, teamPicked: { team in
print(team.pasteData())
team.groupStage = groupStage.id
team.groupStagePosition = index
groupStage._matches().forEach({ $0.updateTeamScores() })

@ -65,7 +65,6 @@ struct MatchSetupView: View {
HStack {
let luckyLosers = walkOutSpot ? match.luckyLosers() : []
TeamPickerView(shouldConfirm: shouldConfirm, round: match.roundObject, pickTypeContext: matchTypeContext == .bracket ? .bracket : .loserBracket, luckyLosers: luckyLosers, teamPicked: { team in
print(team.pasteData())
if walkOutSpot || team.bracketPosition != nil || matchTypeContext == .loserBracket {
match.setLuckyLoser(team: team, teamPosition: teamPosition)
do {

@ -21,6 +21,7 @@ struct ToolboxView: View {
@State private var tapCount = 0
@State private var lastTapTime: Date? = nil
private let tapTimeThreshold: TimeInterval = 1.0
@State private var displaySearchPlayer: Bool = false
var lastDataSource: String? {
dataStore.appSettings.lastDataSource
@ -69,9 +70,8 @@ struct ToolboxView: View {
}
Section {
NavigationLink {
SelectablePlayerListView(isPresented: false, lastDataSource: true)
.toolbar(.hidden, for: .tabBar)
Button {
displaySearchPlayer = true
} label: {
Label("Rechercher un joueur", systemImage: "person.fill.viewfinder")
}
@ -121,6 +121,19 @@ struct ToolboxView: View {
}
}
}
.sheet(isPresented: $displaySearchPlayer, content: {
NavigationStack {
SelectablePlayerListView(isPresented: false, lastDataSource: true)
.toolbar(.hidden, for: .tabBar)
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("Fermer") {
displaySearchPlayer = false
}
}
}
}
})
.onAppear {
#if DEBUG
self.showDebugViews = true

@ -728,9 +728,6 @@ struct PlanningView: View {
}
}
private func _eventCourtCount() -> Int { timeSlots.first?.value.first?.currentTournament()?.eventObject()?.eventCourtCount() ?? 2
}
private func _save() {
let groupByTournaments = allMatches.grouped { match in
match.currentTournament()
@ -749,16 +746,27 @@ struct PlanningView: View {
Button("Tirer au sort") {
_removeCourts()
let eventCourtCount = _eventCourtCount()
for slot in timeSlots {
var courtsAvailable = Array(0...eventCourtCount)
let matches = slot.value
matches.forEach { match in
if let rand = courtsAvailable.randomElement() {
var courtsByTournament: [String: Set<Int>] = [:]
for match in matches {
if let tournament = match.currentTournament(),
let available = tournament.matchScheduler()?.courtsAvailable {
courtsByTournament[tournament.id, default: []].formUnion(available)
}
}
for match in matches {
guard let tournament = match.currentTournament() else { continue }
// Get current set of available courts for this tournament id
guard var courts = courtsByTournament[tournament.id], !courts.isEmpty else { continue }
// Pick a random court
if let rand = courts.randomElement() {
match.courtIndex = rand
courtsAvailable.remove(elements: [rand])
// Remove from local copy and assign back into the dictionary
courts.remove(rand)
courtsByTournament[tournament.id] = courts
}
}
}
@ -768,16 +776,27 @@ struct PlanningView: View {
Button("Fixer par ordre croissant") {
_removeCourts()
let eventCourtCount = _eventCourtCount()
for slot in timeSlots {
var courtsAvailable = Array(0..<eventCourtCount)
let matches = slot.value.sorted(by: \.computedOrder)
var courtsByTournament: [String: Set<Int>] = [:]
for match in matches {
if let tournament = match.currentTournament(),
let available = tournament.matchScheduler()?.courtsAvailable {
courtsByTournament[tournament.id, default: []].formUnion(available.sorted())
}
}
for i in 0..<matches.count {
if !courtsAvailable.isEmpty {
let court = courtsAvailable.removeFirst()
guard let tournament = matches[i].currentTournament() else { continue }
// Get current set of available courts for this tournament id
guard var courts = courtsByTournament[tournament.id]?.sorted(), !courts.isEmpty else { continue }
if courts.isEmpty == false {
let court = courts.removeFirst()
matches[i].courtIndex = court
// Remove from local copy and assign back into the dictionary
courtsByTournament[tournament.id] = Set(courts)
}
}
}
@ -984,3 +1003,4 @@ extension EnvironmentValues {
set { self[EnableMoveKey.self] = newValue }
}
}

@ -372,7 +372,7 @@ struct PlayerDetailView: View {
.toolbarBackground(.visible, for: .navigationBar)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
ShareLink(item: player.pasteData()) {
ShareLink(item: player.pasteData(type: .sharing)) {
Label("Partager", systemImage: "square.and.arrow.up")
}
}

@ -160,14 +160,7 @@ struct RoundSettingsView: View {
}
private func _removeRound(_ lastRound: Round) async {
await MainActor.run {
let teams = lastRound.seeds()
teams.forEach { team in
team.resetBracketPosition()
}
tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams)
tournamentStore?.rounds.delete(instance: lastRound)
}
await tournament.removeRound(lastRound)
}
}

@ -28,7 +28,7 @@ struct LearnMoreSheetView: View {
""")
} actions: {
ShareLink(item: tournament.pasteDataForImporting().createFile(tournament.tournamentTitle(.short))) {
ShareLink(item: tournament.pasteDataForImporting(type: .sharing).createFile(tournament.tournamentTitle(.short))) {
Text("Exporter les inscriptions")
}

@ -31,6 +31,7 @@ struct EditingTeamView: View {
@State private var registrationDateModified: Date
@State private var uniqueRandomIndex: Int
@State private var isDeleting: Bool = false
@State private var showPaymentLinkManager: Bool = false
var messageSentFailed: Binding<Bool> {
Binding {
@ -116,7 +117,7 @@ struct EditingTeamView: View {
}
} footer: {
HStack {
CopyPasteButtonView(pasteValue: team.playersPasteData())
CopyPasteButtonView(pasteValue: team.playersPasteData(type: .sharing))
Spacer()
if team.isWildCard(), team.unsortedPlayers().isEmpty {
TeamPickerView(pickTypeContext: .wildcard) { teamregistration in
@ -154,7 +155,11 @@ struct EditingTeamView: View {
}
if team.hasPaidOnline() == false {
PaymentRequestButton(teamRegistration: team)
#if PRODTEST
Button("Récupérer le lien de paiement") {
showPaymentLinkManager = true
}
#endif
}
}
@ -176,6 +181,8 @@ struct EditingTeamView: View {
} footer: {
if team.hasPaidOnline() {
Text("Le remboursement passe part le service de Stripe qui re-crédite le moyen de paiement utilisé du montant payé.")
} else {
PaymentRequestButton(teamRegistration: team)
}
}
}
@ -409,6 +416,19 @@ struct EditingTeamView: View {
}
.tint(.master)
}
.sheet(isPresented: $showPaymentLinkManager) {
NavigationStack {
PaymentLinkManagerView(teamRegistration: team)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Fermer") {
showPaymentLinkManager = false
}
}
}
}
}
.fullScreenCover(item: $editedTeam) { editedTeam in
NavigationStack {
AddTeamView(tournament: tournament, editedTeam: editedTeam)

@ -0,0 +1,268 @@
//
// PaymentLinkManagerView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 16/10/2025.
//
//
// PaymentLinkManagerView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 01/10/2025.
//
import SwiftUI
import PadelClubData
struct PaymentLinkManagerView: View {
let teamRegistration: TeamRegistration
@State private var isLoading = false
@State private var showAlert = false
@State private var alertMessage = ""
@State private var paymentLink: String?
@State private var showCopiedConfirmation = false
var body: some View {
VStack(spacing: 20) {
// Header
header
// Get Payment Link Button
getPaymentLinkButton
// Payment Link Display and Actions
if let link = paymentLink {
paymentLinkSection(link: link)
}
Spacer()
}
.padding()
.alert("Erreur", isPresented: $showAlert) {
Button("OK") { }
} message: {
Text(alertMessage)
}
}
// MARK: - ViewBuilder Components
@ViewBuilder
private var header: some View {
VStack(spacing: 8) {
Image(systemName: "creditcard.circle.fill")
.font(.system(size: 50))
.foregroundColor(.blue)
Text("Lien de paiement")
.font(.title2)
.fontWeight(.bold)
Text("Obtenez un lien de paiement à partager avec l'équipe")
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
}
@ViewBuilder
private var getPaymentLinkButton: some View {
Button {
getPaymentLink()
} label: {
HStack {
if isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
.tint(.white)
} else {
Image(systemName: "link.circle")
}
Text(paymentLink == nil ? "Obtenir le lien de paiement" : "Régénérer le lien")
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(12)
}
.disabled(isLoading)
}
@ViewBuilder
private func paymentLinkSection(link: String) -> some View {
VStack(spacing: 16) {
// Success message
HStack {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
Text("Lien copié dans le presse-papiers!")
.font(.subheadline)
.foregroundColor(.green)
}
.padding(.vertical, 8)
.padding(.horizontal, 12)
.background(Color.green.opacity(0.1))
.cornerRadius(8)
// Link display
linkDisplayView(link: link)
// Action buttons
actionButtons(link: link)
}
.transition(.move(edge: .top).combined(with: .opacity))
.animation(.spring(response: 0.6, dampingFraction: 0.8), value: paymentLink)
}
@ViewBuilder
private func linkDisplayView(link: String) -> some View {
VStack(alignment: .leading, spacing: 8) {
Text("Lien de paiement:")
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(.secondary)
ScrollView(.horizontal, showsIndicators: false) {
Text(link)
.font(.system(.caption, design: .monospaced))
.padding(12)
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
}
}
}
@ViewBuilder
private func actionButtons(link: String) -> some View {
VStack(spacing: 12) {
// Copy button
copyButton(link: link)
// Share button
shareButton(link: link)
// Open in browser button
openInBrowserButton(link: link)
}
}
@ViewBuilder
private func copyButton(link: String) -> some View {
Button {
UIPasteboard.general.string = link
showCopiedConfirmation = true
// Haptic feedback
let generator = UINotificationFeedbackGenerator()
generator.notificationOccurred(.success)
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
showCopiedConfirmation = false
}
} label: {
HStack {
Image(systemName: showCopiedConfirmation ? "checkmark.circle.fill" : "doc.on.doc.fill")
Text(showCopiedConfirmation ? "Copié !" : "Copier le lien")
.fontWeight(.semibold)
}
.frame(maxWidth: .infinity)
.padding()
.background(showCopiedConfirmation ? Color.green : Color.blue.opacity(0.1))
.foregroundColor(showCopiedConfirmation ? .white : .blue)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(showCopiedConfirmation ? Color.green : Color.blue, lineWidth: 1)
)
}
.disabled(showCopiedConfirmation)
.animation(.easeInOut(duration: 0.2), value: showCopiedConfirmation)
}
@ViewBuilder
private func shareButton(link: String) -> some View {
ShareLink(item: link) {
HStack {
Image(systemName: "square.and.arrow.up.fill")
Text("Partager le lien")
.fontWeight(.semibold)
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue.opacity(0.1))
.foregroundColor(.blue)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.blue, lineWidth: 1)
)
}
}
@ViewBuilder
private func openInBrowserButton(link: String) -> some View {
Button {
if let url = URL(string: link) {
UIApplication.shared.open(url)
}
} label: {
HStack {
Image(systemName: "safari.fill")
Text("Ouvrir dans Safari")
.fontWeight(.semibold)
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue.opacity(0.1))
.foregroundColor(.blue)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.blue, lineWidth: 1)
)
}
}
// MARK: - Private Methods
private func getPaymentLink() {
isLoading = true
showCopiedConfirmation = false
Task {
do {
let response = try await PaymentService.getPaymentLink(
teamRegistrationId: teamRegistration.id
)
await MainActor.run {
isLoading = false
if response.success, let link = response.paymentLink {
paymentLink = link
// Automatically copy to clipboard
UIPasteboard.general.string = link
// Haptic feedback
let generator = UINotificationFeedbackGenerator()
generator.notificationOccurred(.success)
} else {
alertMessage = response.message ?? "Impossible d'obtenir le lien de paiement"
showAlert = true
}
}
} catch {
await MainActor.run {
isLoading = false
alertMessage = "Erreur lors de la récupération du lien"
showAlert = true
}
}
}
}
}
#Preview {
PaymentLinkManagerView(teamRegistration: TeamRegistration())
}

@ -16,7 +16,7 @@ struct PaymentRequestButton: View {
@State private var alertMessage = ""
var body: some View {
Button("Renvoyer email de paiement") {
FooterButtonView("Renvoyer l'email de paiement", role: .destructive, confirmationMessage: "Cette action permet de renvoyer le mail de confirmation de sélection de l'équipe incluant la demande du paiement.") {
resendEmail()
}
.disabled(isLoading)

@ -62,7 +62,9 @@ struct HeadManagerView: View {
}
while leftToPlace(heads: heads, teamsPerRound: teamsPerRound) > 0 {
// maxAssignable: On retire toutes les équipes placées dans les tours précédents, pondérées par leur propagation (puissance du tour)
let headsLeft = heads - teamsPerRound.reduce(0, +)
let alreadyPut = teamsPerRound.reduce(0, +)
let headsLeft = heads - alreadyPut
// Calculate how many teams from previous rounds propagate to this round
let currentRound = teamsPerRound.count
var previousTeams = 0
@ -79,7 +81,7 @@ struct HeadManagerView: View {
valueToAppend = theory
} else {
let lastValue = teamsPerRound.last ?? 0
var newValueToAppend = theory
var newValueToAppend = theory == 0 ? maxAssignable / 2 : theory
if theory > maxAssignable || theory < 0 {
newValueToAppend = valueToAppend / 2
}
@ -102,33 +104,62 @@ struct HeadManagerView: View {
Picker(selection: $selectedSeedRound) {
Text("Choisir").tag(nil as Int?)
ForEach(seedRepartition.indices, id: \.self) { idx in
Text(RoundRule.roundName(fromRoundIndex: idx)).tag(idx)
Text(RoundRule.roundName(fromRoundIndex: idx, displayStyle: .short)).tag(idx)
}
} label: {
Text("Tour contenant la meilleure tête de série")
Text("Tour de la tête de série n°1")
}
.onChange(of: selectedSeedRound) {
seedRepartition = Self.place(heads: heads, teamsInBracket: teamsInBracket, initialSeedRound: selectedSeedRound)
}
} footer: {
FooterButtonView("remise à zéro") {
selectedSeedRound = nil
}
}
Section {
LabeledContent {
Text(teamsInBracket.formatted())
Text(heads.formatted())
} label: {
Text("Effectif du tableau")
Text("Équipes à placer en tableau")
}
if (teamsInBracket - heads) > 0 {
LabeledContent {
Text((teamsInBracket - heads).formatted())
} label: {
Text("Qualifiés entrants")
}
}
LabeledContent {
Text(leftToPlace.formatted())
} label: {
Text("Restant à placer")
}
LabeledContent {
let matchCount = seedRepartition.enumerated().map { (index, value) in
var result = 0
var count = value
if count == 0, let selectedSeedRound, index < selectedSeedRound {
let t = RoundRule.numberOfMatches(forRoundIndex: index)
result = RoundRule.cumulatedNumberOfMatches(forTeams: t * 2)
} else {
if index == seedRepartition.count - 1 {
count += (teamsInBracket - heads)
} else if index == seedRepartition.count - 2 {
count += ((seedRepartition[index + 1] + (teamsInBracket - heads)) / 2)
} else {
count += (seedRepartition[index + 1])
}
result = RoundRule.cumulatedNumberOfMatches(forTeams: count)
}
// print(index, value, result, count)
return result
}
.reduce(0, +)
Text(matchCount.formatted())
} label: {
Text("Matchs estimés")
}
}
Section {
@ -155,13 +186,13 @@ struct HeadManagerView: View {
if headsLeft - maxAssignable > 0 {
valueToAppend = valueToAppend - (headsLeft - maxAssignable)
}
print("Appending to seedRepartition: headsLeft=\(headsLeft), maxAssignable=\(maxAssignable), valueToAppend=\(valueToAppend), current seedRepartition=\(seedRepartition)")
// print("Appending to seedRepartition: headsLeft=\(headsLeft), maxAssignable=\(maxAssignable), valueToAppend=\(valueToAppend), current seedRepartition=\(seedRepartition)")
seedRepartition.append(valueToAppend)
}
}
}
}
.navigationTitle("Têtes de série")
.navigationTitle("Répartition")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
ButtonValidateView(title: "Valider") {

@ -25,12 +25,10 @@ struct TournamentMatchFormatsSettingsView: View {
var body: some View {
@Bindable var tournament = tournament
List {
if confirmUpdate {
RowButtonView("Modifier les matchs existants", role: .destructive) {
_updateAllFormat()
}
RowButtonView("Modifier les matchs existants", role: .destructive) {
_updateAllFormat()
}
TournamentFormatSelectionView()
Section {

@ -271,9 +271,7 @@ struct InscriptionManagerView: View {
// await _refreshList(forced: true)
// }
.onAppear {
if tournament.enableOnlineRegistration == false || refreshStatus == true {
_setHash(currentSelectedSortedTeams: selectedSortedTeams)
}
_setHash(currentSelectedSortedTeams: selectedSortedTeams)
}
.onDisappear {
_handleHashDiff(selectedSortedTeams: selectedSortedTeams)
@ -381,40 +379,6 @@ struct InscriptionManagerView: View {
ToolbarItem(placement: .navigationBarTrailing) {
Menu {
#if PRODTEST
if tournament.enableOnlinePayment {
Button {
isLoading = true
Task {
do {
try await selectedSortedTeams.filter { team in
team.hasPaidOnline() == false && team.hasPaid() == false
}.concurrentForEach { team in
_ = try await PaymentService.resendPaymentEmail(teamRegistrationId: team.id)
}
await MainActor.run {
isLoading = false
alertMessage = "Relance effectuée avec succès"
showAlert = true
}
} catch {
Logger.error(error)
await MainActor.run {
isLoading = false
alertMessage = "Erreur lors de la requête"
showAlert = true
}
}
}
} label: {
Text("Requête de paiement")
}
.disabled(isLoading)
}
#endif
if tournament.inscriptionClosed() == false {
Menu {
_sortingTypePickerView()
@ -549,11 +513,25 @@ struct InscriptionManagerView: View {
private func _sharingTeamsMenuView() -> some View {
Menu {
ShareLink(item: teamPaste(), preview: .init("Inscriptions")) {
Text("En texte")
Menu {
ShareLink(item: teamPaste(.rawText, type: .sharing), preview: .init(ExportType.sharing.localizedString().capitalized)) {
Text("En texte")
}
ShareLink(item: teamPaste(.csv, type: .sharing), preview: .init(ExportType.sharing.localizedString().capitalized)) {
Text("En csv")
}
} label: {
Text("Pour diffusion")
}
ShareLink(item: teamPaste(.csv), preview: .init("Inscriptions")) {
Text("En csv")
Menu {
ShareLink(item: teamPaste(.rawText, type: .payment), preview: .init(ExportType.payment.localizedString().capitalized)) {
Text("En texte")
}
ShareLink(item: teamPaste(.csv, type: .payment), preview: .init(ExportType.payment.localizedString().capitalized)) {
Text("En csv")
}
} label: {
Text("Pour encaissement")
}
} label: {
Label("Exporter les paires", systemImage: "square.and.arrow.up")
@ -577,8 +555,8 @@ struct InscriptionManagerView: View {
tournament.unsortedTeamsWithoutWO()
}
func teamPaste(_ exportFormat: ExportFormat = .rawText) -> TournamentShareFile {
TournamentShareFile(tournament: tournament, exportFormat: exportFormat)
func teamPaste(_ exportFormat: ExportFormat = .rawText, type: ExportType) -> TournamentShareFile {
TournamentShareFile(tournament: tournament, exportFormat: exportFormat, type: type)
}
var unsortedPlayers: [PlayerRegistration] {
@ -864,6 +842,31 @@ struct InscriptionManagerView: View {
@ViewBuilder
private func _informationView(for teams: [TeamRegistration]) -> some View {
#if PRODTEST
if tournament.enableOnlinePayment {
RowButtonView("Requête de paiement", role: .destructive) {
do {
try await teams.filter { team in
team.hasPaidOnline() == false && team.hasPaid() == false
}.concurrentForEach { team in
_ = try await PaymentService.resendPaymentEmail(teamRegistrationId: team.id)
}
await MainActor.run {
alertMessage = "Relance effectuée avec succès"
showAlert = true
}
} catch {
Logger.error(error)
await MainActor.run {
alertMessage = "Erreur lors de la requête"
showAlert = true
}
}
}
}
#endif
Section {
HStack {
// VStack(alignment: .leading, spacing: 0) {
@ -942,14 +945,9 @@ struct InscriptionManagerView: View {
if tournament.enableOnlineRegistration {
LabeledContent {
Text(tournament.unsortedTeams().filter({ $0.hasRegisteredOnline() }).count.formatted())
.font(.largeTitle)
.fontWeight(.bold)
} label: {
Text("Inscriptions en ligne")
if let refreshResult {
Text(refreshResult).foregroundStyle(.secondary)
} else {
Text(" ")
}
}
// RowButtonView("Rafraîchir les inscriptions en ligne") {
@ -1258,10 +1256,11 @@ struct TournamentGroupStageShareContent: Transferable {
struct TournamentShareFile: Transferable {
let tournament: Tournament
let exportFormat: ExportFormat
let type: ExportType
func shareFile() -> URL {
print("Generating URL...")
return tournament.pasteDataForImporting(exportFormat).createFile(self.tournament.tournamentTitle()+"-inscriptions", exportFormat)
return tournament.pasteDataForImporting(exportFormat, type: type).createFile(self.tournament.tournamentTitle()+"-"+type.localizedString(), exportFormat)
}
static var transferRepresentation: some TransferRepresentation {

@ -267,13 +267,13 @@ struct TableStructureView: View {
}
}
if groupStageCount > 0 {
LabeledContent {
Text(tf.formatted())
} label: {
Text("Effectif")
}
}
// if groupStageCount > 0 {
// LabeledContent {
// Text(tf.formatted())
// } label: {
// Text("Effectif tableau")
// }
// }
} else {
LabeledContent {
let mp1 = teamsPerGroupStage * (teamsPerGroupStage - 1) / 2 * groupStageCount
@ -283,16 +283,30 @@ struct TableStructureView: View {
Text("Total de matchs")
}
}
} footer: {
if tsPure > 0 && structurePreset != .doubleGroupStage, groupStageCount > 0 {
if tsPure > teamCount / 2 {
Text("Le nombre de têtes de série ne devrait pas être supérieur à la moitié de l'effectif.").foregroundStyle(.red)
} else if tsPure < teamCount / 8 {
Text("À partir du moment où vous avez des têtes de série, leur nombre ne devrait pas être inférieur à 1/8ème de l'effectif.").foregroundStyle(.red)
} else if tsPure < qualifiedFromGroupStage + groupStageAdditionalQualified {
Text("Le nombre de têtes de série ne devrait pas être inférieur au nombre de paires qualifiées sortantes.").foregroundStyle(.red)
LabeledContent {
FooterButtonView("configurer") {
showSeedRepartition = true
}
} label: {
if tournament.state() == .build {
Text("Répartition des équipes")
} else if selectedTournament != nil {
Text("La configuration du tournoi séléctionné sera utilisée.")
} else {
Text(_seeds())
}
}
.onAppear {
if seedRepartition.isEmpty && tournament.state() == .initial && selectedTournament == nil {
seedRepartition = HeadManagerView.place(heads: tsPure, teamsInBracket: tf, initialSeedRound: nil)
}
}
} footer: {
if tsPure > 0 && structurePreset != .doubleGroupStage, groupStageCount > 0, tsPure < qualifiedFromGroupStage + groupStageAdditionalQualified {
Text("Le nombre de têtes de série ne devrait pas être inférieur au nombre de paires qualifiées sortantes.").foregroundStyle(.red)
}
}
if structurePreset.hasWildcards() && tournament.level.wildcardArePossible() {
@ -303,27 +317,6 @@ struct TableStructureView: View {
}
}
if tournament.state() != .build {
Section {
LabeledContent {
Image(systemName: seedRepartition.isEmpty ? "xmark" : "checkmark")
} label: {
FooterButtonView("Configuration du tableau") {
showSeedRepartition = true
}
.disabled(selectedTournament != nil)
}
} footer: {
if seedRepartition.isEmpty {
Text("Aucune répartition n'a été indiqué, vous devrez réserver ou placer les têtes de séries dans le tableau manuellement.")
} else {
FooterButtonView("Supprimer la configuration", role: .destructive) {
seedRepartition = []
}
}
}
}
if tournament.rounds().isEmpty && tournament.state() == .build {
Section {
RowButtonView("Ajouter un tableau", role: .destructive) {
@ -336,28 +329,63 @@ struct TableStructureView: View {
if tournament.state() != .initial {
if seedRepartition.isEmpty == false {
Section {
RowButtonView("Répartir les équipes en tableau", role: .destructive, confirmationMessage: "Cette action va effacer le répartition actuelle des équipes dans le tableau.") {
await _handleSeedRepartition()
}
} footer: {
Text("Cette action va effacer le répartition actuelle des équipes dans le tableau et la refaire, les manches seront ré-initialisées")
}
}
Section {
RowButtonView("Sauver sans reconstuire l'existant") {
_saveWithoutRebuild()
}
} footer: {
Text("Cette action sauve les paramètres du tournoi sans modifier vos poules / tableaux actuels.")
}
Section {
RowButtonView("Reconstruire les poules", role:.destructive) {
await _save(rebuildEverything: false)
}
} footer: {
Text("Cette action efface les poules existantes et les reconstruits, leurs données seront perdues.")
}
Section {
RowButtonView("Tout refaire", role: .destructive) {
await _save(rebuildEverything: true)
}
} footer: {
Text("Cette action efface le tableau et les poules existantes et reconstruit tout de zéro, leurs données seront perdues.")
}
Section {
RowButtonView("Remise-à-zéro", role: .destructive) {
_reset()
}
} footer: {
Text("Retourne à la structure initiale, comme si vous veniez de créer le tournoi. Les données existantes seront perdues.")
}
Section {
RowButtonView("Retirer toutes les équipes de poules", role: .destructive) {
tournament.unsortedTeams().forEach {
$0.resetGroupeStagePosition()
}
}
}
Section {
RowButtonView("Retirer toutes les équipes du tableau", role: .destructive) {
tournament.unsortedTeams().forEach {
$0.resetBracketPosition()
}
}
}
}
}
@ -370,13 +398,6 @@ struct TableStructureView: View {
}
}
.toolbarBackground(.visible, for: .navigationBar)
.onChange(of: teamCount) {
if teamCount != tournament.teamCount {
updatedElements.insert(.teamCount)
} else {
updatedElements.remove(.teamCount)
}
}
.sheet(isPresented: $showSeedRepartition, content: {
NavigationStack {
HeadManagerView(teamsInBracket: tf, heads: tsPure, initialSeedRepartition: seedRepartition) { seedRepartition in
@ -384,6 +405,14 @@ struct TableStructureView: View {
}
}
})
.onChange(of: teamCount) {
if teamCount != tournament.teamCount {
updatedElements.insert(.teamCount)
} else {
updatedElements.remove(.teamCount)
}
_verifyValueIntegrity()
}
.onChange(of: groupStageCount) {
if groupStageCount != tournament.groupStageCount {
updatedElements.insert(.groupStageCount)
@ -394,25 +423,31 @@ struct TableStructureView: View {
if structurePreset.isFederalPreset(), groupStageCount == 0 {
teamCount = structurePreset.tableDimension()
}
_verifyValueIntegrity()
}
.onChange(of: teamsPerGroupStage) {
if teamsPerGroupStage != tournament.teamsPerGroupStage {
updatedElements.insert(.teamsPerGroupStage)
} else {
updatedElements.remove(.teamsPerGroupStage)
} }
}
_verifyValueIntegrity()
}
.onChange(of: qualifiedPerGroupStage) {
if qualifiedPerGroupStage != tournament.qualifiedPerGroupStage {
updatedElements.insert(.qualifiedPerGroupStage)
} else {
updatedElements.remove(.qualifiedPerGroupStage)
} }
}
_verifyValueIntegrity()
}
.onChange(of: groupStageAdditionalQualified) {
if groupStageAdditionalQualified != tournament.groupStageAdditionalQualified {
updatedElements.insert(.groupStageAdditionalQualified)
} else {
updatedElements.remove(.groupStageAdditionalQualified)
}
_verifyValueIntegrity()
}
.toolbar {
if tournament.state() != .initial {
@ -484,8 +519,24 @@ struct TableStructureView: View {
}
}
private func _seeds() -> String {
if seedRepartition.isEmpty || seedRepartition.reduce(0, +) == 0 {
return "Aucune configuration"
}
return seedRepartition.enumerated().compactMap { (index, count) in
if count > 0 {
return RoundRule.roundName(fromRoundIndex: index) + " : \(count)"
} else {
return nil
}
}.joined(separator: ", ")
}
private func _reset() {
tournament.unsortedTeams().forEach {
$0.resetPositions()
}
tournament.removeWildCards()
tournament.deleteGroupStages()
tournament.deleteStructure()
@ -573,12 +624,6 @@ struct TableStructureView: View {
}
tournament.deleteAndBuildEverything(preset: structurePreset)
if seedRepartition.count > 0 {
while tournament.rounds().count < seedRepartition.count {
await tournament.addNewRound(tournament.rounds().count)
}
}
if let selectedTournament {
let oldTournamentStart = selectedTournament.startDate
let newTournamentStart = tournament.startDate
@ -614,66 +659,10 @@ struct TableStructureView: View {
}
tournament.tournamentStore?.matches.addOrUpdate(contentOfs: tournament._allMatchesIncludingDisabled())
} else {
for (index, seedCount) in seedRepartition.enumerated() {
if let round = tournament.rounds().first(where: { $0.index == index }) {
let baseIndex = RoundRule.baseIndex(forRoundIndex: round.index)
let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: round.index)
let playedMatches = round.playedMatches().map { $0.index - baseIndex }
let allMatches = round._matches()
let seedSorted = frenchUmpireOrder(for: numberOfMatches).filter({ index in
playedMatches.contains(index)
}).prefix(seedCount)
for (index, value) in seedSorted.enumerated() {
let isOpponentTurn = index >= playedMatches.count
if let match = allMatches[safe:value] {
if match.index - baseIndex < numberOfMatches / 2 {
if isOpponentTurn {
match.previousMatch(.two)?.disableMatch()
} else {
match.previousMatch(.one)?.disableMatch()
}
} else {
if isOpponentTurn {
match.previousMatch(.one)?.disableMatch()
} else {
match.previousMatch(.two)?.disableMatch()
}
}
}
}
if seedCount > 0 {
tournament.tournamentStore?.matches.addOrUpdate(contentOfs: round._matches())
tournament.tournamentStore?.matches.addOrUpdate(contentOfs: round.allLoserRoundMatches())
round.deleteLoserBracket()
round.buildLoserBracket()
round.loserRounds().forEach { loserRound in
loserRound.disableUnplayedLoserBracketMatches()
}
}
}
}
// if initialSeedRound > 0 {
// if let round = tournament.rounds().first(where: { $0.index == initialSeedRound }) {
// let seedSorted = frenchUmpireOrder(for: RoundRule.numberOfMatches(forRoundIndex: round.index))
// print(seedSorted)
// seedSorted.prefix(initialSeedCount).forEach { index in
// if let match = round._matches()[safe:index] {
// if match.indexInRound() < RoundRule.numberOfMatches(forRoundIndex: round.index) / 2 {
// match.previousMatch(.one)?.disableMatch()
// } else {
// match.previousMatch(.two)?.disableMatch()
// }
// }
// }
//
// if initialSeedCount > 0 {
// tournament.tournamentStore?.matches.addOrUpdate(contentOfs: tournament._allMatchesIncludingDisabled())
// }
// }
// }
}
if seedRepartition.count > 0 {
await _handleSeedRepartition()
}
} else if (rebuildEverything == false && requirements.contains(.groupStage)) {
tournament.deleteGroupStages()
@ -693,8 +682,67 @@ struct TableStructureView: View {
}
}
private func _handleSeedRepartition() async {
while tournament.rounds().count < seedRepartition.count {
await tournament.addNewRound(tournament.rounds().count)
}
if seedRepartition.reduce(0, +) > 0 {
let rounds = tournament.rounds()
let roundsToDelete = rounds.suffix(rounds.count - seedRepartition.count)
for round in roundsToDelete {
await tournament.removeRound(round)
}
}
for (index, seedCount) in seedRepartition.enumerated() {
if let round = tournament.rounds().first(where: { $0.index == index }) {
let baseIndex = RoundRule.baseIndex(forRoundIndex: round.index)
let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: round.index)
let playedMatches = round.playedMatches().map { $0.index - baseIndex }
let allMatches = round._matches()
let seedSorted = frenchUmpireOrder(for: numberOfMatches).filter({ index in
playedMatches.contains(index)
}).prefix(seedCount)
if playedMatches.count == numberOfMatches && seedCount == numberOfMatches * 2 {
continue
}
for (index, value) in seedSorted.enumerated() {
let isOpponentTurn = index >= playedMatches.count
if let match = allMatches[safe:value] {
if match.index - baseIndex < numberOfMatches / 2 {
if isOpponentTurn {
match.previousMatch(.two)?.disableMatch()
} else {
match.previousMatch(.one)?.disableMatch()
}
} else {
if isOpponentTurn {
match.previousMatch(.one)?.disableMatch()
} else {
match.previousMatch(.two)?.disableMatch()
}
}
}
}
if seedCount > 0 {
tournament.tournamentStore?.matches.addOrUpdate(contentOfs: round._matches())
tournament.tournamentStore?.matches.addOrUpdate(contentOfs: round.allLoserRoundMatches())
round.deleteLoserBracket()
round.buildLoserBracket()
round.loserRounds().forEach { loserRound in
loserRound.disableUnplayedLoserBracketMatches()
}
}
}
}
}
private func _updatePreset() {
if let selectedTournament {
seedRepartition = []
teamCount = selectedTournament.teamCount
groupStageCount = selectedTournament.groupStageCount
teamsPerGroupStage = selectedTournament.teamsPerGroupStage
@ -709,6 +757,7 @@ struct TableStructureView: View {
groupStageAdditionalQualified = 0
buildWildcards = tournament.level.wildcardArePossible()
}
_verifyValueIntegrity()
}
private func _verifyValueIntegrity() {
@ -754,6 +803,7 @@ struct TableStructureView: View {
}
}
seedRepartition = HeadManagerView.place(heads: tsPure, teamsInBracket: tf, initialSeedRound: nil)
}
}

Loading…
Cancel
Save