sync2
Laurent 11 months ago
commit d74c6abcfa
  1. 220
      PadelClub.xcodeproj/project.pbxproj
  2. 12
      PadelClub/Data/AppSettings.swift
  3. 82
      PadelClub/Data/Club.swift
  4. 4
      PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift
  5. 36
      PadelClub/Data/DataStore.swift
  6. 97
      PadelClub/Data/DrawLog.swift
  7. 2
      PadelClub/Data/Federal/FederalTournament.swift
  8. 9
      PadelClub/Data/Gen/BaseClub.swift
  9. 96
      PadelClub/Data/Gen/BaseDrawLog.swift
  10. 16
      PadelClub/Data/Gen/BaseMatchScheduler.swift
  11. 16
      PadelClub/Data/Gen/BaseTournament.swift
  12. 6
      PadelClub/Data/Gen/Club.json
  13. 47
      PadelClub/Data/Gen/Drawlog.json
  14. 10
      PadelClub/Data/Gen/MatchScheduler.json
  15. 10
      PadelClub/Data/Gen/Tournament.json
  16. 221
      PadelClub/Data/GroupStage.swift
  17. 362
      PadelClub/Data/Match.swift
  18. 283
      PadelClub/Data/MatchScheduler.swift
  19. 63
      PadelClub/Data/PlayerRegistration.swift
  20. 4
      PadelClub/Data/README.md
  21. 76
      PadelClub/Data/Round.swift
  22. 131
      PadelClub/Data/TeamRegistration.swift
  23. 627
      PadelClub/Data/Tournament.swift
  24. 6
      PadelClub/Data/TournamentStore.swift
  25. 23
      PadelClub/Extensions/Date+Extensions.swift
  26. 16
      PadelClub/Extensions/FixedWidthInteger+Extensions.swift
  27. 5
      PadelClub/Extensions/Locale+Extensions.swift
  28. 3
      PadelClub/Extensions/String+Extensions.swift
  29. 2
      PadelClub/Info.plist
  30. 2
      PadelClub/PadelClubApp.swift
  31. 10
      PadelClub/Utils/ContactManager.swift
  32. 9
      PadelClub/Utils/DisplayContext.swift
  33. 3
      PadelClub/Utils/FileImportManager.swift
  34. 326
      PadelClub/Utils/PadelRule.swift
  35. 113
      PadelClub/Utils/Patcher.swift
  36. 4
      PadelClub/Utils/SourceFileManager.swift
  37. 3
      PadelClub/Utils/URLs.swift
  38. 3
      PadelClub/ViewModel/FederalDataViewModel.swift
  39. 55
      PadelClub/ViewModel/MatchDescriptor.swift
  40. 51
      PadelClub/ViewModel/MatchViewStyle.swift
  41. 1
      PadelClub/ViewModel/Screen.swift
  42. 10
      PadelClub/ViewModel/SearchViewModel.swift
  43. 10
      PadelClub/ViewModel/SetDescriptor.swift
  44. 214
      PadelClub/Views/Calling/BracketCallingView.swift
  45. 12
      PadelClub/Views/Calling/CallMessageCustomizationView.swift
  46. 187
      PadelClub/Views/Calling/CallView.swift
  47. 2
      PadelClub/Views/Calling/Components/MenuWarningView.swift
  48. 4
      PadelClub/Views/Calling/Components/PlayersWithoutContactView.swift
  49. 9
      PadelClub/Views/Calling/GroupStageCallingView.swift
  50. 21
      PadelClub/Views/Calling/SeedsCallingView.swift
  51. 153
      PadelClub/Views/Calling/TeamsCallingView.swift
  52. 8
      PadelClub/Views/Cashier/CashierDetailView.swift
  53. 38
      PadelClub/Views/Cashier/CashierSettingsView.swift
  54. 30
      PadelClub/Views/Cashier/CashierView.swift
  55. 2
      PadelClub/Views/Cashier/Event/EventSettingsView.swift
  56. 2
      PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift
  57. 13
      PadelClub/Views/Club/ClubDetailView.swift
  58. 6
      PadelClub/Views/Club/CourtView.swift
  59. 18
      PadelClub/Views/Components/CopyPasteButtonView.swift
  60. 58
      PadelClub/Views/Components/FortuneWheelView.swift
  61. 4
      PadelClub/Views/Components/GenericDestinationPickerView.swift
  62. 7
      PadelClub/Views/Components/MatchListView.swift
  63. 48
      PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift
  64. 14
      PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift
  65. 72
      PadelClub/Views/GroupStage/GroupStageQualificationManagerView.swift
  66. 29
      PadelClub/Views/GroupStage/GroupStageView.swift
  67. 52
      PadelClub/Views/GroupStage/GroupStagesSettingsView.swift
  68. 33
      PadelClub/Views/GroupStage/GroupStagesView.swift
  69. 8
      PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift
  70. 2
      PadelClub/Views/GroupStage/Shared/GroupStageTeamReplacementView.swift
  71. 61
      PadelClub/Views/Match/Components/MatchDateView.swift
  72. 19
      PadelClub/Views/Match/Components/MatchTeamDetailView.swift
  73. 71
      PadelClub/Views/Match/Components/PlayerBlockView.swift
  74. 108
      PadelClub/Views/Match/MatchDetailView.swift
  75. 26
      PadelClub/Views/Match/MatchRowView.swift
  76. 16
      PadelClub/Views/Match/MatchSetupView.swift
  77. 102
      PadelClub/Views/Match/MatchSummaryView.swift
  78. 23
      PadelClub/Views/Navigation/Agenda/ActivityView.swift
  79. 2
      PadelClub/Views/Navigation/Agenda/CalendarView.swift
  80. 10
      PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift
  81. 2
      PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift
  82. 4
      PadelClub/Views/Navigation/MainView.swift
  83. 95
      PadelClub/Views/Navigation/Ongoing/OngoingContainerView.swift
  84. 129
      PadelClub/Views/Navigation/Ongoing/OngoingDestination.swift
  85. 104
      PadelClub/Views/Navigation/Ongoing/OngoingView.swift
  86. 116
      PadelClub/Views/Navigation/Toolbox/GlobalSettingsView.swift
  87. 3
      PadelClub/Views/Navigation/Toolbox/MatchFormatStorageView.swift
  88. 42
      PadelClub/Views/Navigation/Toolbox/ToolboxView.swift
  89. 76
      PadelClub/Views/Navigation/Umpire/PadelClubView.swift
  90. 84
      PadelClub/Views/Navigation/Umpire/UmpireStatisticView.swift
  91. 8
      PadelClub/Views/Navigation/Umpire/UmpireView.swift
  92. 93
      PadelClub/Views/Planning/Components/DatePickingView.swift
  93. 108
      PadelClub/Views/Planning/Components/DatePickingViewWithFormat.swift
  94. 224
      PadelClub/Views/Planning/Components/DateUpdateManagerView.swift
  95. 93
      PadelClub/Views/Planning/Components/GroupStageDatePickingView.swift
  96. 47
      PadelClub/Views/Planning/Components/MatchFormatPickingView.swift
  97. 38
      PadelClub/Views/Planning/Components/MultiCourtPickerView.swift
  98. 130
      PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift
  99. 8
      PadelClub/Views/Planning/GroupStageScheduleEditorView.swift
  100. 16
      PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift
  101. Some files were not shown because too many files have changed in this diff Show More

@ -25,6 +25,12 @@
C45BAE3B2BC6DF10002EEC8A /* SyncedProducts.storekit in Resources */ = {isa = PBXBuildFile; fileRef = C45BAE3A2BC6DF10002EEC8A /* SyncedProducts.storekit */; }; C45BAE3B2BC6DF10002EEC8A /* SyncedProducts.storekit in Resources */ = {isa = PBXBuildFile; fileRef = C45BAE3A2BC6DF10002EEC8A /* SyncedProducts.storekit */; };
C45BAE442BCA753E002EEC8A /* Purchase.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45BAE432BCA753E002EEC8A /* Purchase.swift */; }; C45BAE442BCA753E002EEC8A /* Purchase.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45BAE432BCA753E002EEC8A /* Purchase.swift */; };
C4607A7D2C04DDE2004CB781 /* APICallsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4607A7C2C04DDE2004CB781 /* APICallsListView.swift */; }; C4607A7D2C04DDE2004CB781 /* APICallsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4607A7C2C04DDE2004CB781 /* APICallsListView.swift */; };
C471D1542D0C8FED0068091F /* Drawlog.json in Resources */ = {isa = PBXBuildFile; fileRef = C471D1532D0C8FE80068091F /* Drawlog.json */; };
C471D1552D0C8FED0068091F /* Drawlog.json in Resources */ = {isa = PBXBuildFile; fileRef = C471D1532D0C8FE80068091F /* Drawlog.json */; };
C471D1562D0C8FED0068091F /* Drawlog.json in Resources */ = {isa = PBXBuildFile; fileRef = C471D1532D0C8FE80068091F /* Drawlog.json */; };
C471D1582D0C91FE0068091F /* BaseDrawLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = C471D1572D0C91FE0068091F /* BaseDrawLog.swift */; };
C471D1592D0C91FE0068091F /* BaseDrawLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = C471D1572D0C91FE0068091F /* BaseDrawLog.swift */; };
C471D15A2D0C91FF0068091F /* BaseDrawLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = C471D1572D0C91FE0068091F /* BaseDrawLog.swift */; };
C488C7E92CC7D16F0082001F /* generator.py in Resources */ = {isa = PBXBuildFile; fileRef = C488C7E52CC7D1660082001F /* generator.py */; }; C488C7E92CC7D16F0082001F /* generator.py in Resources */ = {isa = PBXBuildFile; fileRef = C488C7E52CC7D1660082001F /* generator.py */; };
C488C7EA2CC7D16F0082001F /* generator.py in Resources */ = {isa = PBXBuildFile; fileRef = C488C7E52CC7D1660082001F /* generator.py */; }; C488C7EA2CC7D16F0082001F /* generator.py in Resources */ = {isa = PBXBuildFile; fileRef = C488C7E52CC7D1660082001F /* generator.py */; };
C488C7EB2CC7D16F0082001F /* generator.py in Resources */ = {isa = PBXBuildFile; fileRef = C488C7E52CC7D1660082001F /* generator.py */; }; C488C7EB2CC7D16F0082001F /* generator.py in Resources */ = {isa = PBXBuildFile; fileRef = C488C7E52CC7D1660082001F /* generator.py */; };
@ -181,8 +187,20 @@
FF1162832BCFBE4E000C4809 /* EditablePlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162822BCFBE4E000C4809 /* EditablePlayerView.swift */; }; FF1162832BCFBE4E000C4809 /* EditablePlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162822BCFBE4E000C4809 /* EditablePlayerView.swift */; };
FF1162852BD00279000C4809 /* PlayerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162842BD00279000C4809 /* PlayerDetailView.swift */; }; FF1162852BD00279000C4809 /* PlayerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162842BD00279000C4809 /* PlayerDetailView.swift */; };
FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162862BD004AD000C4809 /* EditingTeamView.swift */; }; FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162862BD004AD000C4809 /* EditingTeamView.swift */; };
FF11628A2BD05247000C4809 /* DateUpdateManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */; }; FF11628A2BD05247000C4809 /* DatePickingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DatePickingView.swift */; };
FF11628C2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */; }; FF11628C2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */; };
FF17CA492CB915A1003C7323 /* MultiCourtPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA482CB915A1003C7323 /* MultiCourtPickerView.swift */; };
FF17CA4A2CB915A1003C7323 /* MultiCourtPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA482CB915A1003C7323 /* MultiCourtPickerView.swift */; };
FF17CA4B2CB915A1003C7323 /* MultiCourtPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA482CB915A1003C7323 /* MultiCourtPickerView.swift */; };
FF17CA4D2CB9243E003C7323 /* FollowUpMatchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA4C2CB9243E003C7323 /* FollowUpMatchView.swift */; };
FF17CA4E2CB9243E003C7323 /* FollowUpMatchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA4C2CB9243E003C7323 /* FollowUpMatchView.swift */; };
FF17CA4F2CB9243E003C7323 /* FollowUpMatchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA4C2CB9243E003C7323 /* FollowUpMatchView.swift */; };
FF17CA532CBE4788003C7323 /* BracketCallingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA522CBE4788003C7323 /* BracketCallingView.swift */; };
FF17CA542CBE4788003C7323 /* BracketCallingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA522CBE4788003C7323 /* BracketCallingView.swift */; };
FF17CA552CBE4788003C7323 /* BracketCallingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA522CBE4788003C7323 /* BracketCallingView.swift */; };
FF17CA572CC02FEA003C7323 /* CoachListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA562CC02FEA003C7323 /* CoachListView.swift */; };
FF17CA582CC02FEB003C7323 /* CoachListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA562CC02FEA003C7323 /* CoachListView.swift */; };
FF17CA592CC02FEB003C7323 /* CoachListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA562CC02FEA003C7323 /* CoachListView.swift */; };
FF1CBC1B2BB53D1F0036DAAB /* FederalTournament.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */; }; FF1CBC1B2BB53D1F0036DAAB /* FederalTournament.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */; };
FF1CBC1D2BB53DC10036DAAB /* Calendar+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */; }; FF1CBC1D2BB53DC10036DAAB /* Calendar+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */; };
FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */; }; FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */; };
@ -384,8 +402,8 @@
FF4CBFE02C996C0600151637 /* TournamentFieldsManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26462BAE0ACB00650388 /* TournamentFieldsManagerView.swift */; }; FF4CBFE02C996C0600151637 /* TournamentFieldsManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26462BAE0ACB00650388 /* TournamentFieldsManagerView.swift */; };
FF4CBFE12C996C0600151637 /* PrintSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1F4B812BFA0124000B4573 /* PrintSettingsView.swift */; }; FF4CBFE12C996C0600151637 /* PrintSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1F4B812BFA0124000B4573 /* PrintSettingsView.swift */; };
FF4CBFE22C996C0600151637 /* TournamentMatchFormatsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE22BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift */; }; FF4CBFE22C996C0600151637 /* TournamentMatchFormatsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE22BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift */; };
FF4CBFE32C996C0600151637 /* DateUpdateManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */; }; FF4CBFE32C996C0600151637 /* DatePickingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DatePickingView.swift */; };
FF4CBFE42C996C0600151637 /* MatchTypeSmallSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0192BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift */; }; FF4CBFE42C996C0600151637 /* MatchFormatRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0192BBC5A8500B82851 /* MatchFormatRowView.swift */; };
FF4CBFE52C996C0600151637 /* MonthData.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE82BD1307E00A86CF8 /* MonthData.swift */; }; FF4CBFE52C996C0600151637 /* MonthData.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE82BD1307E00A86CF8 /* MonthData.swift */; };
FF4CBFE62C996C0600151637 /* MenuWarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFEF7F4D2BDE69130033D0F0 /* MenuWarningView.swift */; }; FF4CBFE62C996C0600151637 /* MenuWarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFEF7F4D2BDE69130033D0F0 /* MenuWarningView.swift */; };
FF4CBFE72C996C0600151637 /* TournamentBuildView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1F4B6C2BF9E60B000B4573 /* TournamentBuildView.swift */; }; FF4CBFE72C996C0600151637 /* TournamentBuildView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1F4B6C2BF9E60B000B4573 /* TournamentBuildView.swift */; };
@ -441,7 +459,7 @@
FF4CC0192C996C0600151637 /* EventSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF41852BF75FDA001B24CB /* EventSettingsView.swift */; }; FF4CC0192C996C0600151637 /* EventSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF41852BF75FDA001B24CB /* EventSettingsView.swift */; };
FF4CC01A2C996C0600151637 /* InscriptionInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5D0D772BB42C5B005CB568 /* InscriptionInfoView.swift */; }; FF4CC01A2C996C0600151637 /* InscriptionInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5D0D772BB42C5B005CB568 /* InscriptionInfoView.swift */; };
FF4CC01B2C996C0600151637 /* SelectablePlayerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */; }; FF4CC01B2C996C0600151637 /* SelectablePlayerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */; };
FF4CC01C2C996C0600151637 /* MatchFormatPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26502BAE0BAD00650388 /* MatchFormatPickerView.swift */; }; FF4CC01C2C996C0600151637 /* MatchFormatSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26502BAE0BAD00650388 /* MatchFormatSelectionView.swift */; };
FF4CC01D2C996C0600151637 /* TournamentRankView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5BAF712BE19274008B4B7E /* TournamentRankView.swift */; }; FF4CC01D2C996C0600151637 /* TournamentRankView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5BAF712BE19274008B4B7E /* TournamentRankView.swift */; };
FF4CC01E2C996C0600151637 /* NumberFormatter+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5D0D862BB48AFD005CB568 /* NumberFormatter+Extensions.swift */; }; FF4CC01E2C996C0600151637 /* NumberFormatter+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5D0D862BB48AFD005CB568 /* NumberFormatter+Extensions.swift */; };
FF4CC01F2C996C0600151637 /* SetLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0172BBC5A6800B82851 /* SetLabelView.swift */; }; FF4CC01F2C996C0600151637 /* SetLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0172BBC5A6800B82851 /* SetLabelView.swift */; };
@ -514,6 +532,15 @@
FF6087EC2BE26A2F004E1E47 /* BroadcastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6087EB2BE26A2F004E1E47 /* BroadcastView.swift */; }; FF6087EC2BE26A2F004E1E47 /* BroadcastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6087EB2BE26A2F004E1E47 /* BroadcastView.swift */; };
FF6525C32C8C61B400B9498E /* LoserBracketFromGroupStageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6525C22C8C61B400B9498E /* LoserBracketFromGroupStageView.swift */; }; FF6525C32C8C61B400B9498E /* LoserBracketFromGroupStageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6525C22C8C61B400B9498E /* LoserBracketFromGroupStageView.swift */; };
FF663FBE2BE019EC0031AE83 /* TournamentFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */; }; FF663FBE2BE019EC0031AE83 /* TournamentFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */; };
FF6761532CC77D2100CC9BF2 /* DrawLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761522CC77D1900CC9BF2 /* DrawLog.swift */; };
FF6761542CC77D2100CC9BF2 /* DrawLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761522CC77D1900CC9BF2 /* DrawLog.swift */; };
FF6761552CC77D2100CC9BF2 /* DrawLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761522CC77D1900CC9BF2 /* DrawLog.swift */; };
FF6761572CC7803600CC9BF2 /* DrawLogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */; };
FF6761582CC7803600CC9BF2 /* DrawLogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */; };
FF6761592CC7803600CC9BF2 /* DrawLogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */; };
FF67615B2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */; };
FF67615C2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */; };
FF67615D2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */; };
FF6EC8F72B94773200EA7F5A /* RowButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */; }; FF6EC8F72B94773200EA7F5A /* RowButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */; };
FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */; }; FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */; };
FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FD2B94792300EA7F5A /* Screen.swift */; }; FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FD2B94792300EA7F5A /* Screen.swift */; };
@ -687,8 +714,8 @@
FF70FB5F2C90584900129CC2 /* TournamentFieldsManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26462BAE0ACB00650388 /* TournamentFieldsManagerView.swift */; }; FF70FB5F2C90584900129CC2 /* TournamentFieldsManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26462BAE0ACB00650388 /* TournamentFieldsManagerView.swift */; };
FF70FB602C90584900129CC2 /* PrintSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1F4B812BFA0124000B4573 /* PrintSettingsView.swift */; }; FF70FB602C90584900129CC2 /* PrintSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1F4B812BFA0124000B4573 /* PrintSettingsView.swift */; };
FF70FB612C90584900129CC2 /* TournamentMatchFormatsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE22BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift */; }; FF70FB612C90584900129CC2 /* TournamentMatchFormatsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE22BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift */; };
FF70FB622C90584900129CC2 /* DateUpdateManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */; }; FF70FB622C90584900129CC2 /* DatePickingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DatePickingView.swift */; };
FF70FB632C90584900129CC2 /* MatchTypeSmallSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0192BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift */; }; FF70FB632C90584900129CC2 /* MatchFormatRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0192BBC5A8500B82851 /* MatchFormatRowView.swift */; };
FF70FB642C90584900129CC2 /* MonthData.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE82BD1307E00A86CF8 /* MonthData.swift */; }; FF70FB642C90584900129CC2 /* MonthData.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE82BD1307E00A86CF8 /* MonthData.swift */; };
FF70FB652C90584900129CC2 /* MenuWarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFEF7F4D2BDE69130033D0F0 /* MenuWarningView.swift */; }; FF70FB652C90584900129CC2 /* MenuWarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFEF7F4D2BDE69130033D0F0 /* MenuWarningView.swift */; };
FF70FB662C90584900129CC2 /* TournamentBuildView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1F4B6C2BF9E60B000B4573 /* TournamentBuildView.swift */; }; FF70FB662C90584900129CC2 /* TournamentBuildView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1F4B6C2BF9E60B000B4573 /* TournamentBuildView.swift */; };
@ -744,7 +771,7 @@
FF70FB982C90584900129CC2 /* EventSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF41852BF75FDA001B24CB /* EventSettingsView.swift */; }; FF70FB982C90584900129CC2 /* EventSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF41852BF75FDA001B24CB /* EventSettingsView.swift */; };
FF70FB992C90584900129CC2 /* InscriptionInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5D0D772BB42C5B005CB568 /* InscriptionInfoView.swift */; }; FF70FB992C90584900129CC2 /* InscriptionInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5D0D772BB42C5B005CB568 /* InscriptionInfoView.swift */; };
FF70FB9A2C90584900129CC2 /* SelectablePlayerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */; }; FF70FB9A2C90584900129CC2 /* SelectablePlayerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */; };
FF70FB9B2C90584900129CC2 /* MatchFormatPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26502BAE0BAD00650388 /* MatchFormatPickerView.swift */; }; FF70FB9B2C90584900129CC2 /* MatchFormatSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26502BAE0BAD00650388 /* MatchFormatSelectionView.swift */; };
FF70FB9C2C90584900129CC2 /* TournamentRankView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5BAF712BE19274008B4B7E /* TournamentRankView.swift */; }; FF70FB9C2C90584900129CC2 /* TournamentRankView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5BAF712BE19274008B4B7E /* TournamentRankView.swift */; };
FF70FB9D2C90584900129CC2 /* NumberFormatter+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5D0D862BB48AFD005CB568 /* NumberFormatter+Extensions.swift */; }; FF70FB9D2C90584900129CC2 /* NumberFormatter+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5D0D862BB48AFD005CB568 /* NumberFormatter+Extensions.swift */; };
FF70FB9E2C90584900129CC2 /* SetLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0172BBC5A6800B82851 /* SetLabelView.swift */; }; FF70FB9E2C90584900129CC2 /* SetLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0172BBC5A6800B82851 /* SetLabelView.swift */; };
@ -790,6 +817,18 @@
FF70FBC82C90584900129CC2 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = FF0CA5742BDA4AE10080E843 /* PrivacyInfo.xcprivacy */; }; FF70FBC82C90584900129CC2 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = FF0CA5742BDA4AE10080E843 /* PrivacyInfo.xcprivacy */; };
FF70FBC92C90584900129CC2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C425D4042B6D249E002A7B48 /* Assets.xcassets */; }; FF70FBC92C90584900129CC2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C425D4042B6D249E002A7B48 /* Assets.xcassets */; };
FF70FBCB2C90584900129CC2 /* LeStorage.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C49EF0372BDFF3000077B5AA /* LeStorage.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; FF70FBCB2C90584900129CC2 /* LeStorage.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C49EF0372BDFF3000077B5AA /* LeStorage.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
FF77CE522CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF77CE512CCCD1AF00CBCBB4 /* MatchFormatPickingView.swift */; };
FF77CE532CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF77CE512CCCD1AF00CBCBB4 /* MatchFormatPickingView.swift */; };
FF77CE542CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF77CE512CCCD1AF00CBCBB4 /* MatchFormatPickingView.swift */; };
FF77CE562CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF77CE552CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift */; };
FF77CE572CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF77CE552CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift */; };
FF77CE582CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF77CE552CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift */; };
FF77CE5A2CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF77CE592CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift */; };
FF77CE5B2CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF77CE592CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift */; };
FF77CE5C2CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF77CE592CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift */; };
FF7DCD392CC330270041110C /* TeamRestingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF7DCD382CC330260041110C /* TeamRestingView.swift */; };
FF7DCD3A2CC330270041110C /* TeamRestingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF7DCD382CC330260041110C /* TeamRestingView.swift */; };
FF7DCD3B2CC330270041110C /* TeamRestingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF7DCD382CC330260041110C /* TeamRestingView.swift */; };
FF8044AC2C8F676D00A49A52 /* TournamentSubscriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8044AB2C8F676D00A49A52 /* TournamentSubscriptionView.swift */; }; FF8044AC2C8F676D00A49A52 /* TournamentSubscriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8044AB2C8F676D00A49A52 /* TournamentSubscriptionView.swift */; };
FF82CFC52B911F5B00B0CAF2 /* OrganizedTournamentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF82CFC42B911F5B00B0CAF2 /* OrganizedTournamentView.swift */; }; FF82CFC52B911F5B00B0CAF2 /* OrganizedTournamentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF82CFC42B911F5B00B0CAF2 /* OrganizedTournamentView.swift */; };
FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF82CFC82B9132AF00B0CAF2 /* ActivityView.swift */; }; FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF82CFC82B9132AF00B0CAF2 /* ActivityView.swift */; };
@ -806,7 +845,7 @@
FF8F264C2BAE0B4100650388 /* TournamentFormatSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26482BAE0B4100650388 /* TournamentFormatSelectionView.swift */; }; FF8F264C2BAE0B4100650388 /* TournamentFormatSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26482BAE0B4100650388 /* TournamentFormatSelectionView.swift */; };
FF8F264D2BAE0B4100650388 /* TournamentDatePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F264A2BAE0B4100650388 /* TournamentDatePickerView.swift */; }; FF8F264D2BAE0B4100650388 /* TournamentDatePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F264A2BAE0B4100650388 /* TournamentDatePickerView.swift */; };
FF8F264F2BAE0B9600650388 /* MatchTypeSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F264E2BAE0B9600650388 /* MatchTypeSelectionView.swift */; }; FF8F264F2BAE0B9600650388 /* MatchTypeSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F264E2BAE0B9600650388 /* MatchTypeSelectionView.swift */; };
FF8F26512BAE0BAD00650388 /* MatchFormatPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26502BAE0BAD00650388 /* MatchFormatPickerView.swift */; }; FF8F26512BAE0BAD00650388 /* MatchFormatSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26502BAE0BAD00650388 /* MatchFormatSelectionView.swift */; };
FF8F26542BAE1E4400650388 /* TableStructureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26532BAE1E4400650388 /* TableStructureView.swift */; }; FF8F26542BAE1E4400650388 /* TableStructureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26532BAE1E4400650388 /* TableStructureView.swift */; };
FF90FC1D2C44FB3E009339B2 /* AddTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF90FC1C2C44FB3E009339B2 /* AddTeamView.swift */; }; FF90FC1D2C44FB3E009339B2 /* AddTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF90FC1C2C44FB3E009339B2 /* AddTeamView.swift */; };
FF92660D2C241CE0002361A4 /* Zip in Frameworks */ = {isa = PBXBuildFile; productRef = FF92660C2C241CE0002361A4 /* Zip */; }; FF92660D2C241CE0002361A4 /* Zip in Frameworks */ = {isa = PBXBuildFile; productRef = FF92660C2C241CE0002361A4 /* Zip */; };
@ -840,12 +879,27 @@
FF967D0F2BAF63B000A9A3BD /* PlayerBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967D0E2BAF63B000A9A3BD /* PlayerBlockView.swift */; }; FF967D0F2BAF63B000A9A3BD /* PlayerBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967D0E2BAF63B000A9A3BD /* PlayerBlockView.swift */; };
FF9AC3952BE3627B00C2E883 /* GroupStageTeamReplacementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF9AC3942BE3627B00C2E883 /* GroupStageTeamReplacementView.swift */; }; FF9AC3952BE3627B00C2E883 /* GroupStageTeamReplacementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF9AC3942BE3627B00C2E883 /* GroupStageTeamReplacementView.swift */; };
FFA1B1292BB71773006CE248 /* PadelClubButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA1B1282BB71773006CE248 /* PadelClubButtonView.swift */; }; FFA1B1292BB71773006CE248 /* PadelClubButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA1B1282BB71773006CE248 /* PadelClubButtonView.swift */; };
FFA252A92CDB70520074E63F /* PlayerStatisticView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252A82CDB70520074E63F /* PlayerStatisticView.swift */; };
FFA252AA2CDB70520074E63F /* PlayerStatisticView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252A82CDB70520074E63F /* PlayerStatisticView.swift */; };
FFA252AB2CDB70520074E63F /* PlayerStatisticView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252A82CDB70520074E63F /* PlayerStatisticView.swift */; };
FFA252AD2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */; };
FFA252AE2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */; };
FFA252AF2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */; };
FFA252B12CDD2C080074E63F /* OngoingContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B02CDD2C080074E63F /* OngoingContainerView.swift */; };
FFA252B22CDD2C080074E63F /* OngoingContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B02CDD2C080074E63F /* OngoingContainerView.swift */; };
FFA252B32CDD2C080074E63F /* OngoingContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B02CDD2C080074E63F /* OngoingContainerView.swift */; };
FFA252B52CDD2C6C0074E63F /* OngoingDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B42CDD2C630074E63F /* OngoingDestination.swift */; };
FFA252B62CDD2C6C0074E63F /* OngoingDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B42CDD2C630074E63F /* OngoingDestination.swift */; };
FFA252B72CDD2C6C0074E63F /* OngoingDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B42CDD2C630074E63F /* OngoingDestination.swift */; };
FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */; }; FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */; };
FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */; }; FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */; };
FFB1C98B2C10255100B154A7 /* TournamentBroadcastRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */; }; FFB1C98B2C10255100B154A7 /* TournamentBroadcastRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */; };
FFB9C8712BBADDE200A0EF4F /* Selectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB9C8702BBADDE200A0EF4F /* Selectable.swift */; }; FFB9C8712BBADDE200A0EF4F /* Selectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB9C8702BBADDE200A0EF4F /* Selectable.swift */; };
FFB9C8752BBADDF700A0EF4F /* SeedInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */; }; FFB9C8752BBADDF700A0EF4F /* SeedInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */; };
FFBA2D2D2CA2CE9E00D5BBDD /* CodingContainer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C33F752C9B1EC5006316DE /* CodingContainer+Extensions.swift */; }; FFBA2D2D2CA2CE9E00D5BBDD /* CodingContainer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C33F752C9B1EC5006316DE /* CodingContainer+Extensions.swift */; };
FFBE62052CE9DA0900815D33 /* MatchViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBE62042CE9DA0900815D33 /* MatchViewStyle.swift */; };
FFBE62062CE9DA0900815D33 /* MatchViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBE62042CE9DA0900815D33 /* MatchViewStyle.swift */; };
FFBE62072CE9DA0900815D33 /* MatchViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBE62042CE9DA0900815D33 /* MatchViewStyle.swift */; };
FFBF065C2BBD2657009D6715 /* GroupStageTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF065B2BBD2657009D6715 /* GroupStageTeamView.swift */; }; FFBF065C2BBD2657009D6715 /* GroupStageTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF065B2BBD2657009D6715 /* GroupStageTeamView.swift */; };
FFBF065E2BBD8040009D6715 /* MatchListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF065D2BBD8040009D6715 /* MatchListView.swift */; }; FFBF065E2BBD8040009D6715 /* MatchListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF065D2BBD8040009D6715 /* MatchListView.swift */; };
FFBF06602BBD9F6D009D6715 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */; }; FFBF06602BBD9F6D009D6715 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */; };
@ -875,7 +929,7 @@
FFCFC0142BBC59FC00B82851 /* MatchDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0132BBC59FC00B82851 /* MatchDescriptor.swift */; }; FFCFC0142BBC59FC00B82851 /* MatchDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0132BBC59FC00B82851 /* MatchDescriptor.swift */; };
FFCFC0162BBC5A4C00B82851 /* SetInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0152BBC5A4C00B82851 /* SetInputView.swift */; }; FFCFC0162BBC5A4C00B82851 /* SetInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0152BBC5A4C00B82851 /* SetInputView.swift */; };
FFCFC0182BBC5A6800B82851 /* SetLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0172BBC5A6800B82851 /* SetLabelView.swift */; }; FFCFC0182BBC5A6800B82851 /* SetLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0172BBC5A6800B82851 /* SetLabelView.swift */; };
FFCFC01A2BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0192BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift */; }; FFCFC01A2BBC5A8500B82851 /* MatchFormatRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0192BBC5A8500B82851 /* MatchFormatRowView.swift */; };
FFCFC01C2BBC5AAA00B82851 /* SetDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */; }; FFCFC01C2BBC5AAA00B82851 /* SetDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */; };
FFD655D82C8DE27400E5B35E /* TournamentLookUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD655D72C8DE27400E5B35E /* TournamentLookUpView.swift */; }; FFD655D82C8DE27400E5B35E /* TournamentLookUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD655D72C8DE27400E5B35E /* TournamentLookUpView.swift */; };
FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */; }; FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */; };
@ -981,6 +1035,8 @@
C45BAE3A2BC6DF10002EEC8A /* SyncedProducts.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = SyncedProducts.storekit; sourceTree = "<group>"; }; C45BAE3A2BC6DF10002EEC8A /* SyncedProducts.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = SyncedProducts.storekit; sourceTree = "<group>"; };
C45BAE432BCA753E002EEC8A /* Purchase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Purchase.swift; sourceTree = "<group>"; }; C45BAE432BCA753E002EEC8A /* Purchase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Purchase.swift; sourceTree = "<group>"; };
C4607A7C2C04DDE2004CB781 /* APICallsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APICallsListView.swift; sourceTree = "<group>"; }; C4607A7C2C04DDE2004CB781 /* APICallsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APICallsListView.swift; sourceTree = "<group>"; };
C471D1532D0C8FE80068091F /* Drawlog.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Drawlog.json; sourceTree = "<group>"; };
C471D1572D0C91FE0068091F /* BaseDrawLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseDrawLog.swift; sourceTree = "<group>"; };
C488C7E52CC7D1660082001F /* generator.py */ = {isa = PBXFileReference; lastKnownFileType = text.script.python; path = generator.py; sourceTree = "<group>"; }; C488C7E52CC7D1660082001F /* generator.py */ = {isa = PBXFileReference; lastKnownFileType = text.script.python; path = generator.py; sourceTree = "<group>"; };
C488C7EC2CC7D2290082001F /* Club.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Club.json; sourceTree = "<group>"; }; C488C7EC2CC7D2290082001F /* Club.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Club.json; sourceTree = "<group>"; };
C488C7FE2CC7DCB80082001F /* BaseClub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseClub.swift; sourceTree = "<group>"; }; C488C7FE2CC7DCB80082001F /* BaseClub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseClub.swift; sourceTree = "<group>"; };
@ -1112,8 +1168,12 @@
FF1162822BCFBE4E000C4809 /* EditablePlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditablePlayerView.swift; sourceTree = "<group>"; }; FF1162822BCFBE4E000C4809 /* EditablePlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditablePlayerView.swift; sourceTree = "<group>"; };
FF1162842BD00279000C4809 /* PlayerDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerDetailView.swift; sourceTree = "<group>"; }; FF1162842BD00279000C4809 /* PlayerDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerDetailView.swift; sourceTree = "<group>"; };
FF1162862BD004AD000C4809 /* EditingTeamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditingTeamView.swift; sourceTree = "<group>"; }; FF1162862BD004AD000C4809 /* EditingTeamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditingTeamView.swift; sourceTree = "<group>"; };
FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateUpdateManagerView.swift; sourceTree = "<group>"; }; FF1162892BD05247000C4809 /* DatePickingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickingView.swift; sourceTree = "<group>"; };
FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundStepScheduleEditorView.swift; sourceTree = "<group>"; }; FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundStepScheduleEditorView.swift; sourceTree = "<group>"; };
FF17CA482CB915A1003C7323 /* MultiCourtPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiCourtPickerView.swift; sourceTree = "<group>"; };
FF17CA4C2CB9243E003C7323 /* FollowUpMatchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowUpMatchView.swift; sourceTree = "<group>"; };
FF17CA522CBE4788003C7323 /* BracketCallingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BracketCallingView.swift; sourceTree = "<group>"; };
FF17CA562CC02FEA003C7323 /* CoachListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoachListView.swift; sourceTree = "<group>"; };
FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournament.swift; sourceTree = "<group>"; }; FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournament.swift; sourceTree = "<group>"; };
FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Calendar+Extensions.swift"; sourceTree = "<group>"; }; FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Calendar+Extensions.swift"; sourceTree = "<group>"; };
FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournamentSearchScope.swift; sourceTree = "<group>"; }; FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournamentSearchScope.swift; sourceTree = "<group>"; };
@ -1184,6 +1244,9 @@
FF6087EB2BE26A2F004E1E47 /* BroadcastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BroadcastView.swift; sourceTree = "<group>"; }; FF6087EB2BE26A2F004E1E47 /* BroadcastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BroadcastView.swift; sourceTree = "<group>"; };
FF6525C22C8C61B400B9498E /* LoserBracketFromGroupStageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserBracketFromGroupStageView.swift; sourceTree = "<group>"; }; FF6525C22C8C61B400B9498E /* LoserBracketFromGroupStageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserBracketFromGroupStageView.swift; sourceTree = "<group>"; };
FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentFilterView.swift; sourceTree = "<group>"; }; FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentFilterView.swift; sourceTree = "<group>"; };
FF6761522CC77D1900CC9BF2 /* DrawLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawLog.swift; sourceTree = "<group>"; };
FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawLogsView.swift; sourceTree = "<group>"; };
FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewBracketPositionView.swift; sourceTree = "<group>"; };
FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowButtonView.swift; sourceTree = "<group>"; }; FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowButtonView.swift; sourceTree = "<group>"; };
FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentButtonView.swift; sourceTree = "<group>"; }; FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentButtonView.swift; sourceTree = "<group>"; };
FF6EC8FD2B94792300EA7F5A /* Screen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = "<group>"; }; FF6EC8FD2B94792300EA7F5A /* Screen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = "<group>"; };
@ -1199,6 +1262,10 @@
FF70916B2B91005400AB08DA /* TournamentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentView.swift; sourceTree = "<group>"; }; FF70916B2B91005400AB08DA /* TournamentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentView.swift; sourceTree = "<group>"; };
FF70916D2B9108C600AB08DA /* InscriptionManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InscriptionManagerView.swift; sourceTree = "<group>"; }; FF70916D2B9108C600AB08DA /* InscriptionManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InscriptionManagerView.swift; sourceTree = "<group>"; };
FF70FBCF2C90584900129CC2 /* PadelClub TestFlight.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "PadelClub TestFlight.app"; sourceTree = BUILT_PRODUCTS_DIR; }; FF70FBCF2C90584900129CC2 /* PadelClub TestFlight.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "PadelClub TestFlight.app"; sourceTree = BUILT_PRODUCTS_DIR; };
FF77CE512CCCD1AF00CBCBB4 /* MatchFormatPickingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchFormatPickingView.swift; sourceTree = "<group>"; };
FF77CE552CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickingViewWithFormat.swift; sourceTree = "<group>"; };
FF77CE592CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupStageDatePickingView.swift; sourceTree = "<group>"; };
FF7DCD382CC330260041110C /* TeamRestingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamRestingView.swift; sourceTree = "<group>"; };
FF8044AB2C8F676D00A49A52 /* TournamentSubscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentSubscriptionView.swift; sourceTree = "<group>"; }; FF8044AB2C8F676D00A49A52 /* TournamentSubscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentSubscriptionView.swift; sourceTree = "<group>"; };
FF82CFC42B911F5B00B0CAF2 /* OrganizedTournamentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganizedTournamentView.swift; sourceTree = "<group>"; }; FF82CFC42B911F5B00B0CAF2 /* OrganizedTournamentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganizedTournamentView.swift; sourceTree = "<group>"; };
FF82CFC82B9132AF00B0CAF2 /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = "<group>"; }; FF82CFC82B9132AF00B0CAF2 /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = "<group>"; };
@ -1215,7 +1282,7 @@
FF8F26492BAE0B4100650388 /* TournamentLevelPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentLevelPickerView.swift; sourceTree = "<group>"; }; FF8F26492BAE0B4100650388 /* TournamentLevelPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentLevelPickerView.swift; sourceTree = "<group>"; };
FF8F264A2BAE0B4100650388 /* TournamentDatePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentDatePickerView.swift; sourceTree = "<group>"; }; FF8F264A2BAE0B4100650388 /* TournamentDatePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentDatePickerView.swift; sourceTree = "<group>"; };
FF8F264E2BAE0B9600650388 /* MatchTypeSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchTypeSelectionView.swift; sourceTree = "<group>"; }; FF8F264E2BAE0B9600650388 /* MatchTypeSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchTypeSelectionView.swift; sourceTree = "<group>"; };
FF8F26502BAE0BAD00650388 /* MatchFormatPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchFormatPickerView.swift; sourceTree = "<group>"; }; FF8F26502BAE0BAD00650388 /* MatchFormatSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchFormatSelectionView.swift; sourceTree = "<group>"; };
FF8F26532BAE1E4400650388 /* TableStructureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableStructureView.swift; sourceTree = "<group>"; }; FF8F26532BAE1E4400650388 /* TableStructureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableStructureView.swift; sourceTree = "<group>"; };
FF90FC1C2C44FB3E009339B2 /* AddTeamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddTeamView.swift; sourceTree = "<group>"; }; FF90FC1C2C44FB3E009339B2 /* AddTeamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddTeamView.swift; sourceTree = "<group>"; };
FF92660F2C255E4A002361A4 /* PadelClub.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PadelClub.entitlements; sourceTree = "<group>"; }; FF92660F2C255E4A002361A4 /* PadelClub.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PadelClub.entitlements; sourceTree = "<group>"; };
@ -1249,12 +1316,17 @@
FF967D0E2BAF63B000A9A3BD /* PlayerBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerBlockView.swift; sourceTree = "<group>"; }; FF967D0E2BAF63B000A9A3BD /* PlayerBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerBlockView.swift; sourceTree = "<group>"; };
FF9AC3942BE3627B00C2E883 /* GroupStageTeamReplacementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupStageTeamReplacementView.swift; sourceTree = "<group>"; }; FF9AC3942BE3627B00C2E883 /* GroupStageTeamReplacementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupStageTeamReplacementView.swift; sourceTree = "<group>"; };
FFA1B1282BB71773006CE248 /* PadelClubButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PadelClubButtonView.swift; sourceTree = "<group>"; }; FFA1B1282BB71773006CE248 /* PadelClubButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PadelClubButtonView.swift; sourceTree = "<group>"; };
FFA252A82CDB70520074E63F /* PlayerStatisticView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerStatisticView.swift; sourceTree = "<group>"; };
FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UmpireStatisticView.swift; sourceTree = "<group>"; };
FFA252B02CDD2C080074E63F /* OngoingContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OngoingContainerView.swift; sourceTree = "<group>"; };
FFA252B42CDD2C630074E63F /* OngoingDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OngoingDestination.swift; sourceTree = "<group>"; };
FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileImportManager.swift; sourceTree = "<group>"; }; FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileImportManager.swift; sourceTree = "<group>"; };
FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudConvert.swift; sourceTree = "<group>"; }; FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudConvert.swift; sourceTree = "<group>"; };
FFA6D78A2BB0BEB3003A31F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; }; FFA6D78A2BB0BEB3003A31F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentBroadcastRowView.swift; sourceTree = "<group>"; }; FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentBroadcastRowView.swift; sourceTree = "<group>"; };
FFB9C8702BBADDE200A0EF4F /* Selectable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Selectable.swift; sourceTree = "<group>"; }; FFB9C8702BBADDE200A0EF4F /* Selectable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Selectable.swift; sourceTree = "<group>"; };
FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedInterval.swift; sourceTree = "<group>"; }; FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedInterval.swift; sourceTree = "<group>"; };
FFBE62042CE9DA0900815D33 /* MatchViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchViewStyle.swift; sourceTree = "<group>"; };
FFBF065B2BBD2657009D6715 /* GroupStageTeamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupStageTeamView.swift; sourceTree = "<group>"; }; FFBF065B2BBD2657009D6715 /* GroupStageTeamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupStageTeamView.swift; sourceTree = "<group>"; };
FFBF065D2BBD8040009D6715 /* MatchListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchListView.swift; sourceTree = "<group>"; }; FFBF065D2BBD8040009D6715 /* MatchListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchListView.swift; sourceTree = "<group>"; };
FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.swift; sourceTree = "<group>"; }; FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.swift; sourceTree = "<group>"; };
@ -1283,7 +1355,7 @@
FFCFC0132BBC59FC00B82851 /* MatchDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchDescriptor.swift; sourceTree = "<group>"; }; FFCFC0132BBC59FC00B82851 /* MatchDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchDescriptor.swift; sourceTree = "<group>"; };
FFCFC0152BBC5A4C00B82851 /* SetInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetInputView.swift; sourceTree = "<group>"; }; FFCFC0152BBC5A4C00B82851 /* SetInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetInputView.swift; sourceTree = "<group>"; };
FFCFC0172BBC5A6800B82851 /* SetLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetLabelView.swift; sourceTree = "<group>"; }; FFCFC0172BBC5A6800B82851 /* SetLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetLabelView.swift; sourceTree = "<group>"; };
FFCFC0192BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchTypeSmallSelectionView.swift; sourceTree = "<group>"; }; FFCFC0192BBC5A8500B82851 /* MatchFormatRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchFormatRowView.swift; sourceTree = "<group>"; };
FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetDescriptor.swift; sourceTree = "<group>"; }; FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetDescriptor.swift; sourceTree = "<group>"; };
FFD655D72C8DE27400E5B35E /* TournamentLookUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentLookUpView.swift; sourceTree = "<group>"; }; FFD655D72C8DE27400E5B35E /* TournamentLookUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentLookUpView.swift; sourceTree = "<group>"; };
FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PadelClubView.swift; sourceTree = "<group>"; }; FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PadelClubView.swift; sourceTree = "<group>"; };
@ -1460,6 +1532,7 @@
C488C8132CC7E4240082001F /* Court.json */, C488C8132CC7E4240082001F /* Court.json */,
C488C8142CC7E4240082001F /* CustomUser.json */, C488C8142CC7E4240082001F /* CustomUser.json */,
C488C8152CC7E4240082001F /* DateInterval.json */, C488C8152CC7E4240082001F /* DateInterval.json */,
C471D1532D0C8FE80068091F /* Drawlog.json */,
C488C8162CC7E4240082001F /* Event.json */, C488C8162CC7E4240082001F /* Event.json */,
C488C8172CC7E4240082001F /* GroupStage.json */, C488C8172CC7E4240082001F /* GroupStage.json */,
C488C8182CC7E4240082001F /* Match.json */, C488C8182CC7E4240082001F /* Match.json */,
@ -1475,6 +1548,7 @@
C488C8022CC7E1E40082001F /* BaseCourt.swift */, C488C8022CC7E1E40082001F /* BaseCourt.swift */,
C488C8062CC7E4240082001F /* BaseCustomUser.swift */, C488C8062CC7E4240082001F /* BaseCustomUser.swift */,
C488C8072CC7E4240082001F /* BaseDateInterval.swift */, C488C8072CC7E4240082001F /* BaseDateInterval.swift */,
C471D1572D0C91FE0068091F /* BaseDrawLog.swift */,
C488C8082CC7E4240082001F /* BaseEvent.swift */, C488C8082CC7E4240082001F /* BaseEvent.swift */,
C488C8092CC7E4240082001F /* BaseGroupStage.swift */, C488C8092CC7E4240082001F /* BaseGroupStage.swift */,
C488C80A2CC7E4240082001F /* BaseMatch.swift */, C488C80A2CC7E4240082001F /* BaseMatch.swift */,
@ -1514,6 +1588,7 @@
FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */, FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */,
FFC91B002BD85C2F00B29808 /* Court.swift */, FFC91B002BD85C2F00B29808 /* Court.swift */,
FFF116E02BD2A9B600A33B06 /* DateInterval.swift */, FFF116E02BD2A9B600A33B06 /* DateInterval.swift */,
FF6761522CC77D1900CC9BF2 /* DrawLog.swift */,
FF6EC9012B94799200EA7F5A /* Coredata */, FF6EC9012B94799200EA7F5A /* Coredata */,
FF6EC9022B9479B900EA7F5A /* Federal */, FF6EC9022B9479B900EA7F5A /* Federal */,
); );
@ -1625,6 +1700,7 @@
children = ( children = (
FF089EBC2BB0287D00F0AEC7 /* PlayerView.swift */, FF089EBC2BB0287D00F0AEC7 /* PlayerView.swift */,
FF1162842BD00279000C4809 /* PlayerDetailView.swift */, FF1162842BD00279000C4809 /* PlayerDetailView.swift */,
FFA252A82CDB70520074E63F /* PlayerStatisticView.swift */,
FF089EB02BB001EA00F0AEC7 /* Components */, FF089EB02BB001EA00F0AEC7 /* Components */,
); );
path = Player; path = Player;
@ -1692,7 +1768,11 @@
FF1162882BD0523B000C4809 /* Components */ = { FF1162882BD0523B000C4809 /* Components */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */, FF1162892BD05247000C4809 /* DatePickingView.swift */,
FF77CE592CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift */,
FF77CE552CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift */,
FF77CE512CCCD1AF00CBCBB4 /* MatchFormatPickingView.swift */,
FF17CA482CB915A1003C7323 /* MultiCourtPickerView.swift */,
); );
path = Components; path = Components;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1823,6 +1903,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
FF3F74F52B919E45004CFE0E /* UmpireView.swift */, FF3F74F52B919E45004CFE0E /* UmpireView.swift */,
FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */,
FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */, FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */,
C488C8812CCBE8FC0082001F /* NetworkStatusView.swift */, C488C8812CCBE8FC0082001F /* NetworkStatusView.swift */,
); );
@ -1846,6 +1927,7 @@
FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */, FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */,
FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */, FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */,
FF5BAF6D2BE0B3C8008B4B7E /* FederalDataViewModel.swift */, FF5BAF6D2BE0B3C8008B4B7E /* FederalDataViewModel.swift */,
FFBE62042CE9DA0900815D33 /* MatchViewStyle.swift */,
); );
path = ViewModel; path = ViewModel;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1862,6 +1944,8 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
FF5D30552BD95B1100F2B93D /* OngoingView.swift */, FF5D30552BD95B1100F2B93D /* OngoingView.swift */,
FFA252B02CDD2C080074E63F /* OngoingContainerView.swift */,
FFA252B42CDD2C630074E63F /* OngoingDestination.swift */,
); );
path = Ongoing; path = Ongoing;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1870,11 +1954,11 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
FF8F264E2BAE0B9600650388 /* MatchTypeSelectionView.swift */, FF8F264E2BAE0B9600650388 /* MatchTypeSelectionView.swift */,
FF8F26502BAE0BAD00650388 /* MatchFormatPickerView.swift */, FF8F26502BAE0BAD00650388 /* MatchFormatSelectionView.swift */,
FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */, FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */,
FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */, FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */,
FF5D0D6F2BB3EFA5005CB568 /* LearnMoreSheetView.swift */, FF5D0D6F2BB3EFA5005CB568 /* LearnMoreSheetView.swift */,
FFCFC0192BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift */, FFCFC0192BBC5A8500B82851 /* MatchFormatRowView.swift */,
FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */, FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */,
FFE2D2E12C231BEE00D0C7BE /* SupportButtonView.swift */, FFE2D2E12C231BEE00D0C7BE /* SupportButtonView.swift */,
FFE103112C366E5900684FC9 /* ImagePickerView.swift */, FFE103112C366E5900684FC9 /* ImagePickerView.swift */,
@ -1943,6 +2027,7 @@
FF9268082BCEDC2C0080F940 /* CallView.swift */, FF9268082BCEDC2C0080F940 /* CallView.swift */,
FF1162792BCF8109000C4809 /* CallMessageCustomizationView.swift */, FF1162792BCF8109000C4809 /* CallMessageCustomizationView.swift */,
FF2EFBEF2BDE295E0049CE3B /* SendToAllView.swift */, FF2EFBEF2BDE295E0049CE3B /* SendToAllView.swift */,
FF17CA522CBE4788003C7323 /* BracketCallingView.swift */,
FFEF7F4C2BDE68F80033D0F0 /* Components */, FFEF7F4C2BDE68F80033D0F0 /* Components */,
); );
path = Calling; path = Calling;
@ -1981,6 +2066,8 @@
FF967D0A2BAF3D4C00A9A3BD /* TeamPickerView.swift */, FF967D0A2BAF3D4C00A9A3BD /* TeamPickerView.swift */,
FF089EB52BB00A3800F0AEC7 /* TeamRowView.swift */, FF089EB52BB00A3800F0AEC7 /* TeamRowView.swift */,
FF1162862BD004AD000C4809 /* EditingTeamView.swift */, FF1162862BD004AD000C4809 /* EditingTeamView.swift */,
FF17CA562CC02FEA003C7323 /* CoachListView.swift */,
FF7DCD382CC330260041110C /* TeamRestingView.swift */,
FF025AD62BD0C0FB00A86CF8 /* Components */, FF025AD62BD0C0FB00A86CF8 /* Components */,
); );
path = Team; path = Team;
@ -2027,6 +2114,8 @@
FFC2DCB12BBE75D40046DB9F /* LoserRoundView.swift */, FFC2DCB12BBE75D40046DB9F /* LoserRoundView.swift */,
FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */, FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */,
FF5647122C0B6F380081F995 /* LoserRoundSettingsView.swift */, FF5647122C0B6F380081F995 /* LoserRoundSettingsView.swift */,
FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */,
FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */,
); );
path = Round; path = Round;
sourceTree = "<group>"; sourceTree = "<group>";
@ -2035,6 +2124,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
FFCFC0012BBC39A600B82851 /* EditScoreView.swift */, FFCFC0012BBC39A600B82851 /* EditScoreView.swift */,
FF17CA4C2CB9243E003C7323 /* FollowUpMatchView.swift */,
FFCFC0152BBC5A4C00B82851 /* SetInputView.swift */, FFCFC0152BBC5A4C00B82851 /* SetInputView.swift */,
FFCFC0172BBC5A6800B82851 /* SetLabelView.swift */, FFCFC0172BBC5A6800B82851 /* SetLabelView.swift */,
FFCFC00D2BBC3D4600B82851 /* PointSelectionView.swift */, FFCFC00D2BBC3D4600B82851 /* PointSelectionView.swift */,
@ -2322,6 +2412,7 @@
C488C8202CC7E4240082001F /* Event.json in Resources */, C488C8202CC7E4240082001F /* Event.json in Resources */,
C488C8212CC7E4240082001F /* Court.json in Resources */, C488C8212CC7E4240082001F /* Court.json in Resources */,
C488C8222CC7E4240082001F /* Tournament.json in Resources */, C488C8222CC7E4240082001F /* Tournament.json in Resources */,
C471D1552D0C8FED0068091F /* Drawlog.json in Resources */,
C488C8232CC7E4240082001F /* CustomUser.json in Resources */, C488C8232CC7E4240082001F /* CustomUser.json in Resources */,
C488C8242CC7E4240082001F /* Round.json in Resources */, C488C8242CC7E4240082001F /* Round.json in Resources */,
C488C8252CC7E4240082001F /* MatchScheduler.json in Resources */, C488C8252CC7E4240082001F /* MatchScheduler.json in Resources */,
@ -2376,6 +2467,7 @@
C488C83A2CC7E4240082001F /* Event.json in Resources */, C488C83A2CC7E4240082001F /* Event.json in Resources */,
C488C83B2CC7E4240082001F /* Court.json in Resources */, C488C83B2CC7E4240082001F /* Court.json in Resources */,
C488C83C2CC7E4240082001F /* Tournament.json in Resources */, C488C83C2CC7E4240082001F /* Tournament.json in Resources */,
C471D1562D0C8FED0068091F /* Drawlog.json in Resources */,
C488C83D2CC7E4240082001F /* CustomUser.json in Resources */, C488C83D2CC7E4240082001F /* CustomUser.json in Resources */,
C488C83E2CC7E4240082001F /* Round.json in Resources */, C488C83E2CC7E4240082001F /* Round.json in Resources */,
C488C83F2CC7E4240082001F /* MatchScheduler.json in Resources */, C488C83F2CC7E4240082001F /* MatchScheduler.json in Resources */,
@ -2416,6 +2508,7 @@
C488C8612CC7E4240082001F /* Event.json in Resources */, C488C8612CC7E4240082001F /* Event.json in Resources */,
C488C8622CC7E4240082001F /* Court.json in Resources */, C488C8622CC7E4240082001F /* Court.json in Resources */,
C488C8632CC7E4240082001F /* Tournament.json in Resources */, C488C8632CC7E4240082001F /* Tournament.json in Resources */,
C471D1542D0C8FED0068091F /* Drawlog.json in Resources */,
C488C8642CC7E4240082001F /* CustomUser.json in Resources */, C488C8642CC7E4240082001F /* CustomUser.json in Resources */,
C488C8652CC7E4240082001F /* Round.json in Resources */, C488C8652CC7E4240082001F /* Round.json in Resources */,
C488C8662CC7E4240082001F /* MatchScheduler.json in Resources */, C488C8662CC7E4240082001F /* MatchScheduler.json in Resources */,
@ -2449,6 +2542,7 @@
FF7091662B90F0B000AB08DA /* TabDestination.swift in Sources */, FF7091662B90F0B000AB08DA /* TabDestination.swift in Sources */,
FF9267F82BCE78C70080F940 /* CashierView.swift in Sources */, FF9267F82BCE78C70080F940 /* CashierView.swift in Sources */,
FF8F263F2BAD7D5C00650388 /* Event.swift in Sources */, FF8F263F2BAD7D5C00650388 /* Event.swift in Sources */,
FF77CE562CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift in Sources */,
FF5D30532BD94E2E00F2B93D /* PlayerHolder.swift in Sources */, FF5D30532BD94E2E00F2B93D /* PlayerHolder.swift in Sources */,
FF11628C2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift in Sources */, FF11628C2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift in Sources */,
FF53FBB82BFB302B0051D4C3 /* ClubCourtSetupView.swift in Sources */, FF53FBB82BFB302B0051D4C3 /* ClubCourtSetupView.swift in Sources */,
@ -2471,6 +2565,7 @@
FF025AE12BD0EB9000A86CF8 /* TournamentClubSettingsView.swift in Sources */, FF025AE12BD0EB9000A86CF8 /* TournamentClubSettingsView.swift in Sources */,
FFBF065C2BBD2657009D6715 /* GroupStageTeamView.swift in Sources */, FFBF065C2BBD2657009D6715 /* GroupStageTeamView.swift in Sources */,
FF5DA1932BB9279B00A33061 /* RoundSettingsView.swift in Sources */, FF5DA1932BB9279B00A33061 /* RoundSettingsView.swift in Sources */,
FFBE62052CE9DA0900815D33 /* MatchViewStyle.swift in Sources */,
FFE2D2E22C231BEE00D0C7BE /* SupportButtonView.swift in Sources */, FFE2D2E22C231BEE00D0C7BE /* SupportButtonView.swift in Sources */,
FFB1C98B2C10255100B154A7 /* TournamentBroadcastRowView.swift in Sources */, FFB1C98B2C10255100B154A7 /* TournamentBroadcastRowView.swift in Sources */,
FF025ADF2BD0CE0A00A86CF8 /* TeamWeightView.swift in Sources */, FF025ADF2BD0CE0A00A86CF8 /* TeamWeightView.swift in Sources */,
@ -2485,11 +2580,13 @@
FFE103082C353B7600684FC9 /* EventClubSettingsView.swift in Sources */, FFE103082C353B7600684FC9 /* EventClubSettingsView.swift in Sources */,
C4A47DB32B86387500ADC637 /* AccountView.swift in Sources */, C4A47DB32B86387500ADC637 /* AccountView.swift in Sources */,
FFCEDA4C2C2C08EA00F8C0F2 /* PlayersWithoutContactView.swift in Sources */, FFCEDA4C2C2C08EA00F8C0F2 /* PlayersWithoutContactView.swift in Sources */,
FF17CA4F2CB9243E003C7323 /* FollowUpMatchView.swift in Sources */,
FFCD16B32C3E5E590092707B /* TeamsCallingView.swift in Sources */, FFCD16B32C3E5E590092707B /* TeamsCallingView.swift in Sources */,
FF1CBC1D2BB53DC10036DAAB /* Calendar+Extensions.swift in Sources */, FF1CBC1D2BB53DC10036DAAB /* Calendar+Extensions.swift in Sources */,
FF967CF22BAECC0B00A9A3BD /* TeamScore.swift in Sources */, FF967CF22BAECC0B00A9A3BD /* TeamScore.swift in Sources */,
FF1162832BCFBE4E000C4809 /* EditablePlayerView.swift in Sources */, FF1162832BCFBE4E000C4809 /* EditablePlayerView.swift in Sources */,
FF1162852BD00279000C4809 /* PlayerDetailView.swift in Sources */, FF1162852BD00279000C4809 /* PlayerDetailView.swift in Sources */,
FFA252AE2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */,
FF5D0D762BB428B2005CB568 /* ListRowViewModifier.swift in Sources */, FF5D0D762BB428B2005CB568 /* ListRowViewModifier.swift in Sources */,
FF6EC9002B94794700EA7F5A /* PresentationContext.swift in Sources */, FF6EC9002B94794700EA7F5A /* PresentationContext.swift in Sources */,
FFDB1C6D2BB2A02000F1E467 /* AppSettings.swift in Sources */, FFDB1C6D2BB2A02000F1E467 /* AppSettings.swift in Sources */,
@ -2504,6 +2601,7 @@
C4C01D982C481C0C0059087C /* CapsuleViewModifier.swift in Sources */, C4C01D982C481C0C0059087C /* CapsuleViewModifier.swift in Sources */,
C488C82D2CC7E4240082001F /* BaseDateInterval.swift in Sources */, C488C82D2CC7E4240082001F /* BaseDateInterval.swift in Sources */,
C488C82E2CC7E4240082001F /* BaseMonthData.swift in Sources */, C488C82E2CC7E4240082001F /* BaseMonthData.swift in Sources */,
C471D1592D0C91FE0068091F /* BaseDrawLog.swift in Sources */,
C488C82F2CC7E4240082001F /* BaseTeamRegistration.swift in Sources */, C488C82F2CC7E4240082001F /* BaseTeamRegistration.swift in Sources */,
C488C8302CC7E4240082001F /* BaseGroupStage.swift in Sources */, C488C8302CC7E4240082001F /* BaseGroupStage.swift in Sources */,
C488C8312CC7E4240082001F /* BaseCustomUser.swift in Sources */, C488C8312CC7E4240082001F /* BaseCustomUser.swift in Sources */,
@ -2522,6 +2620,7 @@
FF5D30562BD95B1100F2B93D /* OngoingView.swift in Sources */, FF5D30562BD95B1100F2B93D /* OngoingView.swift in Sources */,
FF1DC5552BAB36DD00FD8220 /* CreateClubView.swift in Sources */, FF1DC5552BAB36DD00FD8220 /* CreateClubView.swift in Sources */,
C4607A7D2C04DDE2004CB781 /* APICallsListView.swift in Sources */, C4607A7D2C04DDE2004CB781 /* APICallsListView.swift in Sources */,
FF7DCD3B2CC330270041110C /* TeamRestingView.swift in Sources */,
FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */, FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */,
FF025AEF2BD1AE9400A86CF8 /* DurationSettingsView.swift in Sources */, FF025AEF2BD1AE9400A86CF8 /* DurationSettingsView.swift in Sources */,
FF025AED2BD1513700A86CF8 /* AppScreen.swift in Sources */, FF025AED2BD1513700A86CF8 /* AppScreen.swift in Sources */,
@ -2543,6 +2642,7 @@
C44B79112BBDA63A00906534 /* Locale+Extensions.swift in Sources */, C44B79112BBDA63A00906534 /* Locale+Extensions.swift in Sources */,
FF1F4B742BFA00FC000B4573 /* HtmlService.swift in Sources */, FF1F4B742BFA00FC000B4573 /* HtmlService.swift in Sources */,
FF967CEA2BAEC70100A9A3BD /* GroupStage.swift in Sources */, FF967CEA2BAEC70100A9A3BD /* GroupStage.swift in Sources */,
FF6761542CC77D2100CC9BF2 /* DrawLog.swift in Sources */,
FF1162812BCF945C000C4809 /* TournamentCashierView.swift in Sources */, FF1162812BCF945C000C4809 /* TournamentCashierView.swift in Sources */,
C4A47D902B7BBBEC00ADC637 /* StoreManager.swift in Sources */, C4A47D902B7BBBEC00ADC637 /* StoreManager.swift in Sources */,
FF4AB6BB2B9256D50002987F /* SearchViewModel.swift in Sources */, FF4AB6BB2B9256D50002987F /* SearchViewModel.swift in Sources */,
@ -2604,7 +2704,9 @@
FF9267FC2BCE84870080F940 /* PlayerPayView.swift in Sources */, FF9267FC2BCE84870080F940 /* PlayerPayView.swift in Sources */,
FF2B51552C7A4DAF00FFF126 /* PlanningByCourtView.swift in Sources */, FF2B51552C7A4DAF00FFF126 /* PlanningByCourtView.swift in Sources */,
FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */, FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */,
FFA252B22CDD2C080074E63F /* OngoingContainerView.swift in Sources */,
FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */, FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */,
FF6761582CC7803600CC9BF2 /* DrawLogsView.swift in Sources */,
FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */, FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */,
FFBF06602BBD9F6D009D6715 /* NavigationViewModel.swift in Sources */, FFBF06602BBD9F6D009D6715 /* NavigationViewModel.swift in Sources */,
FF6EC9092B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift in Sources */, FF6EC9092B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift in Sources */,
@ -2613,6 +2715,7 @@
FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */, FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */,
FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */, FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */,
FF70916E2B9108C600AB08DA /* InscriptionManagerView.swift in Sources */, FF70916E2B9108C600AB08DA /* InscriptionManagerView.swift in Sources */,
FF77CE542CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */,
FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */, FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */,
FFDB1C732BB2CFE900F1E467 /* MySortDescriptor.swift in Sources */, FFDB1C732BB2CFE900F1E467 /* MySortDescriptor.swift in Sources */,
FF5D0D8B2BB4D1E3005CB568 /* CalendarView.swift in Sources */, FF5D0D8B2BB4D1E3005CB568 /* CalendarView.swift in Sources */,
@ -2620,8 +2723,8 @@
FF8F26472BAE0ACB00650388 /* TournamentFieldsManagerView.swift in Sources */, FF8F26472BAE0ACB00650388 /* TournamentFieldsManagerView.swift in Sources */,
FF1F4B822BFA0124000B4573 /* PrintSettingsView.swift in Sources */, FF1F4B822BFA0124000B4573 /* PrintSettingsView.swift in Sources */,
FF025AE32BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift in Sources */, FF025AE32BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift in Sources */,
FF11628A2BD05247000C4809 /* DateUpdateManagerView.swift in Sources */, FF11628A2BD05247000C4809 /* DatePickingView.swift in Sources */,
FFCFC01A2BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift in Sources */, FFCFC01A2BBC5A8500B82851 /* MatchFormatRowView.swift in Sources */,
FF025AE92BD1307F00A86CF8 /* MonthData.swift in Sources */, FF025AE92BD1307F00A86CF8 /* MonthData.swift in Sources */,
FFEF7F4E2BDE69130033D0F0 /* MenuWarningView.swift in Sources */, FFEF7F4E2BDE69130033D0F0 /* MenuWarningView.swift in Sources */,
FF1F4B6D2BF9E60B000B4573 /* TournamentBuildView.swift in Sources */, FF1F4B6D2BF9E60B000B4573 /* TournamentBuildView.swift in Sources */,
@ -2629,6 +2732,7 @@
FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */, FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */,
FFBF41842BF75ED7001B24CB /* EventTournamentsView.swift in Sources */, FFBF41842BF75ED7001B24CB /* EventTournamentsView.swift in Sources */,
FF1DC55B2BAB80C400FD8220 /* DisplayContext.swift in Sources */, FF1DC55B2BAB80C400FD8220 /* DisplayContext.swift in Sources */,
FF17CA4A2CB915A1003C7323 /* MultiCourtPickerView.swift in Sources */,
FF9268072BCE94D90080F940 /* TournamentCallView.swift in Sources */, FF9268072BCE94D90080F940 /* TournamentCallView.swift in Sources */,
FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */, FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */,
FF967CFC2BAEE52E00A9A3BD /* GroupStagesView.swift in Sources */, FF967CFC2BAEE52E00A9A3BD /* GroupStagesView.swift in Sources */,
@ -2636,6 +2740,7 @@
C49EF01B2BD6A1E80077B5AA /* URLs.swift in Sources */, C49EF01B2BD6A1E80077B5AA /* URLs.swift in Sources */,
FFCFC0142BBC59FC00B82851 /* MatchDescriptor.swift in Sources */, FFCFC0142BBC59FC00B82851 /* MatchDescriptor.swift in Sources */,
FF8F264C2BAE0B4100650388 /* TournamentFormatSelectionView.swift in Sources */, FF8F264C2BAE0B4100650388 /* TournamentFormatSelectionView.swift in Sources */,
FF17CA572CC02FEA003C7323 /* CoachListView.swift in Sources */,
FFBF065E2BBD8040009D6715 /* MatchListView.swift in Sources */, FFBF065E2BBD8040009D6715 /* MatchListView.swift in Sources */,
C425D4012B6D249D002A7B48 /* PadelClubApp.swift in Sources */, C425D4012B6D249D002A7B48 /* PadelClubApp.swift in Sources */,
FF8F26432BADFE5B00650388 /* TournamentSettingsView.swift in Sources */, FF8F26432BADFE5B00650388 /* TournamentSettingsView.swift in Sources */,
@ -2649,13 +2754,17 @@
C4A47DAD2B85FCCD00ADC637 /* CustomUser.swift in Sources */, C4A47DAD2B85FCCD00ADC637 /* CustomUser.swift in Sources */,
C4C33F762C9B1ED4006316DE /* CodingContainer+Extensions.swift in Sources */, C4C33F762C9B1ED4006316DE /* CodingContainer+Extensions.swift in Sources */,
FF967D012BAEF0B400A9A3BD /* MatchSummaryView.swift in Sources */, FF967D012BAEF0B400A9A3BD /* MatchSummaryView.swift in Sources */,
FFA252B62CDD2C6C0074E63F /* OngoingDestination.swift in Sources */,
FF8F26452BAE0A3400650388 /* TournamentDurationManagerView.swift in Sources */, FF8F26452BAE0A3400650388 /* TournamentDurationManagerView.swift in Sources */,
FF1DC5532BAB354A00FD8220 /* MockData.swift in Sources */, FF1DC5532BAB354A00FD8220 /* MockData.swift in Sources */,
FF967D092BAF3D4000A9A3BD /* TeamDetailView.swift in Sources */, FF967D092BAF3D4000A9A3BD /* TeamDetailView.swift in Sources */,
C488C8822CCBE8FC0082001F /* NetworkStatusView.swift in Sources */, C488C8822CCBE8FC0082001F /* NetworkStatusView.swift in Sources */,
FFA252A92CDB70520074E63F /* PlayerStatisticView.swift in Sources */,
FF5DA18F2BB9268800A33061 /* GroupStagesSettingsView.swift in Sources */, FF5DA18F2BB9268800A33061 /* GroupStagesSettingsView.swift in Sources */,
FF663FBE2BE019EC0031AE83 /* TournamentFilterView.swift in Sources */, FF663FBE2BE019EC0031AE83 /* TournamentFilterView.swift in Sources */,
FF67615D2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */,
FF1F4B752BFA00FC000B4573 /* HtmlGenerator.swift in Sources */, FF1F4B752BFA00FC000B4573 /* HtmlGenerator.swift in Sources */,
FF17CA532CBE4788003C7323 /* BracketCallingView.swift in Sources */,
FF8F26382BAD523300650388 /* PadelRule.swift in Sources */, FF8F26382BAD523300650388 /* PadelRule.swift in Sources */,
FF967CF42BAECC0B00A9A3BD /* TeamRegistration.swift in Sources */, FF967CF42BAECC0B00A9A3BD /* TeamRegistration.swift in Sources */,
FFF8ACDB2B923F48008466FA /* Date+Extensions.swift in Sources */, FFF8ACDB2B923F48008466FA /* Date+Extensions.swift in Sources */,
@ -2680,8 +2789,9 @@
FFBF41862BF75FDA001B24CB /* EventSettingsView.swift in Sources */, FFBF41862BF75FDA001B24CB /* EventSettingsView.swift in Sources */,
FF5D0D782BB42C5B005CB568 /* InscriptionInfoView.swift in Sources */, FF5D0D782BB42C5B005CB568 /* InscriptionInfoView.swift in Sources */,
FF4AB6BD2B9256E10002987F /* SelectablePlayerListView.swift in Sources */, FF4AB6BD2B9256E10002987F /* SelectablePlayerListView.swift in Sources */,
FF8F26512BAE0BAD00650388 /* MatchFormatPickerView.swift in Sources */, FF8F26512BAE0BAD00650388 /* MatchFormatSelectionView.swift in Sources */,
FF5BAF722BE19274008B4B7E /* TournamentRankView.swift in Sources */, FF5BAF722BE19274008B4B7E /* TournamentRankView.swift in Sources */,
FF77CE5B2CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift in Sources */,
FF5D0D872BB48AFD005CB568 /* NumberFormatter+Extensions.swift in Sources */, FF5D0D872BB48AFD005CB568 /* NumberFormatter+Extensions.swift in Sources */,
FFCFC0182BBC5A6800B82851 /* SetLabelView.swift in Sources */, FFCFC0182BBC5A6800B82851 /* SetLabelView.swift in Sources */,
C4489BE22C05BF5000043F3D /* DebugSettingsView.swift in Sources */, C4489BE22C05BF5000043F3D /* DebugSettingsView.swift in Sources */,
@ -2737,6 +2847,7 @@
FF4CBF452C996C0600151637 /* TabDestination.swift in Sources */, FF4CBF452C996C0600151637 /* TabDestination.swift in Sources */,
FF4CBF462C996C0600151637 /* CashierView.swift in Sources */, FF4CBF462C996C0600151637 /* CashierView.swift in Sources */,
FF4CBF472C996C0600151637 /* Event.swift in Sources */, FF4CBF472C996C0600151637 /* Event.swift in Sources */,
FF77CE582CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift in Sources */,
FF4CBF482C996C0600151637 /* PlayerHolder.swift in Sources */, FF4CBF482C996C0600151637 /* PlayerHolder.swift in Sources */,
FF4CBF492C996C0600151637 /* LoserRoundStepScheduleEditorView.swift in Sources */, FF4CBF492C996C0600151637 /* LoserRoundStepScheduleEditorView.swift in Sources */,
FF4CBF4A2C996C0600151637 /* ClubCourtSetupView.swift in Sources */, FF4CBF4A2C996C0600151637 /* ClubCourtSetupView.swift in Sources */,
@ -2758,6 +2869,7 @@
FF4CBF5A2C996C0600151637 /* TournamentClubSettingsView.swift in Sources */, FF4CBF5A2C996C0600151637 /* TournamentClubSettingsView.swift in Sources */,
FF4CBF5B2C996C0600151637 /* GroupStageTeamView.swift in Sources */, FF4CBF5B2C996C0600151637 /* GroupStageTeamView.swift in Sources */,
FF4CBF5C2C996C0600151637 /* RoundSettingsView.swift in Sources */, FF4CBF5C2C996C0600151637 /* RoundSettingsView.swift in Sources */,
FFBE62072CE9DA0900815D33 /* MatchViewStyle.swift in Sources */,
FF4CBF5D2C996C0600151637 /* SupportButtonView.swift in Sources */, FF4CBF5D2C996C0600151637 /* SupportButtonView.swift in Sources */,
FF4CBF5E2C996C0600151637 /* TournamentBroadcastRowView.swift in Sources */, FF4CBF5E2C996C0600151637 /* TournamentBroadcastRowView.swift in Sources */,
FF4CBF5F2C996C0600151637 /* TeamWeightView.swift in Sources */, FF4CBF5F2C996C0600151637 /* TeamWeightView.swift in Sources */,
@ -2771,12 +2883,14 @@
FF4CBF672C996C0600151637 /* EventClubSettingsView.swift in Sources */, FF4CBF672C996C0600151637 /* EventClubSettingsView.swift in Sources */,
FF4CBF682C996C0600151637 /* AccountView.swift in Sources */, FF4CBF682C996C0600151637 /* AccountView.swift in Sources */,
FF4CBF692C996C0600151637 /* PlayersWithoutContactView.swift in Sources */, FF4CBF692C996C0600151637 /* PlayersWithoutContactView.swift in Sources */,
FF17CA4D2CB9243E003C7323 /* FollowUpMatchView.swift in Sources */,
FF4CBF6A2C996C0600151637 /* TeamsCallingView.swift in Sources */, FF4CBF6A2C996C0600151637 /* TeamsCallingView.swift in Sources */,
FF4CBF6B2C996C0600151637 /* Calendar+Extensions.swift in Sources */, FF4CBF6B2C996C0600151637 /* Calendar+Extensions.swift in Sources */,
FF4CBF6C2C996C0600151637 /* TeamScore.swift in Sources */, FF4CBF6C2C996C0600151637 /* TeamScore.swift in Sources */,
FF4CBF6D2C996C0600151637 /* EditablePlayerView.swift in Sources */, FF4CBF6D2C996C0600151637 /* EditablePlayerView.swift in Sources */,
C488C7F12CC7D22D0082001F /* Club.json in Sources */, C488C7F12CC7D22D0082001F /* Club.json in Sources */,
FF4CBF6E2C996C0600151637 /* PlayerDetailView.swift in Sources */, FF4CBF6E2C996C0600151637 /* PlayerDetailView.swift in Sources */,
FFA252AF2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */,
FF4CBF6F2C996C0600151637 /* ListRowViewModifier.swift in Sources */, FF4CBF6F2C996C0600151637 /* ListRowViewModifier.swift in Sources */,
FF4CBF702C996C0600151637 /* PresentationContext.swift in Sources */, FF4CBF702C996C0600151637 /* PresentationContext.swift in Sources */,
FF4CBF712C996C0600151637 /* AppSettings.swift in Sources */, FF4CBF712C996C0600151637 /* AppSettings.swift in Sources */,
@ -2810,6 +2924,7 @@
FF4CBF802C996C0600151637 /* OngoingView.swift in Sources */, FF4CBF802C996C0600151637 /* OngoingView.swift in Sources */,
FF4CBF812C996C0600151637 /* CreateClubView.swift in Sources */, FF4CBF812C996C0600151637 /* CreateClubView.swift in Sources */,
FF4CBF822C996C0600151637 /* APICallsListView.swift in Sources */, FF4CBF822C996C0600151637 /* APICallsListView.swift in Sources */,
FF7DCD392CC330270041110C /* TeamRestingView.swift in Sources */,
FF4CBF832C996C0600151637 /* NetworkFederalService.swift in Sources */, FF4CBF832C996C0600151637 /* NetworkFederalService.swift in Sources */,
FF4CBF842C996C0600151637 /* DurationSettingsView.swift in Sources */, FF4CBF842C996C0600151637 /* DurationSettingsView.swift in Sources */,
FF4CBF852C996C0600151637 /* AppScreen.swift in Sources */, FF4CBF852C996C0600151637 /* AppScreen.swift in Sources */,
@ -2832,6 +2947,7 @@
FF4CBF952C996C0600151637 /* Locale+Extensions.swift in Sources */, FF4CBF952C996C0600151637 /* Locale+Extensions.swift in Sources */,
FF4CBF962C996C0600151637 /* HtmlService.swift in Sources */, FF4CBF962C996C0600151637 /* HtmlService.swift in Sources */,
FF4CBF972C996C0600151637 /* GroupStage.swift in Sources */, FF4CBF972C996C0600151637 /* GroupStage.swift in Sources */,
FF6761532CC77D2100CC9BF2 /* DrawLog.swift in Sources */,
FF4CBF982C996C0600151637 /* TournamentCashierView.swift in Sources */, FF4CBF982C996C0600151637 /* TournamentCashierView.swift in Sources */,
FF4CBF992C996C0600151637 /* StoreManager.swift in Sources */, FF4CBF992C996C0600151637 /* StoreManager.swift in Sources */,
FF4CBF9A2C996C0600151637 /* SearchViewModel.swift in Sources */, FF4CBF9A2C996C0600151637 /* SearchViewModel.swift in Sources */,
@ -2874,6 +2990,7 @@
FF4CBFBD2C996C0600151637 /* AgendaDestination.swift in Sources */, FF4CBFBD2C996C0600151637 /* AgendaDestination.swift in Sources */,
FF4CBFBE2C996C0600151637 /* PadelClubApp.xcdatamodeld in Sources */, FF4CBFBE2C996C0600151637 /* PadelClubApp.xcdatamodeld in Sources */,
FF4CBFBF2C996C0600151637 /* SetInputView.swift in Sources */, FF4CBFBF2C996C0600151637 /* SetInputView.swift in Sources */,
C471D15A2D0C91FF0068091F /* BaseDrawLog.swift in Sources */,
FF4CBFC02C996C0600151637 /* ButtonValidateView.swift in Sources */, FF4CBFC02C996C0600151637 /* ButtonValidateView.swift in Sources */,
FF4CBFC12C996C0600151637 /* ClubRowView.swift in Sources */, FF4CBFC12C996C0600151637 /* ClubRowView.swift in Sources */,
FF4CBFC22C996C0600151637 /* ClubDetailView.swift in Sources */, FF4CBFC22C996C0600151637 /* ClubDetailView.swift in Sources */,
@ -2893,7 +3010,9 @@
FF4CBFD02C996C0600151637 /* PlayerPayView.swift in Sources */, FF4CBFD02C996C0600151637 /* PlayerPayView.swift in Sources */,
FF4CBFD12C996C0600151637 /* PlanningByCourtView.swift in Sources */, FF4CBFD12C996C0600151637 /* PlanningByCourtView.swift in Sources */,
FF4CBFD22C996C0600151637 /* FileImportManager.swift in Sources */, FF4CBFD22C996C0600151637 /* FileImportManager.swift in Sources */,
FFA252B12CDD2C080074E63F /* OngoingContainerView.swift in Sources */,
FF4CBFD32C996C0600151637 /* TournamentButtonView.swift in Sources */, FF4CBFD32C996C0600151637 /* TournamentButtonView.swift in Sources */,
FF6761592CC7803600CC9BF2 /* DrawLogsView.swift in Sources */,
FF4CBFD42C996C0600151637 /* FederalPlayer.swift in Sources */, FF4CBFD42C996C0600151637 /* FederalPlayer.swift in Sources */,
FF4CBFD52C996C0600151637 /* NavigationViewModel.swift in Sources */, FF4CBFD52C996C0600151637 /* NavigationViewModel.swift in Sources */,
FF4CBFD62C996C0600151637 /* FixedWidthInteger+Extensions.swift in Sources */, FF4CBFD62C996C0600151637 /* FixedWidthInteger+Extensions.swift in Sources */,
@ -2902,6 +3021,7 @@
FF4CBFD92C996C0600151637 /* ClubSearchView.swift in Sources */, FF4CBFD92C996C0600151637 /* ClubSearchView.swift in Sources */,
FF4CBFDA2C996C0600151637 /* PlayerPopoverView.swift in Sources */, FF4CBFDA2C996C0600151637 /* PlayerPopoverView.swift in Sources */,
FF4CBFDB2C996C0600151637 /* InscriptionManagerView.swift in Sources */, FF4CBFDB2C996C0600151637 /* InscriptionManagerView.swift in Sources */,
FF77CE522CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */,
FF4CBFDC2C996C0600151637 /* ActivityView.swift in Sources */, FF4CBFDC2C996C0600151637 /* ActivityView.swift in Sources */,
FF4CBFDD2C996C0600151637 /* MySortDescriptor.swift in Sources */, FF4CBFDD2C996C0600151637 /* MySortDescriptor.swift in Sources */,
FF4CBFDE2C996C0600151637 /* CalendarView.swift in Sources */, FF4CBFDE2C996C0600151637 /* CalendarView.swift in Sources */,
@ -2909,8 +3029,8 @@
FF4CBFE02C996C0600151637 /* TournamentFieldsManagerView.swift in Sources */, FF4CBFE02C996C0600151637 /* TournamentFieldsManagerView.swift in Sources */,
FF4CBFE12C996C0600151637 /* PrintSettingsView.swift in Sources */, FF4CBFE12C996C0600151637 /* PrintSettingsView.swift in Sources */,
FF4CBFE22C996C0600151637 /* TournamentMatchFormatsSettingsView.swift in Sources */, FF4CBFE22C996C0600151637 /* TournamentMatchFormatsSettingsView.swift in Sources */,
FF4CBFE32C996C0600151637 /* DateUpdateManagerView.swift in Sources */, FF4CBFE32C996C0600151637 /* DatePickingView.swift in Sources */,
FF4CBFE42C996C0600151637 /* MatchTypeSmallSelectionView.swift in Sources */, FF4CBFE42C996C0600151637 /* MatchFormatRowView.swift in Sources */,
FF4CBFE52C996C0600151637 /* MonthData.swift in Sources */, FF4CBFE52C996C0600151637 /* MonthData.swift in Sources */,
FF4CBFE62C996C0600151637 /* MenuWarningView.swift in Sources */, FF4CBFE62C996C0600151637 /* MenuWarningView.swift in Sources */,
FF4CBFE72C996C0600151637 /* TournamentBuildView.swift in Sources */, FF4CBFE72C996C0600151637 /* TournamentBuildView.swift in Sources */,
@ -2918,6 +3038,7 @@
FF4CBFE92C996C0600151637 /* CloudConvert.swift in Sources */, FF4CBFE92C996C0600151637 /* CloudConvert.swift in Sources */,
FF4CBFEA2C996C0600151637 /* EventTournamentsView.swift in Sources */, FF4CBFEA2C996C0600151637 /* EventTournamentsView.swift in Sources */,
FF4CBFEB2C996C0600151637 /* DisplayContext.swift in Sources */, FF4CBFEB2C996C0600151637 /* DisplayContext.swift in Sources */,
FF17CA4B2CB915A1003C7323 /* MultiCourtPickerView.swift in Sources */,
FF4CBFEC2C996C0600151637 /* TournamentCallView.swift in Sources */, FF4CBFEC2C996C0600151637 /* TournamentCallView.swift in Sources */,
FF4CBFED2C996C0600151637 /* LoserRoundsView.swift in Sources */, FF4CBFED2C996C0600151637 /* LoserRoundsView.swift in Sources */,
FF4CBFEE2C996C0600151637 /* GroupStagesView.swift in Sources */, FF4CBFEE2C996C0600151637 /* GroupStagesView.swift in Sources */,
@ -2925,6 +3046,7 @@
FF4CBFF02C996C0600151637 /* URLs.swift in Sources */, FF4CBFF02C996C0600151637 /* URLs.swift in Sources */,
FF4CBFF12C996C0600151637 /* MatchDescriptor.swift in Sources */, FF4CBFF12C996C0600151637 /* MatchDescriptor.swift in Sources */,
FF4CBFF22C996C0600151637 /* TournamentFormatSelectionView.swift in Sources */, FF4CBFF22C996C0600151637 /* TournamentFormatSelectionView.swift in Sources */,
FF17CA592CC02FEB003C7323 /* CoachListView.swift in Sources */,
FF4CBFF32C996C0600151637 /* MatchListView.swift in Sources */, FF4CBFF32C996C0600151637 /* MatchListView.swift in Sources */,
FF4CBFF42C996C0600151637 /* PadelClubApp.swift in Sources */, FF4CBFF42C996C0600151637 /* PadelClubApp.swift in Sources */,
FF4CBFF52C996C0600151637 /* TournamentSettingsView.swift in Sources */, FF4CBFF52C996C0600151637 /* TournamentSettingsView.swift in Sources */,
@ -2939,12 +3061,16 @@
FF4CBFFC2C996C0600151637 /* UmpireView.swift in Sources */, FF4CBFFC2C996C0600151637 /* UmpireView.swift in Sources */,
FF4CBFFD2C996C0600151637 /* CustomUser.swift in Sources */, FF4CBFFD2C996C0600151637 /* CustomUser.swift in Sources */,
FF4CBFFE2C996C0600151637 /* MatchSummaryView.swift in Sources */, FF4CBFFE2C996C0600151637 /* MatchSummaryView.swift in Sources */,
FFA252B52CDD2C6C0074E63F /* OngoingDestination.swift in Sources */,
FF4CBFFF2C996C0600151637 /* TournamentDurationManagerView.swift in Sources */, FF4CBFFF2C996C0600151637 /* TournamentDurationManagerView.swift in Sources */,
FF4CC0002C996C0600151637 /* MockData.swift in Sources */, FF4CC0002C996C0600151637 /* MockData.swift in Sources */,
FF4CC0012C996C0600151637 /* TeamDetailView.swift in Sources */, FF4CC0012C996C0600151637 /* TeamDetailView.swift in Sources */,
FFA252AA2CDB70520074E63F /* PlayerStatisticView.swift in Sources */,
FF4CC0022C996C0600151637 /* GroupStagesSettingsView.swift in Sources */, FF4CC0022C996C0600151637 /* GroupStagesSettingsView.swift in Sources */,
FF4CC0032C996C0600151637 /* TournamentFilterView.swift in Sources */, FF4CC0032C996C0600151637 /* TournamentFilterView.swift in Sources */,
FF67615C2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */,
FF4CC0042C996C0600151637 /* HtmlGenerator.swift in Sources */, FF4CC0042C996C0600151637 /* HtmlGenerator.swift in Sources */,
FF17CA542CBE4788003C7323 /* BracketCallingView.swift in Sources */,
FF4CC0052C996C0600151637 /* PadelRule.swift in Sources */, FF4CC0052C996C0600151637 /* PadelRule.swift in Sources */,
FF4CC0062C996C0600151637 /* TeamRegistration.swift in Sources */, FF4CC0062C996C0600151637 /* TeamRegistration.swift in Sources */,
FF4CC0072C996C0600151637 /* Date+Extensions.swift in Sources */, FF4CC0072C996C0600151637 /* Date+Extensions.swift in Sources */,
@ -2969,8 +3095,9 @@
FF4CC0192C996C0600151637 /* EventSettingsView.swift in Sources */, FF4CC0192C996C0600151637 /* EventSettingsView.swift in Sources */,
FF4CC01A2C996C0600151637 /* InscriptionInfoView.swift in Sources */, FF4CC01A2C996C0600151637 /* InscriptionInfoView.swift in Sources */,
FF4CC01B2C996C0600151637 /* SelectablePlayerListView.swift in Sources */, FF4CC01B2C996C0600151637 /* SelectablePlayerListView.swift in Sources */,
FF4CC01C2C996C0600151637 /* MatchFormatPickerView.swift in Sources */, FF4CC01C2C996C0600151637 /* MatchFormatSelectionView.swift in Sources */,
FF4CC01D2C996C0600151637 /* TournamentRankView.swift in Sources */, FF4CC01D2C996C0600151637 /* TournamentRankView.swift in Sources */,
FF77CE5C2CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift in Sources */,
FF4CC01E2C996C0600151637 /* NumberFormatter+Extensions.swift in Sources */, FF4CC01E2C996C0600151637 /* NumberFormatter+Extensions.swift in Sources */,
FF4CC01F2C996C0600151637 /* SetLabelView.swift in Sources */, FF4CC01F2C996C0600151637 /* SetLabelView.swift in Sources */,
FF4CC0202C996C0600151637 /* DebugSettingsView.swift in Sources */, FF4CC0202C996C0600151637 /* DebugSettingsView.swift in Sources */,
@ -3004,6 +3131,7 @@
FF70FAC42C90584900129CC2 /* TabDestination.swift in Sources */, FF70FAC42C90584900129CC2 /* TabDestination.swift in Sources */,
FF70FAC52C90584900129CC2 /* CashierView.swift in Sources */, FF70FAC52C90584900129CC2 /* CashierView.swift in Sources */,
FF70FAC62C90584900129CC2 /* Event.swift in Sources */, FF70FAC62C90584900129CC2 /* Event.swift in Sources */,
FF77CE572CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift in Sources */,
FF70FAC72C90584900129CC2 /* PlayerHolder.swift in Sources */, FF70FAC72C90584900129CC2 /* PlayerHolder.swift in Sources */,
FF70FAC82C90584900129CC2 /* LoserRoundStepScheduleEditorView.swift in Sources */, FF70FAC82C90584900129CC2 /* LoserRoundStepScheduleEditorView.swift in Sources */,
FF70FAC92C90584900129CC2 /* ClubCourtSetupView.swift in Sources */, FF70FAC92C90584900129CC2 /* ClubCourtSetupView.swift in Sources */,
@ -3025,6 +3153,7 @@
FF70FAD92C90584900129CC2 /* TournamentClubSettingsView.swift in Sources */, FF70FAD92C90584900129CC2 /* TournamentClubSettingsView.swift in Sources */,
FF70FADA2C90584900129CC2 /* GroupStageTeamView.swift in Sources */, FF70FADA2C90584900129CC2 /* GroupStageTeamView.swift in Sources */,
FF70FADB2C90584900129CC2 /* RoundSettingsView.swift in Sources */, FF70FADB2C90584900129CC2 /* RoundSettingsView.swift in Sources */,
FFBE62062CE9DA0900815D33 /* MatchViewStyle.swift in Sources */,
FF70FADC2C90584900129CC2 /* SupportButtonView.swift in Sources */, FF70FADC2C90584900129CC2 /* SupportButtonView.swift in Sources */,
FF70FADD2C90584900129CC2 /* TournamentBroadcastRowView.swift in Sources */, FF70FADD2C90584900129CC2 /* TournamentBroadcastRowView.swift in Sources */,
FF70FADE2C90584900129CC2 /* TeamWeightView.swift in Sources */, FF70FADE2C90584900129CC2 /* TeamWeightView.swift in Sources */,
@ -3038,12 +3167,14 @@
FF70FAE62C90584900129CC2 /* EventClubSettingsView.swift in Sources */, FF70FAE62C90584900129CC2 /* EventClubSettingsView.swift in Sources */,
FF70FAE72C90584900129CC2 /* AccountView.swift in Sources */, FF70FAE72C90584900129CC2 /* AccountView.swift in Sources */,
FF70FAE82C90584900129CC2 /* PlayersWithoutContactView.swift in Sources */, FF70FAE82C90584900129CC2 /* PlayersWithoutContactView.swift in Sources */,
FF17CA4E2CB9243E003C7323 /* FollowUpMatchView.swift in Sources */,
FF70FAE92C90584900129CC2 /* TeamsCallingView.swift in Sources */, FF70FAE92C90584900129CC2 /* TeamsCallingView.swift in Sources */,
FF70FAEA2C90584900129CC2 /* Calendar+Extensions.swift in Sources */, FF70FAEA2C90584900129CC2 /* Calendar+Extensions.swift in Sources */,
FF70FAEB2C90584900129CC2 /* TeamScore.swift in Sources */, FF70FAEB2C90584900129CC2 /* TeamScore.swift in Sources */,
FF70FAEC2C90584900129CC2 /* EditablePlayerView.swift in Sources */, FF70FAEC2C90584900129CC2 /* EditablePlayerView.swift in Sources */,
C488C7F22CC7D22D0082001F /* Club.json in Sources */, C488C7F22CC7D22D0082001F /* Club.json in Sources */,
FF70FAED2C90584900129CC2 /* PlayerDetailView.swift in Sources */, FF70FAED2C90584900129CC2 /* PlayerDetailView.swift in Sources */,
FFA252AD2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */,
FF70FAEE2C90584900129CC2 /* ListRowViewModifier.swift in Sources */, FF70FAEE2C90584900129CC2 /* ListRowViewModifier.swift in Sources */,
FF70FAEF2C90584900129CC2 /* PresentationContext.swift in Sources */, FF70FAEF2C90584900129CC2 /* PresentationContext.swift in Sources */,
FF70FAF02C90584900129CC2 /* AppSettings.swift in Sources */, FF70FAF02C90584900129CC2 /* AppSettings.swift in Sources */,
@ -3077,6 +3208,7 @@
FF70FAFF2C90584900129CC2 /* OngoingView.swift in Sources */, FF70FAFF2C90584900129CC2 /* OngoingView.swift in Sources */,
FF70FB002C90584900129CC2 /* CreateClubView.swift in Sources */, FF70FB002C90584900129CC2 /* CreateClubView.swift in Sources */,
FF70FB012C90584900129CC2 /* APICallsListView.swift in Sources */, FF70FB012C90584900129CC2 /* APICallsListView.swift in Sources */,
FF7DCD3A2CC330270041110C /* TeamRestingView.swift in Sources */,
FF70FB022C90584900129CC2 /* NetworkFederalService.swift in Sources */, FF70FB022C90584900129CC2 /* NetworkFederalService.swift in Sources */,
FF70FB032C90584900129CC2 /* DurationSettingsView.swift in Sources */, FF70FB032C90584900129CC2 /* DurationSettingsView.swift in Sources */,
FF70FB042C90584900129CC2 /* AppScreen.swift in Sources */, FF70FB042C90584900129CC2 /* AppScreen.swift in Sources */,
@ -3099,6 +3231,7 @@
FF70FB142C90584900129CC2 /* Locale+Extensions.swift in Sources */, FF70FB142C90584900129CC2 /* Locale+Extensions.swift in Sources */,
FF70FB152C90584900129CC2 /* HtmlService.swift in Sources */, FF70FB152C90584900129CC2 /* HtmlService.swift in Sources */,
FF70FB162C90584900129CC2 /* GroupStage.swift in Sources */, FF70FB162C90584900129CC2 /* GroupStage.swift in Sources */,
FF6761552CC77D2100CC9BF2 /* DrawLog.swift in Sources */,
FF70FB172C90584900129CC2 /* TournamentCashierView.swift in Sources */, FF70FB172C90584900129CC2 /* TournamentCashierView.swift in Sources */,
FF70FB182C90584900129CC2 /* StoreManager.swift in Sources */, FF70FB182C90584900129CC2 /* StoreManager.swift in Sources */,
FF70FB192C90584900129CC2 /* SearchViewModel.swift in Sources */, FF70FB192C90584900129CC2 /* SearchViewModel.swift in Sources */,
@ -3141,6 +3274,7 @@
FF70FB3C2C90584900129CC2 /* AgendaDestination.swift in Sources */, FF70FB3C2C90584900129CC2 /* AgendaDestination.swift in Sources */,
FF70FB3D2C90584900129CC2 /* PadelClubApp.xcdatamodeld in Sources */, FF70FB3D2C90584900129CC2 /* PadelClubApp.xcdatamodeld in Sources */,
FF70FB3E2C90584900129CC2 /* SetInputView.swift in Sources */, FF70FB3E2C90584900129CC2 /* SetInputView.swift in Sources */,
C471D1582D0C91FE0068091F /* BaseDrawLog.swift in Sources */,
FF70FB3F2C90584900129CC2 /* ButtonValidateView.swift in Sources */, FF70FB3F2C90584900129CC2 /* ButtonValidateView.swift in Sources */,
FF70FB402C90584900129CC2 /* ClubRowView.swift in Sources */, FF70FB402C90584900129CC2 /* ClubRowView.swift in Sources */,
FF70FB412C90584900129CC2 /* ClubDetailView.swift in Sources */, FF70FB412C90584900129CC2 /* ClubDetailView.swift in Sources */,
@ -3160,7 +3294,9 @@
FF70FB4F2C90584900129CC2 /* PlayerPayView.swift in Sources */, FF70FB4F2C90584900129CC2 /* PlayerPayView.swift in Sources */,
FF70FB502C90584900129CC2 /* PlanningByCourtView.swift in Sources */, FF70FB502C90584900129CC2 /* PlanningByCourtView.swift in Sources */,
FF70FB512C90584900129CC2 /* FileImportManager.swift in Sources */, FF70FB512C90584900129CC2 /* FileImportManager.swift in Sources */,
FFA252B32CDD2C080074E63F /* OngoingContainerView.swift in Sources */,
FF70FB522C90584900129CC2 /* TournamentButtonView.swift in Sources */, FF70FB522C90584900129CC2 /* TournamentButtonView.swift in Sources */,
FF6761572CC7803600CC9BF2 /* DrawLogsView.swift in Sources */,
FF70FB532C90584900129CC2 /* FederalPlayer.swift in Sources */, FF70FB532C90584900129CC2 /* FederalPlayer.swift in Sources */,
FF70FB542C90584900129CC2 /* NavigationViewModel.swift in Sources */, FF70FB542C90584900129CC2 /* NavigationViewModel.swift in Sources */,
FF70FB552C90584900129CC2 /* FixedWidthInteger+Extensions.swift in Sources */, FF70FB552C90584900129CC2 /* FixedWidthInteger+Extensions.swift in Sources */,
@ -3169,6 +3305,7 @@
FF70FB582C90584900129CC2 /* ClubSearchView.swift in Sources */, FF70FB582C90584900129CC2 /* ClubSearchView.swift in Sources */,
FF70FB592C90584900129CC2 /* PlayerPopoverView.swift in Sources */, FF70FB592C90584900129CC2 /* PlayerPopoverView.swift in Sources */,
FF70FB5A2C90584900129CC2 /* InscriptionManagerView.swift in Sources */, FF70FB5A2C90584900129CC2 /* InscriptionManagerView.swift in Sources */,
FF77CE532CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */,
FF70FB5B2C90584900129CC2 /* ActivityView.swift in Sources */, FF70FB5B2C90584900129CC2 /* ActivityView.swift in Sources */,
FF70FB5C2C90584900129CC2 /* MySortDescriptor.swift in Sources */, FF70FB5C2C90584900129CC2 /* MySortDescriptor.swift in Sources */,
FF70FB5D2C90584900129CC2 /* CalendarView.swift in Sources */, FF70FB5D2C90584900129CC2 /* CalendarView.swift in Sources */,
@ -3176,8 +3313,8 @@
FF70FB5F2C90584900129CC2 /* TournamentFieldsManagerView.swift in Sources */, FF70FB5F2C90584900129CC2 /* TournamentFieldsManagerView.swift in Sources */,
FF70FB602C90584900129CC2 /* PrintSettingsView.swift in Sources */, FF70FB602C90584900129CC2 /* PrintSettingsView.swift in Sources */,
FF70FB612C90584900129CC2 /* TournamentMatchFormatsSettingsView.swift in Sources */, FF70FB612C90584900129CC2 /* TournamentMatchFormatsSettingsView.swift in Sources */,
FF70FB622C90584900129CC2 /* DateUpdateManagerView.swift in Sources */, FF70FB622C90584900129CC2 /* DatePickingView.swift in Sources */,
FF70FB632C90584900129CC2 /* MatchTypeSmallSelectionView.swift in Sources */, FF70FB632C90584900129CC2 /* MatchFormatRowView.swift in Sources */,
FF70FB642C90584900129CC2 /* MonthData.swift in Sources */, FF70FB642C90584900129CC2 /* MonthData.swift in Sources */,
FF70FB652C90584900129CC2 /* MenuWarningView.swift in Sources */, FF70FB652C90584900129CC2 /* MenuWarningView.swift in Sources */,
FF70FB662C90584900129CC2 /* TournamentBuildView.swift in Sources */, FF70FB662C90584900129CC2 /* TournamentBuildView.swift in Sources */,
@ -3185,6 +3322,7 @@
FF70FB682C90584900129CC2 /* CloudConvert.swift in Sources */, FF70FB682C90584900129CC2 /* CloudConvert.swift in Sources */,
FF70FB692C90584900129CC2 /* EventTournamentsView.swift in Sources */, FF70FB692C90584900129CC2 /* EventTournamentsView.swift in Sources */,
FF70FB6A2C90584900129CC2 /* DisplayContext.swift in Sources */, FF70FB6A2C90584900129CC2 /* DisplayContext.swift in Sources */,
FF17CA492CB915A1003C7323 /* MultiCourtPickerView.swift in Sources */,
FF70FB6B2C90584900129CC2 /* TournamentCallView.swift in Sources */, FF70FB6B2C90584900129CC2 /* TournamentCallView.swift in Sources */,
FF70FB6C2C90584900129CC2 /* LoserRoundsView.swift in Sources */, FF70FB6C2C90584900129CC2 /* LoserRoundsView.swift in Sources */,
FF70FB6D2C90584900129CC2 /* GroupStagesView.swift in Sources */, FF70FB6D2C90584900129CC2 /* GroupStagesView.swift in Sources */,
@ -3192,6 +3330,7 @@
FF70FB6F2C90584900129CC2 /* URLs.swift in Sources */, FF70FB6F2C90584900129CC2 /* URLs.swift in Sources */,
FF70FB702C90584900129CC2 /* MatchDescriptor.swift in Sources */, FF70FB702C90584900129CC2 /* MatchDescriptor.swift in Sources */,
FF70FB712C90584900129CC2 /* TournamentFormatSelectionView.swift in Sources */, FF70FB712C90584900129CC2 /* TournamentFormatSelectionView.swift in Sources */,
FF17CA582CC02FEB003C7323 /* CoachListView.swift in Sources */,
FF70FB722C90584900129CC2 /* MatchListView.swift in Sources */, FF70FB722C90584900129CC2 /* MatchListView.swift in Sources */,
FF70FB732C90584900129CC2 /* PadelClubApp.swift in Sources */, FF70FB732C90584900129CC2 /* PadelClubApp.swift in Sources */,
FF70FB742C90584900129CC2 /* TournamentSettingsView.swift in Sources */, FF70FB742C90584900129CC2 /* TournamentSettingsView.swift in Sources */,
@ -3206,12 +3345,16 @@
FF70FB7C2C90584900129CC2 /* CustomUser.swift in Sources */, FF70FB7C2C90584900129CC2 /* CustomUser.swift in Sources */,
C4C33F772C9B1ED4006316DE /* CodingContainer+Extensions.swift in Sources */, C4C33F772C9B1ED4006316DE /* CodingContainer+Extensions.swift in Sources */,
FF70FB7D2C90584900129CC2 /* MatchSummaryView.swift in Sources */, FF70FB7D2C90584900129CC2 /* MatchSummaryView.swift in Sources */,
FFA252B72CDD2C6C0074E63F /* OngoingDestination.swift in Sources */,
FF70FB7E2C90584900129CC2 /* TournamentDurationManagerView.swift in Sources */, FF70FB7E2C90584900129CC2 /* TournamentDurationManagerView.swift in Sources */,
FF70FB7F2C90584900129CC2 /* MockData.swift in Sources */, FF70FB7F2C90584900129CC2 /* MockData.swift in Sources */,
FF70FB802C90584900129CC2 /* TeamDetailView.swift in Sources */, FF70FB802C90584900129CC2 /* TeamDetailView.swift in Sources */,
FFA252AB2CDB70520074E63F /* PlayerStatisticView.swift in Sources */,
FF70FB812C90584900129CC2 /* GroupStagesSettingsView.swift in Sources */, FF70FB812C90584900129CC2 /* GroupStagesSettingsView.swift in Sources */,
FF70FB822C90584900129CC2 /* TournamentFilterView.swift in Sources */, FF70FB822C90584900129CC2 /* TournamentFilterView.swift in Sources */,
FF67615B2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */,
FF70FB832C90584900129CC2 /* HtmlGenerator.swift in Sources */, FF70FB832C90584900129CC2 /* HtmlGenerator.swift in Sources */,
FF17CA552CBE4788003C7323 /* BracketCallingView.swift in Sources */,
FF70FB842C90584900129CC2 /* PadelRule.swift in Sources */, FF70FB842C90584900129CC2 /* PadelRule.swift in Sources */,
FF70FB852C90584900129CC2 /* TeamRegistration.swift in Sources */, FF70FB852C90584900129CC2 /* TeamRegistration.swift in Sources */,
FF70FB862C90584900129CC2 /* Date+Extensions.swift in Sources */, FF70FB862C90584900129CC2 /* Date+Extensions.swift in Sources */,
@ -3236,8 +3379,9 @@
FF70FB982C90584900129CC2 /* EventSettingsView.swift in Sources */, FF70FB982C90584900129CC2 /* EventSettingsView.swift in Sources */,
FF70FB992C90584900129CC2 /* InscriptionInfoView.swift in Sources */, FF70FB992C90584900129CC2 /* InscriptionInfoView.swift in Sources */,
FF70FB9A2C90584900129CC2 /* SelectablePlayerListView.swift in Sources */, FF70FB9A2C90584900129CC2 /* SelectablePlayerListView.swift in Sources */,
FF70FB9B2C90584900129CC2 /* MatchFormatPickerView.swift in Sources */, FF70FB9B2C90584900129CC2 /* MatchFormatSelectionView.swift in Sources */,
FF70FB9C2C90584900129CC2 /* TournamentRankView.swift in Sources */, FF70FB9C2C90584900129CC2 /* TournamentRankView.swift in Sources */,
FF77CE5A2CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift in Sources */,
FF70FB9D2C90584900129CC2 /* NumberFormatter+Extensions.swift in Sources */, FF70FB9D2C90584900129CC2 /* NumberFormatter+Extensions.swift in Sources */,
FF70FB9E2C90584900129CC2 /* SetLabelView.swift in Sources */, FF70FB9E2C90584900129CC2 /* SetLabelView.swift in Sources */,
FF70FB9F2C90584900129CC2 /* DebugSettingsView.swift in Sources */, FF70FB9F2C90584900129CC2 /* DebugSettingsView.swift in Sources */,
@ -3416,7 +3560,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2; CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
@ -3440,7 +3584,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.19; MARKETING_VERSION = 1.0.39;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3461,7 +3605,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2; CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -3484,7 +3628,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.19; MARKETING_VERSION = 1.0.39;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3577,7 +3721,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 12; CURRENT_PROJECT_VERSION = 2;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
@ -3588,6 +3732,7 @@
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)"; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi";
INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
@ -3600,7 +3745,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.15; MARKETING_VERSION = 1.0.30;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3621,7 +3766,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 12; CURRENT_PROJECT_VERSION = 2;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -3631,6 +3776,7 @@
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)"; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi";
INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
@ -3643,7 +3789,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.15; MARKETING_VERSION = 1.0.30;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3665,7 +3811,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 2;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
@ -3675,6 +3821,7 @@
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (Beta)"; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (Beta)";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi";
INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
@ -3687,7 +3834,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.14; MARKETING_VERSION = 1.0.24;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3707,7 +3854,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 2;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -3716,6 +3863,7 @@
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (Beta)"; INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (Beta)";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi";
INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
@ -3728,7 +3876,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.14; MARKETING_VERSION = 1.0.24;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";

@ -30,6 +30,18 @@ final class AppSettings: MicroStorable {
var dayDuration: Int? var dayDuration: Int?
var dayPeriod: DayPeriod var dayPeriod: DayPeriod
func lastDataSourceDate() -> Date? {
guard let lastDataSource else { return nil }
return URL.importDateFormatter.date(from: lastDataSource)
}
func localizedLastDataSource() -> String? {
guard let lastDataSource else { return nil }
guard let date = URL.importDateFormatter.date(from: lastDataSource) else { return nil }
return date.monthYearFormatted
}
func resetSearch() { func resetSearch() {
tournamentAges = Set() tournamentAges = Set()
tournamentTypes = Set() tournamentTypes = Set()

@ -12,50 +12,6 @@ import LeStorage
@Observable @Observable
final class Club: BaseClub { final class Club: BaseClub {
// static func resourceName() -> String { return "clubs" }
// static func tokenExemptedMethods() -> [HTTPMethod] { return [.get] }
// static func filterByStoreIdentifier() -> Bool { return false }
// static var relationshipNames: [String] = []
//
// var id: String = Store.randomId()
// var lastUpdate: Date
// var creator: String?
// var name: String
// var acronym: String
// var phone: String?
// var code: String?
// //var federalClubData: Data?
// var address: String?
// var city: String?
// var zipCode: String?
// var latitude: Double?
// var longitude: Double?
// var courtCount: Int = 2
// var broadcastCode: String?
//// var alphabeticalName: Bool = false
internal init(creator: String? = nil, name: String, acronym: String? = nil, phone: String? = nil, code: String? = nil, address: String? = nil, city: String? = nil, zipCode: String? = nil, latitude: Double? = nil, longitude: Double? = nil, courtCount: Int = 2, broadcastCode: String? = nil) {
super.init()
self.name = name
self.creator = creator
self.acronym = acronym ?? name.acronym()
self.phone = phone
self.code = code
self.address = address
self.city = city
self.zipCode = zipCode
self.latitude = latitude
self.longitude = longitude
self.courtCount = courtCount
self.broadcastCode = broadcastCode
}
required init(from decoder: any Decoder) throws {
try super.init(from: decoder)
}
override func copyFromServerInstance(_ instance: any Storable) -> Bool { override func copyFromServerInstance(_ instance: any Storable) -> Bool {
guard let copy = instance as? Club else { return false } guard let copy = instance as? Club else { return false }
self.broadcastCode = copy.broadcastCode self.broadcastCode = copy.broadcastCode
@ -88,44 +44,6 @@ final class Club: BaseClub {
DataStore.shared.courts.deleteDependencies(customizedCourts) DataStore.shared.courts.deleteDependencies(customizedCourts)
} }
// enum CodingKeys: String, CodingKey {
// case _id = "id"
// case _lastUpdate = "lastUpdate"
// case _creator = "creator"
// case _name = "name"
// case _acronym = "acronym"
// case _phone = "phone"
// case _code = "code"
// case _address = "address"
// case _city = "city"
// case _zipCode = "zipCode"
// case _latitude = "latitude"
// case _longitude = "longitude"
// case _courtCount = "courtCount"
// case _broadcastCode = "broadcastCode"
//// case _alphabeticalName = "alphabeticalName"
// }
//
// func encode(to encoder: Encoder) throws {
//
// var container = encoder.container(keyedBy: CodingKeys.self)
//
// try container.encode(id, forKey: ._id)
// try container.encode(lastUpdate, forKey: ._lastUpdate)
// try container.encode(creator, forKey: ._creator)
// try container.encode(name, forKey: ._name)
// try container.encode(acronym, forKey: ._acronym)
// try container.encode(phone, forKey: ._phone)
// try container.encode(code, forKey: ._code)
// try container.encode(address, forKey: ._address)
// try container.encode(city, forKey: ._city)
// try container.encode(zipCode, forKey: ._zipCode)
// try container.encode(latitude, forKey: ._latitude)
// try container.encode(longitude, forKey: ._longitude)
// try container.encode(courtCount, forKey: ._courtCount)
// try container.encode(broadcastCode, forKey: ._broadcastCode)
// }
} }
extension Club { extension Club {

@ -58,8 +58,8 @@ extension ImportedPlayer: PlayerHolder {
male male
} }
func pasteData() -> String { func pasteData(withRank: Bool = false) -> String {
return [firstName?.capitalized, lastName?.capitalized, license?.computedLicense].compactMap({ $0 }).joined(separator: " ") return [firstName?.capitalized, lastName?.capitalized, license?.computedLicense, withRank ? "(\(rank.ordinalFormatted(feminine: isMalePlayer() == false)))" : nil].compactMap({ $0 }).joined(separator: " ")
} }
func isNotFromCurrentDate() -> Bool { func isNotFromCurrentDate() -> Bool {

@ -105,6 +105,10 @@ class DataStore: ObservableObject {
} }
deinit {
NotificationCenter.default.removeObserver(self)
}
func saveUser() { func saveUser() {
if user.username.count > 0 { if user.username.count > 0 {
self.userStorage.update() self.userStorage.update()
@ -291,15 +295,43 @@ class DataStore: ObservableObject {
func runningMatches() -> [Match] { func runningMatches() -> [Match] {
let dateNow : Date = Date() let dateNow : Date = Date()
let lastTournaments = self.tournaments.filter { $0.isDeleted == false && $0.startDate <= dateNow }.sorted(by: \Tournament.startDate, order: .descending).prefix(10) let lastTournaments = self.tournaments.filter { $0.isDeleted == false && $0.startDate <= dateNow && $0.hasEnded() == false }.sorted(by: \Tournament.startDate, order: .descending).prefix(10)
var runningMatches: [Match] = [] var runningMatches: [Match] = []
for tournament in lastTournaments { for tournament in lastTournaments {
let matches = tournament.tournamentStore.matches.filter { match in let matches = tournament.tournamentStore.matches.filter { match in
match.confirmed && match.startDate != nil && match.endDate == nil } match.disabled == false && match.isRunning()
}
runningMatches.append(contentsOf: matches) runningMatches.append(contentsOf: matches)
} }
return runningMatches return runningMatches
} }
func runningAndNextMatches() -> [Match] {
let dateNow : Date = Date()
let lastTournaments = self.tournaments.filter { $0.isDeleted == false && $0.startDate <= dateNow && $0.hasEnded() == false }.sorted(by: \Tournament.startDate, order: .descending).prefix(10)
var runningMatches: [Match] = []
for tournament in lastTournaments {
let matches = tournament.tournamentStore.matches.filter { match in
match.disabled == false && match.startDate != nil && match.endDate == nil }
runningMatches.append(contentsOf: matches)
}
return runningMatches
}
func endMatches() -> [Match] {
let dateNow : Date = Date()
let lastTournaments = self.tournaments.filter { $0.isDeleted == false && $0.startDate <= dateNow && $0.hasEnded() == false }.sorted(by: \Tournament.startDate, order: .descending).prefix(10)
var runningMatches: [Match] = []
for tournament in lastTournaments {
let matches = tournament.tournamentStore.matches.filter { match in
match.disabled == false && match.hasEnded() }
runningMatches.append(contentsOf: matches)
}
return runningMatches.sorted(by: \.endDate!, order: .descending)
}
} }

@ -0,0 +1,97 @@
//
// DrawLog.swift
// PadelClub
//
// Created by razmig on 22/10/2024.
//
import Foundation
import SwiftUI
import LeStorage
@Observable
final class DrawLog: BaseDrawLog {
func tournamentObject() -> Tournament? {
Store.main.findById(self.tournament)
}
func computedBracketPosition() -> Int {
drawMatchIndex * 2 + drawTeamPosition.rawValue
}
func updateTeamBracketPosition(_ team: TeamRegistration) {
guard let match = drawMatch() else { return }
let seedPosition: Int = match.lockAndGetSeedPosition(atTeamPosition: drawTeamPosition)
team.bracketPosition = seedPosition
tournamentObject()?.updateTeamScores(in: seedPosition)
}
func exportedDrawLog() -> String {
[drawType.localizedDrawType(), drawDate.localizedDate(), localizedDrawLogLabel(), localizedDrawBranch()].filter({ $0.isEmpty == false }).joined(separator: " ")
}
func localizedDrawSeedLabel() -> String {
return "\(drawType.localizedDrawType()) #\(drawSeed + 1)"
}
func localizedDrawLogLabel() -> String {
return [localizedDrawSeedLabel(), positionLabel()].filter({ $0.isEmpty == false }).joined(separator: " -> ")
}
func localizedDrawBranch() -> String {
switch drawType {
case .seed:
return drawTeamPosition.localizedBranchLabel()
default:
return ""
}
}
func drawMatch() -> Match? {
switch drawType {
case .seed:
let roundIndex = RoundRule.roundIndex(fromMatchIndex: drawMatchIndex)
return tournamentStore.rounds.first(where: { $0.parent == nil && $0.index == roundIndex })?._matches().first(where: { $0.index == drawMatchIndex })
default:
return nil
}
}
func positionLabel() -> String {
return drawMatch()?.roundAndMatchTitle() ?? ""
}
func roundLabel() -> String {
return drawMatch()?.roundTitle() ?? ""
}
func matchLabel() -> String {
return drawMatch()?.matchTitle() ?? ""
}
var tournamentStore: TournamentStore {
return TournamentLibrary.shared.store(tournamentId: self.tournament)
}
override func deleteDependencies() {
}
}
enum DrawType: Int, Codable {
case seed
case groupStage
case court
func localizedDrawType() -> String {
switch self {
case .seed:
return "Tête de série"
case .groupStage:
return "Poule"
case .court:
return "Terrain"
}
}
}

@ -239,7 +239,7 @@ struct CategorieAge: Codable {
return FederalTournamentAge(rawValue: id) return FederalTournamentAge(rawValue: id)
} }
if let libelle { if let libelle {
return FederalTournamentAge.allCases.first(where: { $0.localizedLabel().localizedCaseInsensitiveContains(libelle) }) return FederalTournamentAge.allCases.first(where: { $0.localizedFederalAgeLabel().localizedCaseInsensitiveContains(libelle) })
} }
return nil return nil
} }

@ -24,6 +24,7 @@ class BaseClub: SyncedModelObject, SyncedStorable {
var longitude: Double? = nil var longitude: Double? = nil
var courtCount: Int = 2 var courtCount: Int = 2
var broadcastCode: String? = nil var broadcastCode: String? = nil
var timezone: String? = TimeZone.current.identifier
init( init(
id: String = Store.randomId(), id: String = Store.randomId(),
@ -38,7 +39,8 @@ class BaseClub: SyncedModelObject, SyncedStorable {
latitude: Double? = nil, latitude: Double? = nil,
longitude: Double? = nil, longitude: Double? = nil,
courtCount: Int = 2, courtCount: Int = 2,
broadcastCode: String? = nil broadcastCode: String? = nil,
timezone: String? = TimeZone.current.identifier
) { ) {
super.init() super.init()
self.id = id self.id = id
@ -54,6 +56,7 @@ class BaseClub: SyncedModelObject, SyncedStorable {
self.longitude = longitude self.longitude = longitude
self.courtCount = courtCount self.courtCount = courtCount
self.broadcastCode = broadcastCode self.broadcastCode = broadcastCode
self.timezone = timezone
} }
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
@ -70,6 +73,7 @@ class BaseClub: SyncedModelObject, SyncedStorable {
case _longitude = "longitude" case _longitude = "longitude"
case _courtCount = "courtCount" case _courtCount = "courtCount"
case _broadcastCode = "broadcastCode" case _broadcastCode = "broadcastCode"
case _timezone = "timezone"
} }
required init(from decoder: Decoder) throws { required init(from decoder: Decoder) throws {
@ -87,6 +91,7 @@ class BaseClub: SyncedModelObject, SyncedStorable {
self.longitude = try container.decodeIfPresent(Double.self, forKey: ._longitude) ?? nil self.longitude = try container.decodeIfPresent(Double.self, forKey: ._longitude) ?? nil
self.courtCount = try container.decodeIfPresent(Int.self, forKey: ._courtCount) ?? 2 self.courtCount = try container.decodeIfPresent(Int.self, forKey: ._courtCount) ?? 2
self.broadcastCode = try container.decodeIfPresent(String.self, forKey: ._broadcastCode) ?? nil self.broadcastCode = try container.decodeIfPresent(String.self, forKey: ._broadcastCode) ?? nil
self.timezone = try container.decodeIfPresent(String.self, forKey: ._timezone) ?? TimeZone.current.identifier
try super.init(from: decoder) try super.init(from: decoder)
} }
@ -105,6 +110,7 @@ class BaseClub: SyncedModelObject, SyncedStorable {
try container.encode(self.longitude, forKey: ._longitude) try container.encode(self.longitude, forKey: ._longitude)
try container.encode(self.courtCount, forKey: ._courtCount) try container.encode(self.courtCount, forKey: ._courtCount)
try container.encode(self.broadcastCode, forKey: ._broadcastCode) try container.encode(self.broadcastCode, forKey: ._broadcastCode)
try container.encode(self.timezone, forKey: ._timezone)
try super.encode(to: encoder) try super.encode(to: encoder)
} }
@ -128,6 +134,7 @@ class BaseClub: SyncedModelObject, SyncedStorable {
self.longitude = club.longitude self.longitude = club.longitude
self.courtCount = club.courtCount self.courtCount = club.courtCount
self.broadcastCode = club.broadcastCode self.broadcastCode = club.broadcastCode
self.timezone = club.timezone
} }
static func relationships() -> [Relationship] { static func relationships() -> [Relationship] {

@ -0,0 +1,96 @@
// Generated by SwiftModelGenerator
// Do not modify this file manually
import Foundation
import LeStorage
import SwiftUI
@Observable
class BaseDrawLog: SyncedModelObject, SyncedStorable {
static func resourceName() -> String { return "draw-logs" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
var id: String = Store.randomId()
var tournament: String = ""
var drawDate: Date = Date()
var drawSeed: Int = 0
var drawMatchIndex: Int = 0
var drawTeamPosition: TeamPosition = TeamPosition.one
var drawType: DrawType = DrawType.seed
init(
id: String = Store.randomId(),
tournament: String = "",
drawDate: Date = Date(),
drawSeed: Int = 0,
drawMatchIndex: Int = 0,
drawTeamPosition: TeamPosition = TeamPosition.one,
drawType: DrawType = DrawType.seed
) {
super.init()
self.id = id
self.tournament = tournament
self.drawDate = drawDate
self.drawSeed = drawSeed
self.drawMatchIndex = drawMatchIndex
self.drawTeamPosition = drawTeamPosition
self.drawType = drawType
}
enum CodingKeys: String, CodingKey {
case _id = "id"
case _tournament = "tournament"
case _drawDate = "drawDate"
case _drawSeed = "drawSeed"
case _drawMatchIndex = "drawMatchIndex"
case _drawTeamPosition = "drawTeamPosition"
case _drawType = "drawType"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decodeIfPresent(String.self, forKey: ._id) ?? Store.randomId()
self.tournament = try container.decodeIfPresent(String.self, forKey: ._tournament) ?? ""
self.drawDate = try container.decodeIfPresent(Date.self, forKey: ._drawDate) ?? Date()
self.drawSeed = try container.decodeIfPresent(Int.self, forKey: ._drawSeed) ?? 0
self.drawMatchIndex = try container.decodeIfPresent(Int.self, forKey: ._drawMatchIndex) ?? 0
self.drawTeamPosition = try container.decodeIfPresent(TeamPosition.self, forKey: ._drawTeamPosition) ?? TeamPosition.one
self.drawType = try container.decodeIfPresent(DrawType.self, forKey: ._drawType) ?? DrawType.seed
try super.init(from: decoder)
}
override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.id, forKey: ._id)
try container.encode(self.tournament, forKey: ._tournament)
try container.encode(self.drawDate, forKey: ._drawDate)
try container.encode(self.drawSeed, forKey: ._drawSeed)
try container.encode(self.drawMatchIndex, forKey: ._drawMatchIndex)
try container.encode(self.drawTeamPosition, forKey: ._drawTeamPosition)
try container.encode(self.drawType, forKey: ._drawType)
try super.encode(to: encoder)
}
func tournamentValue() -> Tournament? {
return Store.main.findById(tournament)
}
func copy(from other: any Storable) {
guard let drawlog = other as? BaseDrawLog else { return }
self.id = drawlog.id
self.tournament = drawlog.tournament
self.drawDate = drawlog.drawDate
self.drawSeed = drawlog.drawSeed
self.drawMatchIndex = drawlog.drawMatchIndex
self.drawTeamPosition = drawlog.drawTeamPosition
self.drawType = drawlog.drawType
}
static func relationships() -> [Relationship] {
return [
Relationship(type: Tournament.self, keyPath: \BaseDrawLog.tournament),
]
}
}

@ -25,6 +25,8 @@ class BaseMatchScheduler: BaseModelObject, Storable {
var groupStageChunkCount: Int? = nil var groupStageChunkCount: Int? = nil
var overrideCourtsUnavailability: Bool = false var overrideCourtsUnavailability: Bool = false
var shouldTryToFillUpCourtsAvailable: Bool = false var shouldTryToFillUpCourtsAvailable: Bool = false
var courtsAvailable: Set<Int> = Set<Int>()
var simultaneousStart: Bool = true
init( init(
id: String = Store.randomId(), id: String = Store.randomId(),
@ -40,7 +42,9 @@ class BaseMatchScheduler: BaseModelObject, Storable {
shouldEndRoundBeforeStartingNext: Bool = false, shouldEndRoundBeforeStartingNext: Bool = false,
groupStageChunkCount: Int? = nil, groupStageChunkCount: Int? = nil,
overrideCourtsUnavailability: Bool = false, overrideCourtsUnavailability: Bool = false,
shouldTryToFillUpCourtsAvailable: Bool = false shouldTryToFillUpCourtsAvailable: Bool = false,
courtsAvailable: Set<Int> = Set<Int>(),
simultaneousStart: Bool = true
) { ) {
super.init() super.init()
self.id = id self.id = id
@ -57,6 +61,8 @@ class BaseMatchScheduler: BaseModelObject, Storable {
self.groupStageChunkCount = groupStageChunkCount self.groupStageChunkCount = groupStageChunkCount
self.overrideCourtsUnavailability = overrideCourtsUnavailability self.overrideCourtsUnavailability = overrideCourtsUnavailability
self.shouldTryToFillUpCourtsAvailable = shouldTryToFillUpCourtsAvailable self.shouldTryToFillUpCourtsAvailable = shouldTryToFillUpCourtsAvailable
self.courtsAvailable = courtsAvailable
self.simultaneousStart = simultaneousStart
} }
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
@ -74,6 +80,8 @@ class BaseMatchScheduler: BaseModelObject, Storable {
case _groupStageChunkCount = "groupStageChunkCount" case _groupStageChunkCount = "groupStageChunkCount"
case _overrideCourtsUnavailability = "overrideCourtsUnavailability" case _overrideCourtsUnavailability = "overrideCourtsUnavailability"
case _shouldTryToFillUpCourtsAvailable = "shouldTryToFillUpCourtsAvailable" case _shouldTryToFillUpCourtsAvailable = "shouldTryToFillUpCourtsAvailable"
case _courtsAvailable = "courtsAvailable"
case _simultaneousStart = "simultaneousStart"
} }
required init(from decoder: Decoder) throws { required init(from decoder: Decoder) throws {
@ -92,6 +100,8 @@ class BaseMatchScheduler: BaseModelObject, Storable {
self.groupStageChunkCount = try container.decodeIfPresent(Int.self, forKey: ._groupStageChunkCount) ?? nil self.groupStageChunkCount = try container.decodeIfPresent(Int.self, forKey: ._groupStageChunkCount) ?? nil
self.overrideCourtsUnavailability = try container.decodeIfPresent(Bool.self, forKey: ._overrideCourtsUnavailability) ?? false self.overrideCourtsUnavailability = try container.decodeIfPresent(Bool.self, forKey: ._overrideCourtsUnavailability) ?? false
self.shouldTryToFillUpCourtsAvailable = try container.decodeIfPresent(Bool.self, forKey: ._shouldTryToFillUpCourtsAvailable) ?? false self.shouldTryToFillUpCourtsAvailable = try container.decodeIfPresent(Bool.self, forKey: ._shouldTryToFillUpCourtsAvailable) ?? false
self.courtsAvailable = try container.decodeIfPresent(Set<Int>.self, forKey: ._courtsAvailable) ?? Set<Int>()
self.simultaneousStart = try container.decodeIfPresent(Bool.self, forKey: ._simultaneousStart) ?? true
try super.init(from: decoder) try super.init(from: decoder)
} }
@ -111,6 +121,8 @@ class BaseMatchScheduler: BaseModelObject, Storable {
try container.encode(self.groupStageChunkCount, forKey: ._groupStageChunkCount) try container.encode(self.groupStageChunkCount, forKey: ._groupStageChunkCount)
try container.encode(self.overrideCourtsUnavailability, forKey: ._overrideCourtsUnavailability) try container.encode(self.overrideCourtsUnavailability, forKey: ._overrideCourtsUnavailability)
try container.encode(self.shouldTryToFillUpCourtsAvailable, forKey: ._shouldTryToFillUpCourtsAvailable) try container.encode(self.shouldTryToFillUpCourtsAvailable, forKey: ._shouldTryToFillUpCourtsAvailable)
try container.encode(self.courtsAvailable, forKey: ._courtsAvailable)
try container.encode(self.simultaneousStart, forKey: ._simultaneousStart)
try super.encode(to: encoder) try super.encode(to: encoder)
} }
@ -134,6 +146,8 @@ class BaseMatchScheduler: BaseModelObject, Storable {
self.groupStageChunkCount = matchscheduler.groupStageChunkCount self.groupStageChunkCount = matchscheduler.groupStageChunkCount
self.overrideCourtsUnavailability = matchscheduler.overrideCourtsUnavailability self.overrideCourtsUnavailability = matchscheduler.overrideCourtsUnavailability
self.shouldTryToFillUpCourtsAvailable = matchscheduler.shouldTryToFillUpCourtsAvailable self.shouldTryToFillUpCourtsAvailable = matchscheduler.shouldTryToFillUpCourtsAvailable
self.courtsAvailable = matchscheduler.courtsAvailable
self.simultaneousStart = matchscheduler.simultaneousStart
} }
static func relationships() -> [Relationship] { static func relationships() -> [Relationship] {

@ -52,6 +52,8 @@ class BaseTournament: SyncedModelObject, SyncedStorable {
var hidePointsEarned: Bool = false var hidePointsEarned: Bool = false
var publishRankings: Bool = false var publishRankings: Bool = false
var loserBracketMode: LoserBracketMode = .automatic var loserBracketMode: LoserBracketMode = .automatic
var initialSeedRound: Int = 0
var initialSeedCount: Int = 0
init( init(
id: String = Store.randomId(), id: String = Store.randomId(),
@ -94,7 +96,9 @@ class BaseTournament: SyncedModelObject, SyncedStorable {
publishTournament: Bool = false, publishTournament: Bool = false,
hidePointsEarned: Bool = false, hidePointsEarned: Bool = false,
publishRankings: Bool = false, publishRankings: Bool = false,
loserBracketMode: LoserBracketMode = .automatic loserBracketMode: LoserBracketMode = .automatic,
initialSeedRound: Int = 0,
initialSeedCount: Int = 0
) { ) {
super.init() super.init()
self.id = id self.id = id
@ -138,6 +142,8 @@ class BaseTournament: SyncedModelObject, SyncedStorable {
self.hidePointsEarned = hidePointsEarned self.hidePointsEarned = hidePointsEarned
self.publishRankings = publishRankings self.publishRankings = publishRankings
self.loserBracketMode = loserBracketMode self.loserBracketMode = loserBracketMode
self.initialSeedRound = initialSeedRound
self.initialSeedCount = initialSeedCount
} }
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
@ -182,6 +188,8 @@ class BaseTournament: SyncedModelObject, SyncedStorable {
case _hidePointsEarned = "hidePointsEarned" case _hidePointsEarned = "hidePointsEarned"
case _publishRankings = "publishRankings" case _publishRankings = "publishRankings"
case _loserBracketMode = "loserBracketMode" case _loserBracketMode = "loserBracketMode"
case _initialSeedRound = "initialSeedRound"
case _initialSeedCount = "initialSeedCount"
} }
private static func _decodePayment(container: KeyedDecodingContainer<CodingKeys>) throws -> TournamentPayment? { private static func _decodePayment(container: KeyedDecodingContainer<CodingKeys>) throws -> TournamentPayment? {
@ -288,6 +296,8 @@ class BaseTournament: SyncedModelObject, SyncedStorable {
self.hidePointsEarned = try container.decodeIfPresent(Bool.self, forKey: ._hidePointsEarned) ?? false self.hidePointsEarned = try container.decodeIfPresent(Bool.self, forKey: ._hidePointsEarned) ?? false
self.publishRankings = try container.decodeIfPresent(Bool.self, forKey: ._publishRankings) ?? false self.publishRankings = try container.decodeIfPresent(Bool.self, forKey: ._publishRankings) ?? false
self.loserBracketMode = try container.decodeIfPresent(LoserBracketMode.self, forKey: ._loserBracketMode) ?? .automatic self.loserBracketMode = try container.decodeIfPresent(LoserBracketMode.self, forKey: ._loserBracketMode) ?? .automatic
self.initialSeedRound = try container.decodeIfPresent(Int.self, forKey: ._initialSeedRound) ?? 0
self.initialSeedCount = try container.decodeIfPresent(Int.self, forKey: ._initialSeedCount) ?? 0
try super.init(from: decoder) try super.init(from: decoder)
} }
@ -334,6 +344,8 @@ class BaseTournament: SyncedModelObject, SyncedStorable {
try container.encode(self.hidePointsEarned, forKey: ._hidePointsEarned) try container.encode(self.hidePointsEarned, forKey: ._hidePointsEarned)
try container.encode(self.publishRankings, forKey: ._publishRankings) try container.encode(self.publishRankings, forKey: ._publishRankings)
try container.encode(self.loserBracketMode, forKey: ._loserBracketMode) try container.encode(self.loserBracketMode, forKey: ._loserBracketMode)
try container.encode(self.initialSeedRound, forKey: ._initialSeedRound)
try container.encode(self.initialSeedCount, forKey: ._initialSeedCount)
try super.encode(to: encoder) try super.encode(to: encoder)
} }
@ -385,6 +397,8 @@ class BaseTournament: SyncedModelObject, SyncedStorable {
self.hidePointsEarned = tournament.hidePointsEarned self.hidePointsEarned = tournament.hidePointsEarned
self.publishRankings = tournament.publishRankings self.publishRankings = tournament.publishRankings
self.loserBracketMode = tournament.loserBracketMode self.loserBracketMode = tournament.loserBracketMode
self.initialSeedRound = tournament.initialSeedRound
self.initialSeedCount = tournament.initialSeedCount
} }
static func relationships() -> [Relationship] { static func relationships() -> [Relationship] {

@ -79,6 +79,12 @@
"type": "String", "type": "String",
"optional": true, "optional": true,
"defaultValue": "nil" "defaultValue": "nil"
},
{
"name": "timezone",
"type": "String",
"optional": true,
"defaultValue": "TimeZone.current.identifier"
} }
] ]
} }

@ -0,0 +1,47 @@
{
"models": [
{
"name": "DrawLog",
"synchronizable": true,
"sideStorable": true,
"observable": true,
"relationshipNames": [],
"properties": [
{
"name": "id",
"type": "String",
"defaultValue": "Store.randomId()"
},
{
"name": "tournament",
"type": "String",
"foreignKey": "Tournament"
},
{
"name": "drawDate",
"type": "Date",
"defaultValue": "Date()"
},
{
"name": "drawSeed",
"type": "Int"
},
{
"name": "drawMatchIndex",
"type": "Int"
},
{
"name": "drawTeamPosition",
"type": "TeamPosition",
"defaultValue": "TeamPosition.one"
},
{
"name": "drawType",
"type": "DrawType",
"defaultValue": "DrawType.seed"
}
]
}
]
}

@ -67,6 +67,16 @@
"name": "shouldTryToFillUpCourtsAvailable", "name": "shouldTryToFillUpCourtsAvailable",
"type": "Bool", "type": "Bool",
"defaultValue": "false" "defaultValue": "false"
},
{
"name": "courtsAvailable",
"type": "Set<Int>",
"defaultValue": "Set<Int>()"
},
{
"name": "simultaneousStart",
"type": "Bool",
"defaultValue": "true"
} }
] ]
} }

@ -209,6 +209,16 @@
"name": "loserBracketMode", "name": "loserBracketMode",
"type": "LoserBracketMode", "type": "LoserBracketMode",
"defaultValue": ".automatic" "defaultValue": ".automatic"
},
{
"name": "initialSeedRound",
"type": "Int",
"defaultValue": "0"
},
{
"name": "initialSeedCount",
"type": "Int",
"defaultValue": "0"
} }
] ]
} }

@ -13,21 +13,6 @@ import SwiftUI
@Observable @Observable
final class GroupStage: BaseGroupStage, SideStorable { final class GroupStage: BaseGroupStage, SideStorable {
// static func resourceName() -> String { "group-stages" }
// static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
// static func filterByStoreIdentifier() -> Bool { return true }
// static var relationshipNames: [String] = []
//
// var id: String = Store.randomId()
// var lastUpdate: Date
// var tournament: String
// var index: Int
// var size: Int
// private(set) var format: MatchFormat?
// var startDate: Date?
// var name: String?
// var step: Int = 0
var matchFormat: MatchFormat { var matchFormat: MatchFormat {
get { get {
format ?? .defaultFormatForMatchType(.groupStage) format ?? .defaultFormatForMatchType(.groupStage)
@ -37,26 +22,6 @@ final class GroupStage: BaseGroupStage, SideStorable {
} }
} }
// var storeId: String? = nil
internal init(tournament: String, index: Int, size: Int, matchFormat: MatchFormat? = nil, startDate: Date? = nil, name: String? = nil, step: Int = 0) {
super.init(tournament: tournament, index: index, size: size, format: matchFormat, startDate: startDate, name: name, step: step)
// self.lastUpdate = Date()
// self.tournament = tournament
// self.index = index
// self.size = size
// self.format = matchFormat
// self.startDate = startDate
// self.name = name
// self.step = step
}
required init(from decoder: any Decoder) throws {
try super.init(from: decoder)
}
var tournamentStore: TournamentStore { var tournamentStore: TournamentStore {
return TournamentLibrary.shared.store(tournamentId: self.tournament) return TournamentLibrary.shared.store(tournamentId: self.tournament)
} }
@ -124,17 +89,39 @@ final class GroupStage: BaseGroupStage, SideStorable {
format: self.matchFormat, format: self.matchFormat,
name: self.localizedMatchUpLabel(for: index)) name: self.localizedMatchUpLabel(for: index))
match.store = self.store match.store = self.store
print("_createMatch(index)", index)
return match return match
} }
func buildMatches() { func removeReturnMatches(onlyLast: Bool = false) {
_removeMatches()
var matches = [Match]() var returnMatches = _matches().filter({ $0.index >= matchCount })
var teamScores = [TeamScore]() if onlyLast {
let matchPhaseCount = matchPhaseCount - 1
returnMatches = returnMatches.filter({ $0.index >= matchCount * matchPhaseCount })
}
do {
try self.tournamentStore.matches.delete(contentOfs: returnMatches)
} catch {
Logger.error(error)
}
}
var matchPhaseCount: Int {
let count = _matches().count
if matchCount > 0 {
return count / matchCount
} else {
return 0
}
}
func addReturnMatches() {
var teamScores = [TeamScore]()
var matches = [Match]()
let matchPhaseCount = matchPhaseCount
for i in 0..<_numberOfMatchesToBuild() { for i in 0..<_numberOfMatchesToBuild() {
let newMatch = self._createMatch(index: i) let newMatch = self._createMatch(index: i + matchCount * matchPhaseCount)
// let newMatch = Match(groupStage: self.id, index: i, matchFormat: self.matchFormat, name: localizedMatchUpLabel(for: i)) // let newMatch = Match(groupStage: self.id, index: i, matchFormat: self.matchFormat, name: localizedMatchUpLabel(for: i))
teamScores.append(contentsOf: newMatch.createTeamScores()) teamScores.append(contentsOf: newMatch.createTeamScores())
matches.append(newMatch) matches.append(newMatch)
@ -144,29 +131,71 @@ final class GroupStage: BaseGroupStage, SideStorable {
self.tournamentStore.teamScores.addOrUpdate(contentOfs: teamScores) self.tournamentStore.teamScores.addOrUpdate(contentOfs: teamScores)
} }
func buildMatches(keepExistingMatches: Bool = false) {
var teamScores = [TeamScore]()
var matches = [Match]()
clearScoreCache()
if keepExistingMatches == false {
_removeMatches()
for i in 0..<_numberOfMatchesToBuild() {
let newMatch = self._createMatch(index: i)
// let newMatch = Match(groupStage: self.id, index: i, matchFormat: self.matchFormat, name: localizedMatchUpLabel(for: i))
teamScores.append(contentsOf: newMatch.createTeamScores())
matches.append(newMatch)
}
} else {
for match in _matches() {
match.resetTeamScores(outsideOf: [])
teamScores.append(contentsOf: match.createTeamScores())
}
}
do {
try self.tournamentStore.matches.addOrUpdate(contentOfs: matches)
try self.tournamentStore.teamScores.addOrUpdate(contentOfs: teamScores)
} catch {
Logger.error(error)
}
}
func playedMatches() -> [Match] { func playedMatches() -> [Match] {
let ordered = _matches() let ordered = _matches()
if ordered.isEmpty == false && ordered.count == _matchOrder().count { let order = _matchOrder()
return _matchOrder().map { let matchCount = max(1, matchCount)
ordered[$0] let count = ordered.count / matchCount
} if ordered.isEmpty == false && ordered.count % order.count == 0 {
let repeatedArray = (0..<count).flatMap { i in
order.map { $0 + i * order.count }
}
let result = repeatedArray.map { ordered[$0] }
return result
} else { } else {
return ordered return ordered
} }
} }
func orderedIndexOfMatch(_ match: Match) -> Int {
_matchOrder()[safe: match.index] ?? match.index
}
func updateGroupStageState() { func updateGroupStageState() {
clearScoreCache() clearScoreCache()
if hasEnded(), let tournament = tournamentObject() { if hasEnded(), let tournament = tournamentObject() {
let teams = teams(true) let teams = teams(true)
for (index, team) in teams.enumerated() { for (index, team) in teams.enumerated() {
team.qualified = index < tournament.qualifiedPerGroupStage team.qualified = index < tournament.qualifiedPerGroupStage
if team.bracketPosition != nil && team.qualified == false { if team.bracketPosition != nil && team.qualified == false {
tournamentObject()?.resetTeamScores(in: team.bracketPosition) tournamentObject()?.shouldVerifyBracket = true
team.bracketPosition = nil
} }
} }
try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams)
if let tournamentObject = tournamentObject() {
try DataStore.shared.tournaments.addOrUpdate(instance: tournamentObject)
}
self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams) self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams)
let groupStagesAreOverAtFirstStep = tournament.groupStagesAreOver(atStep: 0) let groupStagesAreOverAtFirstStep = tournament.groupStagesAreOver(atStep: 0)
@ -183,8 +212,8 @@ final class GroupStage: BaseGroupStage, SideStorable {
func scoreLabel(forGroupStagePosition groupStagePosition: Int, score: TeamGroupStageScore? = nil) -> (wins: String, losses: String, setsDifference: String?, gamesDifference: String?)? { func scoreLabel(forGroupStagePosition groupStagePosition: Int, score: TeamGroupStageScore? = nil) -> (wins: String, losses: String, setsDifference: String?, gamesDifference: String?)? {
if let scoreData = (score ?? _score(forGroupStagePosition: groupStagePosition, nilIfEmpty: true)) { if let scoreData = (score ?? _score(forGroupStagePosition: groupStagePosition, nilIfEmpty: true)) {
let hideSetDifference = matchFormat.setsToWin == 1 let hideSetDifference = matchFormat.setsToWin == 1
let setDifference = scoreData.setDifference.formatted(.number.sign(strategy: .always(includingZero: false))) let setDifference = scoreData.setDifference.formatted(.number.sign(strategy: .always(includingZero: true))) + " set" + scoreData.setDifference.pluralSuffix
let gameDifference = scoreData.gameDifference.formatted(.number.sign(strategy: .always(includingZero: false))) let gameDifference = scoreData.gameDifference.formatted(.number.sign(strategy: .always(includingZero: true))) + " jeu" + scoreData.gameDifference.localizedPluralSuffix("x")
return (wins: scoreData.wins.formatted(), losses: scoreData.loses.formatted(), setsDifference: hideSetDifference ? nil : setDifference, gamesDifference: gameDifference) return (wins: scoreData.wins.formatted(), losses: scoreData.loses.formatted(), setsDifference: hideSetDifference ? nil : setDifference, gamesDifference: gameDifference)
// return "\(scoreData.wins)/\(scoreData.loses) " + differenceAsString // return "\(scoreData.wins)/\(scoreData.loses) " + differenceAsString
} else { } else {
@ -218,7 +247,7 @@ final class GroupStage: BaseGroupStage, SideStorable {
matchIndexes.append(index) matchIndexes.append(index)
} }
} }
return _matches().filter { matchIndexes.contains($0.index) } return _matches().filter { matchIndexes.contains($0.index%matchCount) }
} }
func initialStartDate(forTeam team: TeamRegistration) -> Date? { func initialStartDate(forTeam team: TeamRegistration) -> Date? {
@ -238,7 +267,7 @@ final class GroupStage: BaseGroupStage, SideStorable {
return _matches().first(where: { matchIndexes.contains($0.index) }) return _matches().first(where: { matchIndexes.contains($0.index) })
} }
func availableToStart(playedMatches: [Match], in runningMatches: [Match]) -> [Match] { func availableToStart(playedMatches: [Match], in runningMatches: [Match], checkCanPlay: Bool = true) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME #if _DEBUG_TIME //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
@ -246,7 +275,7 @@ final class GroupStage: BaseGroupStage, SideStorable {
print("func group stage availableToStart", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) print("func group stage availableToStart", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
} }
#endif #endif
return playedMatches.filter({ $0.canBeStarted(inMatches: runningMatches) && $0.isRunning() == false }) return playedMatches.filter({ $0.isRunning() == false && $0.canBeStarted(inMatches: runningMatches, checkCanPlay: checkCanPlay) }).sorted(by: \.computedStartDateForSorting)
} }
func runningMatches(playedMatches: [Match]) -> [Match] { func runningMatches(playedMatches: [Match]) -> [Match] {
@ -282,40 +311,67 @@ final class GroupStage: BaseGroupStage, SideStorable {
return playedMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed() return playedMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed()
} }
func isReturnMatchEnabled() -> Bool {
_matches().count > matchCount
}
private func _matchOrder() -> [Int] { private func _matchOrder() -> [Int] {
var order: [Int]
switch size { switch size {
case 3: case 3:
return [1, 2, 0] order = [1, 2, 0]
case 4: case 4:
return [2, 3, 1, 4, 5, 0] order = [2, 3, 1, 4, 5, 0]
case 5: case 5:
// return [5, 8, 0, 7, 3, 4, 2, 6, 1, 9] order = [3, 5, 8, 2, 6, 1, 9, 4, 7, 0]
return [3, 5, 8, 2, 6, 1, 9, 4, 7, 0]
case 6: case 6:
//return [1, 7, 13, 11, 3, 6, 10, 2, 8, 12, 5, 4, 9, 14, 0] order = [4, 7, 9, 3, 6, 11, 2, 8, 10, 1, 13, 5, 12, 14, 0]
return [4, 7, 9, 3, 6, 11, 2, 8, 10, 1, 13, 5, 12, 14, 0]
default: default:
return [] order = []
} }
return order
} }
func indexOf(_ matchIndex: Int) -> Int { func indexOf(_ matchIndex: Int) -> Int {
_matchOrder().firstIndex(of: matchIndex) ?? matchIndex _matchOrder().firstIndex(of: matchIndex) ?? matchIndex
} }
private func _matchUp(for matchIndex: Int) -> [Int] { func _matchUp(for matchIndex: Int) -> [Int] {
Array((0..<size).combinations(ofCount: 2))[safe: matchIndex] ?? [] let combinations = Array((0..<size).combinations(ofCount: 2))
return combinations[safe: matchIndex%matchCount] ?? []
}
func returnMatchesSuffix(for matchIndex: Int) -> String {
if matchCount > 0 {
let count = _matches().count
if count > matchCount * 2 {
return " - vague \((matchIndex / matchCount) + 1)"
}
if matchIndex >= matchCount {
return " - retour"
}
}
return ""
} }
func localizedMatchUpLabel(for matchIndex: Int) -> String { func localizedMatchUpLabel(for matchIndex: Int) -> String {
let matchUp = _matchUp(for: matchIndex) let matchUp = _matchUp(for: matchIndex)
if let index = matchUp.first, let index2 = matchUp.last { if let index = matchUp.first, let index2 = matchUp.last {
return "#\(index + 1) vs #\(index2 + 1)" return "#\(index + 1) vs #\(index2 + 1)" + returnMatchesSuffix(for: matchIndex)
} else { } else {
return "--" return "--"
} }
} }
var matchCount: Int {
(size * (size - 1)) / 2
}
func team(teamPosition team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? { func team(teamPosition team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? {
let _teams = _teams(for: matchIndex) let _teams = _teams(for: matchIndex)
switch team { switch team {
@ -328,7 +384,7 @@ final class GroupStage: BaseGroupStage, SideStorable {
private func _teams(for matchIndex: Int) -> [TeamRegistration?] { private func _teams(for matchIndex: Int) -> [TeamRegistration?] {
let combinations = Array(0..<size).combinations(ofCount: 2).map {$0} let combinations = Array(0..<size).combinations(ofCount: 2).map {$0}
return combinations[safe: matchIndex]?.map { teamAt(groupStagePosition: $0) } ?? [] return combinations[safe: matchIndex%matchCount]?.map { teamAt(groupStagePosition: $0) } ?? []
} }
private func _removeMatches() { private func _removeMatches() {
@ -350,11 +406,41 @@ final class GroupStage: BaseGroupStage, SideStorable {
fileprivate func _headToHead(_ teamPosition: TeamRegistration, _ otherTeam: TeamRegistration) -> Bool { fileprivate func _headToHead(_ teamPosition: TeamRegistration, _ otherTeam: TeamRegistration) -> Bool {
let indexes = [teamPosition, otherTeam].compactMap({ $0.groupStagePosition }).sorted() let indexes = [teamPosition, otherTeam].compactMap({ $0.groupStagePosition }).sorted()
let combos = Array((0..<size).combinations(ofCount: 2)) let combos = Array((0..<size).combinations(ofCount: 2))
let matchIndexes = combos.enumerated().compactMap { $0.element == indexes ? $0.offset : nil }
let matches = _matches().filter { matchIndexes.contains($0.index) }
if matches.count > 1 {
let scoreA = calculateScore(for: teamPosition, matches: matches, groupStagePosition: teamPosition.groupStagePosition!)
let scoreB = calculateScore(for: otherTeam, matches: matches, groupStagePosition: otherTeam.groupStagePosition!)
let teamsSorted = [scoreA, scoreB].sorted { (lhs, rhs) in
let predicates: [TeamScoreAreInIncreasingOrder] = [
{ $0.wins < $1.wins },
{ $0.setDifference < $1.setDifference },
{ $0.gameDifference < $1.gameDifference},
{ [self] in $0.team.groupStagePositionAtStep(self.step)! > $1.team.groupStagePositionAtStep(self.step)! }
]
for predicate in predicates {
if !predicate(lhs, rhs) && !predicate(rhs, lhs) {
continue
}
return predicate(lhs, rhs)
}
return false
}.map({ $0.team })
return teamsSorted.first == teamPosition
} else {
if let matchIndex = combos.firstIndex(of: indexes), let match = _matches().first(where: { $0.index == matchIndex }) { if let matchIndex = combos.firstIndex(of: indexes), let match = _matches().first(where: { $0.index == matchIndex }) {
return teamPosition.id == match.losingTeamId return teamPosition.id == match.losingTeamId
} else { } else {
return false return false
} }
}
} }
func unsortedTeams() -> [TeamRegistration] { func unsortedTeams() -> [TeamRegistration] {
@ -415,16 +501,19 @@ final class GroupStage: BaseGroupStage, SideStorable {
guard let team = teamAt(groupStagePosition: groupStagePosition) else { return nil } guard let team = teamAt(groupStagePosition: groupStagePosition) else { return nil }
let matches = matches(forGroupStagePosition: groupStagePosition).filter({ $0.hasEnded() }) let matches = matches(forGroupStagePosition: groupStagePosition).filter({ $0.hasEnded() })
if matches.isEmpty && nilIfEmpty { return nil } if matches.isEmpty && nilIfEmpty { return nil }
let score = calculateScore(for: team, matches: matches, groupStagePosition: groupStagePosition)
scoreCache[groupStagePosition] = score
return score
}
private func calculateScore(for team: TeamRegistration, matches: [Match], groupStagePosition: Int) -> TeamGroupStageScore {
let wins = matches.filter { $0.winningTeamId == team.id }.count let wins = matches.filter { $0.winningTeamId == team.id }.count
let loses = matches.filter { $0.losingTeamId == team.id }.count let loses = matches.filter { $0.losingTeamId == team.id }.count
let differences = matches.compactMap { $0.scoreDifference(groupStagePosition, atStep: step) } let differences = matches.compactMap { $0.scoreDifference(groupStagePosition, atStep: step) }
let setDifference = differences.map { $0.set }.reduce(0,+) let setDifference = differences.map { $0.set }.reduce(0,+)
let gameDifference = differences.map { $0.game }.reduce(0,+) let gameDifference = differences.map { $0.game }.reduce(0,+)
// Calculate the score and store it in the cache return (team, wins, loses, setDifference, gameDifference)
let score = (team, wins, loses, setDifference, gameDifference)
scoreCache[groupStagePosition] = score
return score
} }
// Clear the cache if necessary, for example when starting a new step or when matches update // Clear the cache if necessary, for example when starting a new step or when matches update

@ -11,10 +11,9 @@ import LeStorage
@Observable @Observable
final class Match: BaseMatch, SideStorable { final class Match: BaseMatch, SideStorable {
// static func resourceName() -> String { "matches" } static func == (lhs: Match, rhs: Match) -> Bool {
// static func tokenExemptedMethods() -> [HTTPMethod] { return [] } lhs.id == rhs.id && lhs.startDate == rhs.startDate
// static func filterByStoreIdentifier() -> Bool { return true } }
// static var relationshipNames: [String] = ["round", "groupStage"]
static func setServerTitle(upperRound: Round, matchIndex: Int) -> String { static func setServerTitle(upperRound: Round, matchIndex: Int) -> String {
if upperRound.index == 0 { return upperRound.roundTitle() } if upperRound.index == 0 { return upperRound.roundTitle() }
@ -23,54 +22,30 @@ final class Match: BaseMatch, SideStorable {
var byeState: Bool = false var byeState: Bool = false
// var id: String = Store.randomId()
// var lastUpdate: Date
// var round: String?
// var groupStage: String?
// var startDate: Date?
// var endDate: Date?
// var index: Int
// var format: MatchFormat?
// //var court: String?
// var servingTeamId: String?
// var winningTeamId: String?
// var losingTeamId: String?
// //var broadcasted: Bool
// var name: String?
// //var order: Int
// var disabled: Bool = false
// private(set) var courtIndex: Int?
// var confirmed: Bool = false
//
// var storeId: String? = nil
init(round: String? = nil, groupStage: String? = nil, startDate: Date? = nil, endDate: Date? = nil, index: Int, format: MatchFormat? = nil, servingTeamId: String? = nil, winningTeamId: String? = nil, losingTeamId: String? = nil, name: String? = nil, disabled: Bool = false, courtIndex: Int? = nil, confirmed: Bool = false) { init(round: String? = nil, groupStage: String? = nil, startDate: Date? = nil, endDate: Date? = nil, index: Int, format: MatchFormat? = nil, servingTeamId: String? = nil, winningTeamId: String? = nil, losingTeamId: String? = nil, name: String? = nil, disabled: Bool = false, courtIndex: Int? = nil, confirmed: Bool = false) {
super.init(round: round, groupStage: groupStage, startDate: startDate, endDate: endDate, index: index, format: format, servingTeamId: servingTeamId, winningTeamId: winningTeamId, losingTeamId: losingTeamId, name: name, disabled: disabled, courtIndex: courtIndex, confirmed: confirmed) super.init(round: round, groupStage: groupStage, startDate: startDate, endDate: endDate, index: index, format: format, servingTeamId: servingTeamId, winningTeamId: winningTeamId, losingTeamId: losingTeamId, name: name, disabled: disabled, courtIndex: courtIndex, confirmed: confirmed)
// self.lastUpdate = Date()
// self.round = round
// self.groupStage = groupStage
// self.startDate = startDate
// self.endDate = endDate
// self.index = index
// self.format = matchFormat
// //self.court = court
// self.servingTeamId = servingTeamId
// self.winningTeamId = winningTeamId
// self.losingTeamId = losingTeamId
// self.disabled = disabled
// self.name = name
// self.courtIndex = courtIndex
// self.confirmed = confirmed
//// self.broadcasted = broadcasted
//// self.order = order
} }
required init(from decoder: Decoder) throws { required init(from decoder: Decoder) throws {
try super.init(from: decoder) try super.init(from: decoder)
} }
func setMatchName(_ serverName: String?) {
self.name = serverName
}
func isFromLastRound() -> Bool {
guard let roundObject, roundObject.parent == nil else { return false }
guard let currentTournament = currentTournament() else { return false }
if currentTournament.rounds().count - 1 == roundObject.index {
return true
} else {
return false
}
}
var tournamentStore: TournamentStore { var tournamentStore: TournamentStore {
if let id = self.store?.identifier { if let id = self.store?.identifier {
return TournamentLibrary.shared.store(tournamentId: id) return TournamentLibrary.shared.store(tournamentId: id)
@ -119,7 +94,7 @@ defer {
} }
func matchWarningMessage() -> String { func matchWarningMessage() -> String {
[roundTitle(), matchTitle(.short), startDate?.localizedDate(), courtName()].compacted().joined(separator: "\n") [roundAndMatchTitle(), startDate?.localizedDate(), courtName(), matchFormat.computedLongLabel].compacted().joined(separator: "\n")
} }
func matchTitle(_ displayStyle: DisplayStyle = .wide, inMatches matches: [Match]? = nil) -> String { func matchTitle(_ displayStyle: DisplayStyle = .wide, inMatches matches: [Match]? = nil) -> String {
@ -142,7 +117,7 @@ defer {
case .wide, .title: case .wide, .title:
return "Match \(indexInRound(in: matches) + 1)" return "Match \(indexInRound(in: matches) + 1)"
case .short: case .short:
return "#\(indexInRound(in: matches) + 1)" return "\(indexInRound(in: matches) + 1)"
} }
} }
@ -155,22 +130,8 @@ defer {
} }
@discardableResult @discardableResult
func lockAndGetSeedPosition(atTeamPosition slot: TeamPosition?, opposingSeeding: Bool = false) -> Int { func lockAndGetSeedPosition(atTeamPosition teamPosition: TeamPosition) -> Int {
let matchIndex = index let matchIndex = index
var teamPosition : TeamPosition {
if let slot {
return slot
} else {
let seedRound = RoundRule.roundIndex(fromMatchIndex: matchIndex)
let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: seedRound)
let isUpper = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) < (numberOfMatches / 2)
var teamPosition = slot ?? (isUpper ? .one : .two)
if opposingSeeding {
teamPosition = slot ?? (isUpper ? .two : .one)
}
return teamPosition
}
}
previousMatch(teamPosition)?.disableMatch() previousMatch(teamPosition)?.disableMatch()
return matchIndex * 2 + teamPosition.rawValue return matchIndex * 2 + teamPosition.rawValue
} }
@ -191,6 +152,12 @@ defer {
return self.tournamentStore.teamRegistrations.findById(winningTeamId) return self.tournamentStore.teamRegistrations.findById(winningTeamId)
} }
func loser() -> TeamRegistration? {
guard let losingTeamId else { return nil }
return self.tournamentStore.teamRegistrations.findById(losingTeamId)
}
func localizedStartDate() -> String { func localizedStartDate() -> String {
if let startDate { if let startDate {
return startDate.formatted(date: .abbreviated, time: .shortened) return startDate.formatted(date: .abbreviated, time: .shortened)
@ -211,8 +178,8 @@ defer {
} }
func cleanScheduleAndSave(_ targetStartDate: Date? = nil) { func cleanScheduleAndSave(_ targetStartDate: Date? = nil) {
startDate = targetStartDate startDate = targetStartDate ?? startDate
confirmed = targetStartDate == nil ? false : true confirmed = false
endDate = nil endDate = nil
followingMatch()?.cleanScheduleAndSave(nil) followingMatch()?.cleanScheduleAndSave(nil)
_loserMatch()?.cleanScheduleAndSave(nil) _loserMatch()?.cleanScheduleAndSave(nil)
@ -228,6 +195,7 @@ defer {
groupStageObject?.updateGroupStageState() groupStageObject?.updateGroupStageState()
roundObject?.updateTournamentState() roundObject?.updateTournamentState()
currentTournament()?.updateTournamentState() currentTournament()?.updateTournamentState()
teams().forEach({ $0.resetRestingTime() })
} }
func resetScores() { func resetScores() {
@ -312,16 +280,22 @@ defer {
guard let forwardMatch = _forwardMatch(inRound: roundObject) else { return } guard let forwardMatch = _forwardMatch(inRound: roundObject) else { return }
guard let next = _otherMatch() else { return } guard let next = _otherMatch() else { return }
if next.disabled && byeState == false && next.byeState == false { if next.disabled && byeState == false && next.byeState == false {
if forwardMatch.disabled != state || forwardMatch.byeState {
forwardMatch.byeState = false forwardMatch.byeState = false
forwardMatch._toggleMatchDisableState(state, forward: true) forwardMatch._toggleMatchDisableState(state, forward: true)
}
} else if byeState && next.byeState { } else if byeState && next.byeState {
print("don't disable forward match") print("don't disable forward match")
if forwardMatch.byeState || forwardMatch.disabled {
forwardMatch.byeState = false forwardMatch.byeState = false
forwardMatch._toggleMatchDisableState(false, forward: true) forwardMatch._toggleMatchDisableState(false, forward: true)
}
} else { } else {
if forwardMatch.byeState == false || forwardMatch.disabled != state {
forwardMatch.byeState = true forwardMatch.byeState = true
forwardMatch._toggleMatchDisableState(state, forward: true) forwardMatch._toggleMatchDisableState(state, forward: true)
} }
}
// if next.disabled == false { // if next.disabled == false {
// forwardMatch.byeState = state // forwardMatch.byeState = state
@ -359,12 +333,18 @@ defer {
func _toggleMatchDisableState(_ state: Bool, forward: Bool = false, single: Bool = false) { func _toggleMatchDisableState(_ state: Bool, forward: Bool = false, single: Bool = false) {
//if disabled == state { return } //if disabled == state { return }
let currentState = disabled
disabled = state disabled = state
if disabled {
self.tournamentStore.teamScores.delete(contentOfs: teamScores) if disabled != currentState {
do {
try self.tournamentStore.teamScores.delete(contentOfs: teamScores)
} catch {
Logger.error(error)
} }
if state == true { }
if state == true, state != currentState {
let teams = teams() let teams = teams()
for team in teams { for team in teams {
if isSeededBy(team: team) { if isSeededBy(team: team) {
@ -374,7 +354,14 @@ defer {
} }
} }
//byeState = false //byeState = false
self.tournamentStore.matches.addOrUpdate(instance: self) roundObject?._cachedSeedInterval = nil
name = nil
do {
try self.tournamentStore.matches.addOrUpdate(instance: self)
} catch {
Logger.error(error)
}
if single == false { if single == false {
_toggleLoserMatchDisableState(state) _toggleLoserMatchDisableState(state)
if forward { if forward {
@ -408,14 +395,14 @@ defer {
} }
} }
func roundTitle() -> String? { func roundTitle(_ displayStyle: DisplayStyle = .wide) -> String? {
if groupStage != nil { return groupStageObject?.groupStageTitle() } if groupStage != nil { return groupStageObject?.groupStageTitle() }
else if let roundObject { return roundObject.roundTitle() } else if let roundObject { return roundObject.roundTitle() }
else { return nil } else { return nil }
} }
func roundAndMatchTitle() -> String { func roundAndMatchTitle(_ displayStyle: DisplayStyle = .wide) -> String {
[roundTitle(), matchTitle()].compactMap({ $0 }).joined(separator: " ") [roundTitle(displayStyle), matchTitle(displayStyle)].compactMap({ $0 }).joined(separator: " ")
} }
func topPreviousRoundMatchIndex() -> Int { func topPreviousRoundMatchIndex() -> Int {
@ -452,12 +439,26 @@ defer {
} }
} }
func loserMatches() -> [Match] {
guard let roundObject else { return [] }
return [roundObject.upperBracketTopMatch(ofMatchIndex: index, previousRound: nil), roundObject.upperBracketBottomMatch(ofMatchIndex: index, previousRound: nil)].compactMap({ $0 })
}
func loserMatch(_ teamPosition: TeamPosition) -> Match? {
if teamPosition == .one {
return roundObject?.upperBracketTopMatch(ofMatchIndex: index, previousRound: nil)
} else {
return roundObject?.upperBracketBottomMatch(ofMatchIndex: index, previousRound: nil)
}
}
var computedOrder: Int { var computedOrder: Int {
if let groupStageObject { if let groupStageObject {
return (groupStageObject.index + 1) * 100 + groupStageObject.indexOf(index) return (groupStageObject.index + 1) * 100 + groupStageObject.indexOf(index)
} }
guard let roundObject else { return index } guard let roundObject else { return index }
return roundObject.isLoserBracket() ? (roundObject.index + 1) * 1000 + indexInRound() : (roundObject.index + 1) * 10000 + indexInRound() return roundObject.isLoserBracket() ? (roundObject.index + 1) * 10000 + indexInRound() : (roundObject.index + 1) * 1000 + indexInRound()
} }
func previousMatches() -> [Match] { func previousMatches() -> [Match] {
@ -487,7 +488,7 @@ defer {
if endDate == nil { if endDate == nil {
endDate = Date() endDate = Date()
} }
teams().forEach({ $0.resetRestingTime() })
winningTeamId = teamScoreWinning.teamRegistration winningTeamId = teamScoreWinning.teamRegistration
losingTeamId = teamScoreWalkout.teamRegistration losingTeamId = teamScoreWalkout.teamRegistration
groupStageObject?.updateGroupStageState() groupStageObject?.updateGroupStageState()
@ -503,6 +504,8 @@ defer {
} }
if startDate == nil { if startDate == nil {
startDate = endDate?.addingTimeInterval(Double(-getDuration()*60)) startDate = endDate?.addingTimeInterval(Double(-getDuration()*60))
} else if let startDate, let endDate, startDate >= endDate {
self.startDate = endDate.addingTimeInterval(Double(-getDuration()*60))
} }
let teamOne = team(matchDescriptor.winner) let teamOne = team(matchDescriptor.winner)
@ -510,6 +513,8 @@ defer {
teamOne?.hasArrived() teamOne?.hasArrived()
teamTwo?.hasArrived() teamTwo?.hasArrived()
teamOne?.resetRestingTime()
teamTwo?.resetRestingTime()
winningTeamId = teamOne?.id winningTeamId = teamOne?.id
losingTeamId = teamTwo?.id losingTeamId = teamTwo?.id
@ -518,7 +523,17 @@ defer {
groupStageObject?.updateGroupStageState() groupStageObject?.updateGroupStageState()
roundObject?.updateTournamentState() roundObject?.updateTournamentState()
currentTournament()?.updateTournamentState() if let tournament = currentTournament(), let endDate, let startDate {
if endDate.isEarlierThan(tournament.startDate) {
tournament.startDate = startDate
}
do {
try DataStore.shared.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
tournament.updateTournamentState()
}
updateFollowingMatchTeamScore() updateFollowingMatchTeamScore()
} }
@ -570,7 +585,7 @@ defer {
} }
} }
func validateMatch(fromStartDate: Date, toEndDate: Date, fieldSetup: MatchFieldSetup) { func validateMatch(fromStartDate: Date, toEndDate: Date, fieldSetup: MatchFieldSetup, forced: Bool = false) {
if hasEnded() == false { if hasEnded() == false {
startDate = fromStartDate startDate = fromStartDate
@ -580,7 +595,8 @@ defer {
setCourt(_courtIndex) setCourt(_courtIndex)
} }
case .random: case .random:
if let _courtIndex = availableCourts().randomElement() { let runningMatches: [Match] = DataStore.shared.runningMatches()
if let _courtIndex = availableCourts(runningMatches: runningMatches).randomElement() {
setCourt(_courtIndex) setCourt(_courtIndex)
} }
case .field(let _courtIndex): case .field(let _courtIndex):
@ -592,7 +608,11 @@ defer {
endDate = toEndDate endDate = toEndDate
} }
if let startDate, startDate.timeIntervalSinceNow <= 300 {
confirmed = true confirmed = true
} else {
confirmed = false
}
} }
func courtName() -> String? { func courtName() -> String? {
@ -604,12 +624,20 @@ defer {
} }
} }
func courtName(for selectedIndex: Int) -> String {
if let courtName = currentTournament()?.courtName(atIndex: selectedIndex) {
return courtName
} else {
return Court.courtIndexedTitle(atIndex: selectedIndex)
}
}
func courtCount() -> Int { func courtCount() -> Int {
return currentTournament()?.courtCount ?? 1 return currentTournament()?.courtCount ?? 1
} }
func courtIsAvailable(_ courtIndex: Int) -> Bool { func courtIsAvailable(_ courtIndex: Int, in runningMatches: [Match]) -> Bool {
let courtUsed = currentTournament()?.courtUsed() ?? [] let courtUsed = currentTournament()?.courtUsed(runningMatches: runningMatches) ?? []
return courtUsed.contains(courtIndex) == false return courtUsed.contains(courtIndex) == false
} }
@ -622,9 +650,9 @@ defer {
return availableCourts return availableCourts
} }
func availableCourts() -> [Int] { func availableCourts(runningMatches: [Match]) -> [Int] {
let courtUsed = currentTournament()?.courtUsed() ?? [] let courtUsed = currentTournament()?.courtUsed(runningMatches: runningMatches) ?? []
return Array(Set(allCourts().map { $0 }).subtracting(Set(courtUsed))) return Set(allCourts().map { $0 }).subtracting(Set(courtUsed)).sorted()
} }
func removeCourt() { func removeCourt() {
@ -635,12 +663,17 @@ defer {
self.courtIndex = courtIndex self.courtIndex = courtIndex
} }
func canBeStarted(inMatches matches: [Match]) -> Bool { func canBeStarted(inMatches matches: [Match], checkCanPlay: Bool) -> Bool {
let teams = teamScores let teams = teamScores
guard teams.count == 2 else { return false } guard teams.count == 2 else {
//print("teams.count != 2")
return false
}
guard hasEnded() == false else { return false } guard hasEnded() == false else { return false }
guard hasStarted() == false else { return false } guard hasStarted() == false else { return false }
return teams.compactMap({ $0.team }).allSatisfy({ $0.canPlay() && isTeamPlaying($0, inMatches: matches) == false }) return teams.compactMap({ $0.team }).allSatisfy({
((checkCanPlay && $0.canPlay()) || checkCanPlay == false) && isTeamPlaying($0, inMatches: matches) == false
})
} }
func isTeamPlaying(_ team: TeamRegistration, inMatches matches: [Match]) -> Bool { func isTeamPlaying(_ team: TeamRegistration, inMatches matches: [Match]) -> Bool {
@ -736,9 +769,25 @@ defer {
} else { } else {
setDifference = zip.filter { $0 > $1 }.count - zip.filter { $1 > $0 }.count setDifference = zip.filter { $0 > $1 }.count - zip.filter { $1 > $0 }.count
} }
// si 3 sets et 3eme set super tie break, different des 2 premiers sets, alors super tie points ne sont pas des jeux et doivent etre compté comme un jeu
if matchFormat.canSuperTie, endedSetsOne.count == 3 {
let games = zip.map { ($0, $1) }
let gameDifference = games.enumerated().map({ index, pair in
if index < 2 {
return pair.0 - pair.1
} else {
return pair.0 < pair.1 ? -1 : 1
}
})
.reduce(0,+)
return (setDifference * reverseValue, gameDifference * reverseValue)
} else {
let gameDifference = zip.map { ($0, $1) }.map { $0.0 - $0.1 }.reduce(0,+) let gameDifference = zip.map { ($0, $1) }.map { $0.0 - $0.1 }.reduce(0,+)
return (setDifference * reverseValue, gameDifference * reverseValue) return (setDifference * reverseValue, gameDifference * reverseValue)
} }
}
func groupStageProjectedTeam(_ team: TeamPosition) -> TeamRegistration? { func groupStageProjectedTeam(_ team: TeamPosition) -> TeamRegistration? {
guard let groupStageObject else { return nil } guard let groupStageObject else { return nil }
@ -798,7 +847,7 @@ defer {
func hasStarted() -> Bool { // meaning at least one match is over func hasStarted() -> Bool { // meaning at least one match is over
if let startDate { if let startDate {
return startDate.timeIntervalSinceNow < 0 return startDate.timeIntervalSinceNow < 0 && confirmed
} }
if hasEnded() { if hasEnded() {
return true return true
@ -843,48 +892,111 @@ defer {
} }
} }
// enum CodingKeys: String, CodingKey { var restingTimeForSorting: TimeInterval {
// case _id = "id" (teams().compactMap({ $0.restingTime() }).max() ?? .distantFuture).timeIntervalSinceNow
// case _storeId = "storeId" }
// case _lastUpdate = "lastUpdate"
// case _round = "round" func isValidSpot() -> Bool {
// case _groupStage = "groupStage" previousMatches().allSatisfy({ $0.isSeeded() == false })
// case _startDate = "startDate" }
// case _endDate = "endDate"
// case _index = "index" func expectedToBeRunning() -> Bool {
// case _format = "format" guard let startDate else { return false }
//// case _court = "court" return confirmed == false && startDate.timeIntervalSinceNow < 0
// case _courtIndex = "courtIndex" }
// case _servingTeamId = "servingTeamId"
// case _winningTeamId = "winningTeamId" func expectedFormattedStartDate(canBePlayedInSpecifiedCourt: Bool, availableCourts: [Int], estimatedStartDate: CourtIndexAndDate?, updatedField: Int?) -> String {
// case _losingTeamId = "losingTeamId" guard let startDate else { return "" }
//// case _broadcasted = "broadcasted" guard hasEnded() == false, isRunning() == false else { return "" }
// case _name = "name" let depthReadiness = depthReadiness()
//// case _order = "order" if depthReadiness == 0 {
// case _disabled = "disabled" if canBePlayedInSpecifiedCourt {
// case _confirmed = "confirmed" return "possible tout de suite"
// } } else if let updatedField, availableCourts.contains(updatedField) {
// return "possible tout de suite \(courtName(for: updatedField))"
// func encode(to encoder: Encoder) throws { } else if let first = availableCourts.first {
// var container = encoder.container(keyedBy: CodingKeys.self) return "possible tout de suite \(courtName(for: first))"
// } else if let estimatedStartDate {
// try container.encode(id, forKey: ._id) return "dans ~" + estimatedStartDate.1.timeElapsedString() + " " + courtName(for: estimatedStartDate.0)
// try container.encode(storeId, forKey: ._storeId) }
// try container.encode(lastUpdate, forKey: ._lastUpdate) return "était prévu à " + startDate.formattedAsHourMinute()
// try container.encode(round, forKey: ._round) } else if depthReadiness == 1 {
// try container.encode(groupStage, forKey: ._groupStage) return "possible prochaine rotation"
// try container.encode(startDate, forKey: ._startDate) } else {
// try container.encode(endDate, forKey: ._endDate) return "dans \(depthReadiness) rotation\(depthReadiness.pluralSuffix), ~\((getDuration() * depthReadiness).durationInHourMinutes())"
// try container.encode(format, forKey: ._format) }
// try container.encode(servingTeamId, forKey: ._servingTeamId) }
// try container.encode(index, forKey: ._index)
// try container.encode(winningTeamId, forKey: ._winningTeamId) func runningDuration() -> String {
// try container.encode(losingTeamId, forKey: ._losingTeamId) guard let startDate else { return "" }
// try container.encode(name, forKey: ._name) return " depuis " + startDate.timeElapsedString()
// try container.encode(disabled, forKey: ._disabled) }
// try container.encode(courtIndex, forKey: ._courtIndex)
// try container.encode(confirmed, forKey: ._confirmed) func canBePlayedInSpecifiedCourt(runningMatches: [Match]) -> Bool {
// } guard let courtIndex else { return false }
if expectedToBeRunning() {
return courtIsAvailable(courtIndex, in: runningMatches)
} else {
return true
}
}
typealias CourtIndexAndDate = (courtIndex: Int, startDate: Date)
func nextCourtsAvailable(availableCourts: [Int], runningMatches: [Match]) -> [CourtIndexAndDate] {
guard let tournament = currentTournament() else { return [] }
let startDate = Date().withoutSeconds()
if runningMatches.isEmpty {
return availableCourts.map {
($0, startDate)
}
}
let optionalDates : [CourtIndexAndDate?] = runningMatches.map({ match in
guard let endDate = match.estimatedEndDate(tournament.additionalEstimationDuration) else { return nil }
guard let courtIndex = match.courtIndex else { return nil }
if endDate <= startDate {
return (courtIndex, startDate.addingTimeInterval(600))
} else {
return (courtIndex, endDate)
}
})
let dates : [CourtIndexAndDate] = optionalDates.compacted().sorted { a, b in
a.1 < b.1
}
return dates
}
func estimatedStartDate(availableCourts: [Int], runningMatches: [Match]) -> CourtIndexAndDate? {
guard isReady() else { return nil }
guard let tournament = currentTournament() else { return nil }
let availableCourts = nextCourtsAvailable(availableCourts: availableCourts, runningMatches: runningMatches)
return availableCourts.first(where: { (courtIndex, startDate) in
let endDate = startDate.addingTimeInterval(TimeInterval(matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) * 60)
if tournament.courtUnavailable(courtIndex: courtIndex, from: startDate, to: endDate) == false {
return true
}
return false
})
}
func depthReadiness() -> Int {
// Base case: If this match is ready, the depth is 0
if isReady() {
return 0
}
// Recursive case: If not ready, check the maximum depth of readiness among previous matches
// If previousMatches() is empty, return a default depth of -1
let previousDepth = ancestors().map { $0.depthReadiness() }.max() ?? -1
return previousDepth + 1
}
func ancestors() -> [Match] {
previousMatches() + loserMatches()
}
func insertOnServer() { func insertOnServer() {
self.tournamentStore.matches.writeChangeAndInsertOnServer(instance: self) self.tournamentStore.matches.writeChangeAndInsertOnServer(instance: self)
@ -899,6 +1011,8 @@ enum MatchDateSetup: Hashable, Identifiable {
case inMinutes(Int) case inMinutes(Int)
case now case now
case customDate case customDate
case previousRotation
case nextRotation
var id: Int { hashValue } var id: Int { hashValue }
} }

@ -11,73 +11,42 @@ import SwiftUI
@Observable @Observable
final class MatchScheduler: BaseMatchScheduler, SideStorable { final class MatchScheduler: BaseMatchScheduler, SideStorable {
// static func resourceName() -> String { return "match-scheduler" }
// static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
// static func filterByStoreIdentifier() -> Bool { return false }
// static var relationshipNames: [String] = []
// //
// private(set) var id: String = Store.randomId() // init(tournament: String,
// var tournament: String // timeDifferenceLimit: Int = 5,
// var timeDifferenceLimit: Int // loserBracketRotationDifference: Int = 0,
// var loserBracketRotationDifference: Int // upperBracketRotationDifference: Int = 1,
// var upperBracketRotationDifference: Int // accountUpperBracketBreakTime: Bool = true,
// var accountUpperBracketBreakTime: Bool // accountLoserBracketBreakTime: Bool = false,
// var accountLoserBracketBreakTime: Bool // randomizeCourts: Bool = true,
// var randomizeCourts: Bool // rotationDifferenceIsImportant: Bool = false,
// var rotationDifferenceIsImportant: Bool // shouldHandleUpperRoundSlice: Bool = false,
// var shouldHandleUpperRoundSlice: Bool // shouldEndRoundBeforeStartingNext: Bool = true,
// var shouldEndRoundBeforeStartingNext: Bool //<<<<<<< HEAD
// var groupStageChunkCount: Int? // groupStageChunkCount: Int? = nil, overrideCourtsUnavailability: Bool = false, shouldTryToFillUpCourtsAvailable: Bool = false) {
// var overrideCourtsUnavailability: Bool = false // super.init()
// var shouldTryToFillUpCourtsAvailable: Bool = false //=======
// groupStageChunkCount: Int? = nil,
init(tournament: String, // overrideCourtsUnavailability: Bool = false,
timeDifferenceLimit: Int = 5, // shouldTryToFillUpCourtsAvailable: Bool = true,
loserBracketRotationDifference: Int = 0, // courtsAvailable: Set<Int> = Set<Int>(),
upperBracketRotationDifference: Int = 1, // simultaneousStart: Bool = true) {
accountUpperBracketBreakTime: Bool = true, //>>>>>>> main
accountLoserBracketBreakTime: Bool = false, // self.tournament = tournament
randomizeCourts: Bool = true, // self.timeDifferenceLimit = timeDifferenceLimit
rotationDifferenceIsImportant: Bool = false, // self.loserBracketRotationDifference = loserBracketRotationDifference
shouldHandleUpperRoundSlice: Bool = true, // self.upperBracketRotationDifference = upperBracketRotationDifference
shouldEndRoundBeforeStartingNext: Bool = true, // self.accountUpperBracketBreakTime = accountUpperBracketBreakTime
groupStageChunkCount: Int? = nil, overrideCourtsUnavailability: Bool = false, shouldTryToFillUpCourtsAvailable: Bool = false) { // self.accountLoserBracketBreakTime = accountLoserBracketBreakTime
super.init() // self.randomizeCourts = randomizeCourts
self.tournament = tournament // self.rotationDifferenceIsImportant = rotationDifferenceIsImportant
self.timeDifferenceLimit = timeDifferenceLimit // self.shouldHandleUpperRoundSlice = shouldHandleUpperRoundSlice
self.loserBracketRotationDifference = loserBracketRotationDifference // self.shouldEndRoundBeforeStartingNext = shouldEndRoundBeforeStartingNext
self.upperBracketRotationDifference = upperBracketRotationDifference // self.groupStageChunkCount = groupStageChunkCount
self.accountUpperBracketBreakTime = accountUpperBracketBreakTime // self.overrideCourtsUnavailability = overrideCourtsUnavailability
self.accountLoserBracketBreakTime = accountLoserBracketBreakTime // self.shouldTryToFillUpCourtsAvailable = shouldTryToFillUpCourtsAvailable
self.randomizeCourts = randomizeCourts // self.courtsAvailable = courtsAvailable
self.rotationDifferenceIsImportant = rotationDifferenceIsImportant // self.simultaneousStart = simultaneousStart
self.shouldHandleUpperRoundSlice = shouldHandleUpperRoundSlice
self.shouldEndRoundBeforeStartingNext = shouldEndRoundBeforeStartingNext
self.groupStageChunkCount = groupStageChunkCount
self.overrideCourtsUnavailability = overrideCourtsUnavailability
self.shouldTryToFillUpCourtsAvailable = shouldTryToFillUpCourtsAvailable
}
required init(from decoder: any Decoder) throws {
try super.init(from: decoder)
}
// enum CodingKeys: String, CodingKey {
// case _id = "id"
// case _tournament = "tournament"
// case _timeDifferenceLimit = "timeDifferenceLimit"
// case _loserBracketRotationDifference = "loserBracketRotationDifference"
// case _upperBracketRotationDifference = "upperBracketRotationDifference"
// case _accountUpperBracketBreakTime = "accountUpperBracketBreakTime"
// case _accountLoserBracketBreakTime = "accountLoserBracketBreakTime"
// case _randomizeCourts = "randomizeCourts"
// case _rotationDifferenceIsImportant = "rotationDifferenceIsImportant"
// case _shouldHandleUpperRoundSlice = "shouldHandleUpperRoundSlice"
// case _shouldEndRoundBeforeStartingNext = "shouldEndRoundBeforeStartingNext"
// case _groupStageChunkCount = "groupStageChunkCount"
// case _overrideCourtsUnavailability = "overrideCourtsUnavailability"
// case _shouldTryToFillUpCourtsAvailable = "shouldTryToFillUpCourtsAvailable"
// } // }
var courtsUnavailability: [DateInterval]? { var courtsUnavailability: [DateInterval]? {
@ -105,7 +74,6 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
if let specificGroupStage { if let specificGroupStage {
groupStages = [specificGroupStage] groupStages = [specificGroupStage]
} }
let numberOfCourtsAvailablePerRotation: Int = tournament.courtCount
let matches = groupStages.flatMap { $0._matches() } let matches = groupStages.flatMap { $0._matches() }
matches.forEach({ matches.forEach({
@ -133,7 +101,7 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
lastDate = time lastDate = time
} }
let groups = groupStages.filter({ $0.startDate == time }) let groups = groupStages.filter({ $0.startDate == time })
let dispatch = groupStageDispatcher(numberOfCourtsAvailablePerRotation: numberOfCourtsAvailablePerRotation, groupStages: groups, startingDate: lastDate) let dispatch = groupStageDispatcher(groupStages: groups, startingDate: lastDate)
dispatch.timedMatches.forEach { matchSchedule in dispatch.timedMatches.forEach { matchSchedule in
if let match = matches.first(where: { $0.id == matchSchedule.matchID }) { if let match = matches.first(where: { $0.id == matchSchedule.matchID }) {
@ -157,7 +125,7 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
Logger.error(error) Logger.error(error)
} }
let dispatch = groupStageDispatcher(numberOfCourtsAvailablePerRotation: numberOfCourtsAvailablePerRotation, groupStages: groups, startingDate: lastDate) let dispatch = groupStageDispatcher(groupStages: groups, startingDate: lastDate)
dispatch.timedMatches.forEach { matchSchedule in dispatch.timedMatches.forEach { matchSchedule in
if let match = matches.first(where: { $0.id == matchSchedule.matchID }) { if let match = matches.first(where: { $0.id == matchSchedule.matchID }) {
@ -180,21 +148,25 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
return lastDate return lastDate
} }
func groupStageDispatcher(numberOfCourtsAvailablePerRotation: Int, groupStages: [GroupStage], startingDate: Date) -> GroupStageMatchDispatcher { func groupStageDispatcher(groupStages: [GroupStage], startingDate: Date) -> GroupStageMatchDispatcher {
let _groupStages = groupStages let _groupStages = groupStages
// Get the maximum count of matches in any group // Get the maximum count of matches in any group
let maxMatchesCount = _groupStages.map { $0._matches().count }.max() ?? 0 let maxMatchesCount = _groupStages.map { $0._matches().count }.max() ?? 0
var flattenedMatches = [Match]()
if simultaneousStart {
// Flatten matches in a round-robin order by cycling through each group // Flatten matches in a round-robin order by cycling through each group
let flattenedMatches = (0..<maxMatchesCount).flatMap { index in flattenedMatches = (0..<maxMatchesCount).flatMap { index in
_groupStages.compactMap { group in _groupStages.compactMap { group in
// Safely access matches, return nil if index is out of bounds // Safely access matches, return nil if index is out of bounds
let playedMatches = group.playedMatches() let playedMatches = group.playedMatches()
return playedMatches.indices.contains(index) ? playedMatches[index] : nil return playedMatches.indices.contains(index) ? playedMatches[index] : nil
} }
} }
} else {
flattenedMatches = _groupStages.flatMap({ $0.playedMatches() })
}
var slots = [GroupStageTimeMatch]() var slots = [GroupStageTimeMatch]()
var availableMatches = flattenedMatches var availableMatches = flattenedMatches
@ -215,24 +187,28 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
let counts = Dictionary(previousRotationBracketIndexes, uniquingKeysWith: +) let counts = Dictionary(previousRotationBracketIndexes, uniquingKeysWith: +)
var rotationMatches = Array(availableMatches.filter({ match in var rotationMatches = Array(availableMatches.filter({ match in
// Check if all teams from the match are not already scheduled in the current rotation // Check if all teams from the match are not already scheduled in the current rotation
let teamsAvailable = teamsPerRotation[rotationIndex]!.allSatisfy({ !match.containsTeamId($0) }) let teamsAvailable = teamsPerRotation[rotationIndex]!.allSatisfy({ !match.containsTeamIndex($0) })
if !teamsAvailable { if !teamsAvailable {
print("Match \(match.roundAndMatchTitle()) has teams already scheduled in rotation \(rotationIndex)") print("Match \(match.roundAndMatchTitle()) has teams already scheduled in rotation \(rotationIndex)")
} }
return teamsAvailable return teamsAvailable
}).prefix(numberOfCourtsAvailablePerRotation)) }))
if rotationIndex > 0 { if rotationIndex > 0 {
rotationMatches = rotationMatches.sorted(by: { rotationMatches = rotationMatches.sorted(by: {
if counts[$0.groupStageObject!.index] ?? 0 == counts[$1.groupStageObject!.index] ?? 0 { if counts[$0.groupStageObject!.index] ?? 0 == counts[$1.groupStageObject!.index] ?? 0 {
if simultaneousStart {
return $0.groupStageObject!.orderedIndexOfMatch($0) < $1.groupStageObject!.orderedIndexOfMatch($1)
} else {
return $0.groupStageObject!.index < $1.groupStageObject!.index return $0.groupStageObject!.index < $1.groupStageObject!.index
}
} else { } else {
return counts[$0.groupStageObject!.index] ?? 0 < counts[$1.groupStageObject!.index] ?? 0 return counts[$0.groupStageObject!.index] ?? 0 < counts[$1.groupStageObject!.index] ?? 0
} }
}) })
} }
(0..<numberOfCourtsAvailablePerRotation).forEach { courtIndex in courtsAvailable.forEach { courtIndex in
print("Checking availability for court \(courtIndex) in rotation \(rotationIndex)") print("Checking availability for court \(courtIndex) in rotation \(rotationIndex)")
if let first = rotationMatches.first(where: { match in if let first = rotationMatches.first(where: { match in
let estimatedDuration = match.matchFormat.getEstimatedDuration(additionalEstimationDuration) let estimatedDuration = match.matchFormat.getEstimatedDuration(additionalEstimationDuration)
@ -246,7 +222,7 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
return false return false
} }
let teamsAvailable = teamsPerRotation[rotationIndex]!.allSatisfy({ !match.containsTeamId($0) }) let teamsAvailable = teamsPerRotation[rotationIndex]!.allSatisfy({ !match.containsTeamIndex($0) })
if !teamsAvailable { if !teamsAvailable {
print("Teams from match \(match.roundAndMatchTitle()) are already scheduled in this rotation") print("Teams from match \(match.roundAndMatchTitle()) are already scheduled in this rotation")
return false return false
@ -260,7 +236,7 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
print("Scheduled match: \(first.roundAndMatchTitle()) on court \(courtIndex) at rotation \(rotationIndex)") print("Scheduled match: \(first.roundAndMatchTitle()) on court \(courtIndex) at rotation \(rotationIndex)")
slots.append(timeMatch) slots.append(timeMatch)
teamsPerRotation[rotationIndex]!.append(contentsOf: first.teamIds()) teamsPerRotation[rotationIndex]!.append(contentsOf: first.matchUp())
rotationMatches.removeAll(where: { $0.id == first.id }) rotationMatches.removeAll(where: { $0.id == first.id })
availableMatches.removeAll(where: { $0.id == first.id }) availableMatches.removeAll(where: { $0.id == first.id })
@ -394,7 +370,7 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
print("Setting minimumTargetedEndDate to the earlier of \(minimumPossibleEndDate) and \(minimumTargetedEndDate)") print("Setting minimumTargetedEndDate to the earlier of \(minimumPossibleEndDate) and \(minimumTargetedEndDate)")
minimumTargetedEndDate = min(minimumPossibleEndDate, minimumTargetedEndDate) minimumTargetedEndDate = min(minimumPossibleEndDate, minimumTargetedEndDate)
} }
print("Targeted start date is before the minimum possible end date, returning false.") print("Targeted start date \(targetedStartDate) is before the minimum possible end date, returning false. \(minimumTargetedEndDate)")
return false return false
} }
} }
@ -433,7 +409,7 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
) )
} }
func roundDispatcher(numberOfCourtsAvailablePerRotation: Int, flattenedMatches: [Match], dispatcherStartDate: Date, initialCourts: [Int]?) -> MatchDispatcher { func roundDispatcher(flattenedMatches: [Match], dispatcherStartDate: Date, initialCourts: [Int]?) -> MatchDispatcher {
var slots = [TimeMatch]() var slots = [TimeMatch]()
var _startDate: Date? var _startDate: Date?
var rotationIndex = 0 var rotationIndex = 0
@ -442,7 +418,7 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
var issueFound: Bool = false var issueFound: Bool = false
// Log start of the function // Log start of the function
print("Starting roundDispatcher with \(availableMatchs.count) matches and \(numberOfCourtsAvailablePerRotation) courts available") print("Starting roundDispatcher with \(availableMatchs.count) matches and \(courtsAvailable) courts available")
flattenedMatches.filter { $0.startDate != nil }.sorted(by: \.startDate!).forEach { match in flattenedMatches.filter { $0.startDate != nil }.sorted(by: \.startDate!).forEach { match in
if _startDate == nil { if _startDate == nil {
@ -461,20 +437,28 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
} }
var freeCourtPerRotation = [Int: [Int]]() var freeCourtPerRotation = [Int: [Int]]()
let availableCourt = numberOfCourtsAvailablePerRotation var courts = initialCourts ?? Array(courtsAvailable)
var courts = initialCourts ?? (0..<availableCourt).map { $0 }
var shouldStartAtDispatcherDate = rotationIndex > 0 var shouldStartAtDispatcherDate = rotationIndex > 0
var suitableDate: Date?
while !availableMatchs.isEmpty && !issueFound && rotationIndex < 100 { while !availableMatchs.isEmpty && !issueFound && rotationIndex < 50 {
freeCourtPerRotation[rotationIndex] = [] freeCourtPerRotation[rotationIndex] = []
let previousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 1 }) let previousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 1 })
var rotationStartDate: Date = getNextStartDate(fromPreviousRotationSlots: previousRotationSlots, includeBreakTime: false) ?? dispatcherStartDate
var rotationStartDate: Date
if previousRotationSlots.isEmpty && rotationIndex > 0 {
let computedSuitableDate = slots.sorted(by: \.computedEndDateForSorting).last?.computedEndDateForSorting
print("Previous rotation was empty, find a suitable rotationStartDate \(suitableDate)")
rotationStartDate = suitableDate ?? computedSuitableDate ?? dispatcherStartDate
} else {
rotationStartDate = getNextStartDate(fromPreviousRotationSlots: previousRotationSlots, includeBreakTime: false) ?? dispatcherStartDate
}
if shouldStartAtDispatcherDate { if shouldStartAtDispatcherDate {
rotationStartDate = dispatcherStartDate rotationStartDate = dispatcherStartDate
shouldStartAtDispatcherDate = false shouldStartAtDispatcherDate = false
} else { } else {
courts = rotationIndex == 0 ? courts : (0..<availableCourt).map { $0 } courts = rotationIndex == 0 ? courts : Array(courtsAvailable)
} }
courts.sort() courts.sort()
@ -486,8 +470,16 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
if rotationIndex > 0, let freeCourtPreviousRotation = freeCourtPerRotation[rotationIndex - 1], !freeCourtPreviousRotation.isEmpty { if rotationIndex > 0, let freeCourtPreviousRotation = freeCourtPerRotation[rotationIndex - 1], !freeCourtPreviousRotation.isEmpty {
print("Handling break time conflicts or waiting for free courts") print("Handling break time conflicts or waiting for free courts")
let previousPreviousRotationSlots = slots.filter { $0.rotationIndex == rotationIndex - 2 && freeCourtPreviousRotation.contains($0.courtIndex) } let previousPreviousRotationSlots = slots.filter { $0.rotationIndex == rotationIndex - 2 && freeCourtPreviousRotation.contains($0.courtIndex) }
let previousEndDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: accountUpperBracketBreakTime) var previousEndDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: accountUpperBracketBreakTime)
let previousEndDateNoBreak = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: false) var previousEndDateNoBreak = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: false)
if let courtsUnavailability, previousEndDate != nil {
previousEndDate = getFirstFreeCourt(startDate: previousEndDate!, duration: 0, courts: courts, courtsUnavailability: courtsUnavailability).earliestFreeDate
}
if let courtsUnavailability, previousEndDateNoBreak != nil {
previousEndDateNoBreak = getFirstFreeCourt(startDate: previousEndDateNoBreak!, duration: 0, courts: courts, courtsUnavailability: courtsUnavailability).earliestFreeDate
}
let noBreakAlreadyTested = previousRotationSlots.anySatisfy { $0.startDate == previousEndDateNoBreak } let noBreakAlreadyTested = previousRotationSlots.anySatisfy { $0.startDate == previousEndDateNoBreak }
@ -499,13 +491,23 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
let timeDifferenceLimitInSeconds = Double(timeDifferenceLimit * 60) let timeDifferenceLimitInSeconds = Double(timeDifferenceLimit * 60)
var difference = differenceWithBreak var difference = differenceWithBreak
if differenceWithBreak <= 0 { if differenceWithBreak <= 0, accountUpperBracketBreakTime == false {
difference = differenceWithoutBreak difference = differenceWithoutBreak
} else if differenceWithBreak > timeDifferenceLimitInSeconds && differenceWithoutBreak > timeDifferenceLimitInSeconds { } else if differenceWithBreak > timeDifferenceLimitInSeconds && differenceWithoutBreak > timeDifferenceLimitInSeconds {
difference = noBreakAlreadyTested ? differenceWithBreak : max(differenceWithBreak, differenceWithoutBreak) difference = noBreakAlreadyTested ? differenceWithBreak : max(differenceWithBreak, differenceWithoutBreak)
} }
if difference > timeDifferenceLimitInSeconds && rotationStartDate.addingTimeInterval(-difference) != previousEndDate { print("Final difference to evaluate: \(difference)")
if (difference > timeDifferenceLimitInSeconds && rotationStartDate.addingTimeInterval(-difference) != previousEndDate) || difference < 0 {
print("""
Adjusting rotation start:
- Initial rotationStartDate: \(rotationStartDate)
- Adjusted by difference: \(difference)
- Adjusted rotationStartDate: \(rotationStartDate.addingTimeInterval(-difference))
- PreviousEndDate: \(previousEndDate)
""")
courts.removeAll(where: { freeCourtPreviousRotation.contains($0) }) courts.removeAll(where: { freeCourtPreviousRotation.contains($0) })
freeCourtPerRotation[rotationIndex] = courts freeCourtPerRotation[rotationIndex] = courts
courts = freeCourtPreviousRotation courts = freeCourtPreviousRotation
@ -516,16 +518,22 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
let duration = firstMatch.matchFormat.getEstimatedDuration(additionalEstimationDuration) let duration = firstMatch.matchFormat.getEstimatedDuration(additionalEstimationDuration)
let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: duration, courtsUnavailability: courtsUnavailability) let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: duration, courtsUnavailability: courtsUnavailability)
if courtsUnavailable.count == numberOfCourtsAvailablePerRotation { if Array(Set(courtsAvailable).subtracting(Set(courtsUnavailable))).isEmpty {
print("Issue: All courts unavailable in this rotation") print("Issue: All courts unavailable in this rotation")
if let courtsUnavailability {
let computedStartDateAndCourts = getFirstFreeCourt(startDate: rotationStartDate, duration: duration, courts: courts, courtsUnavailability: courtsUnavailability)
rotationStartDate = computedStartDateAndCourts.earliestFreeDate
courts = computedStartDateAndCourts.availableCourts
} else {
issueFound = true issueFound = true
}
} else { } else {
courts = Array(Set(courts).subtracting(Set(courtsUnavailable))) courts = Array(Set(courtsAvailable).subtracting(Set(courtsUnavailable)))
} }
} }
// Dispatch courts and schedule matches // Dispatch courts and schedule matches
dispatchCourts(availableCourts: numberOfCourtsAvailablePerRotation, courts: courts, availableMatchs: &availableMatchs, slots: &slots, rotationIndex: rotationIndex, rotationStartDate: rotationStartDate, freeCourtPerRotation: &freeCourtPerRotation, courtsUnavailability: courtsUnavailability) suitableDate = dispatchCourts(courts: courts, availableMatchs: &availableMatchs, slots: &slots, rotationIndex: rotationIndex, rotationStartDate: rotationStartDate, freeCourtPerRotation: &freeCourtPerRotation, courtsUnavailability: courtsUnavailability)
rotationIndex += 1 rotationIndex += 1
} }
@ -544,10 +552,10 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
print("Finished roundDispatcher with \(organizedSlots.count) scheduled matches") print("Finished roundDispatcher with \(organizedSlots.count) scheduled matches")
return MatchDispatcher(timedMatches: slots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex, issueFound: issueFound) return MatchDispatcher(timedMatches: organizedSlots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex, issueFound: issueFound)
} }
func dispatchCourts(availableCourts: Int, courts: [Int], availableMatchs: inout [Match], slots: inout [TimeMatch], rotationIndex: Int, rotationStartDate: Date, freeCourtPerRotation: inout [Int: [Int]], courtsUnavailability: [DateInterval]?) { func dispatchCourts(courts: [Int], availableMatchs: inout [Match], slots: inout [TimeMatch], rotationIndex: Int, rotationStartDate: Date, freeCourtPerRotation: inout [Int: [Int]], courtsUnavailability: [DateInterval]?) -> Date {
var matchPerRound = [String: Int]() var matchPerRound = [String: Int]()
var minimumTargetedEndDate = rotationStartDate var minimumTargetedEndDate = rotationStartDate
@ -563,7 +571,7 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: duration, courtsUnavailability: courtsUnavailability) let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: duration, courtsUnavailability: courtsUnavailability)
if courtsUnavailable.contains(courtPosition) { if courtsUnavailable.contains(courtIndex) {
print("Returning false: Court \(courtIndex) unavailable due to schedule conflicts during \(rotationStartDate).") print("Returning false: Court \(courtIndex) unavailable due to schedule conflicts during \(rotationStartDate).")
return false return false
} }
@ -587,8 +595,12 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
let indexInRound = match.indexInRound() let indexInRound = match.indexInRound()
if roundObject.parent == nil && roundObject.index > 0 && indexInRound == 0, let nextMatch = match.next() {
if courtPosition < courts.count - 1 && canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate) { if shouldTryToFillUpCourtsAvailable == false {
if roundObject.parent == nil && roundObject.index > 1 && indexInRound == 0, let nextMatch = match.next() {
var nextMinimumTargetedEndDate = minimumTargetedEndDate
if courtPosition < courts.count - 1 && canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &nextMinimumTargetedEndDate) {
print("Returning true: Both current \(match.index) and next match \(nextMatch.index) can be played in rotation \(rotationIndex).") print("Returning true: Both current \(match.index) and next match \(nextMatch.index) can be played in rotation \(rotationIndex).")
return true return true
} else { } else {
@ -596,6 +608,7 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
return false return false
} }
} }
}
print("Returning true: Match \(match.roundAndMatchTitle()) can be played on court \(courtIndex).") print("Returning true: Match \(match.roundAndMatchTitle()) can be played on court \(courtIndex).")
return canBePlayed return canBePlayed
@ -622,15 +635,22 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
} }
if freeCourtPerRotation[rotationIndex]?.count == availableCourts { if freeCourtPerRotation[rotationIndex]?.count == courtsAvailable.count {
print("All courts in rotation \(rotationIndex) are free") print("All courts in rotation \(rotationIndex) are free, minimumTargetedEndDate : \(minimumTargetedEndDate)")
} }
if let courtsUnavailability {
let computedStartDateAndCourts = getFirstFreeCourt(startDate: minimumTargetedEndDate, duration: 0, courts: courts, courtsUnavailability: courtsUnavailability)
return computedStartDateAndCourts.earliestFreeDate
}
return minimumTargetedEndDate
} }
@discardableResult func updateBracketSchedule(tournament: Tournament, fromRoundId roundId: String?, fromMatchId matchId: String?, startDate: Date) -> Bool { @discardableResult func updateBracketSchedule(tournament: Tournament, fromRoundId roundId: String?, fromMatchId matchId: String?, startDate: Date) -> Bool {
let upperRounds: [Round] = tournament.rounds() let upperRounds: [Round] = tournament.rounds()
let allMatches: [Match] = tournament.allMatches() let allMatches: [Match] = tournament.allMatches().filter({ $0.hasEnded() == false && $0.hasStarted() == false })
var rounds = [Round]() var rounds = [Round]()
@ -651,7 +671,7 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
} }
let flattenedMatches = rounds.flatMap { round in let flattenedMatches = rounds.flatMap { round in
round._matches().filter({ $0.disabled == false }).sorted(by: \.index) round._matches().filter({ $0.disabled == false && $0.hasEnded() == false && $0.hasStarted() == false }).sorted(by: \.index)
} }
flattenedMatches.forEach({ flattenedMatches.forEach({
@ -709,7 +729,7 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
print("initial available courts at beginning: \(courts ?? [])") print("initial available courts at beginning: \(courts ?? [])")
let roundDispatch = self.roundDispatcher(numberOfCourtsAvailablePerRotation: tournament.courtCount, flattenedMatches: flattenedMatches, dispatcherStartDate: startDate, initialCourts: courts) let roundDispatch = self.roundDispatcher(flattenedMatches: flattenedMatches, dispatcherStartDate: startDate, initialCourts: courts)
roundDispatch.timedMatches.forEach { matchSchedule in roundDispatch.timedMatches.forEach { matchSchedule in
if let match = flattenedMatches.first(where: { $0.id == matchSchedule.matchID }) { if let match = flattenedMatches.first(where: { $0.id == matchSchedule.matchID }) {
@ -746,7 +766,50 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
}) })
} }
func getFirstFreeCourt(startDate: Date, duration: Int, courts: [Int], courtsUnavailability: [DateInterval]) -> (earliestFreeDate: Date, availableCourts: [Int]) {
var earliestEndDate: Date?
var availableCourtsAtEarliest: [Int] = []
// Iterate through each court and find the earliest time it becomes free
for courtIndex in courts {
let unavailabilityForCourt = courtsUnavailability.filter { $0.courtIndex == courtIndex }
var isAvailable = true
for interval in unavailabilityForCourt {
if interval.startDate <= startDate && interval.endDate > startDate {
isAvailable = false
if let currentEarliest = earliestEndDate {
earliestEndDate = min(currentEarliest, interval.endDate)
} else {
earliestEndDate = interval.endDate
}
}
}
// If the court is available at the start date, add it to the list of available courts
if isAvailable {
availableCourtsAtEarliest.append(courtIndex)
}
}
// If there are no unavailable courts, return the original start date and all courts
if let earliestEndDate = earliestEndDate {
// Find which courts will be available at the earliest free date
let courtsAvailableAtEarliest = courts.filter { courtIndex in
let unavailabilityForCourt = courtsUnavailability.filter { $0.courtIndex == courtIndex }
return unavailabilityForCourt.allSatisfy { $0.endDate <= earliestEndDate }
}
return (earliestFreeDate: earliestEndDate, availableCourts: courtsAvailableAtEarliest)
} else {
// If no courts were unavailable, all courts are available at the start date
return (earliestFreeDate: startDate.addingTimeInterval(Double(duration) * 60), availableCourts: courts)
}
}
func updateSchedule(tournament: Tournament) -> Bool { func updateSchedule(tournament: Tournament) -> Bool {
if tournament.courtCount < courtsAvailable.count {
courtsAvailable = Set(tournament.courtsAvailable())
}
var lastDate = tournament.startDate var lastDate = tournament.startDate
if tournament.groupStageCount > 0 { if tournament.groupStageCount > 0 {
lastDate = updateGroupStageSchedule(tournament: tournament) lastDate = updateGroupStageSchedule(tournament: tournament)
@ -777,6 +840,10 @@ struct TimeMatch {
let minutesToAdd = Double(durationLeft + (includeBreakTime ? minimumBreakTime : 0)) let minutesToAdd = Double(durationLeft + (includeBreakTime ? minimumBreakTime : 0))
return startDate.addingTimeInterval(minutesToAdd * 60.0) return startDate.addingTimeInterval(minutesToAdd * 60.0)
} }
var computedEndDateForSorting: Date {
estimatedEndDate(includeBreakTime: false)
}
} }
struct GroupStageMatchDispatcher { struct GroupStageMatchDispatcher {
@ -801,4 +868,16 @@ extension Match {
func containsTeamId(_ id: String) -> Bool { func containsTeamId(_ id: String) -> Bool {
return teamIds().contains(id) return teamIds().contains(id)
} }
func containsTeamIndex(_ id: String) -> Bool {
matchUp().contains(id)
}
func matchUp() -> [String] {
guard let groupStageObject else {
return []
}
return groupStageObject._matchUp(for: index).map { groupStageObject.id + "_\($0)" }
}
} }

@ -67,18 +67,18 @@ final class PlayerRegistration: BasePlayerRegistration, SideStorable {
internal init(importedPlayer: ImportedPlayer) { internal init(importedPlayer: ImportedPlayer) {
super.init() super.init()
self.teamRegistration = "" self.teamRegistration = ""
self.firstName = (importedPlayer.firstName ?? "").trimmed.capitalized self.firstName = (importedPlayer.firstName ?? "").prefixTrimmed(50).capitalized
self.lastName = (importedPlayer.lastName ?? "").trimmed.uppercased() self.lastName = (importedPlayer.lastName ?? "").prefixTrimmed(50).uppercased()
self.licenceId = importedPlayer.license ?? nil self.licenceId = importedPlayer.license?.prefixTrimmed(50) ?? nil
self.rank = Int(importedPlayer.rank) self.rank = Int(importedPlayer.rank)
self.sex = importedPlayer.male ? .male : .female self.sex = importedPlayer.male ? .male : .female
self.tournamentPlayed = importedPlayer.tournamentPlayed self.tournamentPlayed = importedPlayer.tournamentPlayed
self.points = importedPlayer.getPoints() self.points = importedPlayer.getPoints()
self.clubName = importedPlayer.clubName self.clubName = importedPlayer.clubName?.prefixTrimmed(200)
self.ligueName = importedPlayer.ligueName self.ligueName = importedPlayer.ligueName?.prefixTrimmed(200)
self.assimilation = importedPlayer.assimilation self.assimilation = importedPlayer.assimilation?.prefixTrimmed(50)
self.source = .frenchFederation self.source = .frenchFederation
self.birthdate = importedPlayer.birthYear self.birthdate = importedPlayer.birthYear?.prefixTrimmed(50)
} }
internal init?(federalData: [String], sex: Int, sexUnknown: Bool) { internal init?(federalData: [String], sex: Int, sexUnknown: Bool) {
@ -86,11 +86,11 @@ final class PlayerRegistration: BasePlayerRegistration, SideStorable {
let _lastName = federalData[0].trimmed.uppercased() let _lastName = federalData[0].trimmed.uppercased()
let _firstName = federalData[1].trimmed.capitalized let _firstName = federalData[1].trimmed.capitalized
if _lastName.isEmpty && _firstName.isEmpty { return nil } if _lastName.isEmpty && _firstName.isEmpty { return nil }
lastName = _lastName lastName = _lastName.prefixTrimmed(50)
firstName = _firstName firstName = _firstName.prefixTrimmed(50)
birthdate = federalData[2].formattedAsBirthdate() birthdate = federalData[2].formattedAsBirthdate().prefixTrimmed(50)
licenceId = federalData[3] licenceId = federalData[3].prefixTrimmed(50)
clubName = federalData[4] clubName = federalData[4].prefixTrimmed(200)
let stringRank = federalData[5] let stringRank = federalData[5]
if stringRank.isEmpty { if stringRank.isEmpty {
rank = nil rank = nil
@ -99,11 +99,11 @@ final class PlayerRegistration: BasePlayerRegistration, SideStorable {
} }
let _email = federalData[6] let _email = federalData[6]
if _email.isEmpty == false { if _email.isEmpty == false {
self.email = _email self.email = _email.prefixTrimmed(50)
} }
let _phoneNumber = federalData[7] let _phoneNumber = federalData[7]
if _phoneNumber.isEmpty == false { if _phoneNumber.isEmpty == false {
self.phoneNumber = _phoneNumber self.phoneNumber = _phoneNumber.prefixTrimmed(50)
} }
source = .beachPadel source = .beachPadel
@ -168,12 +168,27 @@ final class PlayerRegistration: BasePlayerRegistration, SideStorable {
} }
func contains(_ searchField: String) -> Bool { func contains(_ searchField: String) -> Bool {
firstName.localizedCaseInsensitiveContains(searchField) || lastName.localizedCaseInsensitiveContains(searchField) let nameComponents = searchField.canonicalVersion.split(separator: " ")
if nameComponents.count > 1 {
let pairs = nameComponents.pairs()
return pairs.contains(where: {
(firstName.canonicalVersion.localizedCaseInsensitiveContains(String($0)) &&
lastName.canonicalVersion.localizedCaseInsensitiveContains(String($1))) ||
(firstName.canonicalVersion.localizedCaseInsensitiveContains(String($1)) &&
lastName.canonicalVersion.localizedCaseInsensitiveContains(String($0)))
})
} else {
return nameComponents.contains { component in
firstName.canonicalVersion.localizedCaseInsensitiveContains(component) ||
lastName.canonicalVersion.localizedCaseInsensitiveContains(component)
}
}
} }
func isSameAs(_ player: PlayerRegistration) -> Bool { func isSameAs(_ player: PlayerRegistration) -> Bool {
firstName.trimmedMultiline.localizedCaseInsensitiveCompare(player.firstName.trimmedMultiline) == .orderedSame && firstName.trimmedMultiline.canonicalVersion.localizedCaseInsensitiveCompare(player.firstName.trimmedMultiline.canonicalVersion) == .orderedSame &&
lastName.trimmedMultiline.localizedCaseInsensitiveCompare(player.lastName.trimmedMultiline) == .orderedSame lastName.trimmedMultiline.canonicalVersion.localizedCaseInsensitiveCompare(player.lastName.trimmedMultiline.canonicalVersion) == .orderedSame
} }
func tournament() -> Tournament? { func tournament() -> Tournament? {
@ -186,6 +201,10 @@ final class PlayerRegistration: BasePlayerRegistration, SideStorable {
return self.tournamentStore.teamRegistrations.findById(teamRegistration) return self.tournamentStore.teamRegistrations.findById(teamRegistration)
} }
func isHere() -> Bool {
hasArrived
}
func hasPaid() -> Bool { func hasPaid() -> Bool {
paymentType != nil paymentType != nil
} }
@ -313,7 +332,7 @@ final class PlayerRegistration: BasePlayerRegistration, SideStorable {
if let currentLicenceId = licenceId { if let currentLicenceId = licenceId {
if currentLicenceId.trimmed.hasSuffix("(\(year-1))") { if currentLicenceId.trimmed.hasSuffix("(\(year-1))") {
self.licenceId = currentLicenceId.replacingOccurrences(of: "\(year-1)", with: "\(year)") self.licenceId = currentLicenceId.replacingOccurrences(of: "\(year-1)", with: "\(year)")
} else if let computedLicense = currentLicenceId.strippedLicense { } else if let computedLicense = currentLicenceId.strippedLicense?.computedLicense {
self.licenceId = computedLicense + " (\(year))" self.licenceId = computedLicense + " (\(year))"
} }
} }
@ -392,14 +411,8 @@ final class PlayerRegistration: BasePlayerRegistration, SideStorable {
case 0: return 0 case 0: return 0
case womanMax: return manMax - womanMax case womanMax: return manMax - womanMax
case manMax: return 0 case manMax: return 0
case 1...10: return 400
case 11...30: return 1000
case 31...60: return 2000
case 61...100: return 3000
case 101...200: return 8000
case 201...500: return 12000
default: default:
return 15000 return TournamentCategory.femaleInMaleAssimilationAddition(playerRank)
} }
} }

@ -16,14 +16,14 @@ Dans Swift:
- Ajouter le champ dans classe - Ajouter le champ dans classe
- Ajouter le champ dans le constructeur si possible - Ajouter le champ dans le constructeur si possible
- Ajouter la codingKey correspondante - Ajouter la codingKey correspondante
- Ajouter le champ dans l'encoding - Ajouter le champ dans l'encoding/decoding
- Ouvrir **ServerDataTests** et ajouter un test sur le champ - Ouvrir **ServerDataTests** et ajouter un test sur le champ
- Pour que les tests sur les dates fonctionnent, on peut tester date.formatted() par exemple - Pour que les tests sur les dates fonctionnent, on peut tester date.formatted() par exemple
Dans Django: Dans Django:
- Ajouter le champ dans la classe - Ajouter le champ dans la classe
- Si c'est une ForeignKey, toujours mettre un related_name sinon la synchro casse - Si c'est une ForeignKey, toujours mettre un related_name sinon la synchro casse
- S'il c'est un champ dans **CustomUser**: - Si c'est un champ dans **CustomUser**:
- Ajouter le champ à la méthode fields_for_update - Ajouter le champ à la méthode fields_for_update
- Ajouter le champ dans UserSerializer > create > create_user dans serializers.py - Ajouter le champ dans UserSerializer > create > create_user dans serializers.py
- L'ajouter aussi dans admin.py si nécéssaire - L'ajouter aussi dans admin.py si nécéssaire

@ -12,22 +12,7 @@ import SwiftUI
@Observable @Observable
final class Round: BaseRound, SideStorable { final class Round: BaseRound, SideStorable {
// static func resourceName() -> String { "rounds" } var _cachedSeedInterval: SeedInterval?
// static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
// static func filterByStoreIdentifier() -> Bool { return true }
// static var relationshipNames: [String] = []
//
// var id: String = Store.randomId()
// var lastUpdate: Date
// var tournament: String
// var index: Int
// var parent: String?
// private(set) var format: MatchFormat?
// var startDate: Date?
// var groupStageLoserBracket: Bool = false
// var loserBracketMode: LoserBracketMode = .automatic
//
// var storeId: String? = nil
internal init(tournament: String, index: Int, parent: String? = nil, matchFormat: MatchFormat? = nil, startDate: Date? = nil, groupStageLoserBracket: Bool = false, loserBracketMode: LoserBracketMode = .automatic) { internal init(tournament: String, index: Int, parent: String? = nil, matchFormat: MatchFormat? = nil, startDate: Date? = nil, groupStageLoserBracket: Bool = false, loserBracketMode: LoserBracketMode = .automatic) {
@ -130,8 +115,7 @@ final class Round: BaseRound, SideStorable {
func seed(_ team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? { func seed(_ team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? {
return self.tournamentStore.teamRegistrations.first(where: { return self.tournamentStore.teamRegistrations.first(where: {
$0.tournament == tournament $0.bracketPosition != nil
&& $0.bracketPosition != nil
&& ($0.bracketPosition! / 2) == matchIndex && ($0.bracketPosition! / 2) == matchIndex
&& ($0.bracketPosition! % 2) == team.rawValue && ($0.bracketPosition! % 2) == team.rawValue
}) })
@ -156,13 +140,22 @@ final class Round: BaseRound, SideStorable {
let initialMatchIndex = RoundRule.matchIndex(fromRoundIndex: index) let initialMatchIndex = RoundRule.matchIndex(fromRoundIndex: index)
let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: index) let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: index)
return self.tournamentStore.teamRegistrations.filter { return self.tournamentStore.teamRegistrations.filter {
$0.tournament == tournament $0.bracketPosition != nil
&& $0.bracketPosition != nil
&& ($0.bracketPosition! / 2) >= initialMatchIndex && ($0.bracketPosition! / 2) >= initialMatchIndex
&& ($0.bracketPosition! / 2) < initialMatchIndex + numberOfMatches && ($0.bracketPosition! / 2) < initialMatchIndex + numberOfMatches
} }
} }
func teamsOrSeeds() -> [TeamRegistration] {
let seeds = seeds()
if seeds.isEmpty {
return playedMatches().flatMap({ $0.teams() })
} else {
return seeds
}
}
func losers() -> [TeamRegistration] { func losers() -> [TeamRegistration] {
let teamIds: [String] = self._matches().compactMap { $0.losingTeamId } let teamIds: [String] = self._matches().compactMap { $0.losingTeamId }
return teamIds.compactMap { self.tournamentStore.teamRegistrations.findById($0) } return teamIds.compactMap { self.tournamentStore.teamRegistrations.findById($0) }
@ -459,6 +452,8 @@ defer {
func correspondingLoserRoundTitle(_ displayStyle: DisplayStyle = .wide) -> String { func correspondingLoserRoundTitle(_ displayStyle: DisplayStyle = .wide) -> String {
if let _cachedSeedInterval { return _cachedSeedInterval.localizedLabel(displayStyle) }
#if _DEBUG_TIME //DEBUGING TIME #if _DEBUG_TIME //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
@ -477,8 +472,16 @@ defer {
// && $0.bracketPosition != nil // && $0.bracketPosition != nil
// && ($0.bracketPosition! / 2) < initialMatchIndexFromRoundIndex // && ($0.bracketPosition! / 2) < initialMatchIndexFromRoundIndex
// }) // })
var seedsCount = seedsAfterThisRound.count
if seedsAfterThisRound.isEmpty {
let nextRoundsDisableMatches = nextRoundsDisableMatches()
seedsCount = disabledMatches().count - nextRoundsDisableMatches
}
let playedMatches = playedMatches() let playedMatches = playedMatches()
let seedInterval = SeedInterval(first: playedMatches.count + seedsAfterThisRound.count + 1, last: playedMatches.count * 2 + seedsAfterThisRound.count) let seedInterval = SeedInterval(first: playedMatches.count + seedsCount + 1, last: playedMatches.count * 2 + seedsCount)
_cachedSeedInterval = seedInterval
return seedInterval.localizedLabel(displayStyle) return seedInterval.localizedLabel(displayStyle)
} }
@ -500,6 +503,8 @@ defer {
} }
func seedInterval(initialMode: Bool = false) -> SeedInterval? { func seedInterval(initialMode: Bool = false) -> SeedInterval? {
if initialMode == false, let _cachedSeedInterval { return _cachedSeedInterval }
#if _DEBUG_TIME //DEBUGING TIME #if _DEBUG_TIME //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
@ -511,17 +516,32 @@ defer {
if isUpperBracket() { if isUpperBracket() {
if index == 0 { return SeedInterval(first: 1, last: 2) } if index == 0 { return SeedInterval(first: 1, last: 2) }
let initialMatchIndexFromRoundIndex = RoundRule.matchIndex(fromRoundIndex: index) let initialMatchIndexFromRoundIndex = RoundRule.matchIndex(fromRoundIndex: index)
if initialMode {
let playedMatches = RoundRule.numberOfMatches(forRoundIndex: index)
let seedInterval = SeedInterval(first: playedMatches + 1, last: playedMatches * 2)
//print(seedInterval.localizedLabel())
return seedInterval
} else {
let seedsAfterThisRound : [TeamRegistration] = self.tournamentStore.teamRegistrations.filter { let seedsAfterThisRound : [TeamRegistration] = self.tournamentStore.teamRegistrations.filter {
$0.bracketPosition != nil $0.bracketPosition != nil
&& ($0.bracketPosition! / 2) < initialMatchIndexFromRoundIndex && ($0.bracketPosition! / 2) < initialMatchIndexFromRoundIndex
} }
let playedMatches = playedMatches().count var seedsCount = seedsAfterThisRound.count
let minimumMatches = initialMode ? RoundRule.numberOfMatches(forRoundIndex: index) : playedMatches * 2 if seedsAfterThisRound.isEmpty {
let nextRoundsDisableMatches = nextRoundsDisableMatches()
seedsCount = disabledMatches().count - nextRoundsDisableMatches
}
let playedMatches = playedMatches()
//print("playedMatches \(playedMatches)", initialMode, parent, parentRound?.roundTitle(), seedsAfterThisRound.count) //print("playedMatches \(playedMatches)", initialMode, parent, parentRound?.roundTitle(), seedsAfterThisRound.count)
let seedInterval = SeedInterval(first: playedMatches + seedsAfterThisRound.count + 1, last: minimumMatches + seedsAfterThisRound.count) let seedInterval = SeedInterval(first: playedMatches.count + seedsCount + 1, last: playedMatches.count * 2 + seedsCount)
//print(seedInterval.localizedLabel()) //print(seedInterval.localizedLabel())
_cachedSeedInterval = seedInterval
return seedInterval return seedInterval
}
} }
if let previousRound = previousRound() { if let previousRound = previousRound() {
@ -642,6 +662,14 @@ defer {
return self.tournamentStore.rounds.findById(parent) return self.tournamentStore.rounds.findById(parent)
} }
func nextRoundsDisableMatches() -> Int {
if parent == nil, index > 0 {
return tournamentObject()?.rounds().suffix(index).flatMap { $0.disabledMatches() }.count ?? 0
} else {
return 0
}
}
func updateMatchFormat(_ updatedMatchFormat: MatchFormat, checkIfPossible: Bool, andLoserBracket: Bool) { func updateMatchFormat(_ updatedMatchFormat: MatchFormat, checkIfPossible: Bool, andLoserBracket: Bool) {
if updatedMatchFormat.weight < self.matchFormat.weight { if updatedMatchFormat.weight < self.matchFormat.weight {
updateMatchFormatAndAllMatches(updatedMatchFormat) updateMatchFormatAndAllMatches(updatedMatchFormat)

@ -50,7 +50,7 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
// self.storeId = tournament // self.storeId = tournament
self.tournament = tournament self.tournament = tournament
self.groupStage = groupStage self.groupStage = groupStage
self.registrationDate = registrationDate self.registrationDate = registrationDate ?? Date()
self.callDate = callDate self.callDate = callDate
self.bracketPosition = bracketPosition self.bracketPosition = bracketPosition
self.groupStagePosition = groupStagePosition self.groupStagePosition = groupStagePosition
@ -120,13 +120,39 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
} }
func setSeedPosition(inSpot match: Match, slot: TeamPosition?, opposingSeeding: Bool) { func setSeedPosition(inSpot match: Match, slot: TeamPosition?, opposingSeeding: Bool) {
let seedPosition: Int = match.lockAndGetSeedPosition(atTeamPosition: slot, opposingSeeding: opposingSeeding) var teamPosition : TeamPosition {
if let slot {
return slot
} else {
let matchIndex = match.index
let seedRound = RoundRule.roundIndex(fromMatchIndex: matchIndex)
let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: seedRound)
let isUpper = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) < (numberOfMatches / 2)
var teamPosition = slot ?? (isUpper ? .one : .two)
if opposingSeeding {
teamPosition = slot ?? (isUpper ? .two : .one)
}
return teamPosition
}
}
let seedPosition: Int = match.lockAndGetSeedPosition(atTeamPosition: teamPosition)
tournamentObject()?.resetTeamScores(in: bracketPosition) tournamentObject()?.resetTeamScores(in: bracketPosition)
self.bracketPosition = seedPosition self.bracketPosition = seedPosition
if groupStagePosition != nil && qualified == false { if groupStagePosition != nil && qualified == false {
qualified = true qualified = true
} }
tournamentObject()?.updateTeamScores(in: bracketPosition) if let tournament = tournamentObject() {
if let index = index(in: tournament.selectedSortedTeams()) {
let drawLog = DrawLog(tournament: tournament.id, drawSeed: index, drawMatchIndex: match.index, drawTeamPosition: teamPosition, drawType: .seed)
do {
try tournamentStore.drawLogs.addOrUpdate(instance: drawLog)
} catch {
Logger.error(error)
}
}
tournament.updateTeamScores(in: bracketPosition)
}
} }
func expectedSummonDate() -> Date? { func expectedSummonDate() -> Date? {
@ -432,7 +458,7 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
self.tournamentStore.playerRegistrations.filter { $0.teamRegistration == self.id }.sorted { (lhs, rhs) in self.tournamentStore.playerRegistrations.filter { $0.teamRegistration == self.id }.sorted { (lhs, rhs) in
let predicates: [AreInIncreasingOrder] = [ let predicates: [AreInIncreasingOrder] = [
{ $0.sex?.rawValue ?? 0 < $1.sex?.rawValue ?? 0 }, { $0.sex?.rawValue ?? 0 < $1.sex?.rawValue ?? 0 },
{ $0.rank ?? 0 < $1.rank ?? 0 }, { $0.rank ?? Int.max < $1.rank ?? Int.max },
{ $0.lastName < $1.lastName}, { $0.lastName < $1.lastName},
{ $0.firstName < $1.firstName } { $0.firstName < $1.firstName }
] ]
@ -515,59 +541,50 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
return nil return nil
} }
// enum CodingKeys: String, CodingKey { func wildcardLabel() -> String? {
// case _id = "id" if isWildCard() {
// case _lastUpdate = "lastUpdate" let wildcardLabel: String = ["wildcard", (wildCardBracket ? "tableau" : "poule")].joined(separator: " ")
// case _storeId = "storeId" return wildcardLabel
// case _tournament = "tournament" } else {
// case _groupStage = "groupStage" return nil
// case _registrationDate = "registrationDate" }
// case _callDate = "callDate" }
// case _bracketPosition = "bracketPosition"
// case _groupStagePosition = "groupStagePosition" var _cachedRestingTime: (Bool, Date?)?
// case _comment = "comment"
// case _source = "source" func restingTime() -> Date? {
// case _sourceValue = "sourceValue" if let _cachedRestingTime { return _cachedRestingTime.1 }
// case _logo = "logo" let restingTime = matches().filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).last?.endDate
// case _name = "name" _cachedRestingTime = (true, restingTime)
// case _wildCardBracket = "wildCardBracket" return restingTime
// case _wildCardGroupStage = "wildCardGroupStage" }
// case _weight = "weight"
// case _walkOut = "walkOut" func resetRestingTime() {
// case _lockedWeight = "lockedWeight" _cachedRestingTime = nil
// case _confirmationDate = "confirmationDate" }
// case _qualified = "qualified"
// case _finalRanking = "finalRanking" var restingTimeForSorting: Date {
// case _pointsEarned = "pointsEarned" restingTime()!
// } }
//
// func encode(to encoder: Encoder) throws { func teamNameLabel() -> String {
// var container = encoder.container(keyedBy: CodingKeys.self) if let name, name.isEmpty == false {
// return name
// try container.encode(id, forKey: ._id) } else {
// try container.encode(storeId, forKey: ._storeId) return "Toute l'équipe"
// try container.encode(lastUpdate, forKey: ._lastUpdate) }
// try container.encode(tournament, forKey: ._tournament) }
// try container.encode(groupStage, forKey: ._groupStage)
// try container.encode(registrationDate, forKey: ._registrationDate) func isDifferentPosition(_ drawMatchIndex: Int?) -> Bool {
// try container.encode(callDate, forKey: ._callDate) if let bracketPosition, let drawMatchIndex {
// try container.encode(bracketPosition, forKey: ._bracketPosition) return drawMatchIndex != bracketPosition
// try container.encode(groupStagePosition, forKey: ._groupStagePosition) } else if let bracketPosition {
// try container.encode(comment, forKey: ._comment) return true
// try container.encode(source, forKey: ._source) } else if let drawMatchIndex {
// try container.encode(sourceValue, forKey: ._sourceValue) return true
// try container.encode(logo, forKey: ._logo) }
// try container.encode(name, forKey: ._name) return false
// try container.encode(walkOut, forKey: ._walkOut) }
// try container.encode(wildCardBracket, forKey: ._wildCardBracket)
// try container.encode(wildCardGroupStage, forKey: ._wildCardGroupStage)
// try container.encode(weight, forKey: ._weight)
// try container.encode(lockedWeight, forKey: ._lockedWeight)
// try container.encode(confirmationDate, forKey: ._confirmationDate)
// try container.encode(qualified, forKey: ._qualified)
// try container.encode(finalRanking, forKey: ._finalRanking)
// try container.encode(pointsEarned, forKey: ._pointsEarned)
// }
func insertOnServer() { func insertOnServer() {
self.tournamentStore.teamRegistrations.writeChangeAndInsertOnServer(instance: self) self.tournamentStore.teamRegistrations.writeChangeAndInsertOnServer(instance: self)

@ -1,5 +1,5 @@
// //
// Tournament.swift // swift
// PadelClub // PadelClub
// //
// Created by Laurent Morvillier on 02/02/2024. // Created by Laurent Morvillier on 02/02/2024.
@ -12,115 +12,21 @@ import SwiftUI
@Observable @Observable
final class Tournament: BaseTournament { final class Tournament: BaseTournament {
// static func resourceName() -> String { "tournaments" }
// static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
// static func filterByStoreIdentifier() -> Bool { return false }
// static var relationshipNames: [String] = []
//
// var id: String = Store.randomId()
// var lastUpdate: Date
// var event: String?
// var name: String?
// var startDate: Date
// var endDate: Date?
// private(set) var creationDate: Date
// var isPrivate: Bool
// private(set) var groupStageFormat: MatchFormat?
// private(set) var roundFormat: MatchFormat?
// private(set) var loserRoundFormat: MatchFormat?
// var groupStageSortMode: GroupStageOrderingMode
// var groupStageCount: Int
// var rankSourceDate: Date?
// var dayDuration: Int
// var teamCount: Int
// var teamSorting: TeamSortingType
// var federalCategory: TournamentCategory
// var federalLevelCategory: TournamentLevel
// var federalAgeCategory: FederalTournamentAge
// var closedRegistrationDate: Date?
// var groupStageAdditionalQualified: Int
// var courtCount: Int = 2
// var prioritizeClubMembers: Bool
// var qualifiedPerGroupStage: Int
// var teamsPerGroupStage: Int
// var entryFee: Double?
// var payment: TournamentPayment? = nil
// var additionalEstimationDuration: Int = 0
// var isDeleted: Bool = false
// var isCanceled: Bool = false
// var publishTeams: Bool = false
// //var publishWaitingList: Bool = false
// var publishSummons: Bool = false
// var publishGroupStages: Bool = false
// var publishBrackets: Bool = false
// var shouldVerifyGroupStage: Bool = false
// var shouldVerifyBracket: Bool = false
// var hideTeamsWeight: Bool = false
// var publishTournament: Bool = false
// var hidePointsEarned: Bool = false
// var publishRankings: Bool = false
// var loserBracketMode: LoserBracketMode = .automatic
@ObservationIgnored @ObservationIgnored
var navigationPath: [Screen] = [] var navigationPath: [Screen] = []
// enum CodingKeys: String, CodingKey { internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = false, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false, shouldVerifyBracket: Bool = false, shouldVerifyGroupStage: Bool = false, hideTeamsWeight: Bool = false, publishTournament: Bool = false, hidePointsEarned: Bool = false, publishRankings: Bool = false, loserBracketMode: LoserBracketMode = .automatic, initialSeedRound: Int = 0, initialSeedCount: Int = 0) {
// case _id = "id"
// case _lastUpdate = "lastUpdate"
// case _event = "event"
// case _creator = "creator"
// case _name = "name"
// case _startDate = "startDate"
// case _endDate = "endDate"
// case _creationDate = "creationDate"
// case _isPrivate = "isPrivate"
// case _groupStageFormat = "groupStageFormat"
// case _roundFormat = "roundFormat"
// case _loserRoundFormat = "loserRoundFormat"
// case _groupStageSortMode = "groupStageSortMode"
// case _groupStageCount = "groupStageCount"
// case _rankSourceDate = "rankSourceDate"
// case _dayDuration = "dayDuration"
// case _teamCount = "teamCount"
// case _teamSorting = "teamSorting"
// case _federalCategory = "federalCategory"
// case _federalLevelCategory = "federalLevelCategory"
// case _federalAgeCategory = "federalAgeCategory"
// case _seedCount = "seedCount"
// case _closedRegistrationDate = "closedRegistrationDate"
// case _groupStageAdditionalQualified = "groupStageAdditionalQualified"
// case _courtCount = "courtCount"
// case _prioritizeClubMembers = "prioritizeClubMembers"
// case _qualifiedPerGroupStage = "qualifiedPerGroupStage"
// case _teamsPerGroupStage = "teamsPerGroupStage"
// case _entryFee = "entryFee"
// case _additionalEstimationDuration = "additionalEstimationDuration"
// case _isDeleted = "isDeleted"
// case _isCanceled = "localId"
// case _payment = "globalId"
// case _publishTeams = "publishTeams"
// //case _publishWaitingList = "publishWaitingList"
// case _publishSummons = "publishSummons"
// case _publishGroupStages = "publishGroupStages"
// case _publishBrackets = "publishBrackets"
// case _shouldVerifyGroupStage = "shouldVerifyGroupStage"
// case _shouldVerifyBracket = "shouldVerifyBracket"
// case _hideTeamsWeight = "hideTeamsWeight"
// case _publishTournament = "publishTournament"
// case _hidePointsEarned = "hidePointsEarned"
// case _publishRankings = "publishRankings"
// case _loserBracketMode = "loserBracketMode"
// }
internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = false, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false, shouldVerifyBracket: Bool = false, shouldVerifyGroupStage: Bool = false, hideTeamsWeight: Bool = false, publishTournament: Bool = false, hidePointsEarned: Bool = false, publishRankings: Bool = false, loserBracketMode: LoserBracketMode = .automatic) {
super.init() super.init()
self.event = event self.event = event
self.name = name self.name = name
self.startDate = startDate self.startDate = startDate
self.endDate = endDate self.endDate = endDate
self.creationDate = creationDate self.creationDate = creationDate
#if DEBUG
self.isPrivate = false
#else
self.isPrivate = Guard.main.purchasedTransactions.isEmpty self.isPrivate = Guard.main.purchasedTransactions.isEmpty
#endif
self.groupStageFormat = groupStageFormat self.groupStageFormat = groupStageFormat
self.roundFormat = roundFormat self.roundFormat = roundFormat
self.loserRoundFormat = loserRoundFormat self.loserRoundFormat = loserRoundFormat
@ -142,198 +48,47 @@ final class Tournament: BaseTournament {
self.entryFee = entryFee self.entryFee = entryFee
self.additionalEstimationDuration = additionalEstimationDuration self.additionalEstimationDuration = additionalEstimationDuration
self.isDeleted = isDeleted self.isDeleted = isDeleted
#if DEBUG
self.publishTeams = true
self.publishSummons = true
self.publishBrackets = true
self.publishGroupStages = true
self.publishRankings = true
self.publishTournament = true
#else
self.publishTeams = publishTeams self.publishTeams = publishTeams
self.publishSummons = publishSummons self.publishSummons = publishSummons
self.publishBrackets = publishBrackets self.publishBrackets = publishBrackets
self.publishGroupStages = publishGroupStages self.publishGroupStages = publishGroupStages
self.publishRankings = publishRankings
self.publishTournament = publishTournament
#endif
self.shouldVerifyBracket = shouldVerifyBracket self.shouldVerifyBracket = shouldVerifyBracket
self.shouldVerifyGroupStage = shouldVerifyGroupStage self.shouldVerifyGroupStage = shouldVerifyGroupStage
self.hideTeamsWeight = hideTeamsWeight self.hideTeamsWeight = hideTeamsWeight
self.publishTournament = publishTournament
self.hidePointsEarned = hidePointsEarned self.hidePointsEarned = hidePointsEarned
self.publishRankings = publishRankings
self.loserBracketMode = loserBracketMode self.loserBracketMode = loserBracketMode
self.initialSeedRound = initialSeedRound
self.initialSeedCount = initialSeedCount
} }
required init(from decoder: Decoder) throws { required init(from decoder: Decoder) throws {
try super.init(from: decoder) try super.init(from: decoder)
} }
// required init(from decoder: Decoder) throws {
// let container = try decoder.container(keyedBy: CodingKeys.self)
// id = try container.decode(String.self, forKey: ._id)
// lastUpdate = try container.decode(Date.self, forKey: ._lastUpdate)
// event = try container.decodeIfPresent(String.self, forKey: ._event)
// name = try container.decodeIfPresent(String.self, forKey: ._name)
// startDate = try container.decode(Date.self, forKey: ._startDate)
// endDate = try container.decodeIfPresent(Date.self, forKey: ._endDate)
// creationDate = try container.decode(Date.self, forKey: ._creationDate)
// isPrivate = try container.decode(Bool.self, forKey: ._isPrivate)
// groupStageFormat = try container.decodeIfPresent(MatchFormat.self, forKey: ._groupStageFormat)
// roundFormat = try container.decodeIfPresent(MatchFormat.self, forKey: ._roundFormat)
// loserRoundFormat = try container.decodeIfPresent(MatchFormat.self, forKey: ._loserRoundFormat)
// groupStageSortMode = try container.decode(GroupStageOrderingMode.self, forKey: ._groupStageSortMode)
// groupStageCount = try container.decode(Int.self, forKey: ._groupStageCount)
// rankSourceDate = try container.decodeIfPresent(Date.self, forKey: ._rankSourceDate)
// dayDuration = try container.decode(Int.self, forKey: ._dayDuration)
// teamCount = try container.decode(Int.self, forKey: ._teamCount)
// teamSorting = try container.decode(TeamSortingType.self, forKey: ._teamSorting)
// federalCategory = try container.decode(TournamentCategory.self, forKey: ._federalCategory)
// federalLevelCategory = try container.decode(TournamentLevel.self, forKey: ._federalLevelCategory)
// federalAgeCategory = try container.decode(FederalTournamentAge.self, forKey: ._federalAgeCategory)
// closedRegistrationDate = try container.decodeIfPresent(Date.self, forKey: ._closedRegistrationDate)
// groupStageAdditionalQualified = try container.decode(Int.self, forKey: ._groupStageAdditionalQualified)
// courtCount = try container.decode(Int.self, forKey: ._courtCount)
// prioritizeClubMembers = try container.decode(Bool.self, forKey: ._prioritizeClubMembers)
// qualifiedPerGroupStage = try container.decode(Int.self, forKey: ._qualifiedPerGroupStage)
// teamsPerGroupStage = try container.decode(Int.self, forKey: ._teamsPerGroupStage)
// entryFee = try container.decodeIfPresent(Double.self, forKey: ._entryFee)
// payment = try Tournament._decodePayment(container: container)
// additionalEstimationDuration = try container.decode(Int.self, forKey: ._additionalEstimationDuration)
// isDeleted = try container.decode(Bool.self, forKey: ._isDeleted)
// isCanceled = try Tournament._decodeCanceled(container: container)
// publishTeams = try container.decodeIfPresent(Bool.self, forKey: ._publishTeams) ?? false
// publishSummons = try container.decodeIfPresent(Bool.self, forKey: ._publishSummons) ?? false
// publishGroupStages = try container.decodeIfPresent(Bool.self, forKey: ._publishGroupStages) ?? false
// publishBrackets = try container.decodeIfPresent(Bool.self, forKey: ._publishBrackets) ?? false
// shouldVerifyBracket = try container.decodeIfPresent(Bool.self, forKey: ._shouldVerifyBracket) ?? false
// shouldVerifyGroupStage = try container.decodeIfPresent(Bool.self, forKey: ._shouldVerifyGroupStage) ?? false
// hideTeamsWeight = try container.decodeIfPresent(Bool.self, forKey: ._hideTeamsWeight) ?? false
// publishTournament = try container.decodeIfPresent(Bool.self, forKey: ._publishTournament) ?? false
// hidePointsEarned = try container.decodeIfPresent(Bool.self, forKey: ._hidePointsEarned) ?? false
// publishRankings = try container.decodeIfPresent(Bool.self, forKey: ._publishRankings) ?? false
// loserBracketMode = try container.decodeIfPresent(LoserBracketMode.self, forKey: ._loserBracketMode) ?? .automatic
// }
//
// fileprivate static let _numberFormatter: NumberFormatter = NumberFormatter()
//
// fileprivate static func _decodePayment(container: KeyedDecodingContainer<CodingKeys>) throws -> TournamentPayment? {
// let data = try container.decodeIfPresent(Data.self, forKey: ._payment)
//
// if let data {
// do {
// let decoded: String = try data.decryptData(pass: CryptoKey.pass.rawValue)
// let sequence = decoded.compactMap { _numberFormatter.number(from: String($0))?.intValue }
// return TournamentPayment(rawValue: sequence[18])
// } catch {
// Logger.error(error)
// }
// }
// return nil
// }
//
// fileprivate static func _decodeCanceled(container: KeyedDecodingContainer<CodingKeys>) throws -> Bool {
// let data = try container.decodeIfPresent(Data.self, forKey: ._isCanceled)
// if let data {
// do {
// let decoded: String = try data.decryptData(pass: CryptoKey.pass.rawValue)
// let sequence = decoded.compactMap { _numberFormatter.number(from: String($0))?.intValue }
// return Bool.decodeInt(sequence[18])
// } catch {
// Logger.error(error)
// }
// }
// return false
// }
//
// func encode(to encoder: Encoder) throws {
// var container = encoder.container(keyedBy: CodingKeys.self)
//
// try container.encode(id, forKey: ._id)
// try container.encode(lastUpdate, forKey: ._lastUpdate)
// try container.encode(event, forKey: ._event)
// try container.encode(name, forKey: ._name)
//
// try container.encode(startDate, forKey: ._startDate)
// try container.encode(endDate, forKey: ._endDate)
//
// try container.encode(creationDate, forKey: ._creationDate)
// try container.encode(isPrivate, forKey: ._isPrivate)
//
// try container.encode(groupStageFormat, forKey: ._groupStageFormat)
// try container.encode(roundFormat, forKey: ._roundFormat)
// try container.encode(loserRoundFormat, forKey: ._loserRoundFormat)
//
// try container.encode(groupStageSortMode, forKey: ._groupStageSortMode)
// try container.encode(groupStageCount, forKey: ._groupStageCount)
//
// try container.encode(rankSourceDate, forKey: ._rankSourceDate)
//
// try container.encode(dayDuration, forKey: ._dayDuration)
// try container.encode(teamCount, forKey: ._teamCount)
// try container.encode(teamSorting, forKey: ._teamSorting)
// try container.encode(federalCategory, forKey: ._federalCategory)
// try container.encode(federalLevelCategory, forKey: ._federalLevelCategory)
// try container.encode(federalAgeCategory, forKey: ._federalAgeCategory)
//
// try container.encode(closedRegistrationDate, forKey: ._closedRegistrationDate)
//
// try container.encode(groupStageAdditionalQualified, forKey: ._groupStageAdditionalQualified)
// try container.encode(courtCount, forKey: ._courtCount)
// try container.encode(prioritizeClubMembers, forKey: ._prioritizeClubMembers)
// try container.encode(qualifiedPerGroupStage, forKey: ._qualifiedPerGroupStage)
// try container.encode(teamsPerGroupStage, forKey: ._teamsPerGroupStage)
// try container.encode(entryFee, forKey: ._entryFee)
//
// try self._encodePayment(container: &container)
// try container.encode(additionalEstimationDuration, forKey: ._additionalEstimationDuration)
// try container.encode(isDeleted, forKey: ._isDeleted)
// try self._encodeIsCanceled(container: &container)
// try container.encode(publishTeams, forKey: ._publishTeams)
// try container.encode(publishSummons, forKey: ._publishSummons)
// try container.encode(publishBrackets, forKey: ._publishBrackets)
// try container.encode(publishGroupStages, forKey: ._publishGroupStages)
// try container.encode(shouldVerifyBracket, forKey: ._shouldVerifyBracket)
// try container.encode(shouldVerifyGroupStage, forKey: ._shouldVerifyGroupStage)
// try container.encode(hideTeamsWeight, forKey: ._hideTeamsWeight)
// try container.encode(publishTournament, forKey: ._publishTournament)
// try container.encode(hidePointsEarned, forKey: ._hidePointsEarned)
// try container.encode(publishRankings, forKey: ._publishRankings)
// try container.encode(loserBracketMode, forKey: ._loserBracketMode)
// }
//
// fileprivate func _encodePayment(container: inout KeyedEncodingContainer<CodingKeys>) throws {
//
// guard let payment else {
// try container.encodeNil(forKey: ._payment)
// return
// }
//
// let max: Int = TournamentPayment.allCases.count
// var sequence = (1...18).map { _ in Int.random(in: (0..<max)) }
// sequence.append(payment.rawValue)
// sequence.append(contentsOf: (1...13).map { _ in Int.random(in: (0..<max ))} )
//
// let stringCombo: [String] = sequence.map { $0.formatted() }
// let joined: String = stringCombo.joined(separator: "")
// if let data = joined.data(using: .utf8) {
// let encryped: Data = try data.encrypt(pass: CryptoKey.pass.rawValue)
// try container.encodeIfPresent(encryped, forKey: ._payment)
// }
//
// }
//
// func _encodeIsCanceled(container: inout KeyedEncodingContainer<CodingKeys>) throws {
//
// let max: Int = 9
// var sequence = (1...18).map { _ in Int.random(in: (0...max)) }
// sequence.append(self.isCanceled.encodedValue)
// sequence.append(contentsOf: (1...13).map { _ in Int.random(in: (0...max ))} )
//
// let stringCombo: [String] = sequence.map { $0.formatted() }
// let joined: String = stringCombo.joined(separator: "")
// if let data = joined.data(using: .utf8) {
// let encryped: Data = try data.encrypt(pass: CryptoKey.pass.rawValue)
// try container.encode(encryped, forKey: ._isCanceled)
// }
// }
var tournamentStore: TournamentStore { var tournamentStore: TournamentStore {
return TournamentLibrary.shared.store(tournamentId: self.id) return TournamentLibrary.shared.store(tournamentId: self.id)
} }
override func deleteDependencies() { override func deleteDependencies() {
let store = self.tournamentStore let store = self.tournamentStore
let drawLogs = self.tournamentStore.drawLogs
for drawLog in drawLogs {
drawLog.deleteDependencies()
}
store.drawLogs.deleteDependencies(drawLogs)
let teams = self.tournamentStore.teamRegistrations let teams = self.tournamentStore.teamRegistrations
for team in Array(teams) { for team in Array(teams) {
team.deleteDependencies() team.deleteDependencies()
@ -485,16 +240,14 @@ final class Tournament: BaseTournament {
return URLs.main.url.appending(path: "tournament/\(id)").appending(path: pageLink.path) return URLs.main.url.appending(path: "tournament/\(id)").appending(path: pageLink.path)
} }
func courtUsed() -> [Int] { func courtUsed(runningMatches: [Match]) -> [Int] {
#if DEBUG //DEBUGING TIME #if _DEBUGING_TIME //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func courtUsed()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) print("func courtUsed()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
} }
#endif #endif
let runningMatches: [Match] = self.tournamentStore.matches.filter { $0.isRunning() }
return Set(runningMatches.compactMap { $0.courtIndex }).sorted() return Set(runningMatches.compactMap { $0.courtIndex }).sorted()
} }
@ -543,7 +296,7 @@ defer {
return endDate != nil return endDate != nil
} }
func state() -> Tournament.State { func state() -> State {
if self.isCanceled == true { if self.isCanceled == true {
return .canceled return .canceled
} }
@ -676,11 +429,15 @@ defer {
if availableSeeds().isEmpty == false && roundIndex >= lastSeedRound() { if availableSeeds().isEmpty == false && roundIndex >= lastSeedRound() {
if availableSeedGroup == SeedInterval(first: 1, last: 2) { return availableSeedGroup } if availableSeedGroup == SeedInterval(first: 1, last: 2) { return availableSeedGroup }
let availableSeeds = seeds(inSeedGroup: availableSeedGroup) let availableSeeds = seeds(inSeedGroup: availableSeedGroup)
let availableSeedSpot = availableSeedSpot(inRoundIndex: roundIndex) let availableSeedSpot = availableSeedSpot(inRoundIndex: roundIndex)
let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex) let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex)
if availableSeedGroup == SeedInterval(first: 3, last: 4), availableSeedSpot.count == 6 {
print("availableSeedGroup == SeedInterval(first: 3, last: 4)")
return availableSeedGroup
}
if availableSeeds.count == availableSeedSpot.count && availableSeedGroup.count == availableSeeds.count { if availableSeeds.count == availableSeedSpot.count && availableSeedGroup.count == availableSeeds.count {
return availableSeedGroup return availableSeedGroup
} else if availableSeeds.count == availableSeedOpponentSpot.count && availableSeedGroup.count == availableSeedOpponentSpot.count { } else if availableSeeds.count == availableSeedOpponentSpot.count && availableSeedGroup.count == availableSeedOpponentSpot.count {
@ -714,6 +471,15 @@ defer {
let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex) let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex)
let availableSeeds = seeds(inSeedGroup: seedGroup) let availableSeeds = seeds(inSeedGroup: seedGroup)
if seedGroup == SeedInterval(first: 3, last: 4), availableSeedSpot.count == 6 {
var spots = [Match]()
spots.append(availableSeedSpot[1])
spots.append(availableSeedSpot[4])
spots = spots.shuffled()
for (index, seed) in availableSeeds.enumerated() {
seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: false)
}
} else {
if availableSeeds.count <= availableSeedSpot.count { if availableSeeds.count <= availableSeedSpot.count {
let spots = availableSeedSpot.shuffled() let spots = availableSeedSpot.shuffled()
for (index, seed) in availableSeeds.enumerated() { for (index, seed) in availableSeeds.enumerated() {
@ -734,6 +500,7 @@ defer {
} }
} }
} }
}
func inscriptionClosed() -> Bool { func inscriptionClosed() -> Bool {
@ -825,14 +592,14 @@ defer {
let defaultSorting : [MySortDescriptor<TeamRegistration>] = _defaultSorting() let defaultSorting : [MySortDescriptor<TeamRegistration>] = _defaultSorting()
let _completeTeams = _teams.sorted(using: defaultSorting, order: .ascending).filter { $0.isWildCard() == false }.prefix(teamCount).sorted(by: \.initialWeight) let _completeTeams = _teams.sorted(using: defaultSorting, order: .ascending).filter { $0.isWildCard() == false }.prefix(teamCount).sorted(using: [.keyPath(\.initialWeight), .keyPath(\.id)], order: .ascending)
let wcGroupStage = _teams.filter { $0.wildCardGroupStage }.sorted(using: _currentSelectionSorting, order: .ascending) let wcGroupStage = _teams.filter { $0.wildCardGroupStage }.sorted(using: _currentSelectionSorting, order: .ascending)
let wcBracket = _teams.filter { $0.wildCardBracket }.sorted(using: _currentSelectionSorting, order: .ascending) let wcBracket = _teams.filter { $0.wildCardBracket }.sorted(using: _currentSelectionSorting, order: .ascending)
let groupStageSpots: Int = self.groupStageSpots() let groupStageSpots: Int = self.groupStageSpots()
var bracketSeeds: Int = min(teamCount, _completeTeams.count) - groupStageSpots - wcBracket.count var bracketSeeds: Int = min(teamCount, _teams.count) - groupStageSpots - wcBracket.count
var groupStageTeamCount: Int = groupStageSpots - wcGroupStage.count var groupStageTeamCount: Int = groupStageSpots - wcGroupStage.count
if groupStageTeamCount < 0 { groupStageTeamCount = 0 } if groupStageTeamCount < 0 { groupStageTeamCount = 0 }
if bracketSeeds < 0 { bracketSeeds = 0 } if bracketSeeds < 0 { bracketSeeds = 0 }
@ -1024,8 +791,20 @@ defer {
func playersWithoutValidLicense(in players: [PlayerRegistration], isImported: Bool) -> [PlayerRegistration] { func playersWithoutValidLicense(in players: [PlayerRegistration], isImported: Bool) -> [PlayerRegistration] {
let licenseYearValidity = self.licenseYearValidity() let licenseYearValidity = self.licenseYearValidity()
return players.filter({ return players.filter({ player in
($0.isImported() && $0.isValidLicenseNumber(year: licenseYearValidity) == false) || ($0.isImported() == false && ($0.licenceId == nil || $0.formattedLicense().isLicenseNumber == false || $0.licenceId?.isEmpty == true) || ($0.isImported() == false && isImported)) if player.isImported() {
// Player is marked as imported: check if the license is valid
return !player.isValidLicenseNumber(year: licenseYearValidity)
} else {
// Player is not imported: validate license and handle `isImported` flag for non-imported players
let noLicenseId = player.licenceId == nil || player.licenceId?.isEmpty == true
let invalidFormattedLicense = player.formattedLicense().isLicenseNumber == false
// If global `isImported` is true, check license number as well
let invalidLicenseForImportedFlag = isImported && !player.isValidLicenseNumber(year: licenseYearValidity)
return noLicenseId || invalidFormattedLicense || invalidLicenseForImportedFlag
}
}) })
} }
@ -1075,13 +854,15 @@ defer {
} }
} }
func registrationIssues() -> Int { func registrationIssues() async -> Int {
let players : [PlayerRegistration] = unsortedPlayers() let players : [PlayerRegistration] = unsortedPlayers()
let selectedTeams : [TeamRegistration] = selectedSortedTeams() let selectedTeams : [TeamRegistration] = selectedSortedTeams()
let callDateIssue : [TeamRegistration] = selectedTeams.filter { $0.callDate != nil && isStartDateIsDifferentThanCallDate($0) } let callDateIssue : [TeamRegistration] = selectedTeams.filter { $0.callDate != nil && isStartDateIsDifferentThanCallDate($0) }
let duplicates : [PlayerRegistration] = duplicates(in: players) let duplicates : [PlayerRegistration] = duplicates(in: players)
let problematicPlayers : [PlayerRegistration] = players.filter({ $0.sex == nil }) let problematicPlayers : [PlayerRegistration] = players.filter({ $0.sex == nil })
let inadequatePlayers : [PlayerRegistration] = inadequatePlayers(in: players) let inadequatePlayers : [PlayerRegistration] = inadequatePlayers(in: players)
let homonyms = homonyms(in: players)
let ageInadequatePlayers = ageInadequatePlayers(in: players)
let isImported = players.anySatisfy({ $0.isImported() }) let isImported = players.anySatisfy({ $0.isImported() })
let playersWithoutValidLicense : [PlayerRegistration] = playersWithoutValidLicense(in: players, isImported: isImported) let playersWithoutValidLicense : [PlayerRegistration] = playersWithoutValidLicense(in: players, isImported: isImported)
let playersMissing : [TeamRegistration] = selectedTeams.filter({ $0.unsortedPlayers().count < 2 }) let playersMissing : [TeamRegistration] = selectedTeams.filter({ $0.unsortedPlayers().count < 2 })
@ -1089,12 +870,13 @@ defer {
let waitingListInBracket = waitingList.filter({ $0.bracketPosition != nil }) let waitingListInBracket = waitingList.filter({ $0.bracketPosition != nil })
let waitingListInGroupStage = waitingList.filter({ $0.groupStage != nil }) let waitingListInGroupStage = waitingList.filter({ $0.groupStage != nil })
return callDateIssue.count + duplicates.count + problematicPlayers.count + inadequatePlayers.count + playersWithoutValidLicense.count + playersMissing.count + waitingListInBracket.count + waitingListInGroupStage.count return callDateIssue.count + duplicates.count + problematicPlayers.count + inadequatePlayers.count + playersWithoutValidLicense.count + playersMissing.count + waitingListInBracket.count + waitingListInGroupStage.count + ageInadequatePlayers.count + homonyms.count
} }
func isStartDateIsDifferentThanCallDate(_ team: TeamRegistration) -> Bool { func isStartDateIsDifferentThanCallDate(_ team: TeamRegistration, expectedSummonDate: Date? = nil) -> Bool {
guard let summonDate = team.callDate else { return true } guard let summonDate = team.callDate else { return true }
guard let expectedSummonDate = team.expectedSummonDate() else { return true } let expectedSummonDate : Date? = team.expectedSummonDate() ?? expectedSummonDate
guard let expectedSummonDate else { return true }
return Calendar.current.compare(summonDate, to: expectedSummonDate, toGranularity: .minute) != ComparisonResult.orderedSame return Calendar.current.compare(summonDate, to: expectedSummonDate, toGranularity: .minute) != ComparisonResult.orderedSame
} }
@ -1103,7 +885,9 @@ defer {
// return Store.main.filter(isIncluded: { $0.groupStage != nil && groupStageIds.contains($0.groupStage!) }) // return Store.main.filter(isIncluded: { $0.groupStage != nil && groupStageIds.contains($0.groupStage!) })
} }
func availableToStart(_ allMatches: [Match], in runningMatches: [Match]) -> [Match] { static let defaultSorting : [MySortDescriptor<Match>] = [.keyPath(\Match.computedStartDateForSorting), .keyPath(\Match.index)]
static func availableToStart(_ allMatches: [Match], in runningMatches: [Match], checkCanPlay: Bool = true) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME #if _DEBUG_TIME //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
@ -1111,10 +895,10 @@ defer {
print("func tournament availableToStart", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) print("func tournament availableToStart", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
} }
#endif #endif
return allMatches.filter({ $0.canBeStarted(inMatches: runningMatches) && $0.isRunning() == false }).sorted(by: \.computedStartDateForSorting) return allMatches.filter({ $0.isRunning() == false && $0.canBeStarted(inMatches: runningMatches, checkCanPlay: checkCanPlay) }).sorted(using: defaultSorting, order: .ascending)
} }
func runningMatches(_ allMatches: [Match]) -> [Match] { static func runningMatches(_ allMatches: [Match]) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME #if _DEBUG_TIME //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
@ -1122,10 +906,10 @@ defer {
print("func tournament runningMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) print("func tournament runningMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
} }
#endif #endif
return allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(by: \.computedStartDateForSorting) return allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(using: defaultSorting, order: .ascending)
} }
func readyMatches(_ allMatches: [Match]) -> [Match] { static func readyMatches(_ allMatches: [Match]) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME #if _DEBUG_TIME //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
@ -1133,10 +917,22 @@ defer {
print("func tournament readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) print("func tournament readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
} }
#endif #endif
return allMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }).sorted(by: \.computedStartDateForSorting) return allMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }).sorted(using: defaultSorting, order: .ascending)
} }
func finishedMatches(_ allMatches: [Match], limit: Int?) -> [Match] { static func matchesLeft(_ allMatches: [Match]) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func tournament readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
return allMatches.filter({ $0.isRunning() == false && $0.hasEnded() == false }).sorted(using: defaultSorting, order: .ascending)
}
static func finishedMatches(_ allMatches: [Match], limit: Int?) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME #if _DEBUG_TIME //DEBUGING TIME
let start = Date() let start = Date()
defer { defer {
@ -1277,7 +1073,7 @@ defer {
if team.qualified == false && alreadyPlaceTeams.contains(team.id) == false { if team.qualified == false && alreadyPlaceTeams.contains(team.id) == false {
let groupStageWidth = max(((index == qualifiedPerGroupStage) ? groupStageCount - groupStageAdditionalQualified : groupStageCount) * (index - qualifiedPerGroupStage), 0) let groupStageWidth = max(((index == qualifiedPerGroupStage) ? groupStageCount - groupStageAdditionalQualified : groupStageCount) * (index - qualifiedPerGroupStage), 0)
let _index = baseRank + groupStageWidth + 1 let _index = baseRank + groupStageWidth + 1 - (index > qualifiedPerGroupStage ? groupStageAdditionalQualified : 0)
if let existingTeams = teams[_index] { if let existingTeams = teams[_index] {
teams[_index] = existingTeams + [team.id] teams[_index] = existingTeams + [team.id]
} else { } else {
@ -1390,7 +1186,7 @@ defer {
return tournamentLevel.localizedLevelLabel(.title) return tournamentLevel.localizedLevelLabel(.title)
} }
} }
let title: String = [tournamentLevel.localizedLevelLabel(displayStyle), tournamentCategory.localizedLabel(displayStyle), federalTournamentAge.localizedLabel(displayStyle)].filter({ $0.isEmpty == false }).joined(separator: " ") let title: String = [tournamentLevel.localizedLevelLabel(displayStyle), tournamentCategory.localizedLabel(displayStyle), federalTournamentAge.localizedFederalAgeLabel(displayStyle)].filter({ $0.isEmpty == false }).joined(separator: " ")
if displayStyle == .wide, let name { if displayStyle == .wide, let name {
return [title, name].joined(separator: " - ") return [title, name].joined(separator: " - ")
} else { } else {
@ -1488,10 +1284,10 @@ defer {
var entryFeeMessage: String { var entryFeeMessage: String {
if let entryFee { if let entryFee {
let message: String = "Inscription: \(entryFee.formatted(.currency(code: "EUR"))) par joueur." let message: String = "Inscription : \(entryFee.formatted(.currency(code: Locale.defaultCurrency()))) par joueur."
return [message, self._paymentMethodMessage()].compactMap { $0 }.joined(separator: "\n") return [message, self._paymentMethodMessage()].compactMap { $0 }.joined(separator: "\n")
} else { } else {
return "Inscription: gratuite." return "Inscription : gratuite."
} }
} }
@ -1551,7 +1347,9 @@ defer {
func callStatus() async -> TournamentStatus { func callStatus() async -> TournamentStatus {
let selectedSortedTeams = selectedSortedTeams() let selectedSortedTeams = selectedSortedTeams()
let called = selectedSortedTeams.filter { isStartDateIsDifferentThanCallDate($0) == false } let called = selectedSortedTeams.filter { isStartDateIsDifferentThanCallDate($0) == false }
let label = "\(called.count.formatted()) / \(selectedSortedTeams.count.formatted()) convoquées au bon horaire" let justCalled = selectedSortedTeams.filter { $0.called() }
let label = "\(justCalled.count.formatted()) / \(selectedSortedTeams.count.formatted()) (\(called.count.formatted()) au bon horaire)"
let completion = (Double(called.count) / Double(selectedSortedTeams.count)) let completion = (Double(called.count) / Double(selectedSortedTeams.count))
let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0))) let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0)))
return TournamentStatus(label: label, completion: completionLabel) return TournamentStatus(label: label, completion: completionLabel)
@ -1629,16 +1427,61 @@ defer {
deleteGroupStages() deleteGroupStages()
switch preset { switch preset {
case .manual:
buildGroupStages()
buildBracket()
case .doubleGroupStage: case .doubleGroupStage:
buildGroupStages() buildGroupStages()
addNewGroupStageStep() addNewGroupStageStep()
qualifiedPerGroupStage = 0 qualifiedPerGroupStage = 0
groupStageAdditionalQualified = 0 groupStageAdditionalQualified = 0
default:
buildGroupStages()
buildBracket()
}
}
func addWildCardIfNeeded(_ count: Int, _ type: MatchType) {
let currentCount = selectedSortedTeams().filter({
if type == .bracket {
return $0.wildCardBracket
} else {
return $0.wildCardGroupStage
}
}).count
if currentCount < count {
let _diff = count - currentCount
addWildCard(_diff, type)
}
}
func addWildCard(_ count: Int, _ type: MatchType) {
let wcs = (0..<count).map { _ in
let team = TeamRegistration(tournament: id)
if type == .bracket {
team.wildCardBracket = true
} else {
team.wildCardGroupStage = true
}
return team
}
do {
try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: wcs)
} catch {
Logger.error(error)
}
}
func addEmptyTeamRegistration(_ count: Int) {
let teams = (0..<count).map { _ in
let team = TeamRegistration(tournament: id)
return team
}
do {
try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams)
} catch {
Logger.error(error)
} }
} }
@ -1649,7 +1492,7 @@ defer {
var _groupStages = [GroupStage]() var _groupStages = [GroupStage]()
for index in 0..<groupStageCount { for index in 0..<groupStageCount {
let groupStage = GroupStage(tournament: id, index: index, size: teamsPerGroupStage, matchFormat: groupStageSmartMatchFormat()) let groupStage = GroupStage(tournament: id, index: index, size: teamsPerGroupStage, format: groupStageSmartMatchFormat())
_groupStages.append(groupStage) _groupStages.append(groupStage)
} }
@ -1670,7 +1513,15 @@ defer {
return Round(tournament: id, index: $0, matchFormat: roundSmartMatchFormat($0), loserBracketMode: loserBracketMode) return Round(tournament: id, index: $0, matchFormat: roundSmartMatchFormat($0), loserBracketMode: loserBracketMode)
} }
self.tournamentStore.rounds.addOrUpdate(contentOfs: rounds) if rounds.isEmpty {
return
}
do {
try self.tournamentStore.rounds.addOrUpdate(contentOfs: rounds)
} catch {
Logger.error(error)
}
let matchCount = RoundRule.numberOfMatches(forTeams: bracketTeamCount()) let matchCount = RoundRule.numberOfMatches(forTeams: bracketTeamCount())
let matches = (0..<matchCount).map { //0 is final match let matches = (0..<matchCount).map { //0 is final match
@ -1724,7 +1575,7 @@ defer {
self.tournamentStore.groupStages.delete(contentOfs: allGroupStages()) self.tournamentStore.groupStages.delete(contentOfs: allGroupStages())
} }
func refreshGroupStages() { func refreshGroupStages(keepExistingMatches: Bool = false) {
unsortedTeams().forEach { team in unsortedTeams().forEach { team in
team.groupStage = nil team.groupStage = nil
team.groupStagePosition = nil team.groupStagePosition = nil
@ -1733,16 +1584,16 @@ defer {
if groupStageCount > 0 { if groupStageCount > 0 {
switch groupStageOrderingMode { switch groupStageOrderingMode {
case .random: case .random:
setGroupStage(randomize: true) setGroupStage(randomize: true, keepExistingMatches: keepExistingMatches)
case .snake: case .snake:
setGroupStage(randomize: false) setGroupStage(randomize: false, keepExistingMatches: keepExistingMatches)
case .swiss: case .swiss:
setGroupStage(randomize: true) setGroupStage(randomize: true, keepExistingMatches: keepExistingMatches)
} }
} }
} }
func setGroupStage(randomize: Bool) { func setGroupStage(randomize: Bool, keepExistingMatches: Bool = false) {
let groupStages = groupStages() let groupStages = groupStages()
let numberOfBracketsAsInt = groupStages.count let numberOfBracketsAsInt = groupStages.count
// let teamsPerBracket = teamsPerBracket // let teamsPerBracket = teamsPerBracket
@ -1751,7 +1602,7 @@ defer {
buildGroupStages() buildGroupStages()
} else { } else {
setGroupStageTeams(randomize: randomize) setGroupStageTeams(randomize: randomize)
groupStages.forEach { $0.buildMatches() } groupStages.forEach { $0.buildMatches(keepExistingMatches: keepExistingMatches) }
} }
} }
@ -1787,15 +1638,14 @@ defer {
func labelIndexOf(team: TeamRegistration) -> String? { func labelIndexOf(team: TeamRegistration) -> String? {
if let teamIndex = indexOf(team: team) { if let teamIndex = indexOf(team: team) {
return "#" + (teamIndex + 1).formatted() return "Tête de série #" + (teamIndex + 1).formatted()
} else { } else {
return nil return nil
} }
} }
func addTeam(_ players: Set<PlayerRegistration>, registrationDate: Date? = nil, name: String? = nil) -> TeamRegistration { func addTeam(_ players: Set<PlayerRegistration>, registrationDate: Date? = nil, name: String? = nil) -> TeamRegistration {
let date: Date = registrationDate ?? Date() let team = TeamRegistration(tournament: id, registrationDate: registrationDate, name: name)
let team = TeamRegistration(tournament: id, registrationDate: date, name: name)
team.setWeight(from: Array(players), inTournamentCategory: tournamentCategory) team.setWeight(from: Array(players), inTournamentCategory: tournamentCategory)
players.forEach { player in players.forEach { player in
player.teamRegistration = team.id player.teamRegistration = team.id
@ -1921,7 +1771,7 @@ defer {
private func _defaultSorting() -> [MySortDescriptor<TeamRegistration>] { private func _defaultSorting() -> [MySortDescriptor<TeamRegistration>] {
switch teamSorting { switch teamSorting {
case .rank: case .rank:
[.keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.registrationDate!), .keyPath(\TeamRegistration.id)] [.keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.id)]
case .inscriptionDate: case .inscriptionDate:
[.keyPath(\TeamRegistration.registrationDate!), .keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.id)] [.keyPath(\TeamRegistration.registrationDate!), .keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.id)]
} }
@ -1933,7 +1783,7 @@ defer {
&& federalTournamentAge == build.age && federalTournamentAge == build.age
} }
private let _currentSelectionSorting : [MySortDescriptor<TeamRegistration>] = [.keyPath(\.weight), .keyPath(\.registrationDate!), .keyPath(\.id)] private let _currentSelectionSorting : [MySortDescriptor<TeamRegistration>] = [.keyPath(\.weight), .keyPath(\.id)]
private func _matchSchedulers() -> [MatchScheduler] { private func _matchSchedulers() -> [MatchScheduler] {
return self.tournamentStore.matchSchedulers.filter { $0.tournament == self.id } return self.tournamentStore.matchSchedulers.filter { $0.tournament == self.id }
@ -1944,6 +1794,10 @@ defer {
return self._matchSchedulers().first return self._matchSchedulers().first
} }
func courtsAvailable() -> [Int] {
(0..<courtCount).map { $0 }
}
func currentMonthData() -> MonthData? { func currentMonthData() -> MonthData? {
guard let rankSourceDate else { return nil } guard let rankSourceDate else { return nil }
let dateString = URL.importDateFormatter.string(from: rankSourceDate) let dateString = URL.importDateFormatter.string(from: rankSourceDate)
@ -2022,7 +1876,8 @@ defer {
let newGroup = selected.prefix(seedCount) + selected.filter({ $0.qualified }) let newGroup = selected.prefix(seedCount) + selected.filter({ $0.qualified })
let currentGroup = allTeams.filter({ $0.bracketPosition != nil }) let currentGroup = allTeams.filter({ $0.bracketPosition != nil })
let selectedIds = newGroup.map { $0.id } let selectedIds = newGroup.map { $0.id }
let groupIds = currentGroup.map { $0.id } let groupStageTeamsInBracket = selected.filter({ $0.qualified == false && $0.inGroupStage() && $0.inRound() })
let groupIds = currentGroup.map { $0.id } + groupStageTeamsInBracket.map { $0.id }
let shouldBeInIt = Set(selectedIds).subtracting(groupIds) let shouldBeInIt = Set(selectedIds).subtracting(groupIds)
let shouldNotBeInIt = Set(groupIds).subtracting(selectedIds) let shouldNotBeInIt = Set(groupIds).subtracting(selectedIds)
return (Array(shouldBeInIt), Array(shouldNotBeInIt)) return (Array(shouldBeInIt), Array(shouldNotBeInIt))
@ -2056,8 +1911,12 @@ defer {
groupStages().chunked(into: 2).forEach { gss in groupStages().chunked(into: 2).forEach { gss in
let placeCount = i * 2 + 1 let placeCount = i * 2 + 1
let match = Match(round: groupStageLoserBracket.id, index: placeCount, format: groupStageLoserBracket.matchFormat) let match = Match(round: groupStageLoserBracket.id, index: placeCount, format: groupStageLoserBracket.matchFormat)
match.name = "\(placeCount)\(placeCount.ordinalFormattedSuffix(feminine: true)) place" match.setMatchName("\(placeCount)\(placeCount.ordinalFormattedSuffix(feminine: true)) place")
tournamentStore.matches.addOrUpdate(instance: match) do {
try tournamentStore.matches.addOrUpdate(instance: match)
} catch {
Logger.error(error)
}
if let gs1 = gss.first, let gs2 = gss.last, let score1 = gs1.teams(true)[safe: i], let score2 = gs2.teams(true)[safe: i] { if let gs1 = gss.first, let gs2 = gss.last, let score1 = gs1.teams(true)[safe: i], let score2 = gs2.teams(true)[safe: i] {
print("rang \(i)") print("rang \(i)")
@ -2084,6 +1943,138 @@ defer {
rounds().flatMap { $0.loserRoundsAndChildren().flatMap({ $0._matches() }) } rounds().flatMap { $0.loserRoundsAndChildren().flatMap({ $0._matches() }) }
} }
func seedsCount() -> Int {
selectedSortedTeams().count - groupStageSpots()
}
func lastDrawnDate() -> Date? {
drawLogs().last?.drawDate
}
func drawLogs() -> [DrawLog] {
self.tournamentStore.drawLogs.sorted(by: \.drawDate)
}
func seedSpotsLeft() -> Bool {
let alreadySeededRounds = rounds().filter({ $0.seeds().isEmpty == false })
if alreadySeededRounds.isEmpty { return true }
let spotsLeft = alreadySeededRounds.flatMap({ $0.playedMatches() }).filter { $0.isEmpty() || $0.isValidSpot() }
return spotsLeft.isEmpty == false
}
func isRoundValidForSeeding(roundIndex: Int) -> Bool {
if let lastRoundWithSeeds = rounds().last(where: { $0.seeds().isEmpty == false }) {
return roundIndex >= lastRoundWithSeeds.index
} else {
return true
}
}
func updateSeedsBracketPosition() async {
await removeAllSeeds()
let drawLogs = drawLogs().reversed()
let seeds = seeds()
for (index, seed) in seeds.enumerated() {
if let drawLog = drawLogs.first(where: { $0.drawSeed == index }) {
drawLog.updateTeamBracketPosition(seed)
}
}
do {
try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: seeds)
} catch {
Logger.error(error)
}
}
func removeAllSeeds() async {
unsortedTeams().forEach({ team in
team.bracketPosition = nil
})
let ts = allRoundMatches().flatMap { match in
match.teamScores
}
do {
try tournamentStore.teamScores.delete(contentOfs: ts)
} catch {
Logger.error(error)
}
do {
try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams())
} catch {
Logger.error(error)
}
allRounds().forEach({ round in
round.enableRound()
})
}
func addNewRound(_ roundIndex: Int) async {
let round = Round(tournament: id, index: roundIndex, matchFormat: matchFormat)
let matchCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex)
let matchStartIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex)
let nextRound = round.nextRound()
var currentIndex = 0
let matches = (0..<matchCount).map { index in //0 is final match
let computedIndex = index + matchStartIndex
let match = Match(round: round.id, index: computedIndex, format: round.matchFormat)
if let nextRound, let followingMatch = self.tournamentStore.matches.first(where: { $0.round == nextRound.id && $0.index == (computedIndex - 1) / 2 }) {
if followingMatch.disabled {
match.disabled = true
} else if computedIndex%2 == 1 && followingMatch.team(.one) != nil {
//index du match courant impair = position haut du prochain match
match.disabled = true
} else if computedIndex%2 == 0 && followingMatch.team(.two) != nil {
//index du match courant pair = position basse du prochain match
match.disabled = true
} else {
match.setMatchName(Match.setServerTitle(upperRound: round, matchIndex: currentIndex))
currentIndex += 1
}
} else {
match.setMatchName(Match.setServerTitle(upperRound: round, matchIndex: currentIndex))
currentIndex += 1
}
return match
}
do {
try tournamentStore.rounds.addOrUpdate(instance: round)
} catch {
Logger.error(error)
}
do {
try tournamentStore.matches.addOrUpdate(contentOfs: matches)
} catch {
Logger.error(error)
}
round.buildLoserBracket()
matches.filter { $0.disabled }.forEach {
$0._toggleLoserMatchDisableState(true)
}
}
func exportedDrawLogs() -> String {
var logs : [String] = ["Journal des tirages\n\n"]
logs.append(drawLogs().map { $0.exportedDrawLog() }.joined(separator: "\n\n"))
return logs.joined()
}
func courtUnavailable(courtIndex: Int, from startDate: Date, to endDate: Date) -> Bool {
guard let source = eventObject()?.courtsUnavailability else { return false }
let courtLockedSchedule = source.filter({ $0.courtIndex == courtIndex })
return courtLockedSchedule.anySatisfy({ dateInterval in
let range = startDate..<endDate
return dateInterval.range.overlaps(range)
})
}
// MARK: - // MARK: -
func insertOnServer() throws { func insertOnServer() throws {
@ -2203,7 +2194,7 @@ extension Tournament: FederalTournamentHolder {
func subtitleLabel(forBuild build: any TournamentBuildHolder) -> String { func subtitleLabel(forBuild build: any TournamentBuildHolder) -> String {
if isAnimation() { if isAnimation() {
if displayAgeAndCategory(forBuild: build) == false { if displayAgeAndCategory(forBuild: build) == false {
return [build.category.localizedLabel(), build.age.localizedLabel()].filter({ $0.isEmpty == false }).joined(separator: " ") return [build.category.localizedLabel(), build.age.localizedFederalAgeLabel()].filter({ $0.isEmpty == false }).joined(separator: " ")
} else if name != nil { } else if name != nil {
return build.level.localizedLevelLabel(.title) return build.level.localizedLevelLabel(.title)
} else { } else {

@ -21,6 +21,7 @@ class TournamentStore: ObservableObject {
fileprivate(set) var teamScores: StoredCollection<TeamScore> = StoredCollection.placeholder() fileprivate(set) var teamScores: StoredCollection<TeamScore> = StoredCollection.placeholder()
fileprivate(set) var matchSchedulers: StoredCollection<MatchScheduler> = StoredCollection.placeholder() fileprivate(set) var matchSchedulers: StoredCollection<MatchScheduler> = StoredCollection.placeholder()
fileprivate(set) var drawLogs: StoredCollection<DrawLog> = StoredCollection.placeholder()
// convenience init(tournament: Tournament) { // convenience init(tournament: Tournament) {
// let store = StoreCenter.main.store(identifier: tournament.id) // let store = StoreCenter.main.store(identifier: tournament.id)
@ -35,12 +36,10 @@ class TournamentStore: ObservableObject {
fileprivate func _initialize() { fileprivate func _initialize() {
// super.init(identifier: identifier, parameter: parameter)
var synchronized: Bool = true var synchronized: Bool = true
let indexed: Bool = true let indexed: Bool = true
#if _DEBUG_OPTIONS #if DEBUG
if let sync = PListReader.readBool(plist: "local", key: "synchronized") { if let sync = PListReader.readBool(plist: "local", key: "synchronized") {
synchronized = sync synchronized = sync
} }
@ -53,6 +52,7 @@ class TournamentStore: ObservableObject {
self.matches = self.store.registerSynchronizedCollection(indexed: indexed) self.matches = self.store.registerSynchronizedCollection(indexed: indexed)
self.teamScores = self.store.registerSynchronizedCollection(indexed: indexed) self.teamScores = self.store.registerSynchronizedCollection(indexed: indexed)
self.matchSchedulers = self.store.registerCollection(indexed: indexed) self.matchSchedulers = self.store.registerCollection(indexed: indexed)
self.drawLogs = self.store.registerCollection(indexed: indexed)
self.store.loadCollectionsFromServerIfNoFile() self.store.loadCollectionsFromServerIfNoFile()

@ -36,6 +36,14 @@ enum TimeOfDay {
extension Date { extension Date {
func withoutSeconds() -> Date {
let calendar = Calendar.current
return calendar.date(bySettingHour: calendar.component(.hour, from: self),
minute: calendar.component(.minute, from: self),
second: 0,
of: self)!
}
func localizedDate() -> String { func localizedDate() -> String {
self.formatted(.dateTime.weekday().day().month()) + " à " + self.formattedAsHourMinute() self.formatted(.dateTime.weekday().day().month()) + " à " + self.formattedAsHourMinute()
} }
@ -231,4 +239,19 @@ extension Date {
func localizedWeekDay() -> String { func localizedWeekDay() -> String {
self.formatted(.dateTime.weekday(.wide)) self.formatted(.dateTime.weekday(.wide))
} }
func timeElapsedString() -> String {
let timeInterval = abs(Date().timeIntervalSince(self))
let duration = Duration.seconds(timeInterval)
let formatStyle = Duration.UnitsFormatStyle(allowedUnits: [.hours, .minutes], width: .narrow)
return formatStyle.format(duration)
}
static var hourMinuteFormatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.hour, .minute] // Customize units
formatter.unitsStyle = .abbreviated // You can choose .abbreviated or .short
return formatter
}()
} }

@ -19,11 +19,25 @@ public extension FixedWidthInteger {
return self.formatted() + self.ordinalFormattedSuffix(feminine: feminine) return self.formatted() + self.ordinalFormattedSuffix(feminine: feminine)
} }
private var isMany: Bool {
self > 1 || self < -1
}
var pluralSuffix: String { var pluralSuffix: String {
return self > 1 ? "s" : "" return isMany ? "s" : ""
}
func localizedPluralSuffix(_ plural: String = "s") -> String {
return isMany ? plural : ""
} }
func formattedAsRawString() -> String { func formattedAsRawString() -> String {
String(self) String(self)
} }
func durationInHourMinutes() -> String {
let duration = Duration.seconds(self*60)
let formatStyle = Duration.UnitsFormatStyle(allowedUnits: [.hours, .minutes], width: .narrow)
return formatStyle.format(duration)
}
} }

@ -20,4 +20,9 @@ extension Locale {
return countries.sorted() return countries.sorted()
} }
static func defaultCurrency() -> String {
// return "EUR"
Locale.current.currency?.identifier ?? "EUR"
}
} }

@ -166,8 +166,7 @@ extension String {
// MARK: - FFT Source Importing // MARK: - FFT Source Importing
extension String { extension String {
enum RegexStatic { enum RegexStatic {
static let mobileNumber = /^0[6-7]/ static let mobileNumber = /^(?:\+33|0033|0)[6-7](?:[ .-]?[0-9]{2}){4}$/
//static let mobileNumber = /^(?:(?:\+|00)33[\s.-]{0,3}(?:\(0\)[\s.-]{0,3})?|0)[1-9](?:(?:[\s.-]?\d{2}){4}|\d{2}(?:[\s.-]?\d{3}){2})$/
} }
func isMobileNumber() -> Bool { func isMobileNumber() -> Bool {

@ -33,7 +33,5 @@
</array> </array>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false/> <false/>
<key>UIFileSharingEnabled</key>
<true/>
</dict> </dict>
</plist> </plist>

@ -17,6 +17,7 @@ struct PadelClubApp: App {
@StateObject var dataStore = DataStore.shared @StateObject var dataStore = DataStore.shared
@State private var registrationError: RegistrationError? = nil @State private var registrationError: RegistrationError? = nil
@State private var importObserverViewModel = ImportObserver() @State private var importObserverViewModel = ImportObserver()
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@ -62,6 +63,7 @@ struct PadelClubApp: App {
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
MainView() MainView()
.environment(\.horizontalSizeClass, .compact)
.alert(isPresented: presentError, error: registrationError) { .alert(isPresented: presentError, error: registrationError) {
Button("Contactez-nous") { Button("Contactez-nous") {
_openMail() _openMail()

@ -117,8 +117,16 @@ Il est conseillé de vous présenter 10 minutes avant de jouer.\n\nMerci de me c
(DataStore.shared.user.summonsDisplayEntryFee) ? tournament?.entryFeeMessage : nil (DataStore.shared.user.summonsDisplayEntryFee) ? tournament?.entryFeeMessage : nil
} }
var linkMessage: String? {
if let tournament, tournament.isPrivate == false, let shareLink = tournament.shareURL(.matches)?.absoluteString {
return "Vous pourrez suivre tous les résultats de ce tournoi sur le site :\n\n".appending(shareLink)
} else {
return nil
}
}
var computedMessage: String { var computedMessage: String {
[entryFeeMessage, message].compacted().map { $0.trimmedMultiline }.joined(separator: "\n\n") [entryFeeMessage, message, linkMessage].compacted().map { $0.trimmedMultiline }.joined(separator: "\n\n")
} }
let intro = reSummon ? "Suite à des forfaits, vous êtes finalement" : "Vous êtes" let intro = reSummon ? "Suite à des forfaits, vous êtes finalement" : "Vous êtes"

@ -21,12 +21,9 @@ enum DisplayStyle {
case short case short
} }
enum MatchViewStyle { enum SummoningDisplayContext {
case standardStyle // vue normal case footer
case sectionedStandardStyle // vue normal avec des sections indiquant déjà la manche case menu
case feedStyle // vue programmation
case plainStyle // vue detail
case tournamentResultStyle //vue resultat tournoi
} }
struct DeviceHelper { struct DeviceHelper {

@ -64,9 +64,10 @@ class FileImportManager {
importedPlayer.firstName = firstName.trimmed.capitalized importedPlayer.firstName = firstName.trimmed.capitalized
} }
} }
playersLeft.removeAll(where: { $0.lastName.isEmpty == false })
} }
}) })
players = playersLeft
} }
func foundInWomenData(license: String?) -> Bool { func foundInWomenData(license: String?) -> Bool {

@ -48,7 +48,7 @@ struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identifiable {
} }
var identifier: String { var identifier: String {
level.localizedLevelLabel()+":"+category.localizedLabel()+":"+age.localizedLabel() level.localizedLevelLabel()+":"+category.localizedLabel()+":"+age.localizedFederalAgeLabel()
} }
func computedLabel(_ displayStyle: DisplayStyle = .wide) -> String { func computedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
@ -65,7 +65,7 @@ struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identifiable {
} }
func localizedAge(_ displayStyle: DisplayStyle = .wide) -> String { func localizedAge(_ displayStyle: DisplayStyle = .wide) -> String {
age.localizedLabel(displayStyle) age.localizedFederalAgeLabel(displayStyle)
} }
} }
@ -252,7 +252,7 @@ enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifiable {
} }
} }
func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { func localizedFederalAgeLabel(_ displayStyle: DisplayStyle = .wide) -> String {
switch self { switch self {
case .unlisted: case .unlisted:
return displayStyle == .title ? "Aucune" : "" return displayStyle == .title ? "Aucune" : ""
@ -265,7 +265,7 @@ enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifiable {
case .a17_18: case .a17_18:
return "17/18 ans" return "17/18 ans"
case .senior: case .senior:
return "Senior" return displayStyle == .short ? "" : "Senior"
case .a45: case .a45:
return "+45 ans" return "+45 ans"
case .a55: case .a55:
@ -274,7 +274,7 @@ enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifiable {
} }
var tournamentDescriptionLabel: String { var tournamentDescriptionLabel: String {
return localizedLabel() return localizedFederalAgeLabel()
} }
func isAgeValid(age: Int?) -> Bool { func isAgeValid(age: Int?) -> Bool {
@ -540,7 +540,7 @@ enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable {
case .p25: case .p25:
switch count { switch count {
case 9...12: case 9...12:
return [17, 13, 11, 9, 7, 5, 4, 3, 2, 1] return [17, 15, 13, 11, 9, 7, 5, 4, 3, 2, 1]
case 13...16: case 13...16:
return [18,16,15,14,13,12,11,10,9,7,5,4,3,2, 1] return [18,16,15,14,13,12,11,10,9,7,5,4,3,2, 1]
case 17...20: case 17...20:
@ -996,6 +996,15 @@ enum TeamPosition: Int, Identifiable, Hashable, Codable, CaseIterable {
return shortName return shortName
} }
} }
func localizedBranchLabel() -> String {
switch self {
case .one:
return "Branche du haut"
case .two:
return "Branche du bas"
}
}
} }
enum SetFormat: Int, Hashable, Codable { enum SetFormat: Int, Hashable, Codable {
@ -1126,7 +1135,8 @@ enum MatchType: String {
case loserBracket = "loserBracket" case loserBracket = "loserBracket"
} }
enum MatchFormat: Int, Hashable, Codable, CaseIterable { enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable {
var id: Int { self.rawValue }
case twoSets case twoSets
case twoSetsSuperTie case twoSetsSuperTie
case twoSetsOfFourGames case twoSetsOfFourGames
@ -1139,6 +1149,13 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
case twoSetsOfFourGamesDecisivePoint case twoSetsOfFourGamesDecisivePoint
case nineGamesDecisivePoint case nineGamesDecisivePoint
case twoSetsOfSuperTie
case singleSet
case singleSetDecisivePoint
case singleSetOfFourGames
case singleSetOfFourGamesDecisivePoint
init?(rawValue: Int?) { init?(rawValue: Int?) {
guard let value = rawValue else { return nil } guard let value = rawValue else { return nil }
self.init(rawValue: value) self.init(rawValue: value)
@ -1162,6 +1179,12 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
return 4 return 4
case .megaTie: case .megaTie:
return 5 return 5
case .twoSetsOfSuperTie:
return 6
case .singleSet, .singleSetDecisivePoint:
return 7
case .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint:
return 8
} }
} }
@ -1187,6 +1210,12 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
return 4 return 4
case .megaTie: case .megaTie:
return 5 return 5
case .twoSetsOfSuperTie:
return 6
case .singleSet, .singleSetDecisivePoint:
return 7
case .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint:
return 8
} }
} }
@ -1202,7 +1231,7 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
} }
static var allCases: [MatchFormat] { static var allCases: [MatchFormat] {
[.twoSets, .twoSetsDecisivePoint, .twoSetsSuperTie, .twoSetsDecisivePointSuperTie, .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .nineGames, .nineGamesDecisivePoint, .superTie, .megaTie] [.twoSets, .twoSetsDecisivePoint, .twoSetsSuperTie, .twoSetsDecisivePointSuperTie, .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .nineGames, .nineGamesDecisivePoint, .superTie, .megaTie, .twoSetsOfSuperTie, .singleSet, .singleSetDecisivePoint, .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint]
} }
func winner(scoreTeamOne: Int, scoreTeamTwo: Int) -> TeamPosition { func winner(scoreTeamOne: Int, scoreTeamTwo: Int) -> TeamPosition {
@ -1215,7 +1244,7 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
var canSuperTie: Bool { var canSuperTie: Bool {
switch self { switch self {
case .twoSetsSuperTie, .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .twoSetsDecisivePointSuperTie: case .twoSetsSuperTie, .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .twoSetsDecisivePointSuperTie, .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint:
return true return true
default: default:
return false return false
@ -1237,8 +1266,10 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
func formattedEstimatedBreakDuration() -> String { func formattedEstimatedBreakDuration() -> String {
var label = Duration.seconds(breakTime.breakTime * 60).formatted(.units(allowed: [.minutes])) var label = Duration.seconds(breakTime.breakTime * 60).formatted(.units(allowed: [.minutes]))
if breakTime.matchCount > 1 { if breakTime.matchCount > 1 {
label += " après \(breakTime.matchCount) match" label += " de pause après \(breakTime.matchCount) match"
label += breakTime.matchCount.pluralSuffix label += breakTime.matchCount.pluralSuffix
} else {
label += " de pause"
} }
return label return label
} }
@ -1262,9 +1293,19 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
case .nineGamesDecisivePoint: case .nineGamesDecisivePoint:
return 40 return 40
case .megaTie: case .megaTie:
return 30 return 20
case .superTie: case .superTie:
return 15
case .twoSetsOfSuperTie:
return 25 return 25
case .singleSet:
return 30
case .singleSetDecisivePoint:
return 25
case .singleSetOfFourGames:
return 15
case .singleSetOfFourGamesDecisivePoint:
return 10
} }
} }
@ -1283,7 +1324,7 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
return (30, 1) return (30, 1)
case .superTie: case .superTie:
return (15, 3) return (15, 3)
case .megaTie: default:
return (5, 1) return (5, 1)
} }
} }
@ -1298,14 +1339,14 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
return matchCount < 7 ? 6 : 2 return matchCount < 7 ? 6 : 2
case .superTie: case .superTie:
return 7 return 7
case .megaTie: default:
return 7 return 10
} }
} }
var hasDecisivePoint: Bool { var hasDecisivePoint: Bool {
switch self { switch self {
case .nineGamesDecisivePoint, .twoSetsDecisivePoint, .twoSetsOfFourGamesDecisivePoint, .twoSetsDecisivePointSuperTie: case .nineGamesDecisivePoint, .twoSetsDecisivePoint, .twoSetsOfFourGamesDecisivePoint, .twoSetsDecisivePointSuperTie, .singleSetDecisivePoint, .singleSetOfFourGamesDecisivePoint:
return true return true
default: default:
return false return false
@ -1319,9 +1360,18 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
return setFormat return setFormat
} }
func formatTitle(_ displayStyle: DisplayStyle = .wide) -> String {
switch displayStyle {
case .short:
return ["Format ", shortFormat].joined()
default:
return ["Format ", shortFormat, suffix].joined()
}
}
var suffix: String { var suffix: String {
switch self { switch self {
case .twoSetsDecisivePoint, .twoSetsDecisivePointSuperTie, .twoSetsOfFourGamesDecisivePoint, .nineGamesDecisivePoint: case .twoSetsDecisivePoint, .twoSetsDecisivePointSuperTie, .twoSetsOfFourGamesDecisivePoint, .nineGamesDecisivePoint, .singleSetDecisivePoint:
return " [Point Décisif]" return " [Point Décisif]"
default: default:
return "" return ""
@ -1336,7 +1386,19 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
return "\(format) : " return "\(format) : "
} }
var isFederal: Bool {
switch self {
case .megaTie, .twoSetsOfSuperTie, .singleSet, .singleSetDecisivePoint, .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint:
return false
default:
return true
}
}
var format: String { var format: String {
shortFormat + (isFederal ? "" : " (non officiel)")
}
var shortFormat: String {
switch self { switch self {
case .twoSets: case .twoSets:
return "A1" return "A1"
@ -1348,8 +1410,14 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
return "D1" return "D1"
case .superTie: case .superTie:
return "E" return "E"
case .twoSetsOfSuperTie:
return "G"
case .megaTie: case .megaTie:
return "F" return "F"
case .singleSet:
return "H1"
case .singleSetDecisivePoint:
return "H2"
case .twoSetsDecisivePoint: case .twoSetsDecisivePoint:
return "A2" return "A2"
case .twoSetsDecisivePointSuperTie: case .twoSetsDecisivePointSuperTie:
@ -1358,11 +1426,17 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
return "C2" return "C2"
case .nineGamesDecisivePoint: case .nineGamesDecisivePoint:
return "D2" return "D2"
case .singleSetOfFourGames:
return "I1"
case .singleSetOfFourGamesDecisivePoint:
return "I2"
} }
} }
var longLabel: String { var longLabel: String {
switch self { switch self {
case .singleSet, .singleSetDecisivePoint:
return "1 set de 6"
case .twoSets, .twoSetsDecisivePoint: case .twoSets, .twoSetsDecisivePoint:
return "2 sets de 6" return "2 sets de 6"
case .twoSetsSuperTie, .twoSetsDecisivePointSuperTie: case .twoSetsSuperTie, .twoSetsDecisivePointSuperTie:
@ -1371,10 +1445,14 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
return "2 sets de 4, tiebreak à 4/4, supertie au 3ème" return "2 sets de 4, tiebreak à 4/4, supertie au 3ème"
case .nineGames, .nineGamesDecisivePoint: case .nineGames, .nineGamesDecisivePoint:
return "9 jeux, tiebreak à 8/8" return "9 jeux, tiebreak à 8/8"
case .twoSetsOfSuperTie:
return "2 sets de supertie de 10 points"
case .superTie: case .superTie:
return "supertie de 10 points" return "supertie de 10 points"
case .megaTie: case .megaTie:
return "supertie de 15 points" return "supertie de 15 points"
case .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint:
return "1 set de 4 jeux, tiebreak à 4/4"
} }
} }
@ -1392,22 +1470,22 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
var setsToWin: Int { var setsToWin: Int {
switch self { switch self {
case .twoSets, .twoSetsSuperTie, .twoSetsOfFourGames, .twoSetsDecisivePoint, .twoSetsOfFourGamesDecisivePoint, .twoSetsDecisivePointSuperTie: case .twoSets, .twoSetsSuperTie, .twoSetsOfFourGames, .twoSetsDecisivePoint, .twoSetsOfFourGamesDecisivePoint, .twoSetsDecisivePointSuperTie, .twoSetsOfSuperTie:
return 2 return 2
case .nineGames, .nineGamesDecisivePoint, .superTie, .megaTie: case .nineGames, .nineGamesDecisivePoint, .superTie, .megaTie, .singleSet, .singleSetDecisivePoint, .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint:
return 1 return 1
} }
} }
var setFormat: SetFormat { var setFormat: SetFormat {
switch self { switch self {
case .twoSets, .twoSetsSuperTie, .twoSetsDecisivePoint, .twoSetsDecisivePointSuperTie: case .twoSets, .twoSetsSuperTie, .twoSetsDecisivePoint, .twoSetsDecisivePointSuperTie, .singleSet, .singleSetDecisivePoint:
return .six return .six
case .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint: case .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint:
return .four return .four
case .nineGames, .nineGamesDecisivePoint: case .nineGames, .nineGamesDecisivePoint:
return .nine return .nine
case .superTie: case .superTie, .twoSetsOfSuperTie:
return .superTieBreak return .superTieBreak
case .megaTie: case .megaTie:
return .megaTieBreak return .megaTieBreak
@ -1592,7 +1670,8 @@ enum RoundRule {
} }
static func numberOfRounds(forTeams teams: Int) -> Int { static func numberOfRounds(forTeams teams: Int) -> Int {
Int(log2(Double(teamsInFirstRound(forTeams: teams)))) if teams == 0 { return 0 }
return Int(log2(Double(teamsInFirstRound(forTeams: teams))))
} }
static func matchIndex(fromRoundIndex roundIndex: Int) -> Int { static func matchIndex(fromRoundIndex roundIndex: Int) -> Int {
@ -1680,6 +1759,113 @@ enum PadelTournamentStructurePreset: Int, Identifiable, CaseIterable {
case manual case manual
case doubleGroupStage case doubleGroupStage
case federalStructure_8
case federalStructure_12
case federalStructure_16
case federalStructure_20
case federalStructure_24
case federalStructure_32
case federalStructure_48
case federalStructure_64
// Maximum qualified pairs based on the structure preset
func tableDimension() -> Int {
switch self {
case .federalStructure_8:
return 8
case .federalStructure_12:
return 12
case .federalStructure_16:
return 16
case .federalStructure_20:
return 20
case .federalStructure_24:
return 24
case .federalStructure_32:
return 32
case .federalStructure_48:
return 48
case .federalStructure_64:
return 64
case .manual:
return 24
case .doubleGroupStage:
return 9
}
}
// Wildcards allowed in the Qualifiers
func wildcardBrackets() -> Int {
switch self {
case .federalStructure_8:
return 0
case .federalStructure_12:
return 1
case .federalStructure_16, .federalStructure_20, .federalStructure_24, .federalStructure_32:
return 2
case .federalStructure_48, .federalStructure_64:
return 4
case .manual, .doubleGroupStage:
return 0
}
}
// Wildcards allowed in the Qualifiers
func wildcardQualifiers() -> Int {
switch self {
case .federalStructure_8:
return 0
case .federalStructure_12, .federalStructure_16:
return 1
case .federalStructure_20, .federalStructure_24:
return 2
case .federalStructure_32:
return 4
case .federalStructure_48:
return 6
case .federalStructure_64:
return 8
case .manual, .doubleGroupStage:
return 0
}
}
// Number of teams admitted to the Qualifiers
func teamsInQualifiers() -> Int {
switch self {
case .federalStructure_8:
return 8
case .federalStructure_12:
return 12
case .federalStructure_16:
return 16
case .federalStructure_20:
return 20
case .federalStructure_24:
return 24
case .federalStructure_32:
return 32
case .federalStructure_48:
return 48
case .federalStructure_64:
return 64
case .manual, .doubleGroupStage:
return 0
}
}
// Maximum teams that can qualify from the Qualifiers to the Final Table
func maxTeamsFromQualifiers() -> Int {
switch self {
case .federalStructure_8, .federalStructure_12:
return 2
case .federalStructure_16, .federalStructure_20, .federalStructure_24:
return 4
case .federalStructure_32, .federalStructure_48, .federalStructure_64:
return 8
case .manual, .doubleGroupStage:
return 0
}
}
func localizedStructurePresetTitle() -> String { func localizedStructurePresetTitle() -> String {
switch self { switch self {
@ -1687,6 +1873,22 @@ enum PadelTournamentStructurePreset: Int, Identifiable, CaseIterable {
return "Défaut" return "Défaut"
case .doubleGroupStage: case .doubleGroupStage:
return "2 phases de poules" return "2 phases de poules"
case .federalStructure_8:
return "Structure fédérale 8"
case .federalStructure_12:
return "Structure fédérale 12"
case .federalStructure_16:
return "Structure fédérale 16"
case .federalStructure_20:
return "Structure fédérale 20"
case .federalStructure_24:
return "Structure fédérale 24"
case .federalStructure_32:
return "Structure fédérale 32"
case .federalStructure_48:
return "Structure fédérale 48"
case .federalStructure_64:
return "Structure fédérale 64"
} }
} }
@ -1695,7 +1897,85 @@ enum PadelTournamentStructurePreset: Int, Identifiable, CaseIterable {
case .manual: case .manual:
return "24 équipes, 4 poules de 4, 1 qualifié par poule" return "24 équipes, 4 poules de 4, 1 qualifié par poule"
case .doubleGroupStage: case .doubleGroupStage:
return "Poules qui enchaîne sur une autre phase de poule : les premiers de chaque se retrouve ensemble, puis les 2èmes, etc." return "Poules qui enchaînent sur une autre phase de poules: les premiers de chaque se retrouvent ensemble, puis les deuxièmes, etc."
case .federalStructure_8:
return "Tableau final à 8 paires, dont 2 qualifiées sortant de qualifications à 8 paires maximum. Aucune wildcard."
case .federalStructure_12, .federalStructure_16, .federalStructure_20, .federalStructure_24, .federalStructure_32, .federalStructure_48, .federalStructure_64:
return "Tableau final à \(tableDimension()) paires, dont \(maxTeamsFromQualifiers()) qualifiées sortant de qualifications à \(teamsInQualifiers()) paires maximum. \(wildcardBrackets()) wildcard\(wildcardBrackets().pluralSuffix) en tableau et \(wildcardQualifiers()) wildcard\(wildcardQualifiers().pluralSuffix) en qualifications."
}
}
func groupStageCount() -> Int {
switch self {
case .manual:
4
case .doubleGroupStage:
3
case .federalStructure_8:
2
case .federalStructure_12:
2
case .federalStructure_16:
4
case .federalStructure_20:
4
case .federalStructure_24:
4
case .federalStructure_32:
8
case .federalStructure_48:
8
case .federalStructure_64:
8
}
}
func teamsPerGroupStage() -> Int {
switch self {
case .manual:
4
case .doubleGroupStage:
3
case .federalStructure_8:
4
case .federalStructure_12:
6
case .federalStructure_16:
4
case .federalStructure_20:
5
case .federalStructure_24:
6
case .federalStructure_32:
4
case .federalStructure_48:
6
case .federalStructure_64:
8
}
}
func qualifiedPerGroupStage() -> Int {
switch self {
case .doubleGroupStage:
0
default:
1
}
}
func hasWildcards() -> Bool {
wildcardBrackets() > 0 || wildcardQualifiers() > 0
}
func isFederalPreset() -> Bool {
switch self {
case .manual:
return false
case .doubleGroupStage:
return false
case .federalStructure_8, .federalStructure_12, .federalStructure_16, .federalStructure_20, .federalStructure_24, .federalStructure_32, .federalStructure_48, .federalStructure_64:
return true
} }
} }
} }

@ -47,120 +47,9 @@ class Patcher {
case .syncUpgrade: self._syncUpgrade() case .syncUpgrade: self._syncUpgrade()
} }
} }
//
// fileprivate static func _patchAlexisLeDu() {
// guard StoreCenter.main.userId == "94f45ed2-8938-4c32-a4b6-e4525073dd33" else { return }
//
// let clubs = DataStore.shared.clubs
// StoreCenter.main.resetApiCalls(collection: clubs)
//// clubs.resetApiCalls()
//
// for club in clubs.filter({ $0.creator == "d5060b89-e979-4c19-bf78-e459a6ed5318"}) {
// club.creator = StoreCenter.main.userId
// clubs.writeChangeAndInsertOnServer(instance: club)
// }
//
// }
//
// fileprivate static func _importDataFromDev() throws {
//
// let devServices = Services(url: "https://xlr.alwaysdata.net/roads/")
// guard devServices.hasToken() else {
// return
// }
// guard StoreCenter.main.synchronizationApiURL == "https://padelclub.app/roads/" else {
// return
// }
//
// guard let userId = StoreCenter.main.userId else {
// return
// }
//
// try StoreCenter.main.migrateToken(devServices)
//
//
// let myClubs: [Club] = DataStore.shared.clubs.filter { $0.creator == userId }
// let clubIds: [String] = myClubs.map { $0.id }
//
// myClubs.forEach { club in
// DataStore.shared.clubs.insertIntoCurrentService(item: club)
//
// let courts = DataStore.shared.courts.filter { clubIds.contains($0.club) }
// for court in courts {
// DataStore.shared.courts.insertIntoCurrentService(item: court)
// }
// }
//
// DataStore.shared.user.clubs = Array(clubIds)
// DataStore.shared.saveUser()
//
// DataStore.shared.events.insertAllIntoCurrentService()
// DataStore.shared.tournaments.insertAllIntoCurrentService()
// DataStore.shared.dateIntervals.insertAllIntoCurrentService()
//
// for tournament in DataStore.shared.tournaments {
// let store = tournament.tournamentStore
//
// Task { // need to wait for the collections to load
// try await Task.sleep(until: .now + .seconds(2))
//
// store.teamRegistrations.insertAllIntoCurrentService()
// store.rounds.insertAllIntoCurrentService()
// store.groupStages.insertAllIntoCurrentService()
// store.matches.insertAllIntoCurrentService()
// store.playerRegistrations.insertAllIntoCurrentService()
// store.teamScores.insertAllIntoCurrentService()
//
// }
// }
//
// }
//
// fileprivate static func _patchMissingMatches() {
//
// guard let url = StoreCenter.main.synchronizationApiURL else {
// return
// }
// guard url == "https://padelclub.app/roads/" else {
// return
// }
// let services = Services(url: url)
//
// for tournament in DataStore.shared.tournaments {
//
// let store = tournament.tournamentStore
// let identifier = StoreIdentifier(value: tournament.id, parameterName: "tournament")
//
// Task {
//
// do {
// // if nothing is online we upload the data
// let matches: [Match] = try await services.get(identifier: identifier)
// if matches.isEmpty {
// store.matches.insertAllIntoCurrentService()
// }
//
// let playerRegistrations: [PlayerRegistration] = try await services.get(identifier: identifier)
// if playerRegistrations.isEmpty {
// store.playerRegistrations.insertAllIntoCurrentService()
// }
//
// let teamScores: [TeamScore] = try await services.get(identifier: identifier)
// if teamScores.isEmpty {
// store.teamScores.insertAllIntoCurrentService()
// }
//
// } catch {
// Logger.error(error)
// }
//
// }
// }
//
// }
fileprivate static func _cleanLogs() { fileprivate static func _cleanLogs() {
// StoreCenter.main.resetLoggingCollections() StoreCenter.main.resetLoggingCollections()
} }
fileprivate static func _syncUpgrade() { fileprivate static func _syncUpgrade() {

@ -60,9 +60,9 @@ class SourceFileManager {
} }
} }
func exportToCSV(players: [FederalPlayer], sourceFileType: SourceFile, date: Date) { func exportToCSV(_ prefix: String = "", players: [FederalPlayer], sourceFileType: SourceFile, date: Date) {
let lastDateString = URL.importDateFormatter.string(from: date) let lastDateString = URL.importDateFormatter.string(from: date)
let dateString = ["CLASSEMENT-PADEL", sourceFileType.rawValue, lastDateString].joined(separator: "-") + "." + "csv" let dateString = [prefix, "CLASSEMENT-PADEL", sourceFileType.rawValue, lastDateString].filter({ $0.isEmpty == false }).joined(separator: "-") + "." + "csv"
let documentsUrl:URL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first as URL?)! let documentsUrl:URL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first as URL?)!
let destinationFileUrl = documentsUrl.appendingPathComponent("\(dateString)") let destinationFileUrl = documentsUrl.appendingPathComponent("\(dateString)")

@ -50,6 +50,7 @@ enum URLs: String, Identifiable {
} }
enum PageLink: String, Identifiable, CaseIterable { enum PageLink: String, Identifiable, CaseIterable {
case info = "Informations"
case teams = "Équipes" case teams = "Équipes"
case summons = "Convocations" case summons = "Convocations"
case groupStages = "Poules" case groupStages = "Poules"
@ -68,6 +69,8 @@ enum PageLink: String, Identifiable, CaseIterable {
switch self { switch self {
case .matches: case .matches:
return "" return ""
case .info:
return "info"
case .teams: case .teams:
return "teams" return "teams"
case .summons: case .summons:

@ -22,12 +22,13 @@ class FederalDataViewModel {
var searchAttemptCount: Int = 0 var searchAttemptCount: Int = 0
var dayDuration: Int? var dayDuration: Int?
var dayPeriod: DayPeriod = .all var dayPeriod: DayPeriod = .all
var lastError: NetworkManagerError?
func filterStatus() -> String { func filterStatus() -> String {
var labels: [String] = [] var labels: [String] = []
labels.append(contentsOf: levels.map { $0.localizedLevelLabel() }.formatList()) labels.append(contentsOf: levels.map { $0.localizedLevelLabel() }.formatList())
labels.append(contentsOf: categories.map { $0.localizedLabel() }.formatList()) labels.append(contentsOf: categories.map { $0.localizedLabel() }.formatList())
labels.append(contentsOf: ageCategories.map { $0.localizedLabel() }.formatList()) labels.append(contentsOf: ageCategories.map { $0.localizedFederalAgeLabel() }.formatList())
let clubNames = selectedClubs.compactMap { codeClub in let clubNames = selectedClubs.compactMap { codeClub in
let club: Club? = DataStore.shared.clubs.first(where: { $0.code == codeClub }) let club: Club? = DataStore.shared.clubs.first(where: { $0.code == codeClub })
return club?.clubTitle(.short) return club?.clubTitle(.short)

@ -1,11 +1,12 @@
// //
// MatchDescriptor.swift // swift
// PadelClub // PadelClub
// //
// Created by Razmig Sarkissian on 02/04/2024. // Created by Razmig Sarkissian on 02/04/2024.
// //
import Foundation import Foundation
import SwiftUI
class MatchDescriptor: ObservableObject { class MatchDescriptor: ObservableObject {
@Published var matchFormat: MatchFormat @Published var matchFormat: MatchFormat
@ -16,6 +17,58 @@ class MatchDescriptor: ObservableObject {
var teamLabelTwo: String = "" var teamLabelTwo: String = ""
var startDate: Date = Date() var startDate: Date = Date()
var match: Match? var match: Match?
let colorTeamOne: Color = .teal
let colorTeamTwo: Color = .indigo
var teamOneSetupIsActive: Bool {
if hasEnded && showSetInputView == false && showTieBreakInputView == false {
return false
}
guard let setDescriptor = setDescriptors.last else {
return false
}
if setDescriptor.valueTeamOne == nil {
return true
} else if setDescriptor.valueTeamTwo == nil {
return false
} else if setDescriptor.tieBreakValueTeamOne == nil, setDescriptor.shouldTieBreak {
return true
} else if setDescriptor.tieBreakValueTeamTwo == nil, setDescriptor.shouldTieBreak {
return false
}
return false
}
var teamTwoSetupIsActive: Bool {
if hasEnded && showSetInputView == false && showTieBreakInputView == false {
return false
}
guard let setDescriptor = setDescriptors.last else {
return false
}
if setDescriptor.valueTeamOne == nil {
return false
} else if setDescriptor.valueTeamTwo == nil {
return true
} else if setDescriptor.tieBreakValueTeamOne == nil, setDescriptor.shouldTieBreak {
return false
} else if setDescriptor.tieBreakValueTeamTwo == nil, setDescriptor.shouldTieBreak {
return true
}
return true
}
var showSetInputView: Bool {
return setDescriptors.anySatisfy({ $0.showSetInputView })
}
var showTieBreakInputView: Bool {
return setDescriptors.anySatisfy({ $0.showTieBreakInputView })
}
init(match: Match? = nil) { init(match: Match? = nil) {
self.match = match self.match = match

@ -0,0 +1,51 @@
//
// MatchViewStyle.swift
// PadelClub
//
// Created by razmig on 17/11/2024.
//
import SwiftUI
enum MatchViewStyle {
case standardStyle // vue normal
case sectionedStandardStyle // vue normal avec des sections indiquant déjà la manche
case feedStyle // vue programmation
case plainStyle // vue detail
//case tournamentResultStyle //vue resultat tournoi
case followUpStyle // vue normal
func displayRestingTime() -> Bool {
switch self {
case .standardStyle:
return false
case .sectionedStandardStyle:
return false
case .feedStyle:
return false
case .plainStyle:
return false
// case .tournamentResultStyle:
// return false
case .followUpStyle:
return true
}
}
}
struct MatchViewStyleKey: EnvironmentKey {
static let defaultValue: MatchViewStyle = .standardStyle
}
extension EnvironmentValues {
var matchViewStyle: MatchViewStyle {
get { self[MatchViewStyleKey.self] }
set { self[MatchViewStyleKey.self] = newValue }
}
}
extension View {
func matchViewStyle(_ style: MatchViewStyle) -> some View {
environment(\.matchViewStyle, style)
}
}

@ -21,4 +21,5 @@ enum Screen: String, Codable {
case event case event
case print case print
case share case share
case restingTime
} }

@ -36,8 +36,7 @@ class SearchViewModel: ObservableObject, Identifiable {
@Published var filterSelectionEnabled: Bool = false @Published var filterSelectionEnabled: Bool = false
@Published var isPresented: Bool = false @Published var isPresented: Bool = false
@Published var selectedAgeCategory: FederalTournamentAge = .unlisted @Published var selectedAgeCategory: FederalTournamentAge = .unlisted
@Published var mostRecentDate: Date? = nil
var mostRecentDate: Date? = nil
var selectionIsOver: Bool { var selectionIsOver: Bool {
if allowSingleSelection && selectedPlayers.count == 1 { if allowSingleSelection && selectedPlayers.count == 1 {
@ -69,9 +68,6 @@ class SearchViewModel: ObservableObject, Identifiable {
var message = ["Vérifiez l'ortographe ou lancez une nouvelle recherche."] var message = ["Vérifiez l'ortographe ou lancez une nouvelle recherche."]
if tokens.isEmpty { if tokens.isEmpty {
message.append("Il est possible que cette personne n'est joué aucun tournoi depuis les 12 derniers mois, dans ce cas, Padel Club ne pourra pas le trouver.") message.append("Il est possible que cette personne n'est joué aucun tournoi depuis les 12 derniers mois, dans ce cas, Padel Club ne pourra pas le trouver.")
if filterOption == .male {
message.append("Depuis août 2024, le classement fédérale disponible est limité aux 40.000 premiers joueurs. Si le joueur n'a pas encore assez de points pour être visible, Padel Club ne pourra pas non plus le trouver.")
}
} }
return message.joined(separator: "\n") return message.joined(separator: "\n")
} }
@ -231,7 +227,7 @@ class SearchViewModel: ObservableObject, Identifiable {
] ]
if let mostRecentDate { if let mostRecentDate {
//predicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg)) predicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg))
} }
if hideAssimilation { if hideAssimilation {
@ -344,7 +340,7 @@ class SearchViewModel: ObservableObject, Identifiable {
} }
if let mostRecentDate { if let mostRecentDate {
//andPredicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg)) andPredicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg))
} }
if nameComponents.count > 1 { if nameComponents.count > 1 {

@ -14,6 +14,12 @@ struct SetDescriptor: Identifiable, Equatable {
var tieBreakValueTeamOne: Int? var tieBreakValueTeamOne: Int?
var tieBreakValueTeamTwo: Int? var tieBreakValueTeamTwo: Int?
var setFormat: SetFormat var setFormat: SetFormat
var showSetInputView: Bool = true
var showTieBreakInputView: Bool = false
var isTeamOneSet: Bool {
return valueTeamOne != nil || tieBreakValueTeamOne != nil
}
var hasEnded: Bool { var hasEnded: Bool {
if let valueTeamTwo, let valueTeamOne { if let valueTeamTwo, let valueTeamOne {
@ -30,4 +36,8 @@ struct SetDescriptor: Identifiable, Equatable {
return nil return nil
} }
} }
var shouldTieBreak: Bool {
setFormat.shouldTiebreak(scoreTeamOne: valueTeamOne ?? 0, scoreTeamTwo: valueTeamTwo ?? 0)
}
} }

@ -0,0 +1,214 @@
//
// BracketCallingView.swift
// PadelClub
//
// Created by razmig on 15/10/2024.
//
import SwiftUI
import LeStorage
struct BracketCallingView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
@State private var initialSeedRound: Int = 0
@State private var initialSeedCount: Int = 0
let tournamentRounds: [Round]
let teams: [TeamRegistration]
init(tournament: Tournament) {
let rounds = tournament.rounds()
self.tournamentRounds = rounds
self.teams = tournament.availableSeeds()
if tournament.initialSeedRound == 0, rounds.count > 0 {
let index = rounds.count - 1
_initialSeedRound = .init(wrappedValue: index)
_initialSeedCount = .init(wrappedValue: RoundRule.numberOfMatches(forRoundIndex: index))
} else if tournament.initialSeedRound < rounds.count {
_initialSeedRound = .init(wrappedValue: tournament.initialSeedRound)
_initialSeedCount = .init(wrappedValue: tournament.initialSeedCount)
} else if rounds.count > 0 {
let index = rounds.count - 1
_initialSeedRound = .init(wrappedValue: index)
_initialSeedCount = .init(wrappedValue: RoundRule.numberOfMatches(forRoundIndex: index))
}
}
var initialRound: Round {
tournamentRounds.first(where: { $0.index == initialSeedRound })!
}
func filteredRounds() -> [Round] {
tournamentRounds.filter({ $0.index >= initialSeedRound }).reversed()
}
func seedCount(forRoundIndex roundIndex: Int) -> Int {
if roundIndex < initialSeedRound { return 0 }
if roundIndex == initialSeedRound {
return initialSeedCount
}
let seedCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex)
let previousSeedCount = self.seedCount(forRoundIndex: roundIndex - 1)
let total = seedCount - previousSeedCount
if total < 0 { return 0 }
return total
}
func seeds(forRoundIndex roundIndex: Int) -> [TeamRegistration] {
let previousSeeds: Int = (initialSeedRound..<roundIndex).map { seedCount(forRoundIndex: $0) }.reduce(0, +)
if roundIndex == tournamentRounds.count - 1 {
return Array(teams.dropFirst(previousSeeds))
} else {
return Array(teams.dropFirst(previousSeeds).prefix(seedCount(forRoundIndex: roundIndex)))
}
}
var body: some View {
List {
let uncalledTeams = teams.filter({ $0.callDate == nil })
if uncalledTeams.isEmpty == false {
NavigationLink {
TeamsCallingView(teams: uncalledTeams)
.environment(tournament)
} label: {
LabeledContent("Équipe\(uncalledTeams.count.pluralSuffix) non contactée\(uncalledTeams.count.pluralSuffix)", value: uncalledTeams.count.formatted())
}
}
PlayersWithoutContactView(players: teams.flatMap({ $0.unsortedPlayers() }).sorted(by: \.computedRank))
Section {
Picker(selection: $initialSeedRound) {
ForEach(tournamentRounds) {
Text($0.roundTitle()).tag($0.index)
}
} label: {
Text("Premier tour")
}
.onChange(of: initialSeedRound) {
initialSeedCount = RoundRule.numberOfMatches(forRoundIndex: initialSeedRound)
}
LabeledContent {
StepperView(count: $initialSeedCount, minimum: 0, maximum: RoundRule.numberOfMatches(forRoundIndex: initialSeedRound))
} label: {
Text("Têtes de série")
}
} footer: {
Text("Permet de convoquer par tour du tableau sans avoir tirer au sort les tétes de série. Vous pourrez ensuite confirmer leur horaire plus précis si le tour se joue sur plusieurs rotations. Les équipes ne peuvent pas être considéré comme convoqué au bon horaire en dehors de cet écran tant qu'elles n'ont pas été placé dans le tableau.")
}
ForEach(filteredRounds()) { round in
let seeds = seeds(forRoundIndex: round.index)
let startDate = round.startDate ?? round.playedMatches().first?.startDate
let callSeeds = seeds.filter({ tournament.isStartDateIsDifferentThanCallDate($0, expectedSummonDate: startDate) == false })
if seeds.isEmpty == false {
Section {
NavigationLink {
_roundView(round: round, seeds: seeds)
.environment(tournament)
} label: {
CallView.CallStatusView(count: callSeeds.count, total: seeds.count, startDate: startDate, title: "convoquées")
}
} header: {
Text(round.roundTitle())
} footer: {
if let startDate {
CallView(teams: seeds, callDate: startDate, matchFormat: round.matchFormat, roundLabel: round.roundTitle())
}
}
}
}
}
.onDisappear(perform: {
tournament.initialSeedCount = initialSeedCount
tournament.initialSeedRound = initialSeedRound
_save()
})
.headerProminence(.increased)
.navigationTitle("Pré-convocation")
}
private func _save() {
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
}
@ViewBuilder
private func _roundView(round: Round, seeds: [TeamRegistration]) -> some View {
List {
let uncalledTeams = seeds.filter({ $0.callDate == nil })
if uncalledTeams.isEmpty == false {
NavigationLink {
TeamsCallingView(teams: uncalledTeams)
.environment(tournament)
} label: {
LabeledContent("Équipe\(uncalledTeams.count.pluralSuffix) non contactée\(uncalledTeams.count.pluralSuffix)", value: uncalledTeams.count.formatted())
}
}
let startDate = round.startDate ?? round.playedMatches().first?.startDate
let badCalled = seeds.filter({ tournament.isStartDateIsDifferentThanCallDate($0, expectedSummonDate: startDate) })
if badCalled.isEmpty == false {
Section {
ForEach(badCalled) { team in
TeamCallView(team: team)
}
} header: {
HStack {
Text("Mauvais horaire")
Spacer()
Text(badCalled.count.formatted() + " équipe\(badCalled.count.pluralSuffix)")
}
} footer: {
if let startDate {
CallView(teams: badCalled, callDate: startDate, matchFormat: round.matchFormat, roundLabel: round.roundTitle())
}
}
}
Section {
ForEach(seeds) { team in
TeamCallView(team: team)
}
} header: {
HStack {
Text(round.roundTitle())
Spacer()
Text(seeds.count.formatted() + " équipe\(seeds.count.pluralSuffix)")
}
}
}
.overlay {
if seeds.isEmpty {
ContentUnavailableView {
Label("Aucune équipe dans ce tour", systemImage: "clock.badge.questionmark")
} description: {
Text("Padel Club n'a pas réussi à déterminer quelles équipes jouent ce tour.")
} actions: {
// RowButtonView("Horaire intelligent") {
// selectedScheduleDestination = nil
// }
}
}
}
.headerProminence(.increased)
.navigationTitle(round.roundTitle())
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
}
}
//#Preview {
// SeedsCallingView()
//}

@ -43,7 +43,15 @@ struct CallMessageCustomizationView: View {
} }
var computedMessage: String { var computedMessage: String {
[entryFeeMessage, customCallMessageBody].compacted().map { $0.trimmedMultiline }.joined(separator: "\n") var linkMessage: String? {
if tournament.isPrivate == false, let shareLink = tournament.shareURL(.matches)?.absoluteString {
return "Vous pourrez suivre tous les résultats de ce tournoi sur le site :\n\n".appending(shareLink)
} else {
return nil
}
}
return [entryFeeMessage, customCallMessageBody, linkMessage].compacted().map { $0.trimmedMultiline }.joined(separator: "\n")
} }
var finalMessage: String? { var finalMessage: String? {
@ -259,7 +267,7 @@ struct CallMessageCustomizationView: View {
} }
}.italic().foregroundStyle(.gray) }.italic().foregroundStyle(.gray)
} header: { } header: {
Text("Rendu généré automatiquement") Text("Exemple généré automatiquement")
} }
} }

@ -14,6 +14,7 @@ struct CallView: View {
let count: Int let count: Int
let total: Int let total: Int
let startDate: Date? let startDate: Date?
var title: String = "convoquées au bon horaire"
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
@ -32,7 +33,7 @@ struct CallView: View {
Text(startDate.formatted(.dateTime.weekday().day(.twoDigits).month().year())) Text(startDate.formatted(.dateTime.weekday().day(.twoDigits).month().year()))
} }
Spacer() Spacer()
Text("convoquées au bon horaire") Text(title)
} }
.font(.caption) .font(.caption)
.foregroundColor(.secondary) .foregroundColor(.secondary)
@ -40,14 +41,6 @@ struct CallView: View {
} }
} }
struct TeamView: View {
let team: TeamRegistration
var body: some View {
TeamRowView(team: team, displayCallDate: true)
}
}
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@EnvironmentObject var networkMonitor: NetworkMonitor @EnvironmentObject var networkMonitor: NetworkMonitor
@ -57,6 +50,7 @@ struct CallView: View {
let callDate: Date let callDate: Date
let matchFormat: MatchFormat let matchFormat: MatchFormat
let roundLabel: String let roundLabel: String
let displayContext: SummoningDisplayContext
@State private var contactType: ContactType? = nil @State private var contactType: ContactType? = nil
@State private var sentError: ContactManagerError? = nil @State private var sentError: ContactManagerError? = nil
@ -67,6 +61,49 @@ struct CallView: View {
@State var summonParamByMessage: Bool = false @State var summonParamByMessage: Bool = false
@State var summonParamReSummon: Bool = false @State var summonParamReSummon: Bool = false
let simpleMode : Bool
init(teams: [TeamRegistration], callDate: Date, matchFormat: MatchFormat, roundLabel: String) {
self.teams = teams
self.callDate = callDate
self.matchFormat = matchFormat
self.roundLabel = roundLabel
self.simpleMode = false
self.displayContext = .footer
}
init(teams: [TeamRegistration]) {
self.teams = teams
self.callDate = Date()
self.matchFormat = MatchFormat.nineGames
self.roundLabel = ""
self.simpleMode = true
self.displayContext = .footer
}
init(team: TeamRegistration, displayContext: SummoningDisplayContext) {
self.teams = [team]
let expectedSummonDate = team.expectedSummonDate()
self.displayContext = displayContext
if let expectedSummonDate, let initialMatch = team.initialMatch() {
self.callDate = expectedSummonDate
self.matchFormat = initialMatch.matchFormat
self.roundLabel = initialMatch.roundTitle() ?? "tableau"
self.simpleMode = false
} else if let expectedSummonDate, let initialGroupStage = team.groupStageObject() {
self.callDate = expectedSummonDate
self.matchFormat = initialGroupStage.matchFormat
self.roundLabel = "poule"
self.simpleMode = false
} else {
self.callDate = Date()
self.matchFormat = MatchFormat.nineGames
self.roundLabel = ""
self.simpleMode = true
}
}
var tournamentStore: TournamentStore { var tournamentStore: TournamentStore {
return self.tournament.tournamentStore return self.tournament.tournamentStore
} }
@ -82,9 +119,15 @@ struct CallView: View {
} }
private func _called(_ calledTeams: [TeamRegistration], _ success: Bool) { private func _called(_ calledTeams: [TeamRegistration], _ success: Bool) {
if simpleMode {
return
}
if success { if success {
calledTeams.forEach { team in calledTeams.forEach { team in
team.callDate = callDate team.callDate = callDate
if reSummon {
team.confirmationDate = nil
}
} }
do { do {
try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: calledTeams) try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: calledTeams)
@ -94,33 +137,39 @@ struct CallView: View {
} }
} }
func finalMessage(reSummon: Bool) -> String { func finalMessage(reSummon: Bool, forcedEmptyMessage: Bool) -> String {
ContactType.callingMessage(tournament: tournament, startDate: callDate, roundLabel: roundLabel, matchFormat: matchFormat, reSummon: reSummon) if simpleMode || forcedEmptyMessage {
let signature = dataStore.user.summonsMessageSignature ?? dataStore.user.defaultSignature()
return "\n\n\n\n" + signature
}
return ContactType.callingMessage(tournament: tournament, startDate: callDate, roundLabel: roundLabel, matchFormat: matchFormat, reSummon: reSummon)
} }
var reSummon: Bool { var reSummon: Bool {
if simpleMode {
return false
}
return self.teams.allSatisfy({ $0.called() }) return self.teams.allSatisfy({ $0.called() })
} }
var body: some View { var mainWord: String {
let callWord : String = (reSummon ? "Reconvoquer" : "Convoquer") if simpleMode {
HStack { return "Contacter"
if self.teams.count == 1 {
if let previousCallDate = teams.first?.callDate, Calendar.current.compare(previousCallDate, to: callDate, toGranularity: .minute) != .orderedSame {
Text("Reconvoquer \(self.callDate.localizedDate()) par")
} else { } else {
Text("\(callWord) cette paire par") return "Convoquer"
} }
} else {
Text("\(callWord) ces \(self.teams.count) paires par")
} }
self._summonMenu(byMessage: true) var body: some View {
Text("ou") Group {
self._summonMenu(byMessage: false) switch displayContext {
case .footer:
_footerStyleView()
case .menu:
_menuStyleView()
}
} }
.font(.subheadline)
.buttonStyle(.borderless)
.alert("Un problème est survenu", isPresented: messageSentFailed) { .alert("Un problème est survenu", isPresented: messageSentFailed) {
Button("OK") { Button("OK") {
} }
@ -217,11 +266,59 @@ struct CallView: View {
}) })
} }
private func _footerStyleView() -> some View {
HStack {
let callWord : String = (reSummon ? "Reconvoquer" : mainWord)
if self.teams.count == 1 {
if simpleMode {
Text("\(callWord) cette paire par")
} else {
if let previousCallDate = teams.first?.callDate, Calendar.current.compare(previousCallDate, to: callDate, toGranularity: .minute) != .orderedSame {
Text("Reconvoquer \(self.callDate.localizedDate()) par")
} else {
Text("\(callWord) cette paire par")
}
}
} else {
Text("\(callWord) ces \(self.teams.count) paires par")
}
self._summonMenu(byMessage: true)
Text("ou")
self._summonMenu(byMessage: false)
}
.font(.subheadline)
.buttonStyle(.borderless)
}
private func _menuStyleView() -> some View {
Menu {
self._summonMenu(byMessage: true)
self._summonMenu(byMessage: false)
} label: {
let callWord : String = (reSummon ? "Reconvoquer" : mainWord)
if self.teams.count == 1 {
if simpleMode {
Text("\(callWord) cette paire")
} else {
if let previousCallDate = teams.first?.callDate, Calendar.current.compare(previousCallDate, to: callDate, toGranularity: .minute) != .orderedSame {
Text("Reconvoquer \(self.callDate.localizedDate())")
} else {
Text("\(callWord) cette paire")
}
}
} else {
Text("\(callWord) ces \(self.teams.count) paires")
}
}
}
@ViewBuilder @ViewBuilder
private func _summonMenu(byMessage: Bool) -> some View { private func _summonMenu(byMessage: Bool) -> some View {
if self.reSummon { if self.reSummon {
Menu { Menu {
Button("Convoquer") { Button(mainWord) {
self._summon(byMessage: byMessage, reSummon: false) self._summon(byMessage: byMessage, reSummon: false)
} }
@ -229,6 +326,13 @@ struct CallView: View {
self._summon(byMessage: byMessage, reSummon: true) self._summon(byMessage: byMessage, reSummon: true)
} }
if simpleMode == false {
Divider()
Button("Contacter") {
self._summon(byMessage: byMessage, reSummon: false, forcedEmptyMessage: true)
}
}
} label: { } label: {
Text(byMessage ? "sms" : "mail") Text(byMessage ? "sms" : "mail")
.underline() .underline()
@ -240,15 +344,15 @@ struct CallView: View {
} }
} }
private func _summon(byMessage: Bool, reSummon: Bool) { private func _summon(byMessage: Bool, reSummon: Bool, forcedEmptyMessage: Bool = false) {
self.summonParamByMessage = byMessage self.summonParamByMessage = byMessage
self.summonParamReSummon = reSummon self.summonParamReSummon = reSummon
self._verifyUser { self._verifyUser {
self._payTournamentAndExecute { self._payTournamentAndExecute {
if byMessage { if byMessage {
self._contactByMessage(reSummon: reSummon) self._contactByMessage(reSummon: reSummon, forcedEmptyMessage: forcedEmptyMessage)
} else { } else {
self._contactByMail(reSummon: reSummon) self._contactByMail(reSummon: reSummon, forcedEmptyMessage: forcedEmptyMessage)
} }
} }
} }
@ -271,18 +375,18 @@ struct CallView: View {
} }
} }
fileprivate func _contactByMessage(reSummon: Bool) { fileprivate func _contactByMessage(reSummon: Bool, forcedEmptyMessage: Bool) {
self.contactType = .message(date: callDate, self.contactType = .message(date: callDate,
recipients: teams.flatMap { $0.getPhoneNumbers() }, recipients: teams.flatMap { $0.getPhoneNumbers() },
body: finalMessage(reSummon: reSummon), body: finalMessage(reSummon: reSummon, forcedEmptyMessage: forcedEmptyMessage),
tournamentBuild: nil) tournamentBuild: nil)
} }
fileprivate func _contactByMail(reSummon: Bool) { fileprivate func _contactByMail(reSummon: Bool, forcedEmptyMessage: Bool) {
self.contactType = .mail(date: callDate, self.contactType = .mail(date: callDate,
recipients: tournament.umpireMail(), recipients: tournament.umpireMail(),
bccRecipients: teams.flatMap { $0.getMail() }, bccRecipients: teams.flatMap { $0.getMail() },
body: finalMessage(reSummon: reSummon), body: finalMessage(reSummon: reSummon, forcedEmptyMessage: forcedEmptyMessage),
subject: tournament.tournamentTitle(), subject: tournament.tournamentTitle(),
tournamentBuild: nil) tournamentBuild: nil)
} }
@ -292,3 +396,20 @@ struct CallView: View {
} }
} }
struct TeamCallView: View {
@Environment(Tournament.self) var tournament: Tournament
let team: TeamRegistration
var action: (() -> Void)?
var body: some View {
NavigationLink {
CallMenuOptionsView(team: team, action: action)
.environment(tournament)
} label: {
TeamRowView(team: team, displayCallDate: true)
}
.buttonStyle(.plain)
.listRowView(isActive: team.confirmed(), color: .green, hideColorVariation: true)
}
}

@ -124,7 +124,7 @@ struct MenuWarningView: View {
@ViewBuilder @ViewBuilder
func _teamActionView(_ team: TeamRegistration) -> some View { func _teamActionView(_ team: TeamRegistration) -> some View {
Menu(team.name ?? "Toute l'équipe") { Menu(team.teamNameLabel()) {
let players = team.players() let players = team.players()
_actionView(players: players) _actionView(players: players)
} }

@ -31,7 +31,7 @@ struct PlayersWithoutContactView: View {
} }
} }
let withoutPhones = players.filter({ $0.phoneNumber?.isEmpty == true || $0.phoneNumber == nil }) let withoutPhones = players.filter({ $0.phoneNumber?.isEmpty == true || $0.phoneNumber == nil || $0.phoneNumber?.isMobileNumber() == false })
DisclosureGroup { DisclosureGroup {
ForEach(withoutPhones) { player in ForEach(withoutPhones) { player in
NavigationLink { NavigationLink {
@ -45,7 +45,7 @@ struct PlayersWithoutContactView: View {
LabeledContent { LabeledContent {
Text(withoutPhones.count.formatted()) Text(withoutPhones.count.formatted())
} label: { } label: {
Text("Joueurs sans téléphone") Text("Joueurs sans téléphone portable")
} }
} }
} header: { } header: {

@ -15,11 +15,14 @@ struct GroupStageCallingView: View {
let groupStages = tournament.groupStages() let groupStages = tournament.groupStages()
List { List {
let uncalled = groupStages.flatMap({ $0.unsortedTeams() }).filter({ $0.callDate == nil })
if uncalled.isEmpty == false {
NavigationLink { NavigationLink {
TeamsCallingView(teams: groupStages.flatMap({ $0.unsortedTeams() }).filter({ $0.callDate == nil })) TeamsCallingView(teams: uncalled)
.environment(tournament) .environment(tournament)
} label: { } label: {
LabeledContent("Équipes non contactées", value: groupStages.flatMap({ $0.unsortedTeams() }).filter({ $0.callDate == nil }).count.formatted()) LabeledContent("Équipe\(uncalled.count.pluralSuffix) non contactée\(uncalled.count.pluralSuffix)", value: uncalled.count.formatted())
}
} }
PlayersWithoutContactView(players: groupStages.flatMap({ $0.unsortedTeams() }).flatMap({ $0.unsortedPlayers() }).sorted(by: \.computedRank)) PlayersWithoutContactView(players: groupStages.flatMap({ $0.unsortedTeams() }).flatMap({ $0.unsortedPlayers() }).sorted(by: \.computedRank))
@ -83,7 +86,7 @@ struct GroupStageCallingView: View {
ForEach(teams) { team in ForEach(teams) { team in
if let startDate = groupStage.initialStartDate(forTeam: team) { if let startDate = groupStage.initialStartDate(forTeam: team) {
Section { Section {
CallView.TeamView(team: team) TeamCallView(team: team)
} header: { } header: {
Text(startDate.localizedDate()) Text(startDate.localizedDate())
} footer: { } footer: {

@ -14,18 +14,21 @@ struct SeedsCallingView: View {
var body: some View { var body: some View {
List { List {
let tournamentRounds = tournament.rounds() let tournamentRounds = tournament.rounds()
let uncalledSeeds = tournament.seededTeams().filter({ $0.callDate == nil })
if uncalledSeeds.isEmpty == false {
NavigationLink { NavigationLink {
TeamsCallingView(teams: tournament.seededTeams().filter({ $0.callDate == nil })) TeamsCallingView(teams: uncalledSeeds)
.environment(tournament) .environment(tournament)
} label: { } label: {
LabeledContent("Équipes non contactées", value: tournament.seededTeams().filter({ $0.callDate == nil }).count.formatted()) LabeledContent("Équipe\(uncalledSeeds.count.pluralSuffix) non contactée\(uncalledSeeds.count.pluralSuffix)", value: uncalledSeeds.count.formatted())
}
} }
PlayersWithoutContactView(players: tournament.seededTeams().flatMap({ $0.unsortedPlayers() }).sorted(by: \.computedRank)) PlayersWithoutContactView(players: tournament.seededTeams().flatMap({ $0.unsortedPlayers() }).sorted(by: \.computedRank))
ForEach(tournamentRounds) { round in ForEach(tournamentRounds) { round in
let seeds = round.seeds() let seeds = round.teamsOrSeeds()
let callSeeds = seeds.filter({ tournament.isStartDateIsDifferentThanCallDate($0) == false }) let callSeeds = seeds.filter({ tournament.isStartDateIsDifferentThanCallDate($0) == false })
if seeds.isEmpty == false { if seeds.isEmpty == false {
Section { Section {
@ -63,8 +66,14 @@ struct SeedsCallingView: View {
} }
} }
NavigationLink("Équipes non contactées") { let uncalledTeams = round.teams().filter({ $0.callDate == nil })
TeamsCallingView(teams: round.teams().filter({ $0.callDate == nil })) if uncalledTeams.isEmpty == false {
NavigationLink {
TeamsCallingView(teams: uncalledTeams)
.environment(tournament)
} label: {
LabeledContent("Équipe\(uncalledTeams.count.pluralSuffix) non contactée\(uncalledTeams.count.pluralSuffix)", value: uncalledTeams.count.formatted())
}
} }
@ -92,7 +101,7 @@ struct SeedsCallingView: View {
let teams = round.seeds(inMatchIndex: match.index) let teams = round.seeds(inMatchIndex: match.index)
Section { Section {
ForEach(teams) { team in ForEach(teams) { team in
CallView.TeamView(team: team) TeamCallView(team: team)
} }
} header: { } header: {
HStack { HStack {

@ -12,77 +12,170 @@ struct TeamsCallingView: View {
@Environment(Tournament.self) var tournament: Tournament @Environment(Tournament.self) var tournament: Tournament
let teams : [TeamRegistration] let teams : [TeamRegistration]
@State private var hideConfirmed: Bool = false
@State private var hideGoodSummoned: Bool = false
@State private var hideSummoned: Bool = false
@State private var searchText: String = ""
var filteredTeams: [TeamRegistration] {
teams
.filter({ hideConfirmed == false || $0.confirmed() == false })
.filter({ hideSummoned == false || $0.called() == false })
.filter({ hideGoodSummoned == false || tournament.isStartDateIsDifferentThanCallDate($0) == true })
.filter({ searchText.isEmpty || $0.contains(searchText) })
}
var anyFilterEnabled: Bool {
hideConfirmed || hideGoodSummoned || hideSummoned
}
var body: some View { var body: some View {
List { List {
PlayersWithoutContactView(players: teams.flatMap({ $0.unsortedPlayers() }).sorted(by: \.computedRank))
let called = teams.filter { tournament.isStartDateIsDifferentThanCallDate($0) == false }
let confirmed = teams.filter { $0.confirmed() }
let justCalled = teams.filter { $0.called() }
let label = "\(justCalled.count.formatted()) / \(teams.count.formatted())"
let subtitle = "dont \(called.count.formatted()) au bon horaire"
let confirmedLabel = "\(confirmed.count.formatted()) / \(teams.count.formatted())"
if teams.isEmpty == false, searchText.isEmpty {
Section { Section {
ForEach(teams) { team in LabeledContent {
Menu { Text(label).font(.title3)
_menuOptions(team: team) } label: {
Text("Paire\(justCalled.count.pluralSuffix) convoquée\(justCalled.count.pluralSuffix)")
Text(subtitle)
}
LabeledContent {
Text(confirmedLabel).font(.title3)
} label: { } label: {
Text("Paire\(confirmed.count.pluralSuffix) confirmée\(confirmed.count.pluralSuffix)")
}
} footer: {
Text("Vous pouvez filtrer cette liste en appuyant sur ") + Text(Image(systemName: "line.3.horizontal.decrease.circle"))
}
}
if filteredTeams.isEmpty == false {
Section {
ForEach(filteredTeams) { team in
TeamCallView(team: team) {
searchText = ""
}
}
} header: {
HStack { HStack {
TeamRowView(team: team, displayCallDate: true) Text("Paire\(filteredTeams.count.pluralSuffix)")
Spacer() Spacer()
Menu { Text(filteredTeams.count.formatted())
_menuOptions(team: team)
} label: {
LabelOptions().labelStyle(.iconOnly)
} }
} footer: {
CallView(teams: filteredTeams)
}
} else {
ContentUnavailableView("Aucune équipe", systemImage: "person.2.slash")
} }
} }
.buttonStyle(.plain) .toolbar(content: {
.listRowView(isActive: team.confirmed(), color: .green, hideColorVariation: true) ToolbarItem(placement: .topBarTrailing) {
Menu {
Toggle(isOn: $hideConfirmed) {
Text("Masquer les confirmées")
} }
Toggle(isOn: $hideSummoned) {
Text("Masquer les convoquées")
} }
Toggle(isOn: $hideGoodSummoned) {
Text("Masquer les convoquées à la bonne heure")
} }
} label: {
LabelFilter()
.symbolVariant(anyFilterEnabled ? .fill : .none)
}
}
})
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
.headerProminence(.increased) .headerProminence(.increased)
.navigationTitle("Statut des équipes") .navigationTitle("Statut des convocations")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
} }
}
@ViewBuilder struct CallMenuOptionsView: View {
func _menuOptions(team: TeamRegistration) -> some View { @Environment(\.dismiss) private var dismiss
Button { @Environment(Tournament.self) var tournament: Tournament
let team: TeamRegistration
let action: (() -> Void)?
var confirmed: Binding<Bool> {
Binding {
team.confirmed()
} set: { _ in
team.toggleSummonConfirmation() team.toggleSummonConfirmation()
do { do {
try self.tournament.tournamentStore.teamRegistrations.addOrUpdate(instance: team) try self.tournament.tournamentStore.teamRegistrations.addOrUpdate(instance: team)
} catch { } catch {
Logger.error(error) Logger.error(error)
} }
action?()
}
}
var body: some View {
List {
Section {
TeamRowView(team: team, displayCallDate: true)
Toggle(isOn: confirmed) {
Text("Confirmation reçue")
}
if team.expectedSummonDate() != nil {
CallView(team: team, displayContext: .menu)
}
} footer: {
CallView(teams: [team])
}
Section {
NavigationLink {
EditingTeamView(team: team)
.environment(tournament)
} label: { } label: {
if team.confirmed() { Text("Détails de l'équipe")
Label("Confirmation reçue", systemImage: "checkmark.circle.fill").foregroundStyle(.green)
} else {
Label("Confirmation reçue", systemImage: "circle").foregroundStyle(.logoRed)
} }
} }
Divider()
Button(role: .destructive) { Section {
RowButtonView("Effacer la date de convocation", role: .destructive) {
team.callDate = nil team.callDate = nil
do { do {
try self.tournament.tournamentStore.teamRegistrations.addOrUpdate(instance: team) try self.tournament.tournamentStore.teamRegistrations.addOrUpdate(instance: team)
} catch { } catch {
Logger.error(error) Logger.error(error)
} }
} label: { action?()
Text("Effacer la date de convocation") dismiss()
}
} }
Section {
Divider() RowButtonView("Indiquer comme convoquée", role: .destructive) {
Button(role: .destructive) {
team.callDate = team.initialMatch()?.startDate ?? tournament.startDate team.callDate = team.initialMatch()?.startDate ?? tournament.startDate
do { do {
try self.tournament.tournamentStore.teamRegistrations.addOrUpdate(instance: team) try self.tournament.tournamentStore.teamRegistrations.addOrUpdate(instance: team)
} catch { } catch {
Logger.error(error) Logger.error(error)
} }
} label: { action?()
Text("Indiquer comme convoquée") dismiss()
} }
}
}
.navigationTitle("Options de convocation")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
} }
} }

@ -26,7 +26,7 @@ struct CashierDetailView: View {
Section { Section {
LabeledContent { LabeledContent {
if let earnings { if let earnings {
Text(earnings.formatted(.currency(code: "EUR").precision(.fractionLength(0)))) Text(earnings.formatted(.currency(code: Locale.defaultCurrency()).precision(.fractionLength(0))))
} else { } else {
ProgressView() ProgressView()
} }
@ -95,7 +95,7 @@ struct CashierDetailView: View {
Section { Section {
LabeledContent { LabeledContent {
if let earnings { if let earnings {
Text(earnings.formatted(.currency(code: "EUR").precision(.fractionLength(0)))) Text(earnings.formatted(.currency(code: Locale.defaultCurrency()).precision(.fractionLength(0))))
} else { } else {
ProgressView() ProgressView()
} }
@ -144,7 +144,7 @@ struct CashierDetailView: View {
var body: some View { var body: some View {
LabeledContent { LabeledContent {
if let value { if let value {
Text(value.formatted(.currency(code: "EUR"))) Text(value.formatted(.currency(code: Locale.defaultCurrency())))
} else { } else {
ProgressView() ProgressView()
} }
@ -173,7 +173,7 @@ struct CashierDetailView: View {
LabeledContent { LabeledContent {
if let entryFee = tournament.entryFee { if let entryFee = tournament.entryFee {
let sum = Double(count) * entryFee let sum = Double(count) * entryFee
Text(sum.formatted(.currency(code: "EUR"))) Text(sum.formatted(.currency(code: Locale.defaultCurrency())))
} }
} label: { } label: {
Text(type.localizedLabel()) Text(type.localizedLabel())

@ -24,71 +24,73 @@ struct CashierSettingsView: View {
var body: some View { var body: some View {
List { List {
Section { Section {
LabeledContent { TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $entryFee, format: .currency(code: Locale.defaultCurrency()))
TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $entryFee, format: .currency(code: Locale.current.currency?.identifier ?? "EUR"))
.keyboardType(.decimalPad) .keyboardType(.decimalPad)
.multilineTextAlignment(.trailing) .multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.focused($focusedField, equals: ._entryFee) .focused($focusedField, equals: ._entryFee)
} label: { } header: {
Text("Inscription") Text("Prix de l'inscription")
}
} footer: { } footer: {
Text("Si vous souhaitez que Padel Club vous aide à suivre les encaissements, indiquer un prix d'inscription. Sinon Padel Club vous aidera à suivre simplement l'arrivée et la présence des joueurs.") Text("Si vous souhaitez que Padel Club vous aide à suivre les encaissements, indiquer un prix d'inscription. Sinon Padel Club vous aidera à suivre simplement l'arrivée et la présence des joueurs.")
} }
let players = tournament.selectedPlayers()
if players.anySatisfy({ $0.hasArrived == false }) {
Section { Section {
RowButtonView("Tout le monde est arrivé", role: .destructive) { RowButtonView("Tout le monde est arrivé", role: .destructive) {
let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() })
players.forEach { player in players.forEach { player in
player.hasArrived = true player.hasArrived = true
} }
tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) _save(players: players)
} }
} footer: { } footer: {
Text("Indique tous les joueurs sont là") Text("Indique tous les joueurs sont là")
} }
}
if players.anySatisfy({ $0.hasArrived == true }) {
Section { Section {
RowButtonView("Personne n'est là", role: .destructive) { RowButtonView("Personne n'est là", role: .destructive) {
let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() })
players.forEach { player in players.forEach { player in
player.hasArrived = false player.hasArrived = false
} }
tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) _save(players: players)
} }
} footer: { } footer: {
Text("Indique qu'aucun joueur n'est arrivé") Text("Indique qu'aucun joueur n'est arrivé")
} }
}
if players.anySatisfy({ $0.hasPaid() == false }) {
Section { Section {
RowButtonView("Tout le monde a réglé", role: .destructive) { RowButtonView("Tout le monde a réglé", role: .destructive) {
let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() })
players.forEach { player in players.forEach { player in
if player.hasPaid() == false { if player.hasPaid() == false {
player.paymentType = .gift player.paymentType = .gift
} }
} }
tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) _save(players: players)
} }
} footer: { } footer: {
Text("Passe tous les joueurs qui n'ont pas réglé en offert") Text("Passe tous les joueurs qui n'ont pas réglé en offert")
} }
}
if players.anySatisfy({ $0.hasPaid() == true }) {
Section { Section {
RowButtonView("Personne n'a réglé", role: .destructive) { RowButtonView("Personne n'a réglé", role: .destructive) {
let store = tournament.tournamentStore
let players = tournament.selectedPlayers()
players.forEach { player in players.forEach { player in
player.paymentType = nil player.paymentType = nil
} }
store.playerRegistrations.addOrUpdate(contentOfs: players) _save(players: players)
} }
} footer: { } footer: {
Text("Remet à zéro le type d'encaissement de tous les joueurs") Text("Remet à zéro le type d'encaissement de tous les joueurs")
} }
} }
}
.navigationBarBackButtonHidden(focusedField != nil) .navigationBarBackButtonHidden(focusedField != nil)
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.toolbar { .toolbar {
@ -103,7 +105,7 @@ struct CashierSettingsView: View {
HStack { HStack {
if tournament.isFree() { if tournament.isFree() {
ForEach(priceTags, id: \.self) { priceTag in ForEach(priceTags, id: \.self) { priceTag in
Button(priceTag.formatted(.currency(code: "EUR"))) { Button(priceTag.formatted(.currency(code: Locale.defaultCurrency()))) {
entryFee = priceTag entryFee = priceTag
tournament.entryFee = priceTag tournament.entryFee = priceTag
focusedField = nil focusedField = nil
@ -134,6 +136,10 @@ struct CashierSettingsView: View {
} }
} }
private func _save(players: [PlayerRegistration]) {
tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players)
}
private func _save() { private func _save() {
dataStore.tournaments.addOrUpdate(instance: tournament) dataStore.tournaments.addOrUpdate(instance: tournament)
} }

@ -56,7 +56,7 @@ extension Array {
class CashierViewModel: ObservableObject { class CashierViewModel: ObservableObject {
let id: UUID = UUID() let id: UUID = UUID()
@Published var sortOption: SortOption = .callDate @Published var sortOption: SortOption = .callDate
@Published var filterOption: FilterOption = .all @Published var filterOption: FilterOption = .didNotPay
@Published var presenceFilterOption: PresenceFilterOption = .all @Published var presenceFilterOption: PresenceFilterOption = .all
@Published var sortOrder: SortOrder = .ascending @Published var sortOrder: SortOrder = .ascending
@Published var searchText: String = "" @Published var searchText: String = ""
@ -70,10 +70,7 @@ class CashierViewModel: ObservableObject {
func _shouldDisplayPlayer(_ player: PlayerRegistration) -> Bool { func _shouldDisplayPlayer(_ player: PlayerRegistration) -> Bool {
if searchText.isEmpty == false { if searchText.isEmpty == false {
sortOption.shouldDisplayPlayer(player) player.contains(searchText)
&& filterOption.shouldDisplayPlayer(player)
&& presenceFilterOption.shouldDisplayPlayer(player)
&& player.contains(searchText)
} else { } else {
sortOption.shouldDisplayPlayer(player) sortOption.shouldDisplayPlayer(player)
&& filterOption.shouldDisplayPlayer(player) && filterOption.shouldDisplayPlayer(player)
@ -306,8 +303,7 @@ struct CashierView: View {
case .alphabeticalLastName, .alphabeticalFirstName, .playerRank, .age: case .alphabeticalLastName, .alphabeticalFirstName, .playerRank, .age:
PlayerCashierView(players: filteredPlayers, displayTournamentTitle: tournaments.count > 1, editingOptions: _editingOptions()) PlayerCashierView(players: filteredPlayers, displayTournamentTitle: tournaments.count > 1, editingOptions: _editingOptions())
case .callDate: case .callDate:
let _teams = teams.filter({ $0.callDate != nil }) TeamCallDateView(teams: teams, displayTournamentTitle: tournaments.count > 1, editingOptions: _editingOptions())
TeamCallDateView(teams: _teams, displayTournamentTitle: tournaments.count > 1, editingOptions: _editingOptions())
} }
} }
.onAppear { .onAppear {
@ -354,7 +350,7 @@ struct CashierView: View {
} }
} footer: { } footer: {
if let teamCallDate = player.team()?.callDate { if let teamCallDate = player.team()?.callDate {
Text("équipe convoqué") + Text(teamCallDate.localizedDate()) Text("équipe convoquée ") + Text(teamCallDate.localizedDate())
} }
} }
} }
@ -369,16 +365,16 @@ struct CashierView: View {
var body: some View { var body: some View {
ForEach(teams) { team in ForEach(teams) { team in
let players = team.players().filter({ cashierViewModel._shouldDisplayPlayer($0) }) let players = team.players()
if players.isEmpty == false { if players.isEmpty == false, cashierViewModel._shouldDisplayTeam(team) {
Section { Section {
ForEach(players) { player in ForEach(players) { player in
EditablePlayerView(player: player, editingOptions: editingOptions) EditablePlayerView(player: player, editingOptions: editingOptions)
} }
} header: { } header: {
HStack { HStack {
if let name = team.name { if let teamName = team.name, teamName.isEmpty == false {
Text(name) Text(teamName)
} }
if displayTournamentTitle, let tournamentTitle = team.tournamentObject()?.tournamentTitle() { if displayTournamentTitle, let tournamentTitle = team.tournamentObject()?.tournamentTitle() {
Spacer() Spacer()
@ -404,22 +400,22 @@ struct CashierView: View {
var body: some View { var body: some View {
let groupedTeams = Dictionary(grouping: teams) { team in let groupedTeams = Dictionary(grouping: teams) { team in
team.callDate team.callDate ?? .distantPast
} }
let keys = cashierViewModel.sortOrder == .ascending ? groupedTeams.keys.compactMap { $0 }.sorted() : groupedTeams.keys.compactMap { $0 }.sorted().reversed() let keys = cashierViewModel.sortOrder == .ascending ? groupedTeams.keys.compactMap { $0 }.sorted() : groupedTeams.keys.compactMap { $0 }.sorted().reversed()
ForEach(keys, id: \.self) { key in ForEach(keys, id: \.self) { key in
if let _teams = groupedTeams[key] { if let _teams = groupedTeams[key] {
ForEach(_teams) { team in ForEach(_teams) { team in
let players = team.players().filter({ cashierViewModel._shouldDisplayPlayer($0) }) let players = team.players()
if players.isEmpty == false { if players.isEmpty == false, cashierViewModel._shouldDisplayTeam(team) {
Section { Section {
ForEach(players) { player in ForEach(players) { player in
EditablePlayerView(player: player, editingOptions: editingOptions) EditablePlayerView(player: player, editingOptions: editingOptions)
} }
} header: { } header: {
if let name = team.name { if let teamName = team.name, teamName.isEmpty == false {
Text(name) Text(teamName)
} }
if displayTournamentTitle, let tournamentTitle = team.tournamentObject()?.tournamentTitle() { if displayTournamentTitle, let tournamentTitle = team.tournamentObject()?.tournamentTitle() {

@ -105,7 +105,7 @@ struct EventSettingsView: View {
Button("Valider") { Button("Valider") {
textFieldIsFocus = false textFieldIsFocus = false
if eventName.trimmed.isEmpty == false { if eventName.trimmed.isEmpty == false {
event.name = eventName.trimmed event.name = eventName.prefixTrimmed(200)
} else { } else {
event.name = nil event.name = nil
} }

@ -46,7 +46,7 @@ struct TournamentConfigurationView: View {
} }
Picker(selection: $tournament.federalAgeCategory, label: Text("Limite d'âge")) { Picker(selection: $tournament.federalAgeCategory, label: Text("Limite d'âge")) {
ForEach(FederalTournamentAge.allCases) { type in ForEach(FederalTournamentAge.allCases) { type in
Text(type.localizedLabel(.title)).tag(type) Text(type.localizedFederalAgeLabel(.title)).tag(type)
} }
} }
LabeledContent { LabeledContent {

@ -19,6 +19,8 @@ struct ClubDetailView: View {
@Bindable var club: Club @Bindable var club: Club
@State private var clubDeleted: Bool = false @State private var clubDeleted: Bool = false
@State private var confirmDeletion: Bool = false @State private var confirmDeletion: Bool = false
@State private var timezone: String = TimeZone.current.identifier
var displayContext: DisplayContext var displayContext: DisplayContext
var selection: ((Club) -> ())? = nil var selection: ((Club) -> ())? = nil
@ -29,6 +31,10 @@ struct ClubDetailView: View {
_acronymMode = State(wrappedValue: club.shortNameMode()) _acronymMode = State(wrappedValue: club.shortNameMode())
_city = State(wrappedValue: club.city ?? "") _city = State(wrappedValue: club.city ?? "")
_zipCode = State(wrappedValue: club.zipCode ?? "") _zipCode = State(wrappedValue: club.zipCode ?? "")
if let timezone = club.timezone {
self.timezone = timezone
}
} }
var body: some View { var body: some View {
@ -216,10 +222,13 @@ struct ClubDetailView: View {
.navigationBarBackButtonHidden(focusedField != nil) .navigationBarBackButtonHidden(focusedField != nil)
.toolbar(content: { .toolbar(content: {
if focusedField != nil { if focusedField != nil {
ToolbarItem(placement: .topBarLeading) { ToolbarItem(placement: .keyboard) {
Button("Annuler", role: .cancel) { HStack {
Button("Fermer", role: .cancel) {
focusedField = nil focusedField = nil
} }
Spacer()
}
} }
} }
}) })

@ -30,9 +30,11 @@ struct CourtView: View {
.multilineTextAlignment(.trailing) .multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.onSubmit { .onSubmit {
court.name = name let courtName = name.prefixTrimmed(50)
if name.isEmpty { if courtName.isEmpty {
court.name = nil court.name = nil
} else {
court.name = courtName
} }
do { do {
try dataStore.courts.addOrUpdate(instance: court) try dataStore.courts.addOrUpdate(instance: court)

@ -11,13 +11,29 @@ struct CopyPasteButtonView: View {
let pasteValue: String? let pasteValue: String?
@State private var copied: Bool = false @State private var copied: Bool = false
@ViewBuilder
var body: some View { var body: some View {
if let pasteValue {
Button { Button {
let pasteboard = UIPasteboard.general let pasteboard = UIPasteboard.general
pasteboard.string = pasteValue pasteboard.string = pasteValue
copied = true copied = true
} label: { } label: {
Label(copied ? "copié" : "copier", systemImage: "doc.on.doc").symbolVariant(copied ? .fill : .none) Label(copied ? "Copié" : "Copier", systemImage: "doc.on.doc").symbolVariant(copied ? .fill : .none)
}
}
}
}
struct PasteButtonView: View {
@Binding var text: String
@ViewBuilder
var body: some View {
PasteButton(payloadType: String.self) { strings in
if let pasteboard = strings.first {
text = pasteboard
}
} }
} }
} }

@ -8,20 +8,20 @@
import SwiftUI import SwiftUI
protocol SpinDrawable { protocol SpinDrawable {
func segmentLabel(_ displayStyle: DisplayStyle) -> [String] func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String]
} }
extension String: SpinDrawable { extension String: SpinDrawable {
func segmentLabel(_ displayStyle: DisplayStyle) -> [String] { func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] {
[self] [self]
} }
} }
extension Match: SpinDrawable { extension Match: SpinDrawable {
func segmentLabel(_ displayStyle: DisplayStyle) -> [String] { func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] {
let teams = teams() let teams = teams()
if teams.count == 1 { if teams.count == 1, hideNames == false {
return teams.first!.segmentLabel(displayStyle) return teams.first!.segmentLabel(displayStyle, hideNames: hideNames)
} else { } else {
return [roundTitle(), matchTitle(displayStyle)].compactMap { $0 } return [roundTitle(), matchTitle(displayStyle)].compactMap { $0 }
} }
@ -29,12 +29,16 @@ extension Match: SpinDrawable {
} }
extension TeamRegistration: SpinDrawable { extension TeamRegistration: SpinDrawable {
func segmentLabel(_ displayStyle: DisplayStyle) -> [String] { func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] {
var strings: [String] = [] var strings: [String] = []
let indexLabel = tournamentObject()?.labelIndexOf(team: self) let indexLabel = tournamentObject()?.labelIndexOf(team: self)
if let indexLabel { if let indexLabel {
strings.append(indexLabel) strings.append(indexLabel)
if hideNames {
return strings
}
} }
strings.append(contentsOf: self.players().map { $0.playerLabel(displayStyle) }) strings.append(contentsOf: self.players().map { $0.playerLabel(displayStyle) })
return strings return strings
} }
@ -51,8 +55,8 @@ struct DrawOption: Identifiable, SpinDrawable {
let initialIndex: Int let initialIndex: Int
let option: SpinDrawable let option: SpinDrawable
func segmentLabel(_ displayStyle: DisplayStyle) -> [String] { func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] {
option.segmentLabel(displayStyle) option.segmentLabel(displayStyle, hideNames: hideNames)
} }
} }
@ -62,6 +66,7 @@ struct SpinDrawView: View {
let drawees: [any SpinDrawable] let drawees: [any SpinDrawable]
@State var segments: [any SpinDrawable] @State var segments: [any SpinDrawable]
var autoMode: Bool = false var autoMode: Bool = false
var hideNames: Bool = false
let completion: ([DrawResult]) async -> Void // Completion closure let completion: ([DrawResult]) async -> Void // Completion closure
@State private var drawCount: Int = 0 @State private var drawCount: Int = 0
@ -89,12 +94,12 @@ struct SpinDrawView: View {
} }
} else if drawCount < drawees.count { } else if drawCount < drawees.count {
Section { Section {
_segmentLabelView(segment: drawees[drawCount].segmentLabel(.wide), horizontalAlignment: .center) _segmentLabelView(segment: drawees[drawCount].segmentLabel(.wide, hideNames: hideNames), horizontalAlignment: .center)
} }
Section { Section {
ZStack { ZStack {
FortuneWheelContainerView(segments: drawOptions, autoMode: autoMode) { index in FortuneWheelContainerView(segments: drawOptions, autoMode: autoMode, hideNames: hideNames) { index in
self.selectedIndex = index self.selectedIndex = index
self.draws.append(DrawResult(drawee: drawCount, drawIndex: drawOptions[index].initialIndex)) self.draws.append(DrawResult(drawee: drawCount, drawIndex: drawOptions[index].initialIndex))
self.drawOptions.remove(at: index) self.drawOptions.remove(at: index)
@ -209,8 +214,8 @@ struct SpinDrawView: View {
private func _segmentLabelView(segment: [String], horizontalAlignment: HorizontalAlignment = .leading) -> some View { private func _segmentLabelView(segment: [String], horizontalAlignment: HorizontalAlignment = .leading) -> some View {
VStack(alignment: horizontalAlignment, spacing: 0.0) { VStack(alignment: horizontalAlignment, spacing: 0.0) {
ForEach(segment, id: \.self) { string in ForEach(segment.indices, id: \.self) { lineIndex in
Text(string).font(.title3) Text(segment[lineIndex]).font(.title3)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.lineLimit(1) .lineLimit(1)
} }
@ -221,13 +226,13 @@ struct SpinDrawView: View {
private func _validationLabelView(drawee: Int, result: SpinDrawable) -> some View { private func _validationLabelView(drawee: Int, result: SpinDrawable) -> some View {
VStack(spacing: 0.0) { VStack(spacing: 0.0) {
let draw = drawees[drawee] let draw = drawees[drawee]
_segmentLabelView(segment: draw.segmentLabel(.wide), horizontalAlignment: .center) _segmentLabelView(segment: draw.segmentLabel(.wide, hideNames: hideNames), horizontalAlignment: .center)
if result as? TeamRegistration != nil { if result as? TeamRegistration != nil {
Image(systemName: "flag.2.crossed.fill").font(.largeTitle).foregroundColor(.logoRed) Image(systemName: "flag.2.crossed.fill").font(.largeTitle).foregroundColor(.logoRed)
} else { } else {
Image(systemName: "arrowshape.down.fill").font(.largeTitle).foregroundColor(.logoRed) Image(systemName: "arrowshape.down.fill").font(.largeTitle).foregroundColor(.logoRed)
} }
_segmentLabelView(segment: result.segmentLabel(.wide), horizontalAlignment: .center) _segmentLabelView(segment: result.segmentLabel(.wide, hideNames: hideNames), horizontalAlignment: .center)
} }
} }
} }
@ -236,10 +241,11 @@ struct FortuneWheelContainerView: View {
@State private var rotation: Double = 0 @State private var rotation: Double = 0
let segments: [any SpinDrawable] let segments: [any SpinDrawable]
let autoMode: Bool let autoMode: Bool
let hideNames: Bool
let completion: (Int) -> Void // Completion closure let completion: (Int) -> Void // Completion closure
var body: some View { var body: some View {
FortuneWheelView(segments: segments) FortuneWheelView(segments: segments, hideNames: hideNames)
.rotationEffect(.degrees(rotation)) .rotationEffect(.degrees(rotation))
.aspectRatio(contentMode: .fill) .aspectRatio(contentMode: .fill)
.padding(.top, 5) .padding(.top, 5)
@ -303,6 +309,7 @@ struct FortuneWheelContainerView: View {
struct FortuneWheelView: View { struct FortuneWheelView: View {
let segments: [any SpinDrawable] let segments: [any SpinDrawable]
let hideNames: Bool
let colors: [Color] = [.yellow, .cyan, .green, .blue, .orange, .purple, .mint, .brown] let colors: [Color] = [.yellow, .cyan, .green, .blue, .orange, .purple, .mint, .brown]
func getColor(forIndex index: Int) -> Color { func getColor(forIndex index: Int) -> Color {
@ -330,12 +337,12 @@ struct FortuneWheelView: View {
path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false) path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false)
path.closeSubpath() path.closeSubpath()
} }
.fill(getColor(forIndex:index)) .fill(getColor(forIndex: index))
VStack(alignment: .trailing, spacing: 0.0) { VStack(alignment: .trailing, spacing: 0.0) {
let strings = segments[index].segmentLabel(.short) let strings = labels(forIndex: index)
ForEach(strings, id: \.self) { string in ForEach(strings.indices, id: \.self) { lineIndex in
Text(string).bold() Text(strings[lineIndex]).bold()
.font(.subheadline) .font(.subheadline)
} }
} }
@ -349,6 +356,19 @@ struct FortuneWheelView: View {
} }
} }
private func labels(forIndex index: Int) -> [String] {
if segments.count < 5 {
return segments[index].segmentLabel(.short, hideNames: hideNames)
} else {
let values = segments[index].segmentLabel(.short, hideNames: hideNames)
if values.count < 3 {
return values
} else {
return Array(segments[index].segmentLabel(.short, hideNames: hideNames).prefix(1))
}
}
}
// Calculate the position for the text in the middle of the arc segment // Calculate the position for the text in the middle of the arc segment
private func arcPosition(index: Int, radius: Double) -> CGPoint { private func arcPosition(index: Int, radius: Double) -> CGPoint {
let segmentAngle = 360.0 / Double(segments.count) let segmentAngle = 360.0 / Double(segments.count)

@ -73,7 +73,7 @@ struct GenericDestinationPickerView<T: Identifiable & Selectable & Equatable >:
) )
.offset(x: 3, y: 3) .offset(x: 3, y: 3)
} else if let count, count > 0 { } else if let count, count > 0 {
Image(systemName: count <= 50 ? "\(String(count)).circle.fill" : "plus.circle.fill") Image(systemName: count <= 50 ? "\(String(count)).circle.fill" : "ellipsis.circle.fill")
.foregroundColor(destination.badgeValueColor() ?? .logoRed) .foregroundColor(destination.badgeValueColor() ?? .logoRed)
.imageScale(.medium) .imageScale(.medium)
.background ( .background (
@ -93,7 +93,7 @@ struct GenericDestinationPickerView<T: Identifiable & Selectable & Equatable >:
) )
.offset(x: 3, y: 3) .offset(x: 3, y: 3)
} else if let count = destination.badgeValue(), count > 0 { } else if let count = destination.badgeValue(), count > 0 {
Image(systemName: count <= 50 ? "\(String(count)).circle.fill" : "plus.circle.fill") Image(systemName: count <= 50 ? "\(String(count)).circle.fill" : "ellipsis.circle.fill")
.foregroundColor(destination.badgeValueColor() ?? .logoRed) .foregroundColor(destination.badgeValueColor() ?? .logoRed)
.imageScale(.medium) .imageScale(.medium)
.background ( .background (

@ -10,11 +10,10 @@ import SwiftUI
struct MatchListView: View { struct MatchListView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament @Environment(\.matchViewStyle) private var matchViewStyle
let section: String let section: String
let matches: [Match]? let matches: [Match]?
var matchViewStyle: MatchViewStyle = .standardStyle
var hideWhenEmpty: Bool = false var hideWhenEmpty: Bool = false
@State var isExpanded: Bool = true @State var isExpanded: Bool = true
@ -30,11 +29,10 @@ struct MatchListView: View {
@ViewBuilder @ViewBuilder
var body: some View { var body: some View {
if _shouldHide() == false { if _shouldHide() == false {
Section {
DisclosureGroup(isExpanded: $isExpanded) { DisclosureGroup(isExpanded: $isExpanded) {
if let matches { if let matches {
ForEach(matches) { match in ForEach(matches) { match in
MatchRowView(match: match, matchViewStyle: matchViewStyle) MatchRowView(match: match)
.listRowInsets(EdgeInsets(top: 0, leading: -2, bottom: 0, trailing: 8)) .listRowInsets(EdgeInsets(top: 0, leading: -2, bottom: 0, trailing: 8))
} }
} }
@ -51,5 +49,4 @@ struct MatchListView: View {
} }
} }
} }
}
} }

@ -42,7 +42,7 @@ struct GroupStageSettingsView: View {
.submitLabel(.done) .submitLabel(.done)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.onSubmit { .onSubmit {
groupStageName = groupStageName.trimmed groupStageName = groupStageName.prefixTrimmed(200)
if groupStageName.isEmpty == false { if groupStageName.isEmpty == false {
groupStage.name = groupStageName groupStage.name = groupStageName
_save() _save()
@ -143,18 +143,60 @@ struct GroupStageSettingsView: View {
Section { Section {
RowButtonView("Recommencer tous les matchs", role: .destructive) { RowButtonView("Recommencer tous les matchs", role: .destructive) {
let isReturnMatchesEnabled = groupStage.isReturnMatchEnabled()
groupStage.buildMatches() groupStage.buildMatches()
if isReturnMatchesEnabled {
groupStage.addReturnMatches()
}
} }
} footer: { } footer: {
Text("Tous les matchs seront recronstruits, les données des matchs seront perdus.") Text("Tous les matchs seront recronstruits, les données des matchs seront perdus.")
} }
Section {
if groupStage.matchPhaseCount > 2 {
RowButtonView("Effacer la dernière vague", role: .destructive) {
groupStage.removeReturnMatches(onlyLast: true)
}
} else if groupStage.isReturnMatchEnabled() {
RowButtonView("Effacer les matchs retours", role: .destructive) {
groupStage.removeReturnMatches()
}
}
}
Section {
if groupStage.isReturnMatchEnabled() == false {
RowButtonView("Rajouter les matchs retours", role: .destructive) {
groupStage.addReturnMatches()
}
} else {
RowButtonView("Rajouter une vague de matchs", role: .destructive) {
groupStage.addReturnMatches()
}
}
}
Section {
RowButtonView("Rafraichir", role: .destructive) {
let playedMatches = groupStage.playedMatches()
playedMatches.forEach { match in
match.updateTeamScores()
}
}
} footer: {
Text("Mets à jour les équipes de la poule si jamais une erreur est persistante.")
}
} }
.onChange(of: size) { .onChange(of: size) {
if size != groupStage.size { if size != groupStage.size {
presentConfirmationButton = true presentConfirmationButton = true
} }
} }
.onChange(of: groupStage.matchFormat) {
_save()
groupStage.updateAllMatchesFormat()
}
.navigationBarBackButtonHidden(focusedField != nil) .navigationBarBackButtonHidden(focusedField != nil)
.toolbar(content: { .toolbar(content: {
if focusedField != nil { if focusedField != nil {
@ -164,6 +206,10 @@ struct GroupStageSettingsView: View {
} }
} }
} }
ToolbarItem(placement: .topBarTrailing) {
MatchTypeSelectionView(selectedFormat: $groupStage.matchFormat, additionalEstimationDuration: tournament.additionalEstimationDuration, displayStyle: .short)
}
}) })
.navigationTitle("Paramètres") .navigationTitle("Paramètres")
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)

@ -48,12 +48,20 @@ struct GroupStageTeamView: View {
var body: some View { var body: some View {
List { List {
Section { Section {
if let name = team.name { if let name = team.name, name.isEmpty == false {
Text(name).foregroundStyle(.secondary) Text(name).foregroundStyle(.secondary)
} }
ForEach(team.players()) { player in ForEach(team.players()) { player in
EditablePlayerView(player: player, editingOptions: _editingOptions()) EditablePlayerView(player: player, editingOptions: _editingOptions())
} }
} footer: {
NavigationLink {
EditingTeamView(team: team)
.environment(tournament)
} label: {
Text("détails de l'équipe")
.underline()
}
} }
if groupStage.tournamentObject()?.hasEnded() == false { if groupStage.tournamentObject()?.hasEnded() == false {
@ -66,6 +74,8 @@ struct GroupStageTeamView: View {
} }
} }
} }
}
Section { Section {
if team.qualified == false { if team.qualified == false {
@ -84,6 +94,8 @@ struct GroupStageTeamView: View {
} }
} }
if groupStage.tournamentObject()?.hasEnded() == false {
if team.qualified == false { if team.qualified == false {
Section { Section {
RowButtonView("Retirer de la poule", role: .destructive) { RowButtonView("Retirer de la poule", role: .destructive) {

@ -0,0 +1,72 @@
//
// GroupStageQualificationManager.swift
// PadelClub
//
// Created by razmig on 05/11/2024.
//
class GroupStageQualificationManager {
private let tournament: Tournament
private let tournamentStore: TournamentStore
init(tournament: Tournament, tournamentStore: TournamentStore) {
self.tournament = tournament
self.tournamentStore = tournamentStore
}
func qualificationSection() -> some View {
guard tournament.groupStageAdditionalQualified > 0 else { return EmptyView() }
let name = "\(tournament.qualifiedPerGroupStage + 1).ordinalFormatted()"
let missingQualifiedFromGroupStages = tournament.missingQualifiedFromGroupStages()
return Section {
NavigationLink {
SpinDrawView(
drawees: missingQualifiedFromGroupStages.isEmpty
? tournament.groupStageAdditionalQualifiedPreDraw()
: ["Qualification d'un \(name) de poule"],
segments: missingQualifiedFromGroupStages.isEmpty
? tournament.groupStageAdditionalLeft()
: missingQualifiedFromGroupStages
) { results in
if !missingQualifiedFromGroupStages.isEmpty {
self.handleDrawResults(results, missingQualifiedFromGroupStages)
}
}
} label: {
Label {
Text("Qualifier un \(name) de poule par tirage au sort")
} icon: {
Image(systemName: "exclamationmark.circle.fill")
.foregroundStyle(.logoBackground)
}
}
.disabled(tournament.moreQualifiedToDraw() == 0)
} footer: {
footerText(missingQualifiedFromGroupStages.isEmpty)
}
}
private func handleDrawResults(_ results: [DrawResult], _ missingQualifiedFromGroupStages: [Team]) {
results.forEach { drawResult in
var team = missingQualifiedFromGroupStages[drawResult.drawIndex]
team.qualified = true
do {
try tournamentStore.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
}
}
}
private func footerText(_ noMoreTeams: Bool) -> Text {
if tournament.moreQualifiedToDraw() == 0 {
return Text("Aucune équipe supplémentaire à qualifier. Vous pouvez en rajouter en modifiant le paramètre dans structure.")
} else if noMoreTeams {
return Text("Aucune équipe supplémentaire à tirer au sort. Attendez la fin des poules.")
}
return Text("")
}
}

@ -28,6 +28,8 @@ struct GroupStageView: View {
var body: some View { var body: some View {
List { List {
let playedMatches = groupStage.playedMatches()
Section { Section {
GroupStageScoreView(groupStage: groupStage, sortByScore: sortingMode == .score) GroupStageScoreView(groupStage: groupStage, sortByScore: sortingMode == .score)
} header: { } header: {
@ -49,15 +51,31 @@ struct GroupStageView: View {
} }
} }
.headerProminence(.increased) .headerProminence(.increased)
.onChange(of: playedMatches) {
if groupStage.hasEnded() {
sortingMode = .score
}
}
let playedMatches = groupStage.playedMatches()
let runningMatches = groupStage.runningMatches(playedMatches: playedMatches) let runningMatches = groupStage.runningMatches(playedMatches: playedMatches)
Section {
MatchListView(section: "en cours", matches: groupStage.runningMatches(playedMatches: playedMatches), hideWhenEmpty: true) MatchListView(section: "en cours", matches: groupStage.runningMatches(playedMatches: playedMatches), hideWhenEmpty: true)
}
let availableToStart = groupStage.availableToStart(playedMatches: playedMatches, in: runningMatches) let availableToStart = groupStage.availableToStart(playedMatches: playedMatches, in: runningMatches)
Section {
MatchListView(section: "prêt à démarrer", matches: availableToStart, hideWhenEmpty: true) MatchListView(section: "prêt à démarrer", matches: availableToStart, hideWhenEmpty: true)
.listRowView(isActive: availableToStart.isEmpty == false, color: .green, hideColorVariation: true) .listRowView(isActive: availableToStart.isEmpty == false, color: .green, hideColorVariation: true)
}
Section {
MatchListView(section: "à lancer", matches: groupStage.readyMatches(playedMatches: playedMatches), hideWhenEmpty: true) MatchListView(section: "à lancer", matches: groupStage.readyMatches(playedMatches: playedMatches), hideWhenEmpty: true)
}
Section {
MatchListView(section: "terminés", matches: groupStage.finishedMatches(playedMatches: playedMatches), hideWhenEmpty: playedMatches.isEmpty || playedMatches.flatMap({ $0.teamScores }).isEmpty, isExpanded: false) MatchListView(section: "terminés", matches: groupStage.finishedMatches(playedMatches: playedMatches), hideWhenEmpty: playedMatches.isEmpty || playedMatches.flatMap({ $0.teamScores }).isEmpty, isExpanded: false)
}
if playedMatches.isEmpty { if playedMatches.isEmpty {
RowButtonView("Créer les matchs de poules") { RowButtonView("Créer les matchs de poules") {
@ -136,9 +154,9 @@ struct GroupStageView: View {
.font(.footnote) .font(.footnote)
HStack { HStack {
VStack(alignment: .leading) { VStack(alignment: .leading) {
if let teamName = team.name { if let teamName = team.name, teamName.isEmpty == false {
Text(teamName).font(.title3) Text(teamName).foregroundStyle(.secondary).font(.footnote)
} else { }
ForEach(team.players()) { player in ForEach(team.players()) { player in
Text(player.playerLabel()).lineLimit(1) Text(player.playerLabel()).lineLimit(1)
.overlay { .overlay {
@ -148,7 +166,6 @@ struct GroupStageView: View {
} }
} }
} }
}
Spacer() Spacer()
if let score = groupStage.scoreLabel(forGroupStagePosition: groupStagePosition, score: scores?.first(where: { $0.team.groupStagePositionAtStep(groupStage.step) == groupStagePosition })) { if let score = groupStage.scoreLabel(forGroupStagePosition: groupStagePosition, score: scores?.first(where: { $0.team.groupStagePositionAtStep(groupStage.step) == groupStagePosition })) {
VStack(alignment: .trailing) { VStack(alignment: .trailing) {
@ -160,13 +177,11 @@ struct GroupStageView: View {
if let setsDifference = score.setsDifference { if let setsDifference = score.setsDifference {
HStack(spacing: 4.0) { HStack(spacing: 4.0) {
Text(setsDifference) Text(setsDifference)
Text("sets")
}.font(.footnote) }.font(.footnote)
} }
if let gamesDifference = score.gamesDifference { if let gamesDifference = score.gamesDifference {
HStack(spacing: 4.0) { HStack(spacing: 4.0) {
Text(gamesDifference) Text(gamesDifference)
Text("jeux")
}.font(.footnote) }.font(.footnote)
} }
} }

@ -12,7 +12,7 @@ struct GroupStagesSettingsView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@Environment(Tournament.self) var tournament: Tournament @Environment(Tournament.self) var tournament: Tournament
@State private var generationDone: Bool = false @State private var generationDoneMessage: String?
let step: Int let step: Int
var tournamentStore: TournamentStore { var tournamentStore: TournamentStore {
@ -168,6 +168,40 @@ struct GroupStagesSettingsView: View {
Text("Redistribue les équipes par la méthode du serpentin") Text("Redistribue les équipes par la méthode du serpentin")
} }
let groupStages = tournament.groupStages()
Section {
if groupStages.anySatisfy({ $0.isReturnMatchEnabled() }) {
RowButtonView("Effacer les matchs retours", role: .destructive) {
groupStages.filter({ $0.isReturnMatchEnabled() }).forEach { groupStage in
groupStage.removeReturnMatches()
}
generationDoneMessage = "Matchs retours effacés"
}
}
}
Section {
if groupStages.anySatisfy({ $0.isReturnMatchEnabled() == false }) {
RowButtonView("Rajouter les matchs retours", role: .destructive) {
groupStages.filter({ $0.isReturnMatchEnabled() == false }).forEach { groupStage in
groupStage.addReturnMatches()
}
generationDoneMessage = "Matchs retours créés"
}
} else if groupStages.allSatisfy({ $0.isReturnMatchEnabled() }) {
RowButtonView("Rajouter une vague de matchs", role: .destructive) {
groupStages.forEach { groupStage in
groupStage.addReturnMatches()
}
generationDoneMessage = "Nouveaux matchs créés"
}
}
}
Section { Section {
RowButtonView("Nommer les poules alphabétiquement", role: .destructive) { RowButtonView("Nommer les poules alphabétiquement", role: .destructive) {
let groupStages = tournament.groupStages() let groupStages = tournament.groupStages()
@ -220,25 +254,29 @@ struct GroupStagesSettingsView: View {
} }
.overlay(alignment: .bottom) { .overlay(alignment: .bottom) {
if generationDone { if let generationDoneMessage {
Label("Poules mises à jour", systemImage: "checkmark.circle.fill") Label(generationDoneMessage, systemImage: "checkmark.circle.fill")
.toastFormatted() .toastFormatted()
.deferredRendering(for: .seconds(2)) .deferredRendering(for: .seconds(2))
} }
} }
.toolbar { .toolbar {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
ShareLink(item: tournament.groupStages().compactMap { $0.pasteData() }.joined(separator: "\n\n")) ShareLink(item: groupStagesPaste(), preview: .init("Données des poules"))
} }
} }
} }
func groupStagesPaste() -> TournamentGroupStageShareContent {
TournamentGroupStageShareContent(tournament: tournament)
}
var menuBuildAllGroupStages: some View { var menuBuildAllGroupStages: some View {
RowButtonView("Refaire les poules", role: .destructive) { RowButtonView("Refaire les poules", role: .destructive) {
tournament.deleteGroupStages() tournament.deleteGroupStages()
tournament.buildGroupStages() tournament.buildGroupStages()
generationDone = true generationDoneMessage = "Poules mises à jour"
tournament.shouldVerifyGroupStage = false tournament.shouldVerifyGroupStage = false
_save() _save()
} }
@ -248,8 +286,8 @@ struct GroupStagesSettingsView: View {
func menuGenerateGroupStage(_ mode: GroupStageOrderingMode) -> some View { func menuGenerateGroupStage(_ mode: GroupStageOrderingMode) -> some View {
RowButtonView("Poule \(mode.localizedLabel().lowercased())", role: .destructive, systemImage: mode.systemImage) { RowButtonView("Poule \(mode.localizedLabel().lowercased())", role: .destructive, systemImage: mode.systemImage) {
tournament.groupStageOrderingMode = mode tournament.groupStageOrderingMode = mode
tournament.refreshGroupStages() tournament.refreshGroupStages(keepExistingMatches: true)
generationDone = true generationDoneMessage = "Poules mises à jour"
tournament.shouldVerifyGroupStage = false tournament.shouldVerifyGroupStage = false
_save() _save()
} }

@ -111,7 +111,7 @@ struct GroupStagesView: View {
GenericDestinationPickerView(selectedDestination: $selectedDestination, destinations: allDestinations(), nilDestinationIsValid: true) GenericDestinationPickerView(selectedDestination: $selectedDestination, destinations: allDestinations(), nilDestinationIsValid: true)
switch selectedDestination { switch selectedDestination {
case .all: case .all:
let finishedMatches = tournament.finishedMatches(allMatches, limit: nil) let finishedMatches = Tournament.finishedMatches(allMatches, limit: nil)
List { List {
if tournament.groupStageAdditionalQualified > 0 { if tournament.groupStageAdditionalQualified > 0 {
@ -131,12 +131,7 @@ struct GroupStagesView: View {
} }
} }
} label: { } label: {
Label {
Text("Qualifier un \(name) de poule par tirage au sort") Text("Qualifier un \(name) de poule par tirage au sort")
} icon: {
Image(systemName: "exclamationmark.circle.fill")
.foregroundStyle(.logoBackground)
}
} }
.disabled(tournament.moreQualifiedToDraw() == 0 || missingQualifiedFromGroupStages.isEmpty) .disabled(tournament.moreQualifiedToDraw() == 0 || missingQualifiedFromGroupStages.isEmpty)
} footer: { } footer: {
@ -148,12 +143,28 @@ struct GroupStagesView: View {
} }
} }
let runningMatches = tournament.runningMatches(allMatches) let runningMatches = Tournament.runningMatches(allMatches)
MatchListView(section: "en cours", matches: runningMatches, matchViewStyle: .standardStyle, isExpanded: false) Section {
MatchListView(section: "prêt à démarrer", matches: tournament.availableToStart(allMatches, in: runningMatches), matchViewStyle: .standardStyle, isExpanded: false)
MatchListView(section: "à lancer", matches: tournament.readyMatches(allMatches), matchViewStyle: .standardStyle, isExpanded: false) MatchListView(section: "en cours", matches: runningMatches, isExpanded: false)
MatchListView(section: "terminés", matches: finishedMatches, matchViewStyle: .standardStyle, isExpanded: false) }
Section {
MatchListView(section: "prêt à démarrer", matches: Tournament.availableToStart(allMatches, in: runningMatches), isExpanded: false)
}
Section {
MatchListView(section: "à lancer", matches: Tournament.readyMatches(allMatches), isExpanded: false)
}
Section {
MatchListView(section: "terminés", matches: finishedMatches, isExpanded: false)
}
} }
.matchViewStyle(.standardStyle)
.navigationTitle("Toutes les poules") .navigationTitle("Toutes les poules")
case .groupStage(let groupStage): case .groupStage(let groupStage):
GroupStageView(groupStage: groupStage).id(groupStage.id) GroupStageView(groupStage: groupStage).id(groupStage.id)

@ -45,7 +45,8 @@ struct LoserBracketFromGroupStageView: View {
ForEach(displayableMatches) { match in ForEach(displayableMatches) { match in
Section { Section {
MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle) MatchRowView(match: match)
.matchViewStyle(.sectionedStandardStyle)
.environment(\.isEditingTournamentSeed, $isEditingLoserBracketGroupStage) .environment(\.isEditingTournamentSeed, $isEditingLoserBracketGroupStage)
} header: { } header: {
let tournamentTeamCount = tournament.teamCount let tournamentTeamCount = tournament.teamCount
@ -106,8 +107,9 @@ struct LoserBracketFromGroupStageView: View {
private func _addNewMatch() { private func _addNewMatch() {
let currentGroupStageLoserBracketsInitialPlace = tournament.groupStageLoserBracketsInitialPlace() let currentGroupStageLoserBracketsInitialPlace = tournament.groupStageLoserBracketsInitialPlace()
let placeCount = displayableMatches.isEmpty ? currentGroupStageLoserBracketsInitialPlace : max(currentGroupStageLoserBracketsInitialPlace, displayableMatches.map({ $0.index }).max()! + 2) let placeCount = displayableMatches.isEmpty ? currentGroupStageLoserBracketsInitialPlace : max(currentGroupStageLoserBracketsInitialPlace, displayableMatches.map({ $0.index }).max()! + 2)
let match = Match(round: loserBracket.id, index: placeCount, format: loserBracket.matchFormat) let match = Match(round: loserBracket.id, index: placeCount, format: loserBracket.matchFormat)
match.name = "\(placeCount)\(placeCount.ordinalFormattedSuffix()) place" match.setMatchName("\(placeCount)\(placeCount.ordinalFormattedSuffix()) place")
tournamentStore.matches.addOrUpdate(instance: match) tournamentStore.matches.addOrUpdate(instance: match)
} }
@ -194,7 +196,7 @@ struct GroupStageLoserBracketMatchFooterView: View {
match.index = newIndexValidated match.index = newIndexValidated
match.name = "\(newIndexValidated)\(newIndexValidated.ordinalFormattedSuffix()) place" match.setMatchName("\(newIndexValidated)\(newIndexValidated.ordinalFormattedSuffix()) place")
match.tournamentStore.teamScores.addOrUpdate(contentOfs: teamScores) match.tournamentStore.teamScores.addOrUpdate(contentOfs: teamScores)

@ -54,7 +54,7 @@ struct GroupStageTeamReplacementView: View {
Section { Section {
Picker(selection: $selectedPlayer) { Picker(selection: $selectedPlayer) {
HStack { HStack {
Text(team.name ?? "Toute l'équipe") Text(team.teamNameLabel())
Spacer() Spacer()
Text(team.weight.formatted()).bold() Text(team.weight.formatted()).bold()
} }

@ -11,19 +11,26 @@ import LeStorage
struct MatchDateView: View { struct MatchDateView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@State private var showScoreEditView: Bool = false
@State private var confirmScoreEdition: Bool = false
var match: Match var match: Match
var showPrefix: Bool = false var showPrefix: Bool = false
private var isReady: Bool private var isReady: Bool
private var hasWalkoutTeam: Bool private var hasWalkoutTeam: Bool
private var hasEnded: Bool private var hasEnded: Bool
private let updatedField: Int?
init(match: Match, showPrefix: Bool) { init(match: Match, showPrefix: Bool, updatedField: Int? = nil) {
self.match = match self.match = match
self.showPrefix = showPrefix self.showPrefix = showPrefix
self.isReady = match.isReady() self.isReady = match.isReady()
self.hasWalkoutTeam = match.hasWalkoutTeam() self.hasWalkoutTeam = match.hasWalkoutTeam()
self.hasEnded = match.hasEnded() self.hasEnded = match.hasEnded()
self.updatedField = updatedField
}
var currentDate: Date {
Date().withoutSeconds()
} }
var body: some View { var body: some View {
@ -32,44 +39,64 @@ struct MatchDateView: View {
} else { } else {
Menu { Menu {
let estimatedDuration = match.getDuration() let estimatedDuration = match.getDuration()
if match.startDate == nil && isReady { if isReady {
Button("Démarrer") { Section {
match.startDate = Date() Button("Démarrer maintenant") {
if let updatedField {
match.setCourt(updatedField)
}
match.startDate = currentDate
match.endDate = nil
match.confirmed = true match.confirmed = true
_save() _save()
} }
Button("Démarrer dans 5 minutes") { Button("Démarrer dans 5 minutes") {
match.startDate = Calendar.current.date(byAdding: .minute, value: 5, to: Date()) if let updatedField {
match.setCourt(updatedField)
}
match.startDate = Calendar.current.date(byAdding: .minute, value: 5, to: currentDate)
match.endDate = nil
match.confirmed = true match.confirmed = true
_save() _save()
} }
Button("Démarrer dans 15 minutes") { Button("Démarrer dans 15 minutes") {
match.startDate = Calendar.current.date(byAdding: .minute, value: 15, to: Date()) if let updatedField {
match.setCourt(updatedField)
}
match.startDate = Calendar.current.date(byAdding: .minute, value: 15, to: currentDate)
match.endDate = nil
match.confirmed = true match.confirmed = true
_save() _save()
} }
Button("Démarrer dans \(estimatedDuration.formatted()) minutes") { Button("Démarrer dans \(estimatedDuration.formatted()) minutes") {
match.startDate = Calendar.current.date(byAdding: .minute, value: estimatedDuration, to: Date()) if let updatedField {
match.confirmed = true match.setCourt(updatedField)
_save()
} }
} else { match.startDate = Calendar.current.date(byAdding: .minute, value: estimatedDuration, to: currentDate)
if isReady {
Button("Démarrer maintenant") {
match.startDate = Date()
match.endDate = nil match.endDate = nil
match.confirmed = true match.confirmed = true
_save() _save()
} }
} header: {
if let updatedField {
Text(match.courtName(for: updatedField))
}
}
} else { } else {
Button("Décaler de \(estimatedDuration) minutes") { Button("Décaler de \(estimatedDuration) minutes") {
if let updatedField {
match.setCourt(updatedField)
}
match.cleanScheduleAndSave(match.startDate?.addingTimeInterval(Double(estimatedDuration) * 60.0)) match.cleanScheduleAndSave(match.startDate?.addingTimeInterval(Double(estimatedDuration) * 60.0))
} }
} }
Button("Indiquer un score") {
showScoreEditView = true
}
Divider()
Button("Retirer l'horaire") { Button("Retirer l'horaire") {
match.cleanScheduleAndSave() match.cleanScheduleAndSave()
} }
}
} label: { } label: {
label label
} }
@ -145,6 +172,10 @@ struct MatchDateView: View {
} }
} }
} }
.sheet(isPresented: $showScoreEditView) {
EditScoreView(match: match, confirmScoreEdition: $confirmScoreEdition)
.tint(.master)
}
} }
private func _save() { private func _save() {

@ -25,17 +25,32 @@ struct MatchTeamDetailView: View {
.headerProminence(.increased) .headerProminence(.increased)
.tint(.master) .tint(.master)
} }
.presentationDetents([.fraction(0.66)])
} }
@ViewBuilder @ViewBuilder
private func _teamDetailView(_ team: TeamRegistration, inTournament tournament: Tournament?) -> some View { private func _teamDetailView(_ team: TeamRegistration, inTournament tournament: Tournament?) -> some View {
Section { Section {
if let teamName = team.name, teamName.isEmpty == false {
Text(teamName).foregroundStyle(.secondary).font(.footnote)
}
ForEach(team.players()) { player in ForEach(team.players()) { player in
EditablePlayerView(player: player, editingOptions: _editingOptions()) EditablePlayerView(player: player, editingOptions: _editingOptions())
} }
if let coachList = team.comment, coachList.isEmpty == false {
Text("Coachs : " + coachList).foregroundStyle(.secondary).font(.footnote)
}
} header: { } header: {
TeamHeaderView(team: team, teamIndex: tournament?.indexOf(team: team)) TeamHeaderView(team: team, teamIndex: tournament?.indexOf(team: team), tournament: tournament)
} footer: {
if let tournament {
NavigationLink {
EditingTeamView(team: team)
.environment(tournament)
} label: {
Text("détails de l'équipe")
.underline()
}
}
} }
} }

@ -8,21 +8,26 @@
import SwiftUI import SwiftUI
struct PlayerBlockView: View { struct PlayerBlockView: View {
@Environment(\.matchViewStyle) private var matchViewStyle
@State var match: Match @State var match: Match
let teamPosition: TeamPosition let teamPosition: TeamPosition
let team: TeamRegistration? let team: TeamRegistration?
let color: Color
let width: CGFloat
let teamScore: TeamScore? let teamScore: TeamScore?
let isWalkOut: Bool let isWalkOut: Bool
init(match: Match, teamPosition: TeamPosition, color: Color, width: CGFloat) { var displayRestingTime: Bool {
matchViewStyle.displayRestingTime()
}
var width: CGFloat {
matchViewStyle == .plainStyle ? 1 : 2
}
init(match: Match, teamPosition: TeamPosition) {
self.match = match self.match = match
self.teamPosition = teamPosition self.teamPosition = teamPosition
let theTeam = match.team(teamPosition) let theTeam = match.team(teamPosition)
self.team = theTeam self.team = theTeam
self.color = color
self.width = width
let theTeamScore = match.teamScore(ofTeam: theTeam) let theTeamScore = match.teamScore(ofTeam: theTeam)
self.teamScore = theTeamScore self.teamScore = theTeamScore
self.isWalkOut = theTeamScore?.isWalkOut() == true self.isWalkOut = theTeamScore?.isWalkOut() == true
@ -44,39 +49,67 @@ struct PlayerBlockView: View {
teamScore?.score?.components(separatedBy: ",") ?? [] teamScore?.score?.components(separatedBy: ",") ?? []
} }
private func _defaultLabel() -> String { private func _defaultLabel() -> [String] {
teamPosition.localizedLabel() var defaultLabels = [String]()
if let previous = match.previousMatch(teamPosition) {
defaultLabels.append("Gagnant \(previous.roundAndMatchTitle(.short))")
if previous.isReady() == true {
if let courtName = previous.courtName(), previous.isRunning() {
defaultLabels.append(courtName + "\(previous.runningDuration())")
}
}
} else if let loser = match.loserMatch(teamPosition) {
defaultLabels.append("Perdant \(loser.roundAndMatchTitle(.short))")
if loser.isReady() == true {
if let courtName = loser.courtName(), loser.isRunning() {
defaultLabels.append(courtName + "\(loser.runningDuration())")
}
}
} else {
defaultLabels.append(teamPosition.localizedLabel())
}
return defaultLabels
} }
var body: some View { var body: some View {
HStack { HStack {
VStack(alignment: .leading) { VStack(alignment: .leading) {
if let names { if let team {
if let teamScore, teamScore.luckyLoser != nil, match.isLoserBracket == false { if let teamScore, teamScore.luckyLoser != nil, match.isLoserBracket == false {
Text("Repêchée").italic().font(.caption) Text("Repêchée").italic().font(.caption)
} }
if let name = team?.name { if let teamName = team.name {
Text(name).font(.title3) Text(teamName).foregroundStyle(.secondary).font(.footnote)
} else {
ForEach(names, id: \.self) { name in
Text(name).lineLimit(1)
} }
ForEach(team.players()) { player in
Text(player.playerLabel()).lineLimit(1)
.italic(player.isHere() == false)
.foregroundStyle(player.isHere() == false ? .secondary : .primary)
} }
} else { } else {
ZStack(alignment: .leading) { ZStack(alignment: .leading) {
VStack { VStack {
if let name = team?.name { if let teamName = team?.name {
Text(name).font(.title3) Text(teamName).foregroundStyle(.secondary).font(.footnote)
} else { }
Text("longLabelPlayerOne").lineLimit(1) Text("longLabelPlayerOne").lineLimit(1)
Text("longLabelPlayerTwo").lineLimit(1) Text("longLabelPlayerTwo").lineLimit(1)
} }
}
.opacity(0) .opacity(0)
Text(_defaultLabel()).foregroundStyle(.secondary).lineLimit(1) VStack(alignment: .leading) {
ForEach(_defaultLabel(), id: \.self) { name in
Text(name)
.foregroundStyle(.secondary)
.lineLimit(1)
}
}
} }
} }
if displayRestingTime, let team {
TeamRowView.TeamRestingView(team: team)
}
} }
.bold(hasWon) .bold(hasWon)
Spacer() Spacer()
@ -92,7 +125,7 @@ struct PlayerBlockView: View {
if width == 1 { if width == 1 {
Divider() Divider()
} else { } else {
Divider().frame(width: width).overlay(color) Divider().frame(width: width).overlay(Color(white: 0.9))
} }
Text(string) Text(string)
.font(.title3) .font(.title3)

@ -14,7 +14,7 @@ struct MatchDetailView: View {
@EnvironmentObject var networkMonitor: NetworkMonitor @EnvironmentObject var networkMonitor: NetworkMonitor
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
let matchViewStyle: MatchViewStyle @Environment(\.matchViewStyle) private var matchViewStyle
@State private var showLiveScore: Bool = false @State private var showLiveScore: Bool = false
@State private var editScore: Bool = false @State private var editScore: Bool = false
@ -33,6 +33,10 @@ struct MatchDetailView: View {
@State var showSubscriptionView: Bool = false @State var showSubscriptionView: Bool = false
@State var showUserCreationView: Bool = false @State var showUserCreationView: Bool = false
@State private var presentFollowUpMatch: Bool = false
@State private var dismissWhenPresentFollowUpMatchIsDismissed: Bool = false
@State private var presentRanking: Bool = false
@State private var confirmScoreEdition: Bool = false
var tournamentStore: TournamentStore { var tournamentStore: TournamentStore {
return match.tournamentStore return match.tournamentStore
@ -50,9 +54,8 @@ struct MatchDetailView: View {
var match: Match var match: Match
init(match: Match, matchViewStyle: MatchViewStyle = .standardStyle) { init(match: Match, updatedField: Int? = nil) {
self.match = match self.match = match
self.matchViewStyle = matchViewStyle
if match.hasStarted() == false && (match.startDate == nil || match.courtIndex == nil) { if match.hasStarted() == false && (match.startDate == nil || match.courtIndex == nil) {
_isEditing = State(wrappedValue: true) _isEditing = State(wrappedValue: true)
@ -69,7 +72,7 @@ struct MatchDetailView: View {
_endDate = State(wrappedValue: endDate) _endDate = State(wrappedValue: endDate)
} }
if let courtIndex = match.courtIndex { if let courtIndex = updatedField ?? match.courtIndex {
_fieldSetup = State(wrappedValue: .field(courtIndex)) _fieldSetup = State(wrappedValue: .field(courtIndex))
} }
} }
@ -85,7 +88,8 @@ struct MatchDetailView: View {
} }
Section { Section {
MatchSummaryView(match: match, matchViewStyle: .plainStyle) MatchSummaryView(match: match)
.matchViewStyle(.plainStyle)
} footer: { } footer: {
if match.isEmpty() == false { if match.isEmpty() == false {
HStack { HStack {
@ -153,13 +157,47 @@ struct MatchDetailView: View {
} }
} }
}) })
.sheet(item: $scoreType, onDismiss: { .sheet(isPresented: $presentFollowUpMatch, onDismiss: {
if match.hasEnded() { if dismissWhenPresentFollowUpMatchIsDismissed {
dismiss() dismiss()
} }
}) {
NavigationStack {
FollowUpMatchView(match: match, dismissWhenPresentFollowUpMatchIsDismissed: $dismissWhenPresentFollowUpMatchIsDismissed)
}
.tint(.master)
}
.sheet(isPresented: $presentRanking, content: {
if let currentTournament = match.currentTournament() {
NavigationStack {
TournamentRankView()
.environment(currentTournament)
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("Retour", role: .cancel) {
presentRanking = false
dismiss()
}
}
}
}
.tint(.master)
}
})
.sheet(item: $scoreType, onDismiss: {
if match.hasEnded(), confirmScoreEdition {
confirmScoreEdition = false
if match.index == 0, match.isGroupStage() == false, match.roundObject?.parent == nil {
presentRanking = true
} else if match.isGroupStage(), match.currentTournament()?.hasEnded() == true {
presentRanking = true
} else {
presentFollowUpMatch = true
}
}
}) { scoreType in }) { scoreType in
let matchDescriptor = MatchDescriptor(match: match) EditScoreView(match: match, confirmScoreEdition: $confirmScoreEdition)
EditScoreView(matchDescriptor: matchDescriptor)
.tint(.master) .tint(.master)
// switch scoreType { // switch scoreType {
@ -305,6 +343,7 @@ struct MatchDetailView: View {
match.resetScores() match.resetScores()
match.resetMatch() match.resetMatch()
match.confirmed = false match.confirmed = false
match.updateFollowingMatchTeamScore()
save() save()
} label: { } label: {
Text("Supprimer les scores") Text("Supprimer les scores")
@ -319,6 +358,26 @@ struct MatchDetailView: View {
Text("Remise-à-zéro") Text("Remise-à-zéro")
} }
if match.teamScores.isEmpty == false {
Divider()
Menu {
ForEach(match.teamScores) { teamScore in
Button(role: .destructive) {
do {
try tournamentStore.teamScores.delete(instance: teamScore)
} catch {
Logger.error(error)
}
match.confirmed = false
_saveMatch()
} label: {
Text(teamScore.team?.teamLabel() ?? "Aucun nom")
}
}
} label: {
Text("Supprimer une équipe")
}
}
} label: { } label: {
LabelOptions() LabelOptions()
} }
@ -402,13 +461,13 @@ struct MatchDetailView: View {
Text("Partage sur les réseaux sociaux") Text("Partage sur les réseaux sociaux")
} }
// if let followUpMatch = match.followUpMatch { if match.currentTournament()?.hasEnded() == false {
// Section { Section {
// MatchRowView(match: followUpMatch) RowButtonView("Match à suivre") {
// } header: { presentFollowUpMatch = true
// Text("à suivre terrain \(match.fieldIndex)") }
// } }
// } }
} }
var editionView: some View { var editionView: some View {
@ -429,20 +488,27 @@ struct MatchDetailView: View {
Text("Dans 5 minutes").tag(MatchDateSetup.inMinutes(5)) Text("Dans 5 minutes").tag(MatchDateSetup.inMinutes(5))
Text("Dans 15 minutes").tag(MatchDateSetup.inMinutes(15)) Text("Dans 15 minutes").tag(MatchDateSetup.inMinutes(15))
} }
Text("Précédente rotation").tag(MatchDateSetup.inMinutes(-rotationDuration)) Text("Précédente rotation").tag(MatchDateSetup.previousRotation)
Text("Prochaine rotation").tag(MatchDateSetup.inMinutes(rotationDuration)) Text("Prochaine rotation").tag(MatchDateSetup.nextRotation)
Text("À").tag(MatchDateSetup.customDate) Text("À").tag(MatchDateSetup.customDate)
} label: { } label: {
Text("Horaire") Text("Horaire")
} }
.onChange(of: startDateSetup) { .onChange(of: startDateSetup) {
let date = Date().withoutSeconds()
switch startDateSetup { switch startDateSetup {
case .customDate: case .customDate:
break break
case .now: case .now:
startDate = Date() startDate = date
case .nextRotation:
let baseDate = match.startDate ?? date
startDate = baseDate.addingTimeInterval(Double(rotationDuration) * 60)
case .previousRotation:
let baseDate = match.startDate ?? date
startDate = baseDate.addingTimeInterval(Double(-rotationDuration) * 60)
case .inMinutes(let minutes): case .inMinutes(let minutes):
startDate = Date().addingTimeInterval(Double(minutes) * 60) startDate = date.addingTimeInterval(Double(minutes) * 60)
} }
} }
} }
@ -484,7 +550,7 @@ struct MatchDetailView: View {
} }
RowButtonView("Valider") { RowButtonView("Valider") {
match.validateMatch(fromStartDate: startDateSetup == .now ? Date() : startDate, toEndDate: endDate, fieldSetup: fieldSetup) match.validateMatch(fromStartDate: startDateSetup == .now ? Date().withoutSeconds() : startDate, toEndDate: endDate, fieldSetup: fieldSetup)
save() save()

@ -6,13 +6,15 @@
// //
import SwiftUI import SwiftUI
import LeStorage
struct MatchRowView: View { struct MatchRowView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(\.matchViewStyle) private var matchViewStyle
@State var match: Match @State var match: Match
let matchViewStyle: MatchViewStyle
var title: String? = nil var title: String? = nil
var updatedField: Int? = nil
@Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed @Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed
@ -58,10 +60,28 @@ struct MatchRowView: View {
// }) // })
NavigationLink { NavigationLink {
MatchDetailView(match: match, matchViewStyle: matchViewStyle) MatchDetailView(match: match, updatedField: updatedField)
} label: { } label: {
MatchSummaryView(match: match, matchViewStyle: matchViewStyle, title: title) MatchSummaryView(match: match, title: title, updatedField: updatedField)
.contextMenu { .contextMenu {
Section {
ForEach(match.teams().flatMap({ $0.players() })) { player in
Button {
player.hasArrived.toggle()
do {
try player.tournamentStore.playerRegistrations.addOrUpdate(instance: player)
} catch {
Logger.error(error)
}
} label: {
Label(player.playerLabel(), systemImage: player.hasArrived ? "checkmark" : "xmark")
}
}
} header: {
Text("Présence")
}
Divider()
NavigationLink { NavigationLink {
EditSharingView(match: match) EditSharingView(match: match)
} label: { } label: {

@ -9,7 +9,7 @@ import SwiftUI
import LeStorage import LeStorage
struct MatchSetupView: View { struct MatchSetupView: View {
static let confirmationMessage = "Au moins une tête de série a été placée dans la branche de ce match dans les tours précédents. En plaçant une équipe sur ici, les équipes déjà placées dans la même branche seront retirées du tableau et devront être replacées." static let confirmationMessage = "Au moins une tête de série a été placée dans la branche de ce match dans les tours précédents. En plaçant une équipe ici, les équipes déjà placées dans la même branche seront retirées du tableau et devront être replacées."
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@ -166,7 +166,7 @@ struct MatchSetupView: View {
Text("Libérer") Text("Libérer")
.underline() .underline()
} }
} else { } else if match.isFromLastRound() == false {
ConfirmButtonView(shouldConfirm: shouldConfirm, message: MatchSetupView.confirmationMessage) { ConfirmButtonView(shouldConfirm: shouldConfirm, message: MatchSetupView.confirmationMessage) {
_ = match.lockAndGetSeedPosition(atTeamPosition: teamPosition) _ = match.lockAndGetSeedPosition(atTeamPosition: teamPosition)
do { do {
@ -190,16 +190,22 @@ struct MatchSetupView: View {
func _removeTeam(team: TeamRegistration, teamPosition: TeamPosition) -> some View { func _removeTeam(team: TeamRegistration, teamPosition: TeamPosition) -> some View {
Button(role: .cancel) { Button(role: .cancel) {
//todo
if match.isSeededBy(team: team, inTeamPosition: teamPosition) { if match.isSeededBy(team: team, inTeamPosition: teamPosition) {
if let score = match.teamScore(ofTeam: team) {
do {
try tournamentStore.teamScores.delete(instance: score)
} catch {
Logger.error(error)
}
}
team.bracketPosition = nil team.bracketPosition = nil
do { do {
try tournamentStore.teamRegistrations.addOrUpdate(instance: team) try tournamentStore.teamRegistrations.addOrUpdate(instance: team)
} catch { } catch {
Logger.error(error) Logger.error(error)
} }
//match.updateTeamScores() if let previousMatch = match.previousMatch(teamPosition) {
match.previousMatches().forEach { previousMatch in
if previousMatch.disabled { if previousMatch.disabled {
previousMatch.enableMatch() previousMatch.enableMatch()
do { do {

@ -10,27 +10,28 @@ import SwiftUI
struct MatchSummaryView: View { struct MatchSummaryView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@State var match: Match @State var match: Match
let matchViewStyle: MatchViewStyle @Environment(\.matchViewStyle) private var matchViewStyle
let matchTitle: String let matchTitle: String
let roundTitle: String? let roundTitle: String?
let courtName: String? let courtName: String?
let spacing: CGFloat let updatedField: Int?
let padding: CGFloat let estimatedStartDate: Match.CourtIndexAndDate?
let color: Color let availableCourts: [Int]
let width: CGFloat let canBePlayedInSpecifiedCourt: Bool
init(match: Match, matchViewStyle: MatchViewStyle, title: String? = nil) { init(match: Match, title: String? = nil, updatedField: Int? = nil) {
self.match = match self.match = match
self.matchViewStyle = matchViewStyle self.updatedField = updatedField
self.padding = matchViewStyle == .plainStyle ? 0 : 8
self.spacing = matchViewStyle == .plainStyle ? 8 : 0 let runningMatches = DataStore.shared.runningMatches()
self.width = matchViewStyle == .plainStyle ? 1 : 2
self.color = Color(white: 0.9) let currentAvailableCourts = match.availableCourts(runningMatches: runningMatches)
self.availableCourts = currentAvailableCourts
if let groupStage = match.groupStageObject { if let groupStage = match.groupStageObject {
self.roundTitle = groupStage.groupStageTitle(.title) self.roundTitle = groupStage.groupStageTitle(.title)
} else if let round = match.roundObject { } else if let round = match.roundObject {
self.roundTitle = round.roundTitle(matchViewStyle == .feedStyle ? .wide : .short) self.roundTitle = round.roundTitle(.short)
} else { } else {
self.roundTitle = nil self.roundTitle = nil
} }
@ -42,6 +43,20 @@ struct MatchSummaryView: View {
} else { } else {
self.courtName = nil self.courtName = nil
} }
self.estimatedStartDate = match.estimatedStartDate(availableCourts: currentAvailableCourts, runningMatches: runningMatches)
self.canBePlayedInSpecifiedCourt = match.canBePlayedInSpecifiedCourt(runningMatches: runningMatches)
}
var spacing: CGFloat {
matchViewStyle == .plainStyle ? 8 : 0
}
var padding: CGFloat {
matchViewStyle == .plainStyle ? 0 : 8
}
var width: CGFloat {
matchViewStyle == .plainStyle ? 1 : 2
} }
var body: some View { var body: some View {
@ -57,46 +72,93 @@ struct MatchSummaryView: View {
} }
} }
Spacer() Spacer()
VStack(alignment: .trailing, spacing: 0) {
if let courtName { if let courtName {
Spacer()
Text(courtName) Text(courtName)
.foregroundStyle(.gray) .strikethrough(courtIsNotValid())
.font(.caption)
} }
} }
.foregroundStyle(.secondary)
.font(.footnote)
}
.lineLimit(1) .lineLimit(1)
} }
HStack(spacing: 0) { HStack(spacing: 0) {
VStack(alignment: .leading, spacing: spacing) { VStack(alignment: .leading, spacing: spacing) {
PlayerBlockView(match: match, teamPosition: .one, color: color, width: width) PlayerBlockView(match: match, teamPosition: .one)
.padding(padding) .padding(padding)
if width == 1 { if width == 1 {
Divider() Divider()
} else { } else {
Divider().frame(height: width).overlay(color) Divider().frame(height: width).overlay(Color(white: 0.9))
} }
PlayerBlockView(match: match, teamPosition: .two, color: color, width: width) PlayerBlockView(match: match, teamPosition: .two)
.padding(padding) .padding(padding)
} }
} }
.overlay { .overlay {
if matchViewStyle != .plainStyle { if matchViewStyle != .plainStyle {
RoundedRectangle(cornerRadius: 8) RoundedRectangle(cornerRadius: 8)
.stroke(color, lineWidth: 2) .stroke(Color(white: 0.9), lineWidth: 2)
} }
} }
if matchViewStyle != .plainStyle { if matchViewStyle != .plainStyle {
HStack { HStack {
if matchViewStyle == .followUpStyle {
if match.expectedToBeRunning() {
Text(match.expectedFormattedStartDate(canBePlayedInSpecifiedCourt: canBePlayedInSpecifiedCourt, availableCourts: availableCourts, estimatedStartDate: estimatedStartDate, updatedField: updatedField))
.font(.footnote)
.foregroundStyle(.secondary)
}
}
Spacer() Spacer()
MatchDateView(match: match, showPrefix: matchViewStyle == .tournamentResultStyle) MatchDateView(match: match, showPrefix: false, updatedField: possibleCourtIndex)
} }
} }
} }
.padding(.vertical, padding) .padding(.vertical, padding)
.monospacedDigit() .monospacedDigit()
} }
var possibleCourtIndex: Int? {
if canBePlayedInSpecifiedCourt {
return nil
} else if let updatedField, availableCourts.contains(updatedField) {
return updatedField
} else if let first = availableCourts.first {
return first
} else if let estimatedStartDate {
return estimatedStartDate.0
}
return updatedField
}
func courtIsNotValid() -> Bool {
if match.courtIndex == updatedField {
return false
}
if match.isReady() == false {
return false
}
if canBePlayedInSpecifiedCourt {
return false
}
if let estimatedStartDate, estimatedStartDate.0 == updatedField {
return false
}
if let estimatedStartDate, estimatedStartDate.0 == match.courtIndex {
return false
}
return true
}
} }
//#Preview { //#Preview {

@ -134,8 +134,12 @@ struct ActivityView: View {
ContentUnavailableView { ContentUnavailableView {
Label("Une erreur est survenue", systemImage: "exclamationmark.circle.fill") Label("Une erreur est survenue", systemImage: "exclamationmark.circle.fill")
} description: { } description: {
Text(error.localizedDescription) Text("Tenup est peut-être en maintenance. " + error.localizedDescription)
} actions: { } actions: {
Link(destination: URLs.tenup.url) {
Text("Voir si tenup est en maintenance")
}
RowButtonView("D'accord.") { RowButtonView("D'accord.") {
self.error = nil self.error = nil
} }
@ -510,6 +514,7 @@ struct ActivityView: View {
.padding() .padding()
} }
} else { } else {
if federalDataViewModel.lastError == nil {
ContentUnavailableView { ContentUnavailableView {
Label("Aucun tournoi", systemImage: "shield.slash") Label("Aucun tournoi", systemImage: "shield.slash")
} description: { } description: {
@ -520,6 +525,22 @@ struct ActivityView: View {
} }
.padding() .padding()
} }
} else {
ContentUnavailableView {
Label("Une erreur est survenue", systemImage: "exclamationmark.circle.fill")
} description: {
Text("Tenup est peut-être en maintenance, veuillez ré-essayer plus tard.")
} actions: {
Link(destination: URLs.tenup.url) {
Text("Voir si tenup est en maintenance")
}
FooterButtonView("modifier vos critères de recherche") {
displaySearchView = true
}
.padding()
}
}
} }
} }

@ -124,7 +124,7 @@ struct CalendarView: View {
) )
.overlay(alignment: .bottomTrailing) { .overlay(alignment: .bottomTrailing) {
if let count = counts[day.dayInt] { if let count = counts[day.dayInt] {
Image(systemName: count <= 50 ? "\(count).circle.fill" : "plus.circle.fill") Image(systemName: count <= 50 ? "\(count).circle.fill" : "ellipsis.circle.fill")
.foregroundColor(.secondary) .foregroundColor(.secondary)
.imageScale(.medium) .imageScale(.medium)
.background ( .background (

@ -181,6 +181,7 @@ struct TournamentLookUpView: View {
federalDataViewModel.levels = Set(levels) federalDataViewModel.levels = Set(levels)
federalDataViewModel.categories = Set(categories) federalDataViewModel.categories = Set(categories)
federalDataViewModel.ageCategories = Set(ages) federalDataViewModel.ageCategories = Set(ages)
federalDataViewModel.lastError = nil
Task { Task {
await getNewPage() await getNewPage()
@ -223,7 +224,12 @@ struct TournamentLookUpView: View {
await getNewBuildForm() await getNewBuildForm()
} else { } else {
let commands = try await NetworkFederalService.shared.getAllFederalTournaments(sortingOption: dataStore.appSettings.sortingOption, page: page, startDate: dataStore.appSettings.startDate, endDate: dataStore.appSettings.endDate, city: dataStore.appSettings.city, distance: dataStore.appSettings.distance, categories: categories, levels: levels, lat: locationManager.location?.coordinate.latitude.formatted(.number.locale(Locale(identifier: "us"))), lng: locationManager.location?.coordinate.longitude.formatted(.number.locale(Locale(identifier: "us"))), ages: ages, types: types, nationalCup: dataStore.appSettings.nationalCup) let commands = try await NetworkFederalService.shared.getAllFederalTournaments(sortingOption: dataStore.appSettings.sortingOption, page: page, startDate: dataStore.appSettings.startDate, endDate: dataStore.appSettings.endDate, city: dataStore.appSettings.city, distance: dataStore.appSettings.distance, categories: categories, levels: levels, lat: locationManager.location?.coordinate.latitude.formatted(.number.locale(Locale(identifier: "us"))), lng: locationManager.location?.coordinate.longitude.formatted(.number.locale(Locale(identifier: "us"))), ages: ages, types: types, nationalCup: dataStore.appSettings.nationalCup)
if commands.anySatisfy({ $0.command == "alert" }) {
federalDataViewModel.lastError = .maintenance
}
let resultCommand = commands.first(where: { $0.results != nil }) let resultCommand = commands.first(where: { $0.results != nil })
if let newTournaments = resultCommand?.results?.items { if let newTournaments = resultCommand?.results?.items {
newTournaments.forEach { ft in newTournaments.forEach { ft in
// let isValid = ft.tournaments.anySatisfy({ build in // let isValid = ft.tournaments.anySatisfy({ build in
@ -363,7 +369,7 @@ struct TournamentLookUpView: View {
NavigationLink { NavigationLink {
List([FederalTournamentAge.senior, FederalTournamentAge.a45, FederalTournamentAge.a55, FederalTournamentAge.a17_18, FederalTournamentAge.a15_16, FederalTournamentAge.a13_14, FederalTournamentAge.a11_12], selection: $appSettings.tournamentAges) { type in List([FederalTournamentAge.senior, FederalTournamentAge.a45, FederalTournamentAge.a55, FederalTournamentAge.a17_18, FederalTournamentAge.a15_16, FederalTournamentAge.a13_14, FederalTournamentAge.a11_12], selection: $appSettings.tournamentAges) { type in
Text(type.localizedLabel()) Text(type.localizedFederalAgeLabel())
} }
.navigationTitle("Limites d'âge") .navigationTitle("Limites d'âge")
.environment(\.editMode, Binding.constant(EditMode.active)) .environment(\.editMode, Binding.constant(EditMode.active))
@ -375,7 +381,7 @@ struct TournamentLookUpView: View {
Text("Tous les âges") Text("Tous les âges")
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} else { } else {
Text(ages.map({ $0.localizedLabel()}).joined(separator: ", ")) Text(ages.map({ $0.localizedFederalAgeLabel()}).joined(separator: ", "))
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
} }

@ -286,7 +286,7 @@ struct TournamentSubscriptionView: View {
} }
var teamsString: String { var teamsString: String {
selectedPlayers.map { $0.pasteData() }.joined(separator: "\n") selectedPlayers.map { $0.pasteData(withRank: true) }.joined(separator: "\n")
} }
var messageBody: String { var messageBody: String {

@ -79,7 +79,7 @@ struct MainView: View {
TournamentOrganizerView() TournamentOrganizerView()
.tabItem(for: .tournamentOrganizer) .tabItem(for: .tournamentOrganizer)
.toolbarBackground(.visible, for: .tabBar) .toolbarBackground(.visible, for: .tabBar)
OngoingView() OngoingContainerView()
.tabItem(for: .ongoing) .tabItem(for: .ongoing)
.badge(self.dataStore.runningMatches().count) .badge(self.dataStore.runningMatches().count)
.toolbarBackground(.visible, for: .tabBar) .toolbarBackground(.visible, for: .tabBar)
@ -263,7 +263,7 @@ struct MainView: View {
await _startImporting(importingDate: mostRecentDateImported) await _startImporting(importingDate: mostRecentDateImported)
} else if current.dataModelIdentifier != PersistenceController.getModelVersion() && current.fileModelIdentifier != fileURL?.fileModelIdentifier() { } else if current.dataModelIdentifier != PersistenceController.getModelVersion() && current.fileModelIdentifier != fileURL?.fileModelIdentifier() {
await _startImporting(importingDate: mostRecentDateImported) await _startImporting(importingDate: mostRecentDateImported)
} else if current.incompleteMode == false || updated == 0 { } else if updated == 0 {
await _calculateMonthData(dataSource: current.monthKey) await _calculateMonthData(dataSource: current.monthKey)
} }
} }

@ -0,0 +1,95 @@
//
// OngoingContainerView.swift
// PadelClub
//
// Created by razmig on 07/11/2024.
//
import SwiftUI
import LeStorage
@Observable
class OngoingViewModel {
static let shared = OngoingViewModel()
var destination: OngoingDestination? = .running
var hideUnconfirmedMatches: Bool = false
var hideNotReadyMatches: Bool = false
func areFiltersEnabled() -> Bool {
hideUnconfirmedMatches || hideNotReadyMatches
}
let defaultSorting : [MySortDescriptor<Match>] = [.keyPath(\Match.startDate!), .keyPath(\Match.index), .keyPath(\Match.courtIndexForSorting)]
var runningAndNextMatches: [Match] {
DataStore.shared.runningAndNextMatches().sorted(using: defaultSorting, order: .ascending)
}
var filteredRunningAndNextMatches: [Match] {
return runningAndNextMatches.filter({
(hideUnconfirmedMatches == false || hideUnconfirmedMatches == true && $0.confirmed)
&& (hideNotReadyMatches == false || hideNotReadyMatches == true && $0.isReady() )
})
}
}
struct OngoingContainerView: View {
@Environment(NavigationViewModel.self) private var navigation: NavigationViewModel
@State private var showMatchPicker: Bool = false
var body: some View {
@Bindable var navigation = navigation
@Bindable var ongoingViewModel = OngoingViewModel.shared
NavigationStack(path: $navigation.ongoingPath) {
VStack(spacing: 0) {
GenericDestinationPickerView(selectedDestination: $ongoingViewModel.destination, destinations: OngoingDestination.allCases, nilDestinationIsValid: false)
switch ongoingViewModel.destination! {
case .running, .followUp, .over:
OngoingView()
case .court, .free:
OngoingCourtView()
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Programmation")
.toolbar {
if ongoingViewModel.destination == .followUp {
ToolbarItem(placement: .topBarLeading) {
Menu {
Toggle(isOn: $ongoingViewModel.hideUnconfirmedMatches) {
Text("masquer non confirmés")
}
Toggle(isOn: $ongoingViewModel.hideNotReadyMatches) {
Text("masquer incomplets")
}
} label: {
Image(systemName: "line.3.horizontal.decrease.circle")
.resizable()
.scaledToFit()
.frame(minHeight: 32)
}
.symbolVariant(ongoingViewModel.areFiltersEnabled() ? .fill : .none)
}
}
ToolbarItem(placement: .topBarTrailing) {
Button {
showMatchPicker = true
} label: {
Image(systemName: "rectangle.stack.badge.plus")
.resizable()
.scaledToFit()
.frame(minHeight: 32)
}
}
}
}
.environment(ongoingViewModel)
.sheet(isPresented: $showMatchPicker, content: {
FollowUpMatchView(selectedCourt: nil, allMatches: ongoingViewModel.runningAndNextMatches, autoDismiss: false)
.tint(.master)
})
}
}

@ -0,0 +1,129 @@
//
// OngoingDestination.swift
// PadelClub
//
// Created by razmig on 07/11/2024.
//
import SwiftUI
enum OngoingDestination: Int, CaseIterable, Identifiable, Selectable, Equatable {
var id: Int { self.rawValue }
static func == (lhs: OngoingDestination, rhs: OngoingDestination) -> Bool {
return lhs.id == rhs.id
}
case running
case followUp
case court
case free
case over
var runningAndNextMatches: [Match] {
switch self {
case .running, .court, .free:
return OngoingViewModel.shared.runningAndNextMatches
case .followUp:
return OngoingViewModel.shared.filteredRunningAndNextMatches
case .over:
return DataStore.shared.endMatches()
}
}
var sortedMatches: [Match] {
return runningAndNextMatches.filter({ self.shouldDisplay($0) })
}
var filteredMatches: [Match] {
sortedMatches.filter({ OngoingDestination.running.shouldDisplay($0) })
}
var sortedCourtIndex: [Int?] {
let courtUsed = sortedMatches.grouped(by: { $0.courtIndex }).keys
let sortedNumbers = courtUsed.sorted { (a, b) -> Bool in
switch (a, b) {
case (nil, _): return false
case (_, nil): return true
case let (a?, b?): return a < b
}
}
return sortedNumbers
}
func contentUnavailable() -> some View {
switch self {
case .running:
ContentUnavailableView("Aucun match en cours", systemImage: "figure.tennis", description: Text("Tous vos matchs en cours seront visibles ici, quelque soit le tournoi."))
case .followUp:
ContentUnavailableView("Aucun match à suivre", systemImage: "figure.tennis", description: Text("Tous vos matchs planifiés et confirmés, seront visibles ici, quelque soit le tournoi."))
case .court:
ContentUnavailableView("Aucun match en cours", systemImage: "sportscourt", description: Text("Tous vos terrains correspondant aux matchs en cours seront visibles ici, quelque soit le tournoi."))
case .free:
ContentUnavailableView("Aucun terrain libre", systemImage: "sportscourt", description: Text("Les terrains libres seront visibles ici, quelque soit le tournoi."))
case .over:
ContentUnavailableView("Aucun match terminé", systemImage: "clock.badge.xmark", description: Text("Les matchs terminés seront visibles ici, quelque soit le tournoi."))
}
}
func localizedFilterModeLabel() -> String {
switch self {
case .running:
return "En cours"
case .followUp:
return "À suivre"
case .court:
return "Terrains"
case .free:
return "Libres"
case .over:
return "Finis"
}
}
func shouldDisplay(_ match: Match) -> Bool {
switch self {
case .running:
return match.isRunning()
case .court, .free:
return true
case .followUp:
return match.isRunning() == false
case .over:
return match.hasEnded()
}
}
func selectionLabel(index: Int) -> String {
localizedFilterModeLabel()
}
func systemImage() -> String? {
switch self {
default:
return nil
}
}
func badgeValue() -> Int? {
switch self {
case .running, .followUp, .over:
sortedMatches.count
case .court:
sortedCourtIndex.filter({ index in
filteredMatches.filter({ $0.courtIndex == index }).isEmpty == false
}).count
case .free:
sortedCourtIndex.filter({ index in
filteredMatches.filter({ $0.courtIndex == index }).isEmpty
}).count
}
}
func badgeValueColor() -> Color? {
nil
}
func badgeImage() -> Badge? {
nil
}
}

@ -8,32 +8,34 @@
import SwiftUI import SwiftUI
import LeStorage import LeStorage
extension Int: @retroactive Identifiable {
public var id: Int {
return self
}
}
struct OngoingView: View { struct OngoingView: View {
@Environment(NavigationViewModel.self) private var navigation: NavigationViewModel @Environment(NavigationViewModel.self) private var navigation: NavigationViewModel
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(OngoingViewModel.self) private var ongoingViewModel: OngoingViewModel
@State private var sortByField: Bool = false var filterMode: OngoingDestination {
ongoingViewModel.destination!
let fieldSorting : [MySortDescriptor<Match>] = [.keyPath(\Match.courtIndexForSorting), .keyPath(\Match.startDate!)]
let defaultSorting : [MySortDescriptor<Match>] = [.keyPath(\Match.startDate!), .keyPath(\Match.courtIndexForSorting)]
var matches: [Match] {
let sorting = self.sortByField ? fieldSorting : defaultSorting
return self.dataStore.runningMatches().sorted(using: sorting, order: .ascending)
} }
var body: some View { var body: some View {
@Bindable var navigation = navigation let filteredMatches = filterMode.sortedMatches
NavigationStack(path: $navigation.ongoingPath) {
List { List {
ForEach(matches) { match in ForEach(filteredMatches) { match in
let tournament = match.currentTournament()
if let tournament = match.currentTournament() {
Section { Section {
MatchRowView(match: match, matchViewStyle: .standardStyle) MatchRowView(match: match)
.matchViewStyle(.followUpStyle)
} header: { } header: {
if let tournament {
HStack { HStack {
Text(tournament.tournamentTitle(.short)) Text(tournament.tournamentTitle(.short))
Spacer() Spacer()
@ -41,9 +43,12 @@ struct OngoingView: View {
Text("@" + club.clubTitle(.short)) Text("@" + club.clubTitle(.short))
} }
} }
}
} footer: { } footer: {
HStack { HStack {
if let tournament {
Text(tournament.eventLabel()) Text(tournament.eventLabel())
}
#if DEBUG #if DEBUG
Spacer() Spacer()
FooterButtonView("copier l'id") { FooterButtonView("copier l'id") {
@ -53,37 +58,60 @@ struct OngoingView: View {
#endif #endif
} }
} }
}
} }
} }
.headerProminence(.increased) .headerProminence(.increased)
.overlay { .overlay {
if matches.isEmpty { if filteredMatches.isEmpty {
ContentUnavailableView("Aucun match en cours", systemImage: "figure.tennis", description: Text("Tous vos matchs en cours seront visibles ici, quelque soit le tournoi.")) filterMode.contentUnavailable()
} }
} }
.navigationTitle("En cours") }
.toolbarBackground(.visible, for: .bottomBar) }
.toolbar(matches.isEmpty ? .hidden : .visible, for: .navigationBar)
.toolbar { struct OngoingCourtView: View {
ToolbarItem(placement: .status) {
Picker(selection: $sortByField) {
Text("tri par date").tag(true)
Text("tri par terrain").tag(false)
} label: {
@Environment(NavigationViewModel.self) private var navigation: NavigationViewModel
@EnvironmentObject var dataStore: DataStore
@Environment(OngoingViewModel.self) private var ongoingViewModel: OngoingViewModel
var filterMode: OngoingDestination {
ongoingViewModel.destination!
}
@State private var selectedCourtForFollowUp: Int?
var body: some View {
let sortedMatches = filterMode.sortedMatches
let filteredMatches = sortedMatches.filter({ OngoingDestination.running.shouldDisplay($0) })
List {
ForEach(filterMode.sortedCourtIndex, id: \.self) { index in
let courtFilteredMatches = filteredMatches.filter({ $0.courtIndex == index })
let title : String = (index == nil ? "Aucun terrain défini" : "Terrain #\(index! + 1)")
if (filterMode == .free && courtFilteredMatches.isEmpty) || (filterMode == .court && courtFilteredMatches.isEmpty == false) {
Section {
MatchListView(section: "En cours", matches: courtFilteredMatches, hideWhenEmpty: true, isExpanded: false)
MatchListView(section: "À venir", matches: sortedMatches.filter({ $0.courtIndex == index && $0.hasStarted() == false }), isExpanded: false)
} header: {
Text(title)
} footer: {
FooterButtonView("Ajouter un match à suivre") {
selectedCourtForFollowUp = index
} }
.pickerStyle(.segmented)
.fixedSize()
.offset(y: -3)
} }
} }
} }
} }
.sheet(item: $selectedCourtForFollowUp, content: { selectedCourtForFollowUp in
FollowUpMatchView(selectedCourt: selectedCourtForFollowUp, allMatches: filterMode.runningAndNextMatches)
.tint(.master)
})
.headerProminence(.increased)
.overlay {
if (filteredMatches.isEmpty && filterMode != .free) || (filterMode == .free && filterMode.sortedCourtIndex.allSatisfy({ index in filteredMatches.filter({ $0.courtIndex == index }).isEmpty == false })) {
filterMode.contentUnavailable()
}
}
}
} }
//#Preview {
// OngoingView()
//}

@ -11,49 +11,115 @@ import LeStorage
struct GlobalSettingsView: View { struct GlobalSettingsView: View {
@EnvironmentObject var dataStore : DataStore @EnvironmentObject var dataStore : DataStore
var groupStageMatchFormat: Binding<MatchFormat> {
Binding {
dataStore.user.groupStageMatchFormatPreference ?? .nineGames
} set: { value in
dataStore.user.groupStageMatchFormatPreference = value
}
}
var groupStageMatchFormatPreference: Binding<Bool> {
Binding {
dataStore.user.groupStageMatchFormatPreference == nil
} set: { value in
if value {
dataStore.user.groupStageMatchFormatPreference = nil
} else {
dataStore.user.groupStageMatchFormatPreference = .nineGames
}
}
}
var bracketMatchFormat: Binding<MatchFormat> {
Binding {
dataStore.user.bracketMatchFormatPreference ?? .nineGames
} set: { value in
dataStore.user.bracketMatchFormatPreference = value
}
}
var bracketMatchFormatPreference: Binding<Bool> {
Binding {
dataStore.user.bracketMatchFormatPreference == nil
} set: { value in
if value {
dataStore.user.bracketMatchFormatPreference = nil
} else {
dataStore.user.bracketMatchFormatPreference = .nineGames
}
}
}
var loserBracketMatchFormat: Binding<MatchFormat> {
Binding {
dataStore.user.loserBracketMatchFormatPreference ?? .nineGames
} set: { value in
dataStore.user.loserBracketMatchFormatPreference = value
}
}
var loserBracketMatchFormatPreference: Binding<Bool> {
Binding {
dataStore.user.loserBracketMatchFormatPreference == nil
} set: { value in
if value {
dataStore.user.loserBracketMatchFormatPreference = nil
} else {
dataStore.user.loserBracketMatchFormatPreference = .nineGames
}
}
}
var body: some View { var body: some View {
@Bindable var user = dataStore.user @Bindable var user = dataStore.user
List { List {
Section { Section {
Picker(selection: $user.groupStageMatchFormatPreference) { Toggle(isOn: groupStageMatchFormatPreference) {
Text("Automatique").tag(nil as MatchFormat?) Text("Automatique")
ForEach(MatchFormat.allCases, id: \.self) { format in
Text(format.format).tag(format as MatchFormat?)
} }
} label: {
HStack { if groupStageMatchFormatPreference.wrappedValue == false {
MatchTypeSelectionView(selectedFormat: groupStageMatchFormat)
}
} header: {
Text("Poule") Text("Poule")
Spacer() } footer: {
Text("À minima, les règles fédérales seront toujours prises en compte par défaut.")
} }
Section {
Toggle(isOn: bracketMatchFormatPreference) {
Text("Automatique")
} }
Picker(selection: $user.bracketMatchFormatPreference) {
Text("Automatique").tag(nil as MatchFormat?) if bracketMatchFormatPreference.wrappedValue == false {
ForEach(MatchFormat.allCases, id: \.self) { format in MatchTypeSelectionView(selectedFormat: bracketMatchFormat)
Text(format.format).tag(format as MatchFormat?)
} }
} label: { } header: {
HStack {
Text("Tableau") Text("Tableau")
Spacer() } footer: {
} Text("À minima, les règles fédérales seront toujours prises en compte par défaut.")
}
Picker(selection: $user.loserBracketMatchFormatPreference) {
Text("Automatique").tag(nil as MatchFormat?)
ForEach(MatchFormat.allCases, id: \.self) { format in
Text(format.format).tag(format as MatchFormat?)
} }
} label: {
HStack { Section {
Text("Match de classement") Toggle(isOn: loserBracketMatchFormatPreference) {
Spacer() Text("Automatique")
} }
if loserBracketMatchFormatPreference.wrappedValue == false {
MatchTypeSelectionView(selectedFormat: loserBracketMatchFormat)
} }
} header: { } header: {
Text("Vos formats préférés") Text("Match de classement")
} footer: { } footer: {
Text("À minima, les règles fédérales seront toujours prises en compte par défaut.") Text("À minima, les règles fédérales seront toujours prises en compte par défaut.")
} }
} }
.headerProminence(.increased)
.onChange(of: [ .onChange(of: [
user.bracketMatchFormatPreference, user.bracketMatchFormatPreference,
user.groupStageMatchFormatPreference, user.groupStageMatchFormatPreference,

@ -24,8 +24,7 @@ struct MatchFormatStorageView: View {
LabeledContent { LabeledContent {
StepperView(title: "minutes", count: $estimatedDuration, step: 5) StepperView(title: "minutes", count: $estimatedDuration, step: 5)
} label: { } label: {
Text("Durée \(matchFormat.format)") MatchFormatRowView(matchFormat: matchFormat, hideDuration: true)
Text(matchFormat.computedShortLabelWithoutPrefix)
} }
} footer: { } footer: {
if estimatedDuration != matchFormat.defaultEstimatedDuration { if estimatedDuration != matchFormat.defaultEstimatedDuration {

@ -129,7 +129,7 @@ struct ToolboxView: View {
Section { Section {
NavigationLink { NavigationLink {
SelectablePlayerListView(isPresented: false) SelectablePlayerListView(isPresented: false, lastDataSource: true)
} label: { } label: {
Label("Rechercher un joueur", systemImage: "person.fill.viewfinder") Label("Rechercher un joueur", systemImage: "person.fill.viewfinder")
} }
@ -196,10 +196,17 @@ struct ToolboxView: View {
Text("Contrat d'utilisation") Text("Contrat d'utilisation")
} }
} }
Section {
RowButtonView("Effacer les logs", role: .destructive) {
StoreCenter.main.resetLoggingCollections()
didResetApiCalls = true
}
}
} }
.overlay(alignment: .bottom) { .overlay(alignment: .bottom) {
if didResetApiCalls { if didResetApiCalls {
Label("failed api calls deleted", systemImage: "checkmark") Label("logs effacés", systemImage: "checkmark")
.toastFormatted() .toastFormatted()
.deferredRendering(for: .seconds(3)) .deferredRendering(for: .seconds(3))
.onAppear { .onAppear {
@ -221,11 +228,10 @@ struct ToolboxView: View {
ShareLink(item: URLs.appStore.url) { ShareLink(item: URLs.appStore.url) {
Label("Lien AppStore", systemImage: "link") Label("Lien AppStore", systemImage: "link")
} }
if let zip = _getZip() {
ShareLink(item: zip) { ShareLink(item: ZipLog(), preview: .init("Mon archive")) {
Label("Mes données", systemImage: "server.rack") Label("Mes données", systemImage: "server.rack")
} }
}
} label: { } label: {
Label("Partagez", systemImage: "square.and.arrow.up").labelStyle(.iconOnly) Label("Partagez", systemImage: "square.and.arrow.up").labelStyle(.iconOnly)
} }
@ -233,7 +239,14 @@ struct ToolboxView: View {
} }
} }
} }
}
//#Preview {
// ToolboxView()
//}
struct ZipLog: Transferable {
private func _getZip() -> URL? { private func _getZip() -> URL? {
do { do {
let filePath = try Club.storageDirectoryPath() let filePath = try Club.storageDirectoryPath()
@ -243,8 +256,19 @@ struct ToolboxView: View {
return nil return nil
} }
} }
}
//#Preview { func shareFile() -> URL? {
// ToolboxView() print("Generating URL...")
//} return _getZip()
}
static var transferRepresentation: some TransferRepresentation {
FileRepresentation(exportedContentType: .zip) { transferable in
return SentTransferredFile(transferable.shareFile()!)
}.exportingCondition { $0.shareFile() != nil }
ProxyRepresentation { transferable in
return transferable.shareFile()!
}.exportingCondition { $0.shareFile() != nil }
}
}

@ -7,6 +7,7 @@
import SwiftUI import SwiftUI
import LeStorage import LeStorage
import Foundation
struct PadelClubView: View { struct PadelClubView: View {
@State private var uuid: UUID = UUID() @State private var uuid: UUID = UUID()
@ -74,9 +75,14 @@ struct PadelClubView: View {
print("before anonymousPlayers.count", anonymousPlayers.count) print("before anonymousPlayers.count", anonymousPlayers.count)
FileImportManager.shared.updatePlayers(isMale: fileURL.manData, players: &anonymousPlayers) FileImportManager.shared.updatePlayers(isMale: fileURL.manData, players: &anonymousPlayers)
print("after anonymousPlayers.count", anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty } print("after local anonymousPlayers.count", anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty }.count)
await fetchPlayersDataSequentially(for: &anonymousPlayers)
print("after beach anonymousPlayers.count", anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty }
.count) .count)
SourceFileManager.shared.exportToCSV(players: okPlayers + anonymousPlayers, sourceFileType: fileURL.manData ? .messieurs : .dames, date: fileURL.dateFromPath) SourceFileManager.shared.exportToCSV(players: okPlayers + anonymousPlayers, sourceFileType: fileURL.manData ? .messieurs : .dames, date: fileURL.dateFromPath)
SourceFileManager.shared.exportToCSV("anonymes", players: anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty }, sourceFileType: fileURL.manData ? .messieurs : .dames, date: fileURL.dateFromPath)
} catch { } catch {
Logger.error(error) Logger.error(error)
} }
@ -241,3 +247,71 @@ struct PadelClubView: View {
//#Preview { //#Preview {
// PadelClubView() // PadelClubView()
//} //}
// Function to fetch data for a single license ID
func fetchPlayerData(for licenseID: String) async throws -> [Player]? {
guard let url = URL(string: "https://beach-padel.app.fft.fr/beachja/rechercheJoueur/licencies?idHomologation=82477107&numeroLicence=\(licenseID)") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("application/json, text/javascript, */*; q=0.01", forHTTPHeaderField: "Accept")
request.setValue("same-origin", forHTTPHeaderField: "Sec-Fetch-Site")
request.setValue("fr-FR,fr;q=0.9", forHTTPHeaderField: "Accept-Language")
request.setValue("gzip, deflate, br", forHTTPHeaderField: "Accept-Encoding")
request.setValue("cors", forHTTPHeaderField: "Sec-Fetch-Mode")
request.setValue("beach-padel.app.fft.fr", forHTTPHeaderField: "Host")
request.setValue("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.15", forHTTPHeaderField: "User-Agent")
request.setValue("keep-alive", forHTTPHeaderField: "Connection")
request.setValue("https://beach-padel.app.fft.fr/beachja/competitionFiche/inscrireEquipe?identifiantHomologation=82477107", forHTTPHeaderField: "Referer")
request.setValue("XMLHttpRequest", forHTTPHeaderField: "X-Requested-With")
// Add cookies if needed (example cookie header value shown, replace with valid cookies)
request.setValue("JSESSIONID=F4ED2A1BCF3CD2694FE0B111B8027999; AWSALB=JoZEC/+cnAzmCdbbm3Vuc4CtMGx8BvbveFx+RBRuj8dQCQD52C9iDDbL/OVm98uMb7vc8Jv6/bVPkaByXWmOZmSGwAsN2s8/jt6W5L8QGz7omzNbYF01kvqffRvo; AWSALBCORS=JoZEC/+cnAzmCdbbm3Vuc4CtMGx8BvbveFx+RBRuj8dQCQD52C9iDDbL/OVm98uMb7vc8Jv6/bVPkaByXWmOZmSGwAsN2s8/jt6W5L8QGz7omzNbYF01kvqffRvo; datadome=KlbIdnrCgaY1zLVIZ5CfLJm~KXv9_YnXGhaQdqMEn6Ja9R6imBH~vhzmyuiLxGi1D0z90v5x2EiGDvQ7zsw~fajWLbOupFEajulc86PSJ7RIHpOiduCQ~cNoITQYJOXa; tc_cj_v2=m_iZZZ%22**%22%27%20ZZZKQLNQOPLOSLJOZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQLQJRKOQKSMOZZZ%5D777m_iZZZ%22**%22%27%20ZZZKQLQJRKOQMSLNZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQLQJRKOQNSJMZZZ%5D777m_iZZZ%22**%22%27%20ZZZKQLQJRKOSJMLJZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQLRPQMQQNRQRZZZ%5D777m_iZZZ%22**%22%27%20ZZZKQLRPQNKSLOMSZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQLSNSOPMSOPJZZZ%5D777m_iZZZ%22**%22%27%20ZZZKQMJQSRLJSOOJZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQMJRJPJMSSKRZZZ%5D; tc_cj_v2_cmp=; tc_cj_v2_med=; tCdebugLib=1; incap_ses_2222_2712217=ui9wOOAjNziUTlU3gCHWHtv/KWcAAAAAhSzbpyITRp7YwRT3vJB2vg==; incap_ses_2224_2712217=NepDAr2kUDShMiCJaDzdHqbjKWcAAAAA0kLlk3lgvGnwWSTMceZoEw==; xtan=-; xtant=1; incap_ses_1350_2712217=g+XhSJRwOS8JlWTYCSq8EtOBJGcAAAAAffg2IobkPUW2BtvgJGHbMw==; TCSESSION=124101910177775608913; nlbi_2712217=jnhtOC5KDiLvfpy/b9lUTgAAAAA7zduh8JyZOVrEfGsEdFlq; TCID=12481811494814553052; xtvrn=$548419$; TCPID=12471746148351334672; visid_incap_2712217=PSfJngzoSuiowsuXXhvOu5K+7mUAAAAAQUIPAAAAAAAleL9ldvN/FC1VykkU9ret; SessionStatId=10.91.140.42.1662124965429001", forHTTPHeaderField: "Cookie")
let (data, _) = try await URLSession.shared.data(for: request)
let decoder = JSONDecoder()
// Debug: Print raw JSON data for inspection
if let jsonString = String(data: data, encoding: .utf8) {
print("Raw JSON response: \(jsonString)")
}
// Decode the response
let response = try decoder.decode(Response.self, from: data)
let players = response.object.listeJoueurs
// Cast the JSON object to [String: Any] dictionary
return players
}
// Function to fetch data for multiple license IDs using TaskGroup
func fetchPlayersDataSequentially(for licenseIDs: inout [FederalPlayer]) async {
for licenseID in licenseIDs.filter({ $0.firstName.isEmpty && $0.lastName.isEmpty }) {
do {
if let playerData = try await fetchPlayerData(for: licenseID.license)?.first {
licenseID.lastName = playerData.nom
licenseID.firstName = playerData.prenom
}
} catch {
print(error)
}
}
}
struct Player: Codable {
let licence: Int
let nom: String
let prenom: String
let sexe: String
}
struct Response: Codable {
let object: PlayerList
}
struct PlayerList: Codable {
let listeJoueurs: [Player]
}

@ -0,0 +1,84 @@
//
// UmpireStatisticView.swift
// PadelClub
//
// Created by razmig on 06/11/2024.
//
import SwiftUI
import LeStorage
struct UmpireStatisticView: View {
@EnvironmentObject var dataStore: DataStore
let walkoutTeams: [TeamRegistration]
let players: [PlayerRegistration]
let countedPlayers: [String: Int]
let countedWalkoutPlayers: [String: Int]
init() {
let teams = DataStore.shared.tournaments.filter { $0.isDeleted == false }.flatMap({ $0.unsortedTeams() })
let wos = teams.filter({ $0.walkOut })
self.walkoutTeams = wos
var uniquePlayersDict = [String: PlayerRegistration]()
var playerCountDict = [String: Int]()
var playerWalkOutCountDict = [String: Int]()
for team in teams {
for player in team.unsortedPlayers() {
if let licenceId = player.licenceId?.strippedLicense {
if team.walkOut {
uniquePlayersDict[licenceId] = player
playerWalkOutCountDict[licenceId, default: 0] += 1
}
playerCountDict[licenceId, default: 0] += 1
}
}
}
self.players = Array(uniquePlayersDict.values).sorted(by: { a, b in
playerWalkOutCountDict[a.licenceId!.strippedLicense!]! > playerWalkOutCountDict[b.licenceId!.strippedLicense!]!
})
self.countedPlayers = playerCountDict
self.countedWalkoutPlayers = playerWalkOutCountDict
}
var body: some View {
List {
Section {
LabeledContent {
Text(dataStore.tournaments.count.formatted())
} label: {
Text("Tournois")
}
LabeledContent {
Text(walkoutTeams.count.formatted())
} label: {
Text("Équipes forfaites")
}
}
if players.isEmpty == false {
Section {
ForEach(players) { player in
LabeledContent {
if let licenceId = player.licenceId?.strippedLicense, let count = countedPlayers[licenceId], let walkoutCount = countedWalkoutPlayers[licenceId] {
Text(walkoutCount.formatted() + " / " + count.formatted())
.font(.title3)
}
} label: {
Text(player.playerLabel())
}
}
} header: {
Text("Nombre de forfaits / participations")
}
}
}
.navigationTitle("Statistiques")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
}
}

@ -121,6 +121,14 @@ struct UmpireView: View {
Text("Il s'agit des clubs qui sont utilisés pour récupérer les tournois tenup.") Text("Il s'agit des clubs qui sont utilisés pour récupérer les tournois tenup.")
} }
Section {
NavigationLink {
UmpireStatisticView()
} label: {
Text("Statistiques de participations")
}
}
Section { Section {
@Bindable var user = dataStore.user @Bindable var user = dataStore.user

@ -0,0 +1,93 @@
//
// DatePickingView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 17/04/2024.
//
import SwiftUI
struct DatePickingView: View {
let title: String
@Binding var startDate: Date
@Binding var currentDate: Date?
var duration: Int?
var validateAction: (() async -> ())
@State private var confirmFollowingScheduleUpdate: Bool = false
@State private var updatingInProgress: Bool = false
var body: some View {
Section {
DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline)
}
if confirmFollowingScheduleUpdate {
RowButtonView("Modifier la suite du programme") {
updatingInProgress = true
await validateAction()
updatingInProgress = false
confirmFollowingScheduleUpdate = false
}
}
} header: {
Text(title)
} footer: {
if confirmFollowingScheduleUpdate && updatingInProgress == false {
FooterButtonView("non, ne pas modifier la suite") {
currentDate = startDate
confirmFollowingScheduleUpdate = false
}
} else {
HStack {
Menu {
Button("de 30 minutes") {
startDate = startDate.addingTimeInterval(1800)
}
Button("d'une heure") {
startDate = startDate.addingTimeInterval(3600)
}
Button("à 9h") {
startDate = startDate.atNine()
}
Button("à demain 9h") {
startDate = startDate.tomorrowAtNine
}
if let duration {
Button("à la prochaine rotation") {
startDate = startDate.addingTimeInterval(Double(duration) * 60)
}
Button("à la précédente rotation") {
startDate = startDate.addingTimeInterval(Double(duration) * -60)
}
}
} label: {
Text("décaler")
.underline()
}
.buttonStyle(.borderless)
Spacer()
if currentDate != nil {
FooterButtonView("retirer l'horaire bloqué") {
currentDate = nil
}
} else {
FooterButtonView("bloquer l'horaire") {
currentDate = startDate
}
}
}
.buttonStyle(.borderless)
}
}
.onChange(of: startDate) {
confirmFollowingScheduleUpdate = true
}
.headerProminence(.increased)
}
}

@ -0,0 +1,108 @@
//
// DatePickingViewWithFormat.swift
// PadelClub
//
// Created by razmig on 26/10/2024.
//
import SwiftUI
struct DatePickingViewWithFormat: View {
@Environment(Tournament.self) var tournament
@Binding var matchFormat: MatchFormat
let title: String
@Binding var startDate: Date
@Binding var currentDate: Date?
var duration: Int?
var validateAction: ((Bool) async -> ())
@State private var confirmScheduleUpdate: Bool = false
@State private var updatingInProgress : Bool = false
var body: some View {
Section {
MatchTypeSelectionView(selectedFormat: $matchFormat, additionalEstimationDuration: tournament.additionalEstimationDuration)
DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline)
}
if confirmScheduleUpdate {
RowButtonView("Sauver et modifier la suite") {
updatingInProgress = true
await validateAction(true)
updatingInProgress = false
confirmScheduleUpdate = false
}
}
} header: {
Text(title)
} footer: {
if confirmScheduleUpdate && updatingInProgress == false {
HStack {
FooterButtonView("sauver sans modifier la suite") {
Task {
await validateAction(false)
confirmScheduleUpdate = false
}
}
Text("ou")
FooterButtonView("annuler") {
confirmScheduleUpdate = false
}
}
} else {
HStack {
Menu {
Button("de 30 minutes") {
startDate = startDate.addingTimeInterval(1800)
}
Button("d'une heure") {
startDate = startDate.addingTimeInterval(3600)
}
Button("à 9h") {
startDate = startDate.atNine()
}
Button("à demain 9h") {
startDate = startDate.tomorrowAtNine
}
if let duration {
Button("à la prochaine rotation") {
startDate = startDate.addingTimeInterval(Double(duration) * 60)
}
Button("à la précédente rotation") {
startDate = startDate.addingTimeInterval(Double(duration) * -60)
}
}
} label: {
Text("décaler")
.underline()
}
.buttonStyle(.borderless)
Spacer()
if currentDate != nil {
FooterButtonView("retirer l'horaire bloqué") {
currentDate = nil
}
} else {
FooterButtonView("bloquer l'horaire") {
currentDate = startDate
}
}
}
.buttonStyle(.borderless)
}
}
.headerProminence(.increased)
.onChange(of: matchFormat) {
confirmScheduleUpdate = true
}
.onChange(of: startDate) {
confirmScheduleUpdate = true
}
}
}

@ -1,5 +1,5 @@
// //
// DateUpdateManagerView.swift // DatePickingView.swift
// PadelClub // PadelClub
// //
// Created by Razmig Sarkissian on 17/04/2024. // Created by Razmig Sarkissian on 17/04/2024.
@ -91,225 +91,3 @@ struct DatePickingView: View {
.headerProminence(.increased) .headerProminence(.increased)
} }
} }
struct MatchFormatPickingView: View {
var title: String? = nil
@Binding var matchFormat: MatchFormat
var validateAction: (() async -> ())
@State private var confirmScheduleUpdate: Bool = false
@State private var updatingInProgress : Bool = false
var body: some View {
Section {
MatchFormatPickerView(headerLabel: "Format", matchFormat: $matchFormat)
if confirmScheduleUpdate {
RowButtonView("Recalculer les horaires") {
updatingInProgress = true
await validateAction()
updatingInProgress = false
confirmScheduleUpdate = false
}
}
} header: {
if let title {
Text(title)
}
} footer: {
if confirmScheduleUpdate && updatingInProgress == false {
FooterButtonView("non, ne pas modifier les horaires") {
confirmScheduleUpdate = false
}
}
}
.headerProminence(.increased)
.onChange(of: matchFormat) {
confirmScheduleUpdate = true
}
}
}
struct DatePickingViewWithFormat: View {
@Binding var matchFormat: MatchFormat
let title: String
@Binding var startDate: Date
@Binding var currentDate: Date?
var duration: Int?
var validateAction: ((Bool) async -> ())
@State private var confirmScheduleUpdate: Bool = false
@State private var updatingInProgress : Bool = false
var body: some View {
Section {
MatchFormatPickerView(headerLabel: "Format", matchFormat: $matchFormat)
DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline)
}
if confirmScheduleUpdate {
RowButtonView("Sauver et modifier la suite") {
updatingInProgress = true
await validateAction(true)
updatingInProgress = false
confirmScheduleUpdate = false
}
}
} header: {
Text(title)
} footer: {
if confirmScheduleUpdate && updatingInProgress == false {
HStack {
FooterButtonView("sauver sans modifier la suite") {
Task {
await validateAction(false)
confirmScheduleUpdate = false
}
}
Text("ou")
FooterButtonView("annuler") {
confirmScheduleUpdate = false
}
}
} else {
HStack {
Menu {
Button("de 30 minutes") {
startDate = startDate.addingTimeInterval(1800)
}
Button("d'une heure") {
startDate = startDate.addingTimeInterval(3600)
}
Button("à 9h") {
startDate = startDate.atNine()
}
Button("à demain 9h") {
startDate = startDate.tomorrowAtNine
}
if let duration {
Button("à la prochaine rotation") {
startDate = startDate.addingTimeInterval(Double(duration) * 60)
}
Button("à la précédente rotation") {
startDate = startDate.addingTimeInterval(Double(duration) * -60)
}
}
} label: {
Text("décaler")
.underline()
}
.buttonStyle(.borderless)
Spacer()
if currentDate != nil {
FooterButtonView("retirer l'horaire bloqué") {
currentDate = nil
}
} else {
FooterButtonView("bloquer l'horaire") {
currentDate = startDate
}
}
}
.buttonStyle(.borderless)
}
}
.headerProminence(.increased)
.onChange(of: matchFormat) {
confirmScheduleUpdate = true
}
.onChange(of: startDate) {
confirmScheduleUpdate = true
}
}
}
struct GroupStageDatePickingView: View {
let title: String
@Binding var startDate: Date
@Binding var currentDate: Date?
var duration: Int?
var validateAction: (() async -> ())
@State private var confirmFollowingScheduleUpdate: Bool = false
@State private var updatingInProgress: Bool = false
var body: some View {
Section {
DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline)
}
if confirmFollowingScheduleUpdate {
RowButtonView("Confirmer et modifier les matchs") {
updatingInProgress = true
await validateAction()
updatingInProgress = false
confirmFollowingScheduleUpdate = false
}
}
} header: {
Text(title)
} footer: {
if confirmFollowingScheduleUpdate && updatingInProgress == false {
FooterButtonView("Modifier juste l'horaire de la poule") {
currentDate = startDate
confirmFollowingScheduleUpdate = false
}
} else {
HStack {
Menu {
Button("de 30 minutes") {
startDate = startDate.addingTimeInterval(1800)
}
Button("d'une heure") {
startDate = startDate.addingTimeInterval(3600)
}
Button("à 9h") {
startDate = startDate.atNine()
}
Button("à demain 9h") {
startDate = startDate.tomorrowAtNine
}
if let duration {
Button("à la prochaine rotation") {
startDate = startDate.addingTimeInterval(Double(duration) * 60)
}
Button("à la précédente rotation") {
startDate = startDate.addingTimeInterval(Double(duration) * -60)
}
}
} label: {
Text("décaler")
.underline()
}
.buttonStyle(.borderless)
Spacer()
if currentDate != nil {
FooterButtonView("retirer l'horaire bloqué") {
currentDate = nil
}
} else {
FooterButtonView("bloquer l'horaire") {
currentDate = startDate
}
}
}
.buttonStyle(.borderless)
}
}
.onChange(of: startDate) {
confirmFollowingScheduleUpdate = true
}
.headerProminence(.increased)
}
}

@ -0,0 +1,93 @@
//
// GroupStageDatePickingView.swift
// PadelClub
//
// Created by razmig on 26/10/2024.
//
import SwiftUI
struct GroupStageDatePickingView: View {
let title: String
@Binding var startDate: Date
@Binding var currentDate: Date?
var duration: Int?
var validateAction: (() async -> ())
@State private var confirmFollowingScheduleUpdate: Bool = false
@State private var updatingInProgress: Bool = false
var body: some View {
Section {
DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline)
}
if confirmFollowingScheduleUpdate {
RowButtonView("Confirmer et modifier les matchs") {
updatingInProgress = true
await validateAction()
updatingInProgress = false
confirmFollowingScheduleUpdate = false
}
}
} header: {
Text(title)
} footer: {
if confirmFollowingScheduleUpdate && updatingInProgress == false {
FooterButtonView("Modifier juste l'horaire de la poule") {
currentDate = startDate
confirmFollowingScheduleUpdate = false
}
} else {
HStack {
Menu {
Button("de 30 minutes") {
startDate = startDate.addingTimeInterval(1800)
}
Button("d'une heure") {
startDate = startDate.addingTimeInterval(3600)
}
Button("à 9h") {
startDate = startDate.atNine()
}
Button("à demain 9h") {
startDate = startDate.tomorrowAtNine
}
if let duration {
Button("à la prochaine rotation") {
startDate = startDate.addingTimeInterval(Double(duration) * 60)
}
Button("à la précédente rotation") {
startDate = startDate.addingTimeInterval(Double(duration) * -60)
}
}
} label: {
Text("décaler")
.underline()
}
.buttonStyle(.borderless)
Spacer()
if currentDate != nil {
FooterButtonView("retirer l'horaire bloqué") {
currentDate = nil
}
} else {
FooterButtonView("bloquer l'horaire") {
currentDate = startDate
}
}
}
.buttonStyle(.borderless)
}
}
.onChange(of: startDate) {
confirmFollowingScheduleUpdate = true
}
.headerProminence(.increased)
}
}

@ -0,0 +1,47 @@
//
// MatchFormatPickingView.swift
// PadelClub
//
// Created by razmig on 26/10/2024.
//
import SwiftUI
struct MatchFormatPickingView: View {
@Environment(Tournament.self) var tournament
var title: String? = nil
@Binding var matchFormat: MatchFormat
var validateAction: (() async -> ())
@State private var confirmScheduleUpdate: Bool = false
@State private var updatingInProgress : Bool = false
var body: some View {
Section {
MatchTypeSelectionView(selectedFormat: $matchFormat, additionalEstimationDuration: tournament.additionalEstimationDuration)
if confirmScheduleUpdate {
RowButtonView("Recalculer les horaires") {
updatingInProgress = true
await validateAction()
updatingInProgress = false
confirmScheduleUpdate = false
}
}
} header: {
if let title {
Text(title)
}
} footer: {
if confirmScheduleUpdate && updatingInProgress == false {
FooterButtonView("non, ne pas modifier les horaires") {
confirmScheduleUpdate = false
}
}
}
.headerProminence(.increased)
.onChange(of: matchFormat) {
confirmScheduleUpdate = true
}
}
}

@ -0,0 +1,38 @@
//
// MultiCourtPickerView.swift
// PadelClub
//
// Created by razmig on 11/10/2024.
//
import SwiftUI
struct MultiCourtPickerView: View {
@Bindable var matchScheduler: MatchScheduler
@Environment(Tournament.self) var tournament: Tournament
var body: some View {
List {
ForEach(tournament.courtsAvailable(), id: \.self) { courtIndex in
LabeledContent {
Button {
if matchScheduler.courtsAvailable.contains(courtIndex) {
matchScheduler.courtsAvailable.remove(courtIndex)
} else {
matchScheduler.courtsAvailable.insert(courtIndex)
}
} label: {
if matchScheduler.courtsAvailable.contains(courtIndex) {
Image(systemName: "checkmark.circle.fill")
}
}
} label: {
Text(tournament.courtName(atIndex: courtIndex))
}
}
}
.navigationTitle("Terrains disponibles")
.toolbarBackground(.visible, for: .navigationBar)
.environment(\.editMode, Binding.constant(EditMode.active))
}
}

@ -15,9 +15,6 @@ struct CourtAvailabilitySettingsView: View {
let event: Event let event: Event
@State private var showingPopover: Bool = false @State private var showingPopover: Bool = false
@State private var courtIndex: Int = 0
@State private var startDate: Date = Date()
@State private var endDate: Date = Date()
@State private var editingSlot: DateInterval? @State private var editingSlot: DateInterval?
var courtsUnavailability: [Int: [DateInterval]] { var courtsUnavailability: [Int: [DateInterval]] {
@ -45,10 +42,6 @@ struct CourtAvailabilitySettingsView: View {
} }
Button("éditer") { Button("éditer") {
editingSlot = dateInterval editingSlot = dateInterval
courtIndex = dateInterval.courtIndex
startDate = dateInterval.startDate
endDate = dateInterval.endDate
showingPopover = true
} }
Button("effacer", role: .destructive) { Button("effacer", role: .destructive) {
do { do {
@ -110,8 +103,6 @@ struct CourtAvailabilitySettingsView: View {
Text("Vous pouvez précisez l'indisponibilité d'une ou plusieurs terrains, que ce soit pour une journée entière ou un créneau précis.") Text("Vous pouvez précisez l'indisponibilité d'une ou plusieurs terrains, que ce soit pour une journée entière ou un créneau précis.")
} actions: { } actions: {
RowButtonView("Ajouter une indisponibilité", systemImage: "plus.circle.fill") { RowButtonView("Ajouter une indisponibilité", systemImage: "plus.circle.fill") {
startDate = tournament.startDate
endDate = tournament.startDate.addingTimeInterval(5400)
showingPopover = true showingPopover = true
} }
} }
@ -120,8 +111,6 @@ struct CourtAvailabilitySettingsView: View {
.toolbar { .toolbar {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
BarButtonView("Ajouter une indisponibilité", icon: "plus.circle.fill") { BarButtonView("Ajouter une indisponibilité", icon: "plus.circle.fill") {
startDate = tournament.startDate
endDate = tournament.startDate.addingTimeInterval(5400)
showingPopover = true showingPopover = true
} }
} }
@ -130,6 +119,58 @@ struct CourtAvailabilitySettingsView: View {
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Créneau indisponible") .navigationTitle("Créneau indisponible")
.sheet(isPresented: $showingPopover) { .sheet(isPresented: $showingPopover) {
CourtAvailabilityEditorView(event: event)
}
.sheet(item: $editingSlot) { editingSlot in
CourtAvailabilityEditorView(editingSlot: editingSlot, event: event)
}
}
}
struct CourtPicker: View {
@Environment(Tournament.self) var tournament: Tournament
let title: String
@Binding var selection: Int
let maxCourt: Int
var body: some View {
Picker(title, selection: $selection) {
ForEach(0..<maxCourt, id: \.self) {
Text(tournament.courtName(atIndex: $0))
}
}
}
}
struct CourtAvailabilityEditorView: View {
@Environment(Tournament.self) var tournament: Tournament
@EnvironmentObject var dataStore: DataStore
@Environment(\.dismiss) private var dismiss
var editingSlot: DateInterval?
let event: Event
@State private var courtIndex: Int
@State private var startDate: Date
@State private var endDate: Date
init(editingSlot: DateInterval, event: Event) {
self.editingSlot = editingSlot
self.event = event
_courtIndex = .init(wrappedValue: editingSlot.courtIndex)
_startDate = .init(wrappedValue: editingSlot.startDate)
_endDate = .init(wrappedValue: editingSlot.endDate)
}
init(event: Event) {
self.event = event
_courtIndex = .init(wrappedValue: 0)
let startDate = event.eventStartDate()
_startDate = .init(wrappedValue: event.eventStartDate())
_endDate = .init(wrappedValue: startDate.addingTimeInterval(5400))
}
var body: some View {
NavigationStack { NavigationStack {
Form { Form {
Section { Section {
@ -153,11 +194,24 @@ struct CourtAvailabilitySettingsView: View {
} footer: { } footer: {
FooterButtonView("jour entier") { FooterButtonView("jour entier") {
startDate = startDate.startOfDay startDate = startDate.startOfDay
endDate = startDate.endOfDay() endDate = startDate.tomorrowAtNine.startOfDay
}
}
Section {
DateAdjusterView(date: $startDate)
} header: {
Text("Modifier rapidement l'horaire de début")
} }
Section {
DateAdjusterView(date: $endDate)
} header: {
Text("Modifier rapidement l'horaire de fin")
} }
} }
.toolbar { .toolbar {
ToolbarItem(placement: .topBarTrailing) {
ButtonValidateView { ButtonValidateView {
if editingSlot == nil { if editingSlot == nil {
let dateInterval = DateInterval(event: event.id, courtIndex: courtIndex, startDate: startDate, endDate: endDate) let dateInterval = DateInterval(event: event.id, courtIndex: courtIndex, startDate: startDate, endDate: endDate)
@ -176,40 +230,50 @@ struct CourtAvailabilitySettingsView: View {
Logger.error(error) Logger.error(error)
} }
} }
showingPopover = false
dismiss()
}
}
ToolbarItem(placement: .topBarLeading) {
Button("Annuler", role: .cancel) {
dismiss()
}
} }
} }
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Nouveau créneau") .navigationTitle(_navigationTitle())
.tint(.master) .tint(.master)
} }
.onAppear {
UIDatePicker.appearance().minuteInterval = 5
}
.onDisappear {
UIDatePicker.appearance().minuteInterval = 1
}
} }
private func _navigationTitle() -> String {
editingSlot == nil ? "Nouveau créneau" : "Édition du créneau"
} }
} }
struct CourtPicker: View { struct DateAdjusterView: View {
@Environment(Tournament.self) var tournament: Tournament @Binding var date: Date
let title: String
@Binding var selection: Int
let maxCourt: Int
var body: some View { var body: some View {
Picker(title, selection: $selection) { HStack {
ForEach(0..<maxCourt, id: \.self) { _createButton(label: "-1h", timeOffset: -1, component: .hour)
Text(tournament.courtName(atIndex: $0)) _createButton(label: "-30m", timeOffset: -30, component: .minute)
_createButton(label: "+30m", timeOffset: 30, component: .minute)
_createButton(label: "+1h", timeOffset: 1, component: .hour)
} }
.font(.headline)
} }
private func _createButton(label: String, timeOffset: Int, component: Calendar.Component) -> some View {
Button(action: {
date = Calendar.current.date(byAdding: component, value: timeOffset, to: date) ?? date
}) {
Text(label)
.frame(maxWidth: .infinity) // Make buttons take equal space
}
.buttonStyle(.borderedProminent)
.tint(.master)
} }
} }
//#Preview {
// CourtAvailabilitySettingsView(event: Event.mock())
//}

@ -15,6 +15,7 @@ struct GroupStageScheduleEditorView: View {
@Bindable var groupStage: GroupStage @Bindable var groupStage: GroupStage
var tournament: Tournament var tournament: Tournament
@State private var startDate: Date @State private var startDate: Date
@State private var currentDate: Date?
var tournamentStore: TournamentStore { var tournamentStore: TournamentStore {
return self.tournament.tournamentStore return self.tournament.tournamentStore
@ -24,14 +25,19 @@ struct GroupStageScheduleEditorView: View {
self.groupStage = groupStage self.groupStage = groupStage
self.tournament = tournament self.tournament = tournament
self._startDate = State(wrappedValue: groupStage.startDate ?? tournament.startDate) self._startDate = State(wrappedValue: groupStage.startDate ?? tournament.startDate)
self._currentDate = State(wrappedValue: groupStage.startDate)
} }
var body: some View { var body: some View {
GroupStageDatePickingView(title: groupStage.groupStageTitle(.title), startDate: $startDate, currentDate: $groupStage.startDate, duration: groupStage.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { GroupStageDatePickingView(title: groupStage.groupStageTitle(.title), startDate: $startDate, currentDate: $currentDate, duration: groupStage.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) {
groupStage.startDate = startDate groupStage.startDate = startDate
tournament.matchScheduler()?.updateGroupStageSchedule(tournament: tournament, specificGroupStage: groupStage) tournament.matchScheduler()?.updateGroupStageSchedule(tournament: tournament, specificGroupStage: groupStage)
_save() _save()
} }
.onChange(of: currentDate) {
groupStage.startDate = currentDate
_save()
}
} }
private func _save() { private func _save() {

@ -17,6 +17,7 @@ struct LoserRoundScheduleEditorView: View {
var loserRounds: [Round] var loserRounds: [Round]
@State private var startDate: Date @State private var startDate: Date
@State private var matchFormat: MatchFormat @State private var matchFormat: MatchFormat
@State private var currentDate: Date?
var tournamentStore: TournamentStore { var tournamentStore: TournamentStore {
return self.tournament.tournamentStore return self.tournament.tournamentStore
@ -27,8 +28,11 @@ struct LoserRoundScheduleEditorView: View {
self.tournament = tournament self.tournament = tournament
let _loserRounds = upperRound.loserRounds() let _loserRounds = upperRound.loserRounds()
self.loserRounds = _loserRounds self.loserRounds = _loserRounds
self._startDate = State(wrappedValue: _loserRounds.first(where: { $0.startDate != nil })?.startDate ?? _loserRounds.first(where: { $0.isDisabled() == false })?.enabledMatches().first?.startDate ?? tournament.startDate) let startDate = _loserRounds.first(where: { $0.startDate != nil })?.startDate ?? _loserRounds.first(where: { $0.isDisabled() == false })?.enabledMatches().first?.startDate
self._startDate = State(wrappedValue: startDate ?? tournament.startDate)
self._matchFormat = State(wrappedValue: _loserRounds.first?.matchFormat ?? upperRound.matchFormat) self._matchFormat = State(wrappedValue: _loserRounds.first?.matchFormat ?? upperRound.matchFormat)
self._currentDate = State(wrappedValue: startDate)
} }
var body: some View { var body: some View {
@ -37,9 +41,17 @@ struct LoserRoundScheduleEditorView: View {
await _updateSchedule() await _updateSchedule()
} }
DatePickingView(title: "Horaire minimum", startDate: $startDate, currentDate: .constant(nil), duration: matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { DatePickingView(title: "Horaire minimum", startDate: $startDate, currentDate: $currentDate, duration: matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) {
await _updateSchedule() await _updateSchedule()
} }
.onChange(of: currentDate) {
let enabledLoserRounds = upperRound.loserRounds().filter({ $0.isDisabled() == false })
for loserRound in enabledLoserRounds {
loserRound.startDate = currentDate
}
_save()
}
let enabledLoserRounds = upperRound.loserRounds().filter({ $0.isDisabled() == false }) let enabledLoserRounds = upperRound.loserRounds().filter({ $0.isDisabled() == false })
ForEach(enabledLoserRounds.indices, id: \.self) { index in ForEach(enabledLoserRounds.indices, id: \.self) { index in

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save