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. 225
      PadelClub/Data/GroupStage.swift
  17. 380
      PadelClub/Data/Match.swift
  18. 307
      PadelClub/Data/MatchScheduler.swift
  19. 63
      PadelClub/Data/PlayerRegistration.swift
  20. 4
      PadelClub/Data/README.md
  21. 90
      PadelClub/Data/Round.swift
  22. 131
      PadelClub/Data/TeamRegistration.swift
  23. 651
      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. 191
      PadelClub/Views/Calling/CallView.swift
  47. 2
      PadelClub/Views/Calling/Components/MenuWarningView.swift
  48. 4
      PadelClub/Views/Calling/Components/PlayersWithoutContactView.swift
  49. 15
      PadelClub/Views/Calling/GroupStageCallingView.swift
  50. 27
      PadelClub/Views/Calling/SeedsCallingView.swift
  51. 183
      PadelClub/Views/Calling/TeamsCallingView.swift
  52. 8
      PadelClub/Views/Cashier/CashierDetailView.swift
  53. 96
      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. 15
      PadelClub/Views/Club/ClubDetailView.swift
  58. 6
      PadelClub/Views/Club/CourtView.swift
  59. 28
      PadelClub/Views/Components/CopyPasteButtonView.swift
  60. 58
      PadelClub/Views/Components/FortuneWheelView.swift
  61. 4
      PadelClub/Views/Components/GenericDestinationPickerView.swift
  62. 33
      PadelClub/Views/Components/MatchListView.swift
  63. 48
      PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift
  64. 42
      PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift
  65. 72
      PadelClub/Views/GroupStage/GroupStageQualificationManagerView.swift
  66. 51
      PadelClub/Views/GroupStage/GroupStageView.swift
  67. 52
      PadelClub/Views/GroupStage/GroupStagesSettingsView.swift
  68. 35
      PadelClub/Views/GroupStage/GroupStagesView.swift
  69. 8
      PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift
  70. 2
      PadelClub/Views/GroupStage/Shared/GroupStageTeamReplacementView.swift
  71. 93
      PadelClub/Views/Match/Components/MatchDateView.swift
  72. 19
      PadelClub/Views/Match/Components/MatchTeamDetailView.swift
  73. 75
      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. 106
      PadelClub/Views/Match/MatchSummaryView.swift
  78. 39
      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. 142
      PadelClub/Views/Navigation/Ongoing/OngoingView.swift
  86. 128
      PadelClub/Views/Navigation/Toolbox/GlobalSettingsView.swift
  87. 3
      PadelClub/Views/Navigation/Toolbox/MatchFormatStorageView.swift
  88. 44
      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. 180
      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 */; };
C45BAE442BCA753E002EEC8A /* Purchase.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45BAE432BCA753E002EEC8A /* Purchase.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 */; };
C488C7EA2CC7D16F0082001F /* 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 */; };
FF1162852BD00279000C4809 /* PlayerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162842BD00279000C4809 /* PlayerDetailView.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 */; };
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 */; };
FF1CBC1D2BB53DC10036DAAB /* Calendar+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.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 */; };
FF4CBFE12C996C0600151637 /* PrintSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1F4B812BFA0124000B4573 /* PrintSettingsView.swift */; };
FF4CBFE22C996C0600151637 /* TournamentMatchFormatsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE22BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift */; };
FF4CBFE32C996C0600151637 /* DateUpdateManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */; };
FF4CBFE42C996C0600151637 /* MatchTypeSmallSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0192BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift */; };
FF4CBFE32C996C0600151637 /* DatePickingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DatePickingView.swift */; };
FF4CBFE42C996C0600151637 /* MatchFormatRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0192BBC5A8500B82851 /* MatchFormatRowView.swift */; };
FF4CBFE52C996C0600151637 /* MonthData.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE82BD1307E00A86CF8 /* MonthData.swift */; };
FF4CBFE62C996C0600151637 /* MenuWarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFEF7F4D2BDE69130033D0F0 /* MenuWarningView.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 */; };
FF4CC01A2C996C0600151637 /* InscriptionInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5D0D772BB42C5B005CB568 /* InscriptionInfoView.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 */; };
FF4CC01E2C996C0600151637 /* NumberFormatter+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5D0D862BB48AFD005CB568 /* NumberFormatter+Extensions.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 */; };
FF6525C32C8C61B400B9498E /* LoserBracketFromGroupStageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6525C22C8C61B400B9498E /* LoserBracketFromGroupStageView.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 */; };
FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.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 */; };
FF70FB602C90584900129CC2 /* PrintSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1F4B812BFA0124000B4573 /* PrintSettingsView.swift */; };
FF70FB612C90584900129CC2 /* TournamentMatchFormatsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE22BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift */; };
FF70FB622C90584900129CC2 /* DateUpdateManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */; };
FF70FB632C90584900129CC2 /* MatchTypeSmallSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0192BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift */; };
FF70FB622C90584900129CC2 /* DatePickingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DatePickingView.swift */; };
FF70FB632C90584900129CC2 /* MatchFormatRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0192BBC5A8500B82851 /* MatchFormatRowView.swift */; };
FF70FB642C90584900129CC2 /* MonthData.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE82BD1307E00A86CF8 /* MonthData.swift */; };
FF70FB652C90584900129CC2 /* MenuWarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFEF7F4D2BDE69130033D0F0 /* MenuWarningView.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 */; };
FF70FB992C90584900129CC2 /* InscriptionInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5D0D772BB42C5B005CB568 /* InscriptionInfoView.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 */; };
FF70FB9D2C90584900129CC2 /* NumberFormatter+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5D0D862BB48AFD005CB568 /* NumberFormatter+Extensions.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 */; };
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, ); }; };
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 */; };
FF82CFC52B911F5B00B0CAF2 /* OrganizedTournamentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF82CFC42B911F5B00B0CAF2 /* OrganizedTournamentView.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 */; };
FF8F264D2BAE0B4100650388 /* TournamentDatePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F264A2BAE0B4100650388 /* TournamentDatePickerView.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 */; };
FF90FC1D2C44FB3E009339B2 /* AddTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF90FC1C2C44FB3E009339B2 /* AddTeamView.swift */; };
FF92660D2C241CE0002361A4 /* Zip in Frameworks */ = {isa = PBXBuildFile; productRef = FF92660C2C241CE0002361A4 /* Zip */; };
@ -840,12 +879,27 @@
FF967D0F2BAF63B000A9A3BD /* PlayerBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967D0E2BAF63B000A9A3BD /* PlayerBlockView.swift */; };
FF9AC3952BE3627B00C2E883 /* GroupStageTeamReplacementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF9AC3942BE3627B00C2E883 /* GroupStageTeamReplacementView.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 */; };
FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */; };
FFB1C98B2C10255100B154A7 /* TournamentBroadcastRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */; };
FFB9C8712BBADDE200A0EF4F /* Selectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB9C8702BBADDE200A0EF4F /* Selectable.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 */; };
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 */; };
FFBF065E2BBD8040009D6715 /* MatchListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF065D2BBD8040009D6715 /* MatchListView.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 */; };
FFCFC0162BBC5A4C00B82851 /* SetInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0152BBC5A4C00B82851 /* SetInputView.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 */; };
FFD655D82C8DE27400E5B35E /* TournamentLookUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD655D72C8DE27400E5B35E /* TournamentLookUpView.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>"; };
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>"; };
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>"; };
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>"; };
@ -1112,8 +1168,12 @@
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>"; };
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>"; };
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>"; };
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>"; };
@ -1184,6 +1244,9 @@
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>"; };
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>"; };
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>"; };
@ -1199,6 +1262,10 @@
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>"; };
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>"; };
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>"; };
@ -1215,7 +1282,7 @@
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>"; };
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>"; };
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>"; };
@ -1249,12 +1316,17 @@
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>"; };
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>"; };
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>"; };
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>"; };
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>"; };
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>"; };
@ -1283,7 +1355,7 @@
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>"; };
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>"; };
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>"; };
@ -1460,6 +1532,7 @@
C488C8132CC7E4240082001F /* Court.json */,
C488C8142CC7E4240082001F /* CustomUser.json */,
C488C8152CC7E4240082001F /* DateInterval.json */,
C471D1532D0C8FE80068091F /* Drawlog.json */,
C488C8162CC7E4240082001F /* Event.json */,
C488C8172CC7E4240082001F /* GroupStage.json */,
C488C8182CC7E4240082001F /* Match.json */,
@ -1475,6 +1548,7 @@
C488C8022CC7E1E40082001F /* BaseCourt.swift */,
C488C8062CC7E4240082001F /* BaseCustomUser.swift */,
C488C8072CC7E4240082001F /* BaseDateInterval.swift */,
C471D1572D0C91FE0068091F /* BaseDrawLog.swift */,
C488C8082CC7E4240082001F /* BaseEvent.swift */,
C488C8092CC7E4240082001F /* BaseGroupStage.swift */,
C488C80A2CC7E4240082001F /* BaseMatch.swift */,
@ -1514,6 +1588,7 @@
FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */,
FFC91B002BD85C2F00B29808 /* Court.swift */,
FFF116E02BD2A9B600A33B06 /* DateInterval.swift */,
FF6761522CC77D1900CC9BF2 /* DrawLog.swift */,
FF6EC9012B94799200EA7F5A /* Coredata */,
FF6EC9022B9479B900EA7F5A /* Federal */,
);
@ -1625,6 +1700,7 @@
children = (
FF089EBC2BB0287D00F0AEC7 /* PlayerView.swift */,
FF1162842BD00279000C4809 /* PlayerDetailView.swift */,
FFA252A82CDB70520074E63F /* PlayerStatisticView.swift */,
FF089EB02BB001EA00F0AEC7 /* Components */,
);
path = Player;
@ -1692,7 +1768,11 @@
FF1162882BD0523B000C4809 /* Components */ = {
isa = PBXGroup;
children = (
FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */,
FF1162892BD05247000C4809 /* DatePickingView.swift */,
FF77CE592CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift */,
FF77CE552CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift */,
FF77CE512CCCD1AF00CBCBB4 /* MatchFormatPickingView.swift */,
FF17CA482CB915A1003C7323 /* MultiCourtPickerView.swift */,
);
path = Components;
sourceTree = "<group>";
@ -1823,6 +1903,7 @@
isa = PBXGroup;
children = (
FF3F74F52B919E45004CFE0E /* UmpireView.swift */,
FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */,
FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */,
C488C8812CCBE8FC0082001F /* NetworkStatusView.swift */,
);
@ -1846,6 +1927,7 @@
FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */,
FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */,
FF5BAF6D2BE0B3C8008B4B7E /* FederalDataViewModel.swift */,
FFBE62042CE9DA0900815D33 /* MatchViewStyle.swift */,
);
path = ViewModel;
sourceTree = "<group>";
@ -1862,6 +1944,8 @@
isa = PBXGroup;
children = (
FF5D30552BD95B1100F2B93D /* OngoingView.swift */,
FFA252B02CDD2C080074E63F /* OngoingContainerView.swift */,
FFA252B42CDD2C630074E63F /* OngoingDestination.swift */,
);
path = Ongoing;
sourceTree = "<group>";
@ -1870,11 +1954,11 @@
isa = PBXGroup;
children = (
FF8F264E2BAE0B9600650388 /* MatchTypeSelectionView.swift */,
FF8F26502BAE0BAD00650388 /* MatchFormatPickerView.swift */,
FF8F26502BAE0BAD00650388 /* MatchFormatSelectionView.swift */,
FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */,
FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */,
FF5D0D6F2BB3EFA5005CB568 /* LearnMoreSheetView.swift */,
FFCFC0192BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift */,
FFCFC0192BBC5A8500B82851 /* MatchFormatRowView.swift */,
FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */,
FFE2D2E12C231BEE00D0C7BE /* SupportButtonView.swift */,
FFE103112C366E5900684FC9 /* ImagePickerView.swift */,
@ -1943,6 +2027,7 @@
FF9268082BCEDC2C0080F940 /* CallView.swift */,
FF1162792BCF8109000C4809 /* CallMessageCustomizationView.swift */,
FF2EFBEF2BDE295E0049CE3B /* SendToAllView.swift */,
FF17CA522CBE4788003C7323 /* BracketCallingView.swift */,
FFEF7F4C2BDE68F80033D0F0 /* Components */,
);
path = Calling;
@ -1981,6 +2066,8 @@
FF967D0A2BAF3D4C00A9A3BD /* TeamPickerView.swift */,
FF089EB52BB00A3800F0AEC7 /* TeamRowView.swift */,
FF1162862BD004AD000C4809 /* EditingTeamView.swift */,
FF17CA562CC02FEA003C7323 /* CoachListView.swift */,
FF7DCD382CC330260041110C /* TeamRestingView.swift */,
FF025AD62BD0C0FB00A86CF8 /* Components */,
);
path = Team;
@ -2027,6 +2114,8 @@
FFC2DCB12BBE75D40046DB9F /* LoserRoundView.swift */,
FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */,
FF5647122C0B6F380081F995 /* LoserRoundSettingsView.swift */,
FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */,
FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */,
);
path = Round;
sourceTree = "<group>";
@ -2035,6 +2124,7 @@
isa = PBXGroup;
children = (
FFCFC0012BBC39A600B82851 /* EditScoreView.swift */,
FF17CA4C2CB9243E003C7323 /* FollowUpMatchView.swift */,
FFCFC0152BBC5A4C00B82851 /* SetInputView.swift */,
FFCFC0172BBC5A6800B82851 /* SetLabelView.swift */,
FFCFC00D2BBC3D4600B82851 /* PointSelectionView.swift */,
@ -2322,6 +2412,7 @@
C488C8202CC7E4240082001F /* Event.json in Resources */,
C488C8212CC7E4240082001F /* Court.json in Resources */,
C488C8222CC7E4240082001F /* Tournament.json in Resources */,
C471D1552D0C8FED0068091F /* Drawlog.json in Resources */,
C488C8232CC7E4240082001F /* CustomUser.json in Resources */,
C488C8242CC7E4240082001F /* Round.json in Resources */,
C488C8252CC7E4240082001F /* MatchScheduler.json in Resources */,
@ -2376,6 +2467,7 @@
C488C83A2CC7E4240082001F /* Event.json in Resources */,
C488C83B2CC7E4240082001F /* Court.json in Resources */,
C488C83C2CC7E4240082001F /* Tournament.json in Resources */,
C471D1562D0C8FED0068091F /* Drawlog.json in Resources */,
C488C83D2CC7E4240082001F /* CustomUser.json in Resources */,
C488C83E2CC7E4240082001F /* Round.json in Resources */,
C488C83F2CC7E4240082001F /* MatchScheduler.json in Resources */,
@ -2416,6 +2508,7 @@
C488C8612CC7E4240082001F /* Event.json in Resources */,
C488C8622CC7E4240082001F /* Court.json in Resources */,
C488C8632CC7E4240082001F /* Tournament.json in Resources */,
C471D1542D0C8FED0068091F /* Drawlog.json in Resources */,
C488C8642CC7E4240082001F /* CustomUser.json in Resources */,
C488C8652CC7E4240082001F /* Round.json in Resources */,
C488C8662CC7E4240082001F /* MatchScheduler.json in Resources */,
@ -2449,6 +2542,7 @@
FF7091662B90F0B000AB08DA /* TabDestination.swift in Sources */,
FF9267F82BCE78C70080F940 /* CashierView.swift in Sources */,
FF8F263F2BAD7D5C00650388 /* Event.swift in Sources */,
FF77CE562CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift in Sources */,
FF5D30532BD94E2E00F2B93D /* PlayerHolder.swift in Sources */,
FF11628C2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift in Sources */,
FF53FBB82BFB302B0051D4C3 /* ClubCourtSetupView.swift in Sources */,
@ -2471,6 +2565,7 @@
FF025AE12BD0EB9000A86CF8 /* TournamentClubSettingsView.swift in Sources */,
FFBF065C2BBD2657009D6715 /* GroupStageTeamView.swift in Sources */,
FF5DA1932BB9279B00A33061 /* RoundSettingsView.swift in Sources */,
FFBE62052CE9DA0900815D33 /* MatchViewStyle.swift in Sources */,
FFE2D2E22C231BEE00D0C7BE /* SupportButtonView.swift in Sources */,
FFB1C98B2C10255100B154A7 /* TournamentBroadcastRowView.swift in Sources */,
FF025ADF2BD0CE0A00A86CF8 /* TeamWeightView.swift in Sources */,
@ -2485,11 +2580,13 @@
FFE103082C353B7600684FC9 /* EventClubSettingsView.swift in Sources */,
C4A47DB32B86387500ADC637 /* AccountView.swift in Sources */,
FFCEDA4C2C2C08EA00F8C0F2 /* PlayersWithoutContactView.swift in Sources */,
FF17CA4F2CB9243E003C7323 /* FollowUpMatchView.swift in Sources */,
FFCD16B32C3E5E590092707B /* TeamsCallingView.swift in Sources */,
FF1CBC1D2BB53DC10036DAAB /* Calendar+Extensions.swift in Sources */,
FF967CF22BAECC0B00A9A3BD /* TeamScore.swift in Sources */,
FF1162832BCFBE4E000C4809 /* EditablePlayerView.swift in Sources */,
FF1162852BD00279000C4809 /* PlayerDetailView.swift in Sources */,
FFA252AE2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */,
FF5D0D762BB428B2005CB568 /* ListRowViewModifier.swift in Sources */,
FF6EC9002B94794700EA7F5A /* PresentationContext.swift in Sources */,
FFDB1C6D2BB2A02000F1E467 /* AppSettings.swift in Sources */,
@ -2504,6 +2601,7 @@
C4C01D982C481C0C0059087C /* CapsuleViewModifier.swift in Sources */,
C488C82D2CC7E4240082001F /* BaseDateInterval.swift in Sources */,
C488C82E2CC7E4240082001F /* BaseMonthData.swift in Sources */,
C471D1592D0C91FE0068091F /* BaseDrawLog.swift in Sources */,
C488C82F2CC7E4240082001F /* BaseTeamRegistration.swift in Sources */,
C488C8302CC7E4240082001F /* BaseGroupStage.swift in Sources */,
C488C8312CC7E4240082001F /* BaseCustomUser.swift in Sources */,
@ -2522,6 +2620,7 @@
FF5D30562BD95B1100F2B93D /* OngoingView.swift in Sources */,
FF1DC5552BAB36DD00FD8220 /* CreateClubView.swift in Sources */,
C4607A7D2C04DDE2004CB781 /* APICallsListView.swift in Sources */,
FF7DCD3B2CC330270041110C /* TeamRestingView.swift in Sources */,
FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */,
FF025AEF2BD1AE9400A86CF8 /* DurationSettingsView.swift in Sources */,
FF025AED2BD1513700A86CF8 /* AppScreen.swift in Sources */,
@ -2543,6 +2642,7 @@
C44B79112BBDA63A00906534 /* Locale+Extensions.swift in Sources */,
FF1F4B742BFA00FC000B4573 /* HtmlService.swift in Sources */,
FF967CEA2BAEC70100A9A3BD /* GroupStage.swift in Sources */,
FF6761542CC77D2100CC9BF2 /* DrawLog.swift in Sources */,
FF1162812BCF945C000C4809 /* TournamentCashierView.swift in Sources */,
C4A47D902B7BBBEC00ADC637 /* StoreManager.swift in Sources */,
FF4AB6BB2B9256D50002987F /* SearchViewModel.swift in Sources */,
@ -2604,7 +2704,9 @@
FF9267FC2BCE84870080F940 /* PlayerPayView.swift in Sources */,
FF2B51552C7A4DAF00FFF126 /* PlanningByCourtView.swift in Sources */,
FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */,
FFA252B22CDD2C080074E63F /* OngoingContainerView.swift in Sources */,
FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */,
FF6761582CC7803600CC9BF2 /* DrawLogsView.swift in Sources */,
FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */,
FFBF06602BBD9F6D009D6715 /* NavigationViewModel.swift in Sources */,
FF6EC9092B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift in Sources */,
@ -2613,6 +2715,7 @@
FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */,
FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */,
FF70916E2B9108C600AB08DA /* InscriptionManagerView.swift in Sources */,
FF77CE542CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */,
FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */,
FFDB1C732BB2CFE900F1E467 /* MySortDescriptor.swift in Sources */,
FF5D0D8B2BB4D1E3005CB568 /* CalendarView.swift in Sources */,
@ -2620,8 +2723,8 @@
FF8F26472BAE0ACB00650388 /* TournamentFieldsManagerView.swift in Sources */,
FF1F4B822BFA0124000B4573 /* PrintSettingsView.swift in Sources */,
FF025AE32BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift in Sources */,
FF11628A2BD05247000C4809 /* DateUpdateManagerView.swift in Sources */,
FFCFC01A2BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift in Sources */,
FF11628A2BD05247000C4809 /* DatePickingView.swift in Sources */,
FFCFC01A2BBC5A8500B82851 /* MatchFormatRowView.swift in Sources */,
FF025AE92BD1307F00A86CF8 /* MonthData.swift in Sources */,
FFEF7F4E2BDE69130033D0F0 /* MenuWarningView.swift in Sources */,
FF1F4B6D2BF9E60B000B4573 /* TournamentBuildView.swift in Sources */,
@ -2629,6 +2732,7 @@
FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */,
FFBF41842BF75ED7001B24CB /* EventTournamentsView.swift in Sources */,
FF1DC55B2BAB80C400FD8220 /* DisplayContext.swift in Sources */,
FF17CA4A2CB915A1003C7323 /* MultiCourtPickerView.swift in Sources */,
FF9268072BCE94D90080F940 /* TournamentCallView.swift in Sources */,
FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */,
FF967CFC2BAEE52E00A9A3BD /* GroupStagesView.swift in Sources */,
@ -2636,6 +2740,7 @@
C49EF01B2BD6A1E80077B5AA /* URLs.swift in Sources */,
FFCFC0142BBC59FC00B82851 /* MatchDescriptor.swift in Sources */,
FF8F264C2BAE0B4100650388 /* TournamentFormatSelectionView.swift in Sources */,
FF17CA572CC02FEA003C7323 /* CoachListView.swift in Sources */,
FFBF065E2BBD8040009D6715 /* MatchListView.swift in Sources */,
C425D4012B6D249D002A7B48 /* PadelClubApp.swift in Sources */,
FF8F26432BADFE5B00650388 /* TournamentSettingsView.swift in Sources */,
@ -2649,13 +2754,17 @@
C4A47DAD2B85FCCD00ADC637 /* CustomUser.swift in Sources */,
C4C33F762C9B1ED4006316DE /* CodingContainer+Extensions.swift in Sources */,
FF967D012BAEF0B400A9A3BD /* MatchSummaryView.swift in Sources */,
FFA252B62CDD2C6C0074E63F /* OngoingDestination.swift in Sources */,
FF8F26452BAE0A3400650388 /* TournamentDurationManagerView.swift in Sources */,
FF1DC5532BAB354A00FD8220 /* MockData.swift in Sources */,
FF967D092BAF3D4000A9A3BD /* TeamDetailView.swift in Sources */,
C488C8822CCBE8FC0082001F /* NetworkStatusView.swift in Sources */,
FFA252A92CDB70520074E63F /* PlayerStatisticView.swift in Sources */,
FF5DA18F2BB9268800A33061 /* GroupStagesSettingsView.swift in Sources */,
FF663FBE2BE019EC0031AE83 /* TournamentFilterView.swift in Sources */,
FF67615D2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */,
FF1F4B752BFA00FC000B4573 /* HtmlGenerator.swift in Sources */,
FF17CA532CBE4788003C7323 /* BracketCallingView.swift in Sources */,
FF8F26382BAD523300650388 /* PadelRule.swift in Sources */,
FF967CF42BAECC0B00A9A3BD /* TeamRegistration.swift in Sources */,
FFF8ACDB2B923F48008466FA /* Date+Extensions.swift in Sources */,
@ -2680,8 +2789,9 @@
FFBF41862BF75FDA001B24CB /* EventSettingsView.swift in Sources */,
FF5D0D782BB42C5B005CB568 /* InscriptionInfoView.swift in Sources */,
FF4AB6BD2B9256E10002987F /* SelectablePlayerListView.swift in Sources */,
FF8F26512BAE0BAD00650388 /* MatchFormatPickerView.swift in Sources */,
FF8F26512BAE0BAD00650388 /* MatchFormatSelectionView.swift in Sources */,
FF5BAF722BE19274008B4B7E /* TournamentRankView.swift in Sources */,
FF77CE5B2CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift in Sources */,
FF5D0D872BB48AFD005CB568 /* NumberFormatter+Extensions.swift in Sources */,
FFCFC0182BBC5A6800B82851 /* SetLabelView.swift in Sources */,
C4489BE22C05BF5000043F3D /* DebugSettingsView.swift in Sources */,
@ -2737,6 +2847,7 @@
FF4CBF452C996C0600151637 /* TabDestination.swift in Sources */,
FF4CBF462C996C0600151637 /* CashierView.swift in Sources */,
FF4CBF472C996C0600151637 /* Event.swift in Sources */,
FF77CE582CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift in Sources */,
FF4CBF482C996C0600151637 /* PlayerHolder.swift in Sources */,
FF4CBF492C996C0600151637 /* LoserRoundStepScheduleEditorView.swift in Sources */,
FF4CBF4A2C996C0600151637 /* ClubCourtSetupView.swift in Sources */,
@ -2758,6 +2869,7 @@
FF4CBF5A2C996C0600151637 /* TournamentClubSettingsView.swift in Sources */,
FF4CBF5B2C996C0600151637 /* GroupStageTeamView.swift in Sources */,
FF4CBF5C2C996C0600151637 /* RoundSettingsView.swift in Sources */,
FFBE62072CE9DA0900815D33 /* MatchViewStyle.swift in Sources */,
FF4CBF5D2C996C0600151637 /* SupportButtonView.swift in Sources */,
FF4CBF5E2C996C0600151637 /* TournamentBroadcastRowView.swift in Sources */,
FF4CBF5F2C996C0600151637 /* TeamWeightView.swift in Sources */,
@ -2771,12 +2883,14 @@
FF4CBF672C996C0600151637 /* EventClubSettingsView.swift in Sources */,
FF4CBF682C996C0600151637 /* AccountView.swift in Sources */,
FF4CBF692C996C0600151637 /* PlayersWithoutContactView.swift in Sources */,
FF17CA4D2CB9243E003C7323 /* FollowUpMatchView.swift in Sources */,
FF4CBF6A2C996C0600151637 /* TeamsCallingView.swift in Sources */,
FF4CBF6B2C996C0600151637 /* Calendar+Extensions.swift in Sources */,
FF4CBF6C2C996C0600151637 /* TeamScore.swift in Sources */,
FF4CBF6D2C996C0600151637 /* EditablePlayerView.swift in Sources */,
C488C7F12CC7D22D0082001F /* Club.json in Sources */,
FF4CBF6E2C996C0600151637 /* PlayerDetailView.swift in Sources */,
FFA252AF2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */,
FF4CBF6F2C996C0600151637 /* ListRowViewModifier.swift in Sources */,
FF4CBF702C996C0600151637 /* PresentationContext.swift in Sources */,
FF4CBF712C996C0600151637 /* AppSettings.swift in Sources */,
@ -2810,6 +2924,7 @@
FF4CBF802C996C0600151637 /* OngoingView.swift in Sources */,
FF4CBF812C996C0600151637 /* CreateClubView.swift in Sources */,
FF4CBF822C996C0600151637 /* APICallsListView.swift in Sources */,
FF7DCD392CC330270041110C /* TeamRestingView.swift in Sources */,
FF4CBF832C996C0600151637 /* NetworkFederalService.swift in Sources */,
FF4CBF842C996C0600151637 /* DurationSettingsView.swift in Sources */,
FF4CBF852C996C0600151637 /* AppScreen.swift in Sources */,
@ -2832,6 +2947,7 @@
FF4CBF952C996C0600151637 /* Locale+Extensions.swift in Sources */,
FF4CBF962C996C0600151637 /* HtmlService.swift in Sources */,
FF4CBF972C996C0600151637 /* GroupStage.swift in Sources */,
FF6761532CC77D2100CC9BF2 /* DrawLog.swift in Sources */,
FF4CBF982C996C0600151637 /* TournamentCashierView.swift in Sources */,
FF4CBF992C996C0600151637 /* StoreManager.swift in Sources */,
FF4CBF9A2C996C0600151637 /* SearchViewModel.swift in Sources */,
@ -2874,6 +2990,7 @@
FF4CBFBD2C996C0600151637 /* AgendaDestination.swift in Sources */,
FF4CBFBE2C996C0600151637 /* PadelClubApp.xcdatamodeld in Sources */,
FF4CBFBF2C996C0600151637 /* SetInputView.swift in Sources */,
C471D15A2D0C91FF0068091F /* BaseDrawLog.swift in Sources */,
FF4CBFC02C996C0600151637 /* ButtonValidateView.swift in Sources */,
FF4CBFC12C996C0600151637 /* ClubRowView.swift in Sources */,
FF4CBFC22C996C0600151637 /* ClubDetailView.swift in Sources */,
@ -2893,7 +3010,9 @@
FF4CBFD02C996C0600151637 /* PlayerPayView.swift in Sources */,
FF4CBFD12C996C0600151637 /* PlanningByCourtView.swift in Sources */,
FF4CBFD22C996C0600151637 /* FileImportManager.swift in Sources */,
FFA252B12CDD2C080074E63F /* OngoingContainerView.swift in Sources */,
FF4CBFD32C996C0600151637 /* TournamentButtonView.swift in Sources */,
FF6761592CC7803600CC9BF2 /* DrawLogsView.swift in Sources */,
FF4CBFD42C996C0600151637 /* FederalPlayer.swift in Sources */,
FF4CBFD52C996C0600151637 /* NavigationViewModel.swift in Sources */,
FF4CBFD62C996C0600151637 /* FixedWidthInteger+Extensions.swift in Sources */,
@ -2902,6 +3021,7 @@
FF4CBFD92C996C0600151637 /* ClubSearchView.swift in Sources */,
FF4CBFDA2C996C0600151637 /* PlayerPopoverView.swift in Sources */,
FF4CBFDB2C996C0600151637 /* InscriptionManagerView.swift in Sources */,
FF77CE522CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */,
FF4CBFDC2C996C0600151637 /* ActivityView.swift in Sources */,
FF4CBFDD2C996C0600151637 /* MySortDescriptor.swift in Sources */,
FF4CBFDE2C996C0600151637 /* CalendarView.swift in Sources */,
@ -2909,8 +3029,8 @@
FF4CBFE02C996C0600151637 /* TournamentFieldsManagerView.swift in Sources */,
FF4CBFE12C996C0600151637 /* PrintSettingsView.swift in Sources */,
FF4CBFE22C996C0600151637 /* TournamentMatchFormatsSettingsView.swift in Sources */,
FF4CBFE32C996C0600151637 /* DateUpdateManagerView.swift in Sources */,
FF4CBFE42C996C0600151637 /* MatchTypeSmallSelectionView.swift in Sources */,
FF4CBFE32C996C0600151637 /* DatePickingView.swift in Sources */,
FF4CBFE42C996C0600151637 /* MatchFormatRowView.swift in Sources */,
FF4CBFE52C996C0600151637 /* MonthData.swift in Sources */,
FF4CBFE62C996C0600151637 /* MenuWarningView.swift in Sources */,
FF4CBFE72C996C0600151637 /* TournamentBuildView.swift in Sources */,
@ -2918,6 +3038,7 @@
FF4CBFE92C996C0600151637 /* CloudConvert.swift in Sources */,
FF4CBFEA2C996C0600151637 /* EventTournamentsView.swift in Sources */,
FF4CBFEB2C996C0600151637 /* DisplayContext.swift in Sources */,
FF17CA4B2CB915A1003C7323 /* MultiCourtPickerView.swift in Sources */,
FF4CBFEC2C996C0600151637 /* TournamentCallView.swift in Sources */,
FF4CBFED2C996C0600151637 /* LoserRoundsView.swift in Sources */,
FF4CBFEE2C996C0600151637 /* GroupStagesView.swift in Sources */,
@ -2925,6 +3046,7 @@
FF4CBFF02C996C0600151637 /* URLs.swift in Sources */,
FF4CBFF12C996C0600151637 /* MatchDescriptor.swift in Sources */,
FF4CBFF22C996C0600151637 /* TournamentFormatSelectionView.swift in Sources */,
FF17CA592CC02FEB003C7323 /* CoachListView.swift in Sources */,
FF4CBFF32C996C0600151637 /* MatchListView.swift in Sources */,
FF4CBFF42C996C0600151637 /* PadelClubApp.swift in Sources */,
FF4CBFF52C996C0600151637 /* TournamentSettingsView.swift in Sources */,
@ -2939,12 +3061,16 @@
FF4CBFFC2C996C0600151637 /* UmpireView.swift in Sources */,
FF4CBFFD2C996C0600151637 /* CustomUser.swift in Sources */,
FF4CBFFE2C996C0600151637 /* MatchSummaryView.swift in Sources */,
FFA252B52CDD2C6C0074E63F /* OngoingDestination.swift in Sources */,
FF4CBFFF2C996C0600151637 /* TournamentDurationManagerView.swift in Sources */,
FF4CC0002C996C0600151637 /* MockData.swift in Sources */,
FF4CC0012C996C0600151637 /* TeamDetailView.swift in Sources */,
FFA252AA2CDB70520074E63F /* PlayerStatisticView.swift in Sources */,
FF4CC0022C996C0600151637 /* GroupStagesSettingsView.swift in Sources */,
FF4CC0032C996C0600151637 /* TournamentFilterView.swift in Sources */,
FF67615C2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */,
FF4CC0042C996C0600151637 /* HtmlGenerator.swift in Sources */,
FF17CA542CBE4788003C7323 /* BracketCallingView.swift in Sources */,
FF4CC0052C996C0600151637 /* PadelRule.swift in Sources */,
FF4CC0062C996C0600151637 /* TeamRegistration.swift in Sources */,
FF4CC0072C996C0600151637 /* Date+Extensions.swift in Sources */,
@ -2969,8 +3095,9 @@
FF4CC0192C996C0600151637 /* EventSettingsView.swift in Sources */,
FF4CC01A2C996C0600151637 /* InscriptionInfoView.swift in Sources */,
FF4CC01B2C996C0600151637 /* SelectablePlayerListView.swift in Sources */,
FF4CC01C2C996C0600151637 /* MatchFormatPickerView.swift in Sources */,
FF4CC01C2C996C0600151637 /* MatchFormatSelectionView.swift in Sources */,
FF4CC01D2C996C0600151637 /* TournamentRankView.swift in Sources */,
FF77CE5C2CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift in Sources */,
FF4CC01E2C996C0600151637 /* NumberFormatter+Extensions.swift in Sources */,
FF4CC01F2C996C0600151637 /* SetLabelView.swift in Sources */,
FF4CC0202C996C0600151637 /* DebugSettingsView.swift in Sources */,
@ -3004,6 +3131,7 @@
FF70FAC42C90584900129CC2 /* TabDestination.swift in Sources */,
FF70FAC52C90584900129CC2 /* CashierView.swift in Sources */,
FF70FAC62C90584900129CC2 /* Event.swift in Sources */,
FF77CE572CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift in Sources */,
FF70FAC72C90584900129CC2 /* PlayerHolder.swift in Sources */,
FF70FAC82C90584900129CC2 /* LoserRoundStepScheduleEditorView.swift in Sources */,
FF70FAC92C90584900129CC2 /* ClubCourtSetupView.swift in Sources */,
@ -3025,6 +3153,7 @@
FF70FAD92C90584900129CC2 /* TournamentClubSettingsView.swift in Sources */,
FF70FADA2C90584900129CC2 /* GroupStageTeamView.swift in Sources */,
FF70FADB2C90584900129CC2 /* RoundSettingsView.swift in Sources */,
FFBE62062CE9DA0900815D33 /* MatchViewStyle.swift in Sources */,
FF70FADC2C90584900129CC2 /* SupportButtonView.swift in Sources */,
FF70FADD2C90584900129CC2 /* TournamentBroadcastRowView.swift in Sources */,
FF70FADE2C90584900129CC2 /* TeamWeightView.swift in Sources */,
@ -3038,12 +3167,14 @@
FF70FAE62C90584900129CC2 /* EventClubSettingsView.swift in Sources */,
FF70FAE72C90584900129CC2 /* AccountView.swift in Sources */,
FF70FAE82C90584900129CC2 /* PlayersWithoutContactView.swift in Sources */,
FF17CA4E2CB9243E003C7323 /* FollowUpMatchView.swift in Sources */,
FF70FAE92C90584900129CC2 /* TeamsCallingView.swift in Sources */,
FF70FAEA2C90584900129CC2 /* Calendar+Extensions.swift in Sources */,
FF70FAEB2C90584900129CC2 /* TeamScore.swift in Sources */,
FF70FAEC2C90584900129CC2 /* EditablePlayerView.swift in Sources */,
C488C7F22CC7D22D0082001F /* Club.json in Sources */,
FF70FAED2C90584900129CC2 /* PlayerDetailView.swift in Sources */,
FFA252AD2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */,
FF70FAEE2C90584900129CC2 /* ListRowViewModifier.swift in Sources */,
FF70FAEF2C90584900129CC2 /* PresentationContext.swift in Sources */,
FF70FAF02C90584900129CC2 /* AppSettings.swift in Sources */,
@ -3077,6 +3208,7 @@
FF70FAFF2C90584900129CC2 /* OngoingView.swift in Sources */,
FF70FB002C90584900129CC2 /* CreateClubView.swift in Sources */,
FF70FB012C90584900129CC2 /* APICallsListView.swift in Sources */,
FF7DCD3A2CC330270041110C /* TeamRestingView.swift in Sources */,
FF70FB022C90584900129CC2 /* NetworkFederalService.swift in Sources */,
FF70FB032C90584900129CC2 /* DurationSettingsView.swift in Sources */,
FF70FB042C90584900129CC2 /* AppScreen.swift in Sources */,
@ -3099,6 +3231,7 @@
FF70FB142C90584900129CC2 /* Locale+Extensions.swift in Sources */,
FF70FB152C90584900129CC2 /* HtmlService.swift in Sources */,
FF70FB162C90584900129CC2 /* GroupStage.swift in Sources */,
FF6761552CC77D2100CC9BF2 /* DrawLog.swift in Sources */,
FF70FB172C90584900129CC2 /* TournamentCashierView.swift in Sources */,
FF70FB182C90584900129CC2 /* StoreManager.swift in Sources */,
FF70FB192C90584900129CC2 /* SearchViewModel.swift in Sources */,
@ -3141,6 +3274,7 @@
FF70FB3C2C90584900129CC2 /* AgendaDestination.swift in Sources */,
FF70FB3D2C90584900129CC2 /* PadelClubApp.xcdatamodeld in Sources */,
FF70FB3E2C90584900129CC2 /* SetInputView.swift in Sources */,
C471D1582D0C91FE0068091F /* BaseDrawLog.swift in Sources */,
FF70FB3F2C90584900129CC2 /* ButtonValidateView.swift in Sources */,
FF70FB402C90584900129CC2 /* ClubRowView.swift in Sources */,
FF70FB412C90584900129CC2 /* ClubDetailView.swift in Sources */,
@ -3160,7 +3294,9 @@
FF70FB4F2C90584900129CC2 /* PlayerPayView.swift in Sources */,
FF70FB502C90584900129CC2 /* PlanningByCourtView.swift in Sources */,
FF70FB512C90584900129CC2 /* FileImportManager.swift in Sources */,
FFA252B32CDD2C080074E63F /* OngoingContainerView.swift in Sources */,
FF70FB522C90584900129CC2 /* TournamentButtonView.swift in Sources */,
FF6761572CC7803600CC9BF2 /* DrawLogsView.swift in Sources */,
FF70FB532C90584900129CC2 /* FederalPlayer.swift in Sources */,
FF70FB542C90584900129CC2 /* NavigationViewModel.swift in Sources */,
FF70FB552C90584900129CC2 /* FixedWidthInteger+Extensions.swift in Sources */,
@ -3169,6 +3305,7 @@
FF70FB582C90584900129CC2 /* ClubSearchView.swift in Sources */,
FF70FB592C90584900129CC2 /* PlayerPopoverView.swift in Sources */,
FF70FB5A2C90584900129CC2 /* InscriptionManagerView.swift in Sources */,
FF77CE532CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */,
FF70FB5B2C90584900129CC2 /* ActivityView.swift in Sources */,
FF70FB5C2C90584900129CC2 /* MySortDescriptor.swift in Sources */,
FF70FB5D2C90584900129CC2 /* CalendarView.swift in Sources */,
@ -3176,8 +3313,8 @@
FF70FB5F2C90584900129CC2 /* TournamentFieldsManagerView.swift in Sources */,
FF70FB602C90584900129CC2 /* PrintSettingsView.swift in Sources */,
FF70FB612C90584900129CC2 /* TournamentMatchFormatsSettingsView.swift in Sources */,
FF70FB622C90584900129CC2 /* DateUpdateManagerView.swift in Sources */,
FF70FB632C90584900129CC2 /* MatchTypeSmallSelectionView.swift in Sources */,
FF70FB622C90584900129CC2 /* DatePickingView.swift in Sources */,
FF70FB632C90584900129CC2 /* MatchFormatRowView.swift in Sources */,
FF70FB642C90584900129CC2 /* MonthData.swift in Sources */,
FF70FB652C90584900129CC2 /* MenuWarningView.swift in Sources */,
FF70FB662C90584900129CC2 /* TournamentBuildView.swift in Sources */,
@ -3185,6 +3322,7 @@
FF70FB682C90584900129CC2 /* CloudConvert.swift in Sources */,
FF70FB692C90584900129CC2 /* EventTournamentsView.swift in Sources */,
FF70FB6A2C90584900129CC2 /* DisplayContext.swift in Sources */,
FF17CA492CB915A1003C7323 /* MultiCourtPickerView.swift in Sources */,
FF70FB6B2C90584900129CC2 /* TournamentCallView.swift in Sources */,
FF70FB6C2C90584900129CC2 /* LoserRoundsView.swift in Sources */,
FF70FB6D2C90584900129CC2 /* GroupStagesView.swift in Sources */,
@ -3192,6 +3330,7 @@
FF70FB6F2C90584900129CC2 /* URLs.swift in Sources */,
FF70FB702C90584900129CC2 /* MatchDescriptor.swift in Sources */,
FF70FB712C90584900129CC2 /* TournamentFormatSelectionView.swift in Sources */,
FF17CA582CC02FEB003C7323 /* CoachListView.swift in Sources */,
FF70FB722C90584900129CC2 /* MatchListView.swift in Sources */,
FF70FB732C90584900129CC2 /* PadelClubApp.swift in Sources */,
FF70FB742C90584900129CC2 /* TournamentSettingsView.swift in Sources */,
@ -3206,12 +3345,16 @@
FF70FB7C2C90584900129CC2 /* CustomUser.swift in Sources */,
C4C33F772C9B1ED4006316DE /* CodingContainer+Extensions.swift in Sources */,
FF70FB7D2C90584900129CC2 /* MatchSummaryView.swift in Sources */,
FFA252B72CDD2C6C0074E63F /* OngoingDestination.swift in Sources */,
FF70FB7E2C90584900129CC2 /* TournamentDurationManagerView.swift in Sources */,
FF70FB7F2C90584900129CC2 /* MockData.swift in Sources */,
FF70FB802C90584900129CC2 /* TeamDetailView.swift in Sources */,
FFA252AB2CDB70520074E63F /* PlayerStatisticView.swift in Sources */,
FF70FB812C90584900129CC2 /* GroupStagesSettingsView.swift in Sources */,
FF70FB822C90584900129CC2 /* TournamentFilterView.swift in Sources */,
FF67615B2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */,
FF70FB832C90584900129CC2 /* HtmlGenerator.swift in Sources */,
FF17CA552CBE4788003C7323 /* BracketCallingView.swift in Sources */,
FF70FB842C90584900129CC2 /* PadelRule.swift in Sources */,
FF70FB852C90584900129CC2 /* TeamRegistration.swift in Sources */,
FF70FB862C90584900129CC2 /* Date+Extensions.swift in Sources */,
@ -3236,8 +3379,9 @@
FF70FB982C90584900129CC2 /* EventSettingsView.swift in Sources */,
FF70FB992C90584900129CC2 /* InscriptionInfoView.swift in Sources */,
FF70FB9A2C90584900129CC2 /* SelectablePlayerListView.swift in Sources */,
FF70FB9B2C90584900129CC2 /* MatchFormatPickerView.swift in Sources */,
FF70FB9B2C90584900129CC2 /* MatchFormatSelectionView.swift in Sources */,
FF70FB9C2C90584900129CC2 /* TournamentRankView.swift in Sources */,
FF77CE5A2CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift in Sources */,
FF70FB9D2C90584900129CC2 /* NumberFormatter+Extensions.swift in Sources */,
FF70FB9E2C90584900129CC2 /* SetLabelView.swift in Sources */,
FF70FB9F2C90584900129CC2 /* DebugSettingsView.swift in Sources */,
@ -3416,7 +3560,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
@ -3440,7 +3584,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.19;
MARKETING_VERSION = 1.0.39;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -3461,7 +3605,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -3484,7 +3628,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.19;
MARKETING_VERSION = 1.0.39;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -3577,7 +3721,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 12;
CURRENT_PROJECT_VERSION = 2;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
@ -3588,6 +3732,7 @@
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports";
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_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
@ -3600,7 +3745,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.15;
MARKETING_VERSION = 1.0.30;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -3621,7 +3766,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 12;
CURRENT_PROJECT_VERSION = 2;
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -3631,6 +3776,7 @@
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports";
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_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
@ -3643,7 +3789,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.15;
MARKETING_VERSION = 1.0.30;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -3665,7 +3811,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 2;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
@ -3675,6 +3821,7 @@
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (Beta)";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports";
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_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
@ -3687,7 +3834,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.14;
MARKETING_VERSION = 1.0.24;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -3707,7 +3854,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 2;
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -3716,6 +3863,7 @@
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (Beta)";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports";
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_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
@ -3728,7 +3876,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.14;
MARKETING_VERSION = 1.0.24;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

@ -30,6 +30,18 @@ final class AppSettings: MicroStorable {
var dayDuration: Int?
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() {
tournamentAges = Set()
tournamentTypes = Set()

@ -12,50 +12,6 @@ import LeStorage
@Observable
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 {
guard let copy = instance as? Club else { return false }
self.broadcastCode = copy.broadcastCode
@ -88,44 +44,6 @@ final class Club: BaseClub {
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 {

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

@ -105,6 +105,10 @@ class DataStore: ObservableObject {
}
deinit {
NotificationCenter.default.removeObserver(self)
}
func saveUser() {
if user.username.count > 0 {
self.userStorage.update()
@ -291,15 +295,43 @@ class DataStore: ObservableObject {
func runningMatches() -> [Match] {
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] = []
for tournament in lastTournaments {
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)
}
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)
}
if let libelle {
return FederalTournamentAge.allCases.first(where: { $0.localizedLabel().localizedCaseInsensitiveContains(libelle) })
return FederalTournamentAge.allCases.first(where: { $0.localizedFederalAgeLabel().localizedCaseInsensitiveContains(libelle) })
}
return nil
}

@ -24,6 +24,7 @@ class BaseClub: SyncedModelObject, SyncedStorable {
var longitude: Double? = nil
var courtCount: Int = 2
var broadcastCode: String? = nil
var timezone: String? = TimeZone.current.identifier
init(
id: String = Store.randomId(),
@ -38,7 +39,8 @@ class BaseClub: SyncedModelObject, SyncedStorable {
latitude: Double? = nil,
longitude: Double? = nil,
courtCount: Int = 2,
broadcastCode: String? = nil
broadcastCode: String? = nil,
timezone: String? = TimeZone.current.identifier
) {
super.init()
self.id = id
@ -54,6 +56,7 @@ class BaseClub: SyncedModelObject, SyncedStorable {
self.longitude = longitude
self.courtCount = courtCount
self.broadcastCode = broadcastCode
self.timezone = timezone
}
enum CodingKeys: String, CodingKey {
@ -70,6 +73,7 @@ class BaseClub: SyncedModelObject, SyncedStorable {
case _longitude = "longitude"
case _courtCount = "courtCount"
case _broadcastCode = "broadcastCode"
case _timezone = "timezone"
}
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.courtCount = try container.decodeIfPresent(Int.self, forKey: ._courtCount) ?? 2
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)
}
@ -105,6 +110,7 @@ class BaseClub: SyncedModelObject, SyncedStorable {
try container.encode(self.longitude, forKey: ._longitude)
try container.encode(self.courtCount, forKey: ._courtCount)
try container.encode(self.broadcastCode, forKey: ._broadcastCode)
try container.encode(self.timezone, forKey: ._timezone)
try super.encode(to: encoder)
}
@ -128,6 +134,7 @@ class BaseClub: SyncedModelObject, SyncedStorable {
self.longitude = club.longitude
self.courtCount = club.courtCount
self.broadcastCode = club.broadcastCode
self.timezone = club.timezone
}
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 overrideCourtsUnavailability: Bool = false
var shouldTryToFillUpCourtsAvailable: Bool = false
var courtsAvailable: Set<Int> = Set<Int>()
var simultaneousStart: Bool = true
init(
id: String = Store.randomId(),
@ -40,7 +42,9 @@ class BaseMatchScheduler: BaseModelObject, Storable {
shouldEndRoundBeforeStartingNext: Bool = false,
groupStageChunkCount: Int? = nil,
overrideCourtsUnavailability: Bool = false,
shouldTryToFillUpCourtsAvailable: Bool = false
shouldTryToFillUpCourtsAvailable: Bool = false,
courtsAvailable: Set<Int> = Set<Int>(),
simultaneousStart: Bool = true
) {
super.init()
self.id = id
@ -57,6 +61,8 @@ class BaseMatchScheduler: BaseModelObject, Storable {
self.groupStageChunkCount = groupStageChunkCount
self.overrideCourtsUnavailability = overrideCourtsUnavailability
self.shouldTryToFillUpCourtsAvailable = shouldTryToFillUpCourtsAvailable
self.courtsAvailable = courtsAvailable
self.simultaneousStart = simultaneousStart
}
enum CodingKeys: String, CodingKey {
@ -74,6 +80,8 @@ class BaseMatchScheduler: BaseModelObject, Storable {
case _groupStageChunkCount = "groupStageChunkCount"
case _overrideCourtsUnavailability = "overrideCourtsUnavailability"
case _shouldTryToFillUpCourtsAvailable = "shouldTryToFillUpCourtsAvailable"
case _courtsAvailable = "courtsAvailable"
case _simultaneousStart = "simultaneousStart"
}
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.overrideCourtsUnavailability = try container.decodeIfPresent(Bool.self, forKey: ._overrideCourtsUnavailability) ?? 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)
}
@ -111,6 +121,8 @@ class BaseMatchScheduler: BaseModelObject, Storable {
try container.encode(self.groupStageChunkCount, forKey: ._groupStageChunkCount)
try container.encode(self.overrideCourtsUnavailability, forKey: ._overrideCourtsUnavailability)
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)
}
@ -134,6 +146,8 @@ class BaseMatchScheduler: BaseModelObject, Storable {
self.groupStageChunkCount = matchscheduler.groupStageChunkCount
self.overrideCourtsUnavailability = matchscheduler.overrideCourtsUnavailability
self.shouldTryToFillUpCourtsAvailable = matchscheduler.shouldTryToFillUpCourtsAvailable
self.courtsAvailable = matchscheduler.courtsAvailable
self.simultaneousStart = matchscheduler.simultaneousStart
}
static func relationships() -> [Relationship] {

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

@ -79,6 +79,12 @@
"type": "String",
"optional": true,
"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",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "courtsAvailable",
"type": "Set<Int>",
"defaultValue": "Set<Int>()"
},
{
"name": "simultaneousStart",
"type": "Bool",
"defaultValue": "true"
}
]
}

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

@ -13,21 +13,6 @@ import SwiftUI
@Observable
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 {
get {
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 {
return TournamentLibrary.shared.store(tournamentId: self.tournament)
}
@ -124,17 +89,39 @@ final class GroupStage: BaseGroupStage, SideStorable {
format: self.matchFormat,
name: self.localizedMatchUpLabel(for: index))
match.store = self.store
print("_createMatch(index)", index)
return match
}
func buildMatches() {
_removeMatches()
func removeReturnMatches(onlyLast: Bool = false) {
var matches = [Match]()
var teamScores = [TeamScore]()
var returnMatches = _matches().filter({ $0.index >= matchCount })
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() {
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))
teamScores.append(contentsOf: newMatch.createTeamScores())
matches.append(newMatch)
@ -144,29 +131,71 @@ final class GroupStage: BaseGroupStage, SideStorable {
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] {
let ordered = _matches()
if ordered.isEmpty == false && ordered.count == _matchOrder().count {
return _matchOrder().map {
ordered[$0]
let order = _matchOrder()
let matchCount = max(1, matchCount)
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 {
return ordered
}
}
func orderedIndexOfMatch(_ match: Match) -> Int {
_matchOrder()[safe: match.index] ?? match.index
}
func updateGroupStageState() {
clearScoreCache()
if hasEnded(), let tournament = tournamentObject() {
let teams = teams(true)
for (index, team) in teams.enumerated() {
team.qualified = index < tournament.qualifiedPerGroupStage
if team.bracketPosition != nil && team.qualified == false {
tournamentObject()?.resetTeamScores(in: team.bracketPosition)
team.bracketPosition = nil
tournamentObject()?.shouldVerifyBracket = true
}
}
try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams)
if let tournamentObject = tournamentObject() {
try DataStore.shared.tournaments.addOrUpdate(instance: tournamentObject)
}
self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams)
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?)? {
if let scoreData = (score ?? _score(forGroupStagePosition: groupStagePosition, nilIfEmpty: true)) {
let hideSetDifference = matchFormat.setsToWin == 1
let setDifference = scoreData.setDifference.formatted(.number.sign(strategy: .always(includingZero: false)))
let gameDifference = scoreData.gameDifference.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: true))) + " jeu" + scoreData.gameDifference.localizedPluralSuffix("x")
return (wins: scoreData.wins.formatted(), losses: scoreData.loses.formatted(), setsDifference: hideSetDifference ? nil : setDifference, gamesDifference: gameDifference)
// return "\(scoreData.wins)/\(scoreData.loses) " + differenceAsString
} else {
@ -218,7 +247,7 @@ final class GroupStage: BaseGroupStage, SideStorable {
matchIndexes.append(index)
}
}
return _matches().filter { matchIndexes.contains($0.index) }
return _matches().filter { matchIndexes.contains($0.index%matchCount) }
}
func initialStartDate(forTeam team: TeamRegistration) -> Date? {
@ -238,7 +267,7 @@ final class GroupStage: BaseGroupStage, SideStorable {
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
let start = Date()
defer {
@ -246,7 +275,7 @@ final class GroupStage: BaseGroupStage, SideStorable {
print("func group stage availableToStart", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#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] {
@ -282,40 +311,67 @@ final class GroupStage: BaseGroupStage, SideStorable {
return playedMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed()
}
func isReturnMatchEnabled() -> Bool {
_matches().count > matchCount
}
private func _matchOrder() -> [Int] {
var order: [Int]
switch size {
case 3:
return [1, 2, 0]
order = [1, 2, 0]
case 4:
return [2, 3, 1, 4, 5, 0]
order = [2, 3, 1, 4, 5, 0]
case 5:
// return [5, 8, 0, 7, 3, 4, 2, 6, 1, 9]
return [3, 5, 8, 2, 6, 1, 9, 4, 7, 0]
order = [3, 5, 8, 2, 6, 1, 9, 4, 7, 0]
case 6:
//return [1, 7, 13, 11, 3, 6, 10, 2, 8, 12, 5, 4, 9, 14, 0]
return [4, 7, 9, 3, 6, 11, 2, 8, 10, 1, 13, 5, 12, 14, 0]
order = [4, 7, 9, 3, 6, 11, 2, 8, 10, 1, 13, 5, 12, 14, 0]
default:
return []
order = []
}
return order
}
func indexOf(_ matchIndex: Int) -> Int {
_matchOrder().firstIndex(of: matchIndex) ?? matchIndex
}
private func _matchUp(for matchIndex: Int) -> [Int] {
Array((0..<size).combinations(ofCount: 2))[safe: matchIndex] ?? []
func _matchUp(for matchIndex: Int) -> [Int] {
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 {
let matchUp = _matchUp(for: matchIndex)
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 {
return "--"
}
}
var matchCount: Int {
(size * (size - 1)) / 2
}
func team(teamPosition team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? {
let _teams = _teams(for: matchIndex)
switch team {
@ -328,7 +384,7 @@ final class GroupStage: BaseGroupStage, SideStorable {
private func _teams(for matchIndex: Int) -> [TeamRegistration?] {
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() {
@ -350,10 +406,40 @@ final class GroupStage: BaseGroupStage, SideStorable {
fileprivate func _headToHead(_ teamPosition: TeamRegistration, _ otherTeam: TeamRegistration) -> Bool {
let indexes = [teamPosition, otherTeam].compactMap({ $0.groupStagePosition }).sorted()
let combos = Array((0..<size).combinations(ofCount: 2))
if let matchIndex = combos.firstIndex(of: indexes), let match = _matches().first(where: { $0.index == matchIndex }) {
return teamPosition.id == match.losingTeamId
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 {
return false
if let matchIndex = combos.firstIndex(of: indexes), let match = _matches().first(where: { $0.index == matchIndex }) {
return teamPosition.id == match.losingTeamId
} else {
return false
}
}
}
@ -415,16 +501,19 @@ final class GroupStage: BaseGroupStage, SideStorable {
guard let team = teamAt(groupStagePosition: groupStagePosition) else { return nil }
let matches = matches(forGroupStagePosition: groupStagePosition).filter({ $0.hasEnded() })
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 loses = matches.filter { $0.losingTeamId == team.id }.count
let differences = matches.compactMap { $0.scoreDifference(groupStagePosition, atStep: step) }
let setDifference = differences.map { $0.set }.reduce(0,+)
let gameDifference = differences.map { $0.game }.reduce(0,+)
// Calculate the score and store it in the cache
let score = (team, wins, loses, setDifference, gameDifference)
scoreCache[groupStagePosition] = score
return score
return (team, wins, loses, setDifference, gameDifference)
}
// Clear the cache if necessary, for example when starting a new step or when matches update

@ -11,10 +11,9 @@ import LeStorage
@Observable
final class Match: BaseMatch, SideStorable {
// static func resourceName() -> String { "matches" }
// static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
// static func filterByStoreIdentifier() -> Bool { return true }
// static var relationshipNames: [String] = ["round", "groupStage"]
static func == (lhs: Match, rhs: Match) -> Bool {
lhs.id == rhs.id && lhs.startDate == rhs.startDate
}
static func setServerTitle(upperRound: Round, matchIndex: Int) -> String {
if upperRound.index == 0 { return upperRound.roundTitle() }
@ -23,54 +22,30 @@ final class Match: BaseMatch, SideStorable {
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) {
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 {
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 {
if let id = self.store?.identifier {
return TournamentLibrary.shared.store(tournamentId: id)
@ -119,7 +94,7 @@ defer {
}
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 {
@ -142,7 +117,7 @@ defer {
case .wide, .title:
return "Match \(indexInRound(in: matches) + 1)"
case .short:
return "#\(indexInRound(in: matches) + 1)"
return "\(indexInRound(in: matches) + 1)"
}
}
@ -155,22 +130,8 @@ defer {
}
@discardableResult
func lockAndGetSeedPosition(atTeamPosition slot: TeamPosition?, opposingSeeding: Bool = false) -> Int {
func lockAndGetSeedPosition(atTeamPosition teamPosition: TeamPosition) -> Int {
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()
return matchIndex * 2 + teamPosition.rawValue
}
@ -191,6 +152,12 @@ defer {
return self.tournamentStore.teamRegistrations.findById(winningTeamId)
}
func loser() -> TeamRegistration? {
guard let losingTeamId else { return nil }
return self.tournamentStore.teamRegistrations.findById(losingTeamId)
}
func localizedStartDate() -> String {
if let startDate {
return startDate.formatted(date: .abbreviated, time: .shortened)
@ -211,8 +178,8 @@ defer {
}
func cleanScheduleAndSave(_ targetStartDate: Date? = nil) {
startDate = targetStartDate
confirmed = targetStartDate == nil ? false : true
startDate = targetStartDate ?? startDate
confirmed = false
endDate = nil
followingMatch()?.cleanScheduleAndSave(nil)
_loserMatch()?.cleanScheduleAndSave(nil)
@ -228,6 +195,7 @@ defer {
groupStageObject?.updateGroupStageState()
roundObject?.updateTournamentState()
currentTournament()?.updateTournamentState()
teams().forEach({ $0.resetRestingTime() })
}
func resetScores() {
@ -312,15 +280,21 @@ defer {
guard let forwardMatch = _forwardMatch(inRound: roundObject) else { return }
guard let next = _otherMatch() else { return }
if next.disabled && byeState == false && next.byeState == false {
forwardMatch.byeState = false
forwardMatch._toggleMatchDisableState(state, forward: true)
if forwardMatch.disabled != state || forwardMatch.byeState {
forwardMatch.byeState = false
forwardMatch._toggleMatchDisableState(state, forward: true)
}
} else if byeState && next.byeState {
print("don't disable forward match")
forwardMatch.byeState = false
forwardMatch._toggleMatchDisableState(false, forward: true)
if forwardMatch.byeState || forwardMatch.disabled {
forwardMatch.byeState = false
forwardMatch._toggleMatchDisableState(false, forward: true)
}
} else {
forwardMatch.byeState = true
forwardMatch._toggleMatchDisableState(state, forward: true)
if forwardMatch.byeState == false || forwardMatch.disabled != state {
forwardMatch.byeState = true
forwardMatch._toggleMatchDisableState(state, forward: true)
}
}
// if next.disabled == false {
@ -359,12 +333,18 @@ defer {
func _toggleMatchDisableState(_ state: Bool, forward: Bool = false, single: Bool = false) {
//if disabled == state { return }
let currentState = disabled
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()
for team in teams {
if isSeededBy(team: team) {
@ -374,7 +354,14 @@ defer {
}
}
//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 {
_toggleLoserMatchDisableState(state)
if forward {
@ -408,14 +395,14 @@ defer {
}
}
func roundTitle() -> String? {
func roundTitle(_ displayStyle: DisplayStyle = .wide) -> String? {
if groupStage != nil { return groupStageObject?.groupStageTitle() }
else if let roundObject { return roundObject.roundTitle() }
else { return nil }
}
func roundAndMatchTitle() -> String {
[roundTitle(), matchTitle()].compactMap({ $0 }).joined(separator: " ")
func roundAndMatchTitle(_ displayStyle: DisplayStyle = .wide) -> String {
[roundTitle(displayStyle), matchTitle(displayStyle)].compactMap({ $0 }).joined(separator: " ")
}
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 {
if let groupStageObject {
return (groupStageObject.index + 1) * 100 + groupStageObject.indexOf(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] {
@ -487,7 +488,7 @@ defer {
if endDate == nil {
endDate = Date()
}
teams().forEach({ $0.resetRestingTime() })
winningTeamId = teamScoreWinning.teamRegistration
losingTeamId = teamScoreWalkout.teamRegistration
groupStageObject?.updateGroupStageState()
@ -503,6 +504,8 @@ defer {
}
if startDate == nil {
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)
@ -510,6 +513,8 @@ defer {
teamOne?.hasArrived()
teamTwo?.hasArrived()
teamOne?.resetRestingTime()
teamTwo?.resetRestingTime()
winningTeamId = teamOne?.id
losingTeamId = teamTwo?.id
@ -518,7 +523,17 @@ defer {
groupStageObject?.updateGroupStageState()
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()
}
@ -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 {
startDate = fromStartDate
@ -580,7 +595,8 @@ defer {
setCourt(_courtIndex)
}
case .random:
if let _courtIndex = availableCourts().randomElement() {
let runningMatches: [Match] = DataStore.shared.runningMatches()
if let _courtIndex = availableCourts(runningMatches: runningMatches).randomElement() {
setCourt(_courtIndex)
}
case .field(let _courtIndex):
@ -592,7 +608,11 @@ defer {
endDate = toEndDate
}
confirmed = true
if let startDate, startDate.timeIntervalSinceNow <= 300 {
confirmed = true
} else {
confirmed = false
}
}
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 {
return currentTournament()?.courtCount ?? 1
}
func courtIsAvailable(_ courtIndex: Int) -> Bool {
let courtUsed = currentTournament()?.courtUsed() ?? []
func courtIsAvailable(_ courtIndex: Int, in runningMatches: [Match]) -> Bool {
let courtUsed = currentTournament()?.courtUsed(runningMatches: runningMatches) ?? []
return courtUsed.contains(courtIndex) == false
}
@ -622,9 +650,9 @@ defer {
return availableCourts
}
func availableCourts() -> [Int] {
let courtUsed = currentTournament()?.courtUsed() ?? []
return Array(Set(allCourts().map { $0 }).subtracting(Set(courtUsed)))
func availableCourts(runningMatches: [Match]) -> [Int] {
let courtUsed = currentTournament()?.courtUsed(runningMatches: runningMatches) ?? []
return Set(allCourts().map { $0 }).subtracting(Set(courtUsed)).sorted()
}
func removeCourt() {
@ -635,12 +663,17 @@ defer {
self.courtIndex = courtIndex
}
func canBeStarted(inMatches matches: [Match]) -> Bool {
func canBeStarted(inMatches matches: [Match], checkCanPlay: Bool) -> Bool {
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 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 {
@ -736,8 +769,24 @@ defer {
} else {
setDifference = zip.filter { $0 > $1 }.count - zip.filter { $1 > $0 }.count
}
let gameDifference = zip.map { ($0, $1) }.map { $0.0 - $0.1 }.reduce(0,+)
return (setDifference * reverseValue, gameDifference * reverseValue)
// 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,+)
return (setDifference * reverseValue, gameDifference * reverseValue)
}
}
func groupStageProjectedTeam(_ team: TeamPosition) -> TeamRegistration? {
@ -798,7 +847,7 @@ defer {
func hasStarted() -> Bool { // meaning at least one match is over
if let startDate {
return startDate.timeIntervalSinceNow < 0
return startDate.timeIntervalSinceNow < 0 && confirmed
}
if hasEnded() {
return true
@ -843,48 +892,111 @@ defer {
}
}
// enum CodingKeys: String, CodingKey {
// case _id = "id"
// case _storeId = "storeId"
// case _lastUpdate = "lastUpdate"
// case _round = "round"
// case _groupStage = "groupStage"
// case _startDate = "startDate"
// case _endDate = "endDate"
// case _index = "index"
// case _format = "format"
//// case _court = "court"
// case _courtIndex = "courtIndex"
// case _servingTeamId = "servingTeamId"
// case _winningTeamId = "winningTeamId"
// case _losingTeamId = "losingTeamId"
//// case _broadcasted = "broadcasted"
// case _name = "name"
//// case _order = "order"
// case _disabled = "disabled"
// case _confirmed = "confirmed"
// }
//
// func encode(to encoder: Encoder) throws {
// var container = encoder.container(keyedBy: CodingKeys.self)
//
// try container.encode(id, forKey: ._id)
// try container.encode(storeId, forKey: ._storeId)
// try container.encode(lastUpdate, forKey: ._lastUpdate)
// try container.encode(round, forKey: ._round)
// try container.encode(groupStage, forKey: ._groupStage)
// try container.encode(startDate, forKey: ._startDate)
// try container.encode(endDate, forKey: ._endDate)
// try container.encode(format, forKey: ._format)
// try container.encode(servingTeamId, forKey: ._servingTeamId)
// try container.encode(index, forKey: ._index)
// try container.encode(winningTeamId, forKey: ._winningTeamId)
// try container.encode(losingTeamId, forKey: ._losingTeamId)
// try container.encode(name, forKey: ._name)
// try container.encode(disabled, forKey: ._disabled)
// try container.encode(courtIndex, forKey: ._courtIndex)
// try container.encode(confirmed, forKey: ._confirmed)
// }
var restingTimeForSorting: TimeInterval {
(teams().compactMap({ $0.restingTime() }).max() ?? .distantFuture).timeIntervalSinceNow
}
func isValidSpot() -> Bool {
previousMatches().allSatisfy({ $0.isSeeded() == false })
}
func expectedToBeRunning() -> Bool {
guard let startDate else { return false }
return confirmed == false && startDate.timeIntervalSinceNow < 0
}
func expectedFormattedStartDate(canBePlayedInSpecifiedCourt: Bool, availableCourts: [Int], estimatedStartDate: CourtIndexAndDate?, updatedField: Int?) -> String {
guard let startDate else { return "" }
guard hasEnded() == false, isRunning() == false else { return "" }
let depthReadiness = depthReadiness()
if depthReadiness == 0 {
if canBePlayedInSpecifiedCourt {
return "possible tout de suite"
} else if let updatedField, availableCourts.contains(updatedField) {
return "possible tout de suite \(courtName(for: updatedField))"
} else if let first = availableCourts.first {
return "possible tout de suite \(courtName(for: first))"
} else if let estimatedStartDate {
return "dans ~" + estimatedStartDate.1.timeElapsedString() + " " + courtName(for: estimatedStartDate.0)
}
return "était prévu à " + startDate.formattedAsHourMinute()
} else if depthReadiness == 1 {
return "possible prochaine rotation"
} else {
return "dans \(depthReadiness) rotation\(depthReadiness.pluralSuffix), ~\((getDuration() * depthReadiness).durationInHourMinutes())"
}
}
func runningDuration() -> String {
guard let startDate else { return "" }
return " depuis " + startDate.timeElapsedString()
}
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() {
self.tournamentStore.matches.writeChangeAndInsertOnServer(instance: self)
@ -899,6 +1011,8 @@ enum MatchDateSetup: Hashable, Identifiable {
case inMinutes(Int)
case now
case customDate
case previousRotation
case nextRotation
var id: Int { hashValue }
}

@ -11,73 +11,42 @@ import SwiftUI
@Observable
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()
// var tournament: String
// var timeDifferenceLimit: Int
// var loserBracketRotationDifference: Int
// var upperBracketRotationDifference: Int
// var accountUpperBracketBreakTime: Bool
// var accountLoserBracketBreakTime: Bool
// var randomizeCourts: Bool
// var rotationDifferenceIsImportant: Bool
// var shouldHandleUpperRoundSlice: Bool
// var shouldEndRoundBeforeStartingNext: Bool
// var groupStageChunkCount: Int?
// var overrideCourtsUnavailability: Bool = false
// var shouldTryToFillUpCourtsAvailable: Bool = false
init(tournament: String,
timeDifferenceLimit: Int = 5,
loserBracketRotationDifference: Int = 0,
upperBracketRotationDifference: Int = 1,
accountUpperBracketBreakTime: Bool = true,
accountLoserBracketBreakTime: Bool = false,
randomizeCourts: Bool = true,
rotationDifferenceIsImportant: Bool = false,
shouldHandleUpperRoundSlice: Bool = true,
shouldEndRoundBeforeStartingNext: Bool = true,
groupStageChunkCount: Int? = nil, overrideCourtsUnavailability: Bool = false, shouldTryToFillUpCourtsAvailable: Bool = false) {
super.init()
self.tournament = tournament
self.timeDifferenceLimit = timeDifferenceLimit
self.loserBracketRotationDifference = loserBracketRotationDifference
self.upperBracketRotationDifference = upperBracketRotationDifference
self.accountUpperBracketBreakTime = accountUpperBracketBreakTime
self.accountLoserBracketBreakTime = accountLoserBracketBreakTime
self.randomizeCourts = randomizeCourts
self.rotationDifferenceIsImportant = rotationDifferenceIsImportant
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"
// init(tournament: String,
// timeDifferenceLimit: Int = 5,
// loserBracketRotationDifference: Int = 0,
// upperBracketRotationDifference: Int = 1,
// accountUpperBracketBreakTime: Bool = true,
// accountLoserBracketBreakTime: Bool = false,
// randomizeCourts: Bool = true,
// rotationDifferenceIsImportant: Bool = false,
// shouldHandleUpperRoundSlice: Bool = false,
// shouldEndRoundBeforeStartingNext: Bool = true,
//<<<<<<< HEAD
// groupStageChunkCount: Int? = nil, overrideCourtsUnavailability: Bool = false, shouldTryToFillUpCourtsAvailable: Bool = false) {
// super.init()
//=======
// groupStageChunkCount: Int? = nil,
// overrideCourtsUnavailability: Bool = false,
// shouldTryToFillUpCourtsAvailable: Bool = true,
// courtsAvailable: Set<Int> = Set<Int>(),
// simultaneousStart: Bool = true) {
//>>>>>>> main
// self.tournament = tournament
// self.timeDifferenceLimit = timeDifferenceLimit
// self.loserBracketRotationDifference = loserBracketRotationDifference
// self.upperBracketRotationDifference = upperBracketRotationDifference
// self.accountUpperBracketBreakTime = accountUpperBracketBreakTime
// self.accountLoserBracketBreakTime = accountLoserBracketBreakTime
// self.randomizeCourts = randomizeCourts
// self.rotationDifferenceIsImportant = rotationDifferenceIsImportant
// self.shouldHandleUpperRoundSlice = shouldHandleUpperRoundSlice
// self.shouldEndRoundBeforeStartingNext = shouldEndRoundBeforeStartingNext
// self.groupStageChunkCount = groupStageChunkCount
// self.overrideCourtsUnavailability = overrideCourtsUnavailability
// self.shouldTryToFillUpCourtsAvailable = shouldTryToFillUpCourtsAvailable
// self.courtsAvailable = courtsAvailable
// self.simultaneousStart = simultaneousStart
// }
var courtsUnavailability: [DateInterval]? {
@ -105,7 +74,6 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
if let specificGroupStage {
groupStages = [specificGroupStage]
}
let numberOfCourtsAvailablePerRotation: Int = tournament.courtCount
let matches = groupStages.flatMap { $0._matches() }
matches.forEach({
@ -133,7 +101,7 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
lastDate = 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
if let match = matches.first(where: { $0.id == matchSchedule.matchID }) {
@ -157,7 +125,7 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
Logger.error(error)
}
let dispatch = groupStageDispatcher(numberOfCourtsAvailablePerRotation: numberOfCourtsAvailablePerRotation, groupStages: groups, startingDate: lastDate)
let dispatch = groupStageDispatcher(groupStages: groups, startingDate: lastDate)
dispatch.timedMatches.forEach { matchSchedule in
if let match = matches.first(where: { $0.id == matchSchedule.matchID }) {
@ -180,20 +148,24 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
return lastDate
}
func groupStageDispatcher(numberOfCourtsAvailablePerRotation: Int, groupStages: [GroupStage], startingDate: Date) -> GroupStageMatchDispatcher {
func groupStageDispatcher(groupStages: [GroupStage], startingDate: Date) -> GroupStageMatchDispatcher {
let _groupStages = groupStages
// Get the maximum count of matches in any group
let maxMatchesCount = _groupStages.map { $0._matches().count }.max() ?? 0
// Flatten matches in a round-robin order by cycling through each group
let flattenedMatches = (0..<maxMatchesCount).flatMap { index in
_groupStages.compactMap { group in
// Safely access matches, return nil if index is out of bounds
let playedMatches = group.playedMatches()
return playedMatches.indices.contains(index) ? playedMatches[index] : nil
var flattenedMatches = [Match]()
if simultaneousStart {
// Flatten matches in a round-robin order by cycling through each group
flattenedMatches = (0..<maxMatchesCount).flatMap { index in
_groupStages.compactMap { group in
// Safely access matches, return nil if index is out of bounds
let playedMatches = group.playedMatches()
return playedMatches.indices.contains(index) ? playedMatches[index] : nil
}
}
} else {
flattenedMatches = _groupStages.flatMap({ $0.playedMatches() })
}
var slots = [GroupStageTimeMatch]()
@ -215,24 +187,28 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
let counts = Dictionary(previousRotationBracketIndexes, uniquingKeysWith: +)
var rotationMatches = Array(availableMatches.filter({ match in
// 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 {
print("Match \(match.roundAndMatchTitle()) has teams already scheduled in rotation \(rotationIndex)")
}
return teamsAvailable
}).prefix(numberOfCourtsAvailablePerRotation))
}))
if rotationIndex > 0 {
rotationMatches = rotationMatches.sorted(by: {
if counts[$0.groupStageObject!.index] ?? 0 == counts[$1.groupStageObject!.index] ?? 0 {
return $0.groupStageObject!.index < $1.groupStageObject!.index
if simultaneousStart {
return $0.groupStageObject!.orderedIndexOfMatch($0) < $1.groupStageObject!.orderedIndexOfMatch($1)
} else {
return $0.groupStageObject!.index < $1.groupStageObject!.index
}
} else {
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)")
if let first = rotationMatches.first(where: { match in
let estimatedDuration = match.matchFormat.getEstimatedDuration(additionalEstimationDuration)
@ -246,7 +222,7 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
return false
}
let teamsAvailable = teamsPerRotation[rotationIndex]!.allSatisfy({ !match.containsTeamId($0) })
let teamsAvailable = teamsPerRotation[rotationIndex]!.allSatisfy({ !match.containsTeamIndex($0) })
if !teamsAvailable {
print("Teams from match \(match.roundAndMatchTitle()) are already scheduled in this rotation")
return false
@ -260,7 +236,7 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
print("Scheduled match: \(first.roundAndMatchTitle()) on court \(courtIndex) at rotation \(rotationIndex)")
slots.append(timeMatch)
teamsPerRotation[rotationIndex]!.append(contentsOf: first.teamIds())
teamsPerRotation[rotationIndex]!.append(contentsOf: first.matchUp())
rotationMatches.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)")
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
}
}
@ -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 _startDate: Date?
var rotationIndex = 0
@ -442,7 +418,7 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
var issueFound: Bool = false
// 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
if _startDate == nil {
@ -461,20 +437,28 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
}
var freeCourtPerRotation = [Int: [Int]]()
let availableCourt = numberOfCourtsAvailablePerRotation
var courts = initialCourts ?? (0..<availableCourt).map { $0 }
var courts = initialCourts ?? Array(courtsAvailable)
var shouldStartAtDispatcherDate = rotationIndex > 0
var suitableDate: Date?
while !availableMatchs.isEmpty && !issueFound && rotationIndex < 100 {
while !availableMatchs.isEmpty && !issueFound && rotationIndex < 50 {
freeCourtPerRotation[rotationIndex] = []
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 {
rotationStartDate = dispatcherStartDate
shouldStartAtDispatcherDate = false
} else {
courts = rotationIndex == 0 ? courts : (0..<availableCourt).map { $0 }
courts = rotationIndex == 0 ? courts : Array(courtsAvailable)
}
courts.sort()
@ -486,8 +470,16 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
if rotationIndex > 0, let freeCourtPreviousRotation = freeCourtPerRotation[rotationIndex - 1], !freeCourtPreviousRotation.isEmpty {
print("Handling break time conflicts or waiting for free courts")
let previousPreviousRotationSlots = slots.filter { $0.rotationIndex == rotationIndex - 2 && freeCourtPreviousRotation.contains($0.courtIndex) }
let previousEndDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: accountUpperBracketBreakTime)
let previousEndDateNoBreak = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: false)
var previousEndDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: accountUpperBracketBreakTime)
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 }
@ -499,13 +491,23 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
let timeDifferenceLimitInSeconds = Double(timeDifferenceLimit * 60)
var difference = differenceWithBreak
if differenceWithBreak <= 0 {
if differenceWithBreak <= 0, accountUpperBracketBreakTime == false {
difference = differenceWithoutBreak
} else if differenceWithBreak > timeDifferenceLimitInSeconds && differenceWithoutBreak > timeDifferenceLimitInSeconds {
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) })
freeCourtPerRotation[rotationIndex] = courts
courts = freeCourtPreviousRotation
@ -516,16 +518,22 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
let duration = firstMatch.matchFormat.getEstimatedDuration(additionalEstimationDuration)
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")
issueFound = true
if let courtsUnavailability {
let computedStartDateAndCourts = getFirstFreeCourt(startDate: rotationStartDate, duration: duration, courts: courts, courtsUnavailability: courtsUnavailability)
rotationStartDate = computedStartDateAndCourts.earliestFreeDate
courts = computedStartDateAndCourts.availableCourts
} else {
issueFound = true
}
} else {
courts = Array(Set(courts).subtracting(Set(courtsUnavailable)))
courts = Array(Set(courtsAvailable).subtracting(Set(courtsUnavailable)))
}
}
// 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
}
@ -544,10 +552,10 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
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 minimumTargetedEndDate = rotationStartDate
@ -563,7 +571,7 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
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).")
return false
}
@ -587,13 +595,18 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
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) {
print("Returning true: Both current \(match.index) and next match \(nextMatch.index) can be played in rotation \(rotationIndex).")
return true
} else {
print("Returning false: Either current match or next match cannot be played in rotation \(rotationIndex).")
return false
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).")
return true
} else {
print("Returning false: Either current match or next match cannot be played in rotation \(rotationIndex).")
return false
}
}
}
@ -622,15 +635,22 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
}
if freeCourtPerRotation[rotationIndex]?.count == availableCourts {
print("All courts in rotation \(rotationIndex) are free")
if freeCourtPerRotation[rotationIndex]?.count == courtsAvailable.count {
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 {
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]()
@ -651,7 +671,7 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
}
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({
@ -709,7 +729,7 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable {
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
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 {
if tournament.courtCount < courtsAvailable.count {
courtsAvailable = Set(tournament.courtsAvailable())
}
var lastDate = tournament.startDate
if tournament.groupStageCount > 0 {
lastDate = updateGroupStageSchedule(tournament: tournament)
@ -777,6 +840,10 @@ struct TimeMatch {
let minutesToAdd = Double(durationLeft + (includeBreakTime ? minimumBreakTime : 0))
return startDate.addingTimeInterval(minutesToAdd * 60.0)
}
var computedEndDateForSorting: Date {
estimatedEndDate(includeBreakTime: false)
}
}
struct GroupStageMatchDispatcher {
@ -801,4 +868,16 @@ extension Match {
func containsTeamId(_ id: String) -> Bool {
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) {
super.init()
self.teamRegistration = ""
self.firstName = (importedPlayer.firstName ?? "").trimmed.capitalized
self.lastName = (importedPlayer.lastName ?? "").trimmed.uppercased()
self.licenceId = importedPlayer.license ?? nil
self.firstName = (importedPlayer.firstName ?? "").prefixTrimmed(50).capitalized
self.lastName = (importedPlayer.lastName ?? "").prefixTrimmed(50).uppercased()
self.licenceId = importedPlayer.license?.prefixTrimmed(50) ?? nil
self.rank = Int(importedPlayer.rank)
self.sex = importedPlayer.male ? .male : .female
self.tournamentPlayed = importedPlayer.tournamentPlayed
self.points = importedPlayer.getPoints()
self.clubName = importedPlayer.clubName
self.ligueName = importedPlayer.ligueName
self.assimilation = importedPlayer.assimilation
self.clubName = importedPlayer.clubName?.prefixTrimmed(200)
self.ligueName = importedPlayer.ligueName?.prefixTrimmed(200)
self.assimilation = importedPlayer.assimilation?.prefixTrimmed(50)
self.source = .frenchFederation
self.birthdate = importedPlayer.birthYear
self.birthdate = importedPlayer.birthYear?.prefixTrimmed(50)
}
internal init?(federalData: [String], sex: Int, sexUnknown: Bool) {
@ -86,11 +86,11 @@ final class PlayerRegistration: BasePlayerRegistration, SideStorable {
let _lastName = federalData[0].trimmed.uppercased()
let _firstName = federalData[1].trimmed.capitalized
if _lastName.isEmpty && _firstName.isEmpty { return nil }
lastName = _lastName
firstName = _firstName
birthdate = federalData[2].formattedAsBirthdate()
licenceId = federalData[3]
clubName = federalData[4]
lastName = _lastName.prefixTrimmed(50)
firstName = _firstName.prefixTrimmed(50)
birthdate = federalData[2].formattedAsBirthdate().prefixTrimmed(50)
licenceId = federalData[3].prefixTrimmed(50)
clubName = federalData[4].prefixTrimmed(200)
let stringRank = federalData[5]
if stringRank.isEmpty {
rank = nil
@ -99,11 +99,11 @@ final class PlayerRegistration: BasePlayerRegistration, SideStorable {
}
let _email = federalData[6]
if _email.isEmpty == false {
self.email = _email
self.email = _email.prefixTrimmed(50)
}
let _phoneNumber = federalData[7]
if _phoneNumber.isEmpty == false {
self.phoneNumber = _phoneNumber
self.phoneNumber = _phoneNumber.prefixTrimmed(50)
}
source = .beachPadel
@ -168,12 +168,27 @@ final class PlayerRegistration: BasePlayerRegistration, SideStorable {
}
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 {
firstName.trimmedMultiline.localizedCaseInsensitiveCompare(player.firstName.trimmedMultiline) == .orderedSame &&
lastName.trimmedMultiline.localizedCaseInsensitiveCompare(player.lastName.trimmedMultiline) == .orderedSame
firstName.trimmedMultiline.canonicalVersion.localizedCaseInsensitiveCompare(player.firstName.trimmedMultiline.canonicalVersion) == .orderedSame &&
lastName.trimmedMultiline.canonicalVersion.localizedCaseInsensitiveCompare(player.lastName.trimmedMultiline.canonicalVersion) == .orderedSame
}
func tournament() -> Tournament? {
@ -186,6 +201,10 @@ final class PlayerRegistration: BasePlayerRegistration, SideStorable {
return self.tournamentStore.teamRegistrations.findById(teamRegistration)
}
func isHere() -> Bool {
hasArrived
}
func hasPaid() -> Bool {
paymentType != nil
}
@ -313,7 +332,7 @@ final class PlayerRegistration: BasePlayerRegistration, SideStorable {
if let currentLicenceId = licenceId {
if currentLicenceId.trimmed.hasSuffix("(\(year-1))") {
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))"
}
}
@ -392,14 +411,8 @@ final class PlayerRegistration: BasePlayerRegistration, SideStorable {
case 0: return 0
case womanMax: return manMax - womanMax
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:
return 15000
return TournamentCategory.femaleInMaleAssimilationAddition(playerRank)
}
}

@ -16,14 +16,14 @@ Dans Swift:
- Ajouter le champ dans classe
- Ajouter le champ dans le constructeur si possible
- 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
- Pour que les tests sur les dates fonctionnent, on peut tester date.formatted() par exemple
Dans Django:
- Ajouter le champ dans la classe
- 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 dans UserSerializer > create > create_user dans serializers.py
- L'ajouter aussi dans admin.py si nécéssaire

@ -12,22 +12,7 @@ import SwiftUI
@Observable
final class Round: BaseRound, SideStorable {
// static func resourceName() -> String { "rounds" }
// 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
var _cachedSeedInterval: SeedInterval?
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? {
return self.tournamentStore.teamRegistrations.first(where: {
$0.tournament == tournament
&& $0.bracketPosition != nil
$0.bracketPosition != nil
&& ($0.bracketPosition! / 2) == matchIndex
&& ($0.bracketPosition! % 2) == team.rawValue
})
@ -156,13 +140,22 @@ final class Round: BaseRound, SideStorable {
let initialMatchIndex = RoundRule.matchIndex(fromRoundIndex: index)
let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: index)
return self.tournamentStore.teamRegistrations.filter {
$0.tournament == tournament
&& $0.bracketPosition != nil
$0.bracketPosition != nil
&& ($0.bracketPosition! / 2) >= initialMatchIndex
&& ($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] {
let teamIds: [String] = self._matches().compactMap { $0.losingTeamId }
return teamIds.compactMap { self.tournamentStore.teamRegistrations.findById($0) }
@ -459,6 +452,8 @@ defer {
func correspondingLoserRoundTitle(_ displayStyle: DisplayStyle = .wide) -> String {
if let _cachedSeedInterval { return _cachedSeedInterval.localizedLabel(displayStyle) }
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
@ -477,8 +472,16 @@ defer {
// && $0.bracketPosition != nil
// && ($0.bracketPosition! / 2) < initialMatchIndexFromRoundIndex
// })
var seedsCount = seedsAfterThisRound.count
if seedsAfterThisRound.isEmpty {
let nextRoundsDisableMatches = nextRoundsDisableMatches()
seedsCount = disabledMatches().count - nextRoundsDisableMatches
}
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)
}
@ -500,6 +503,8 @@ defer {
}
func seedInterval(initialMode: Bool = false) -> SeedInterval? {
if initialMode == false, let _cachedSeedInterval { return _cachedSeedInterval }
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
@ -511,17 +516,32 @@ defer {
if isUpperBracket() {
if index == 0 { return SeedInterval(first: 1, last: 2) }
let initialMatchIndexFromRoundIndex = RoundRule.matchIndex(fromRoundIndex: index)
let seedsAfterThisRound : [TeamRegistration] = self.tournamentStore.teamRegistrations.filter {
$0.bracketPosition != nil
&& ($0.bracketPosition! / 2) < initialMatchIndexFromRoundIndex
}
let playedMatches = playedMatches().count
let minimumMatches = initialMode ? RoundRule.numberOfMatches(forRoundIndex: index) : playedMatches * 2
//print("playedMatches \(playedMatches)", initialMode, parent, parentRound?.roundTitle(), seedsAfterThisRound.count)
let seedInterval = SeedInterval(first: playedMatches + seedsAfterThisRound.count + 1, last: minimumMatches + seedsAfterThisRound.count)
//print(seedInterval.localizedLabel())
return seedInterval
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 {
$0.bracketPosition != nil
&& ($0.bracketPosition! / 2) < initialMatchIndexFromRoundIndex
}
var seedsCount = seedsAfterThisRound.count
if seedsAfterThisRound.isEmpty {
let nextRoundsDisableMatches = nextRoundsDisableMatches()
seedsCount = disabledMatches().count - nextRoundsDisableMatches
}
let playedMatches = playedMatches()
//print("playedMatches \(playedMatches)", initialMode, parent, parentRound?.roundTitle(), seedsAfterThisRound.count)
let seedInterval = SeedInterval(first: playedMatches.count + seedsCount + 1, last: playedMatches.count * 2 + seedsCount)
//print(seedInterval.localizedLabel())
_cachedSeedInterval = seedInterval
return seedInterval
}
}
if let previousRound = previousRound() {
@ -642,6 +662,14 @@ defer {
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) {
if updatedMatchFormat.weight < self.matchFormat.weight {
updateMatchFormatAndAllMatches(updatedMatchFormat)

@ -50,7 +50,7 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
// self.storeId = tournament
self.tournament = tournament
self.groupStage = groupStage
self.registrationDate = registrationDate
self.registrationDate = registrationDate ?? Date()
self.callDate = callDate
self.bracketPosition = bracketPosition
self.groupStagePosition = groupStagePosition
@ -120,13 +120,39 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
}
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)
self.bracketPosition = seedPosition
if groupStagePosition != nil && qualified == false {
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? {
@ -432,7 +458,7 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
self.tournamentStore.playerRegistrations.filter { $0.teamRegistration == self.id }.sorted { (lhs, rhs) in
let predicates: [AreInIncreasingOrder] = [
{ $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.firstName < $1.firstName }
]
@ -515,59 +541,50 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
return nil
}
// enum CodingKeys: String, CodingKey {
// case _id = "id"
// case _lastUpdate = "lastUpdate"
// case _storeId = "storeId"
// case _tournament = "tournament"
// case _groupStage = "groupStage"
// case _registrationDate = "registrationDate"
// case _callDate = "callDate"
// case _bracketPosition = "bracketPosition"
// case _groupStagePosition = "groupStagePosition"
// case _comment = "comment"
// case _source = "source"
// case _sourceValue = "sourceValue"
// case _logo = "logo"
// case _name = "name"
// case _wildCardBracket = "wildCardBracket"
// case _wildCardGroupStage = "wildCardGroupStage"
// case _weight = "weight"
// case _walkOut = "walkOut"
// case _lockedWeight = "lockedWeight"
// case _confirmationDate = "confirmationDate"
// case _qualified = "qualified"
// case _finalRanking = "finalRanking"
// case _pointsEarned = "pointsEarned"
// }
//
// func encode(to encoder: Encoder) throws {
// var container = encoder.container(keyedBy: CodingKeys.self)
//
// try container.encode(id, forKey: ._id)
// try container.encode(storeId, forKey: ._storeId)
// try container.encode(lastUpdate, forKey: ._lastUpdate)
// try container.encode(tournament, forKey: ._tournament)
// try container.encode(groupStage, forKey: ._groupStage)
// try container.encode(registrationDate, forKey: ._registrationDate)
// try container.encode(callDate, forKey: ._callDate)
// try container.encode(bracketPosition, forKey: ._bracketPosition)
// try container.encode(groupStagePosition, forKey: ._groupStagePosition)
// try container.encode(comment, forKey: ._comment)
// try container.encode(source, forKey: ._source)
// try container.encode(sourceValue, forKey: ._sourceValue)
// try container.encode(logo, forKey: ._logo)
// try container.encode(name, forKey: ._name)
// 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 wildcardLabel() -> String? {
if isWildCard() {
let wildcardLabel: String = ["wildcard", (wildCardBracket ? "tableau" : "poule")].joined(separator: " ")
return wildcardLabel
} else {
return nil
}
}
var _cachedRestingTime: (Bool, Date?)?
func restingTime() -> Date? {
if let _cachedRestingTime { return _cachedRestingTime.1 }
let restingTime = matches().filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).last?.endDate
_cachedRestingTime = (true, restingTime)
return restingTime
}
func resetRestingTime() {
_cachedRestingTime = nil
}
var restingTimeForSorting: Date {
restingTime()!
}
func teamNameLabel() -> String {
if let name, name.isEmpty == false {
return name
} else {
return "Toute l'équipe"
}
}
func isDifferentPosition(_ drawMatchIndex: Int?) -> Bool {
if let bracketPosition, let drawMatchIndex {
return drawMatchIndex != bracketPosition
} else if let bracketPosition {
return true
} else if let drawMatchIndex {
return true
}
return false
}
func insertOnServer() {
self.tournamentStore.teamRegistrations.writeChangeAndInsertOnServer(instance: self)

@ -1,5 +1,5 @@
//
// Tournament.swift
// swift
// PadelClub
//
// Created by Laurent Morvillier on 02/02/2024.
@ -12,115 +12,21 @@ import SwiftUI
@Observable
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
var navigationPath: [Screen] = []
// enum CodingKeys: String, CodingKey {
// 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) {
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) {
super.init()
self.event = event
self.name = name
self.startDate = startDate
self.endDate = endDate
self.creationDate = creationDate
#if DEBUG
self.isPrivate = false
#else
self.isPrivate = Guard.main.purchasedTransactions.isEmpty
#endif
self.groupStageFormat = groupStageFormat
self.roundFormat = roundFormat
self.loserRoundFormat = loserRoundFormat
@ -142,198 +48,47 @@ final class Tournament: BaseTournament {
self.entryFee = entryFee
self.additionalEstimationDuration = additionalEstimationDuration
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.publishSummons = publishSummons
self.publishBrackets = publishBrackets
self.publishGroupStages = publishGroupStages
self.publishRankings = publishRankings
self.publishTournament = publishTournament
#endif
self.shouldVerifyBracket = shouldVerifyBracket
self.shouldVerifyGroupStage = shouldVerifyGroupStage
self.hideTeamsWeight = hideTeamsWeight
self.publishTournament = publishTournament
self.hidePointsEarned = hidePointsEarned
self.publishRankings = publishRankings
self.loserBracketMode = loserBracketMode
self.initialSeedRound = initialSeedRound
self.initialSeedCount = initialSeedCount
}
required init(from decoder: Decoder) throws {
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 {
return TournamentLibrary.shared.store(tournamentId: self.id)
}
override func deleteDependencies() {
let store = self.tournamentStore
let drawLogs = self.tournamentStore.drawLogs
for drawLog in drawLogs {
drawLog.deleteDependencies()
}
store.drawLogs.deleteDependencies(drawLogs)
let teams = self.tournamentStore.teamRegistrations
for team in Array(teams) {
team.deleteDependencies()
@ -485,16 +240,14 @@ final class Tournament: BaseTournament {
return URLs.main.url.appending(path: "tournament/\(id)").appending(path: pageLink.path)
}
func courtUsed() -> [Int] {
#if DEBUG //DEBUGING TIME
func courtUsed(runningMatches: [Match]) -> [Int] {
#if _DEBUGING_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func courtUsed()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
let runningMatches: [Match] = self.tournamentStore.matches.filter { $0.isRunning() }
return Set(runningMatches.compactMap { $0.courtIndex }).sorted()
}
@ -543,7 +296,7 @@ defer {
return endDate != nil
}
func state() -> Tournament.State {
func state() -> State {
if self.isCanceled == true {
return .canceled
}
@ -676,11 +429,15 @@ defer {
if availableSeeds().isEmpty == false && roundIndex >= lastSeedRound() {
if availableSeedGroup == SeedInterval(first: 1, last: 2) { return availableSeedGroup }
let availableSeeds = seeds(inSeedGroup: availableSeedGroup)
let availableSeedSpot = availableSeedSpot(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 {
return availableSeedGroup
} else if availableSeeds.count == availableSeedOpponentSpot.count && availableSeedGroup.count == availableSeedOpponentSpot.count {
@ -714,22 +471,32 @@ defer {
let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex)
let availableSeeds = seeds(inSeedGroup: seedGroup)
if availableSeeds.count <= availableSeedSpot.count {
let spots = availableSeedSpot.shuffled()
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 <= availableSeedOpponentSpot.count && availableSeeds.count <= self.availableSeeds().count) {
} else {
if availableSeeds.count <= availableSeedSpot.count {
let spots = availableSeedSpot.shuffled()
for (index, seed) in availableSeeds.enumerated() {
seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: false)
}
} else if (availableSeeds.count <= availableSeedOpponentSpot.count && availableSeeds.count <= self.availableSeeds().count) {
let spots = availableSeedOpponentSpot.shuffled()
for (index, seed) in availableSeeds.enumerated() {
seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: true)
}
} else if let chunks = seedGroup.chunks() {
if let chunk = chunks.first(where: { seedInterval in
seedInterval.first >= self.seededTeams().count
}) {
setSeeds(inRoundIndex: roundIndex, inSeedGroup: chunk)
let spots = availableSeedOpponentSpot.shuffled()
for (index, seed) in availableSeeds.enumerated() {
seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: true)
}
} else if let chunks = seedGroup.chunks() {
if let chunk = chunks.first(where: { seedInterval in
seedInterval.first >= self.seededTeams().count
}) {
setSeeds(inRoundIndex: roundIndex, inSeedGroup: chunk)
}
}
}
}
@ -825,14 +592,14 @@ defer {
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 wcBracket = _teams.filter { $0.wildCardBracket }.sorted(using: _currentSelectionSorting, order: .ascending)
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
if groupStageTeamCount < 0 { groupStageTeamCount = 0 }
if bracketSeeds < 0 { bracketSeeds = 0 }
@ -1024,8 +791,20 @@ defer {
func playersWithoutValidLicense(in players: [PlayerRegistration], isImported: Bool) -> [PlayerRegistration] {
let licenseYearValidity = self.licenseYearValidity()
return players.filter({
($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))
return players.filter({ player in
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 selectedTeams : [TeamRegistration] = selectedSortedTeams()
let callDateIssue : [TeamRegistration] = selectedTeams.filter { $0.callDate != nil && isStartDateIsDifferentThanCallDate($0) }
let duplicates : [PlayerRegistration] = duplicates(in: players)
let problematicPlayers : [PlayerRegistration] = players.filter({ $0.sex == nil })
let inadequatePlayers : [PlayerRegistration] = inadequatePlayers(in: players)
let homonyms = homonyms(in: players)
let ageInadequatePlayers = ageInadequatePlayers(in: players)
let isImported = players.anySatisfy({ $0.isImported() })
let playersWithoutValidLicense : [PlayerRegistration] = playersWithoutValidLicense(in: players, isImported: isImported)
let playersMissing : [TeamRegistration] = selectedTeams.filter({ $0.unsortedPlayers().count < 2 })
@ -1089,12 +870,13 @@ defer {
let waitingListInBracket = waitingList.filter({ $0.bracketPosition != 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 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
}
@ -1103,7 +885,9 @@ defer {
// 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
let start = Date()
defer {
@ -1111,10 +895,10 @@ defer {
print("func tournament availableToStart", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#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
let start = Date()
defer {
@ -1122,10 +906,10 @@ defer {
print("func tournament runningMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#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
let start = Date()
defer {
@ -1133,10 +917,22 @@ defer {
print("func tournament readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#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
let start = Date()
defer {
@ -1277,7 +1073,7 @@ defer {
if team.qualified == false && alreadyPlaceTeams.contains(team.id) == false {
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] {
teams[_index] = existingTeams + [team.id]
} else {
@ -1390,7 +1186,7 @@ defer {
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 {
return [title, name].joined(separator: " - ")
} else {
@ -1488,10 +1284,10 @@ defer {
var entryFeeMessage: String {
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")
} else {
return "Inscription: gratuite."
return "Inscription : gratuite."
}
}
@ -1551,7 +1347,9 @@ defer {
func callStatus() async -> TournamentStatus {
let selectedSortedTeams = selectedSortedTeams()
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 completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0)))
return TournamentStatus(label: label, completion: completionLabel)
@ -1629,16 +1427,61 @@ defer {
deleteGroupStages()
switch preset {
case .manual:
buildGroupStages()
buildBracket()
case .doubleGroupStage:
buildGroupStages()
addNewGroupStageStep()
qualifiedPerGroupStage = 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]()
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)
}
@ -1670,7 +1513,15 @@ defer {
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 matches = (0..<matchCount).map { //0 is final match
@ -1724,7 +1575,7 @@ defer {
self.tournamentStore.groupStages.delete(contentOfs: allGroupStages())
}
func refreshGroupStages() {
func refreshGroupStages(keepExistingMatches: Bool = false) {
unsortedTeams().forEach { team in
team.groupStage = nil
team.groupStagePosition = nil
@ -1733,16 +1584,16 @@ defer {
if groupStageCount > 0 {
switch groupStageOrderingMode {
case .random:
setGroupStage(randomize: true)
setGroupStage(randomize: true, keepExistingMatches: keepExistingMatches)
case .snake:
setGroupStage(randomize: false)
setGroupStage(randomize: false, keepExistingMatches: keepExistingMatches)
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 numberOfBracketsAsInt = groupStages.count
// let teamsPerBracket = teamsPerBracket
@ -1751,7 +1602,7 @@ defer {
buildGroupStages()
} else {
setGroupStageTeams(randomize: randomize)
groupStages.forEach { $0.buildMatches() }
groupStages.forEach { $0.buildMatches(keepExistingMatches: keepExistingMatches) }
}
}
@ -1787,15 +1638,14 @@ defer {
func labelIndexOf(team: TeamRegistration) -> String? {
if let teamIndex = indexOf(team: team) {
return "#" + (teamIndex + 1).formatted()
return "Tête de série #" + (teamIndex + 1).formatted()
} else {
return nil
}
}
func addTeam(_ players: Set<PlayerRegistration>, registrationDate: Date? = nil, name: String? = nil) -> TeamRegistration {
let date: Date = registrationDate ?? Date()
let team = TeamRegistration(tournament: id, registrationDate: date, name: name)
let team = TeamRegistration(tournament: id, registrationDate: registrationDate, name: name)
team.setWeight(from: Array(players), inTournamentCategory: tournamentCategory)
players.forEach { player in
player.teamRegistration = team.id
@ -1921,7 +1771,7 @@ defer {
private func _defaultSorting() -> [MySortDescriptor<TeamRegistration>] {
switch teamSorting {
case .rank:
[.keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.registrationDate!), .keyPath(\TeamRegistration.id)]
[.keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.id)]
case .inscriptionDate:
[.keyPath(\TeamRegistration.registrationDate!), .keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.id)]
}
@ -1933,7 +1783,7 @@ defer {
&& 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] {
return self.tournamentStore.matchSchedulers.filter { $0.tournament == self.id }
@ -1944,6 +1794,10 @@ defer {
return self._matchSchedulers().first
}
func courtsAvailable() -> [Int] {
(0..<courtCount).map { $0 }
}
func currentMonthData() -> MonthData? {
guard let rankSourceDate else { return nil }
let dateString = URL.importDateFormatter.string(from: rankSourceDate)
@ -2022,7 +1876,8 @@ defer {
let newGroup = selected.prefix(seedCount) + selected.filter({ $0.qualified })
let currentGroup = allTeams.filter({ $0.bracketPosition != nil })
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 shouldNotBeInIt = Set(groupIds).subtracting(selectedIds)
return (Array(shouldBeInIt), Array(shouldNotBeInIt))
@ -2056,8 +1911,12 @@ defer {
groupStages().chunked(into: 2).forEach { gss in
let placeCount = i * 2 + 1
let match = Match(round: groupStageLoserBracket.id, index: placeCount, format: groupStageLoserBracket.matchFormat)
match.name = "\(placeCount)\(placeCount.ordinalFormattedSuffix(feminine: true)) place"
tournamentStore.matches.addOrUpdate(instance: match)
match.setMatchName("\(placeCount)\(placeCount.ordinalFormattedSuffix(feminine: true)) place")
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] {
print("rang \(i)")
@ -2084,6 +1943,138 @@ defer {
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: -
func insertOnServer() throws {
@ -2203,7 +2194,7 @@ extension Tournament: FederalTournamentHolder {
func subtitleLabel(forBuild build: any TournamentBuildHolder) -> String {
if isAnimation() {
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 {
return build.level.localizedLevelLabel(.title)
} else {

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

@ -36,6 +36,14 @@ enum TimeOfDay {
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 {
self.formatted(.dateTime.weekday().day().month()) + " à " + self.formattedAsHourMinute()
}
@ -231,4 +239,19 @@ extension Date {
func localizedWeekDay() -> String {
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)
}
private var isMany: Bool {
self > 1 || self < -1
}
var pluralSuffix: String {
return self > 1 ? "s" : ""
return isMany ? "s" : ""
}
func localizedPluralSuffix(_ plural: String = "s") -> String {
return isMany ? plural : ""
}
func formattedAsRawString() -> String {
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()
}
static func defaultCurrency() -> String {
// return "EUR"
Locale.current.currency?.identifier ?? "EUR"
}
}

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

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

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

@ -21,12 +21,9 @@ enum DisplayStyle {
case short
}
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
enum SummoningDisplayContext {
case footer
case menu
}
struct DeviceHelper {

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

@ -48,7 +48,7 @@ struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identifiable {
}
var identifier: String {
level.localizedLevelLabel()+":"+category.localizedLabel()+":"+age.localizedLabel()
level.localizedLevelLabel()+":"+category.localizedLabel()+":"+age.localizedFederalAgeLabel()
}
func computedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
@ -65,7 +65,7 @@ struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identifiable {
}
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 {
case .unlisted:
return displayStyle == .title ? "Aucune" : ""
@ -265,7 +265,7 @@ enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifiable {
case .a17_18:
return "17/18 ans"
case .senior:
return "Senior"
return displayStyle == .short ? "" : "Senior"
case .a45:
return "+45 ans"
case .a55:
@ -274,7 +274,7 @@ enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifiable {
}
var tournamentDescriptionLabel: String {
return localizedLabel()
return localizedFederalAgeLabel()
}
func isAgeValid(age: Int?) -> Bool {
@ -540,7 +540,7 @@ enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable {
case .p25:
switch count {
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:
return [18,16,15,14,13,12,11,10,9,7,5,4,3,2, 1]
case 17...20:
@ -996,6 +996,15 @@ enum TeamPosition: Int, Identifiable, Hashable, Codable, CaseIterable {
return shortName
}
}
func localizedBranchLabel() -> String {
switch self {
case .one:
return "Branche du haut"
case .two:
return "Branche du bas"
}
}
}
enum SetFormat: Int, Hashable, Codable {
@ -1126,7 +1135,8 @@ enum MatchType: String {
case loserBracket = "loserBracket"
}
enum MatchFormat: Int, Hashable, Codable, CaseIterable {
enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable {
var id: Int { self.rawValue }
case twoSets
case twoSetsSuperTie
case twoSetsOfFourGames
@ -1139,6 +1149,13 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
case twoSetsOfFourGamesDecisivePoint
case nineGamesDecisivePoint
case twoSetsOfSuperTie
case singleSet
case singleSetDecisivePoint
case singleSetOfFourGames
case singleSetOfFourGamesDecisivePoint
init?(rawValue: Int?) {
guard let value = rawValue else { return nil }
self.init(rawValue: value)
@ -1162,6 +1179,12 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
return 4
case .megaTie:
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
case .megaTie:
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] {
[.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 {
@ -1215,7 +1244,7 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
var canSuperTie: Bool {
switch self {
case .twoSetsSuperTie, .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .twoSetsDecisivePointSuperTie:
case .twoSetsSuperTie, .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .twoSetsDecisivePointSuperTie, .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint:
return true
default:
return false
@ -1237,8 +1266,10 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
func formattedEstimatedBreakDuration() -> String {
var label = Duration.seconds(breakTime.breakTime * 60).formatted(.units(allowed: [.minutes]))
if breakTime.matchCount > 1 {
label += " après \(breakTime.matchCount) match"
label += " de pause après \(breakTime.matchCount) match"
label += breakTime.matchCount.pluralSuffix
} else {
label += " de pause"
}
return label
}
@ -1262,9 +1293,19 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
case .nineGamesDecisivePoint:
return 40
case .megaTie:
return 30
return 20
case .superTie:
return 15
case .twoSetsOfSuperTie:
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)
case .superTie:
return (15, 3)
case .megaTie:
default:
return (5, 1)
}
}
@ -1298,14 +1339,14 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
return matchCount < 7 ? 6 : 2
case .superTie:
return 7
case .megaTie:
return 7
default:
return 10
}
}
var hasDecisivePoint: Bool {
switch self {
case .nineGamesDecisivePoint, .twoSetsDecisivePoint, .twoSetsOfFourGamesDecisivePoint, .twoSetsDecisivePointSuperTie:
case .nineGamesDecisivePoint, .twoSetsDecisivePoint, .twoSetsOfFourGamesDecisivePoint, .twoSetsDecisivePointSuperTie, .singleSetDecisivePoint, .singleSetOfFourGamesDecisivePoint:
return true
default:
return false
@ -1319,9 +1360,18 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
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 {
switch self {
case .twoSetsDecisivePoint, .twoSetsDecisivePointSuperTie, .twoSetsOfFourGamesDecisivePoint, .nineGamesDecisivePoint:
case .twoSetsDecisivePoint, .twoSetsDecisivePointSuperTie, .twoSetsOfFourGamesDecisivePoint, .nineGamesDecisivePoint, .singleSetDecisivePoint:
return " [Point Décisif]"
default:
return ""
@ -1336,7 +1386,19 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
return "\(format) : "
}
var isFederal: Bool {
switch self {
case .megaTie, .twoSetsOfSuperTie, .singleSet, .singleSetDecisivePoint, .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint:
return false
default:
return true
}
}
var format: String {
shortFormat + (isFederal ? "" : " (non officiel)")
}
var shortFormat: String {
switch self {
case .twoSets:
return "A1"
@ -1348,8 +1410,14 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
return "D1"
case .superTie:
return "E"
case .twoSetsOfSuperTie:
return "G"
case .megaTie:
return "F"
case .singleSet:
return "H1"
case .singleSetDecisivePoint:
return "H2"
case .twoSetsDecisivePoint:
return "A2"
case .twoSetsDecisivePointSuperTie:
@ -1358,11 +1426,17 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
return "C2"
case .nineGamesDecisivePoint:
return "D2"
case .singleSetOfFourGames:
return "I1"
case .singleSetOfFourGamesDecisivePoint:
return "I2"
}
}
var longLabel: String {
switch self {
case .singleSet, .singleSetDecisivePoint:
return "1 set de 6"
case .twoSets, .twoSetsDecisivePoint:
return "2 sets de 6"
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"
case .nineGames, .nineGamesDecisivePoint:
return "9 jeux, tiebreak à 8/8"
case .twoSetsOfSuperTie:
return "2 sets de supertie de 10 points"
case .superTie:
return "supertie de 10 points"
case .megaTie:
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 {
switch self {
case .twoSets, .twoSetsSuperTie, .twoSetsOfFourGames, .twoSetsDecisivePoint, .twoSetsOfFourGamesDecisivePoint, .twoSetsDecisivePointSuperTie:
case .twoSets, .twoSetsSuperTie, .twoSetsOfFourGames, .twoSetsDecisivePoint, .twoSetsOfFourGamesDecisivePoint, .twoSetsDecisivePointSuperTie, .twoSetsOfSuperTie:
return 2
case .nineGames, .nineGamesDecisivePoint, .superTie, .megaTie:
case .nineGames, .nineGamesDecisivePoint, .superTie, .megaTie, .singleSet, .singleSetDecisivePoint, .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint:
return 1
}
}
var setFormat: SetFormat {
switch self {
case .twoSets, .twoSetsSuperTie, .twoSetsDecisivePoint, .twoSetsDecisivePointSuperTie:
case .twoSets, .twoSetsSuperTie, .twoSetsDecisivePoint, .twoSetsDecisivePointSuperTie, .singleSet, .singleSetDecisivePoint:
return .six
case .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint:
case .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint:
return .four
case .nineGames, .nineGamesDecisivePoint:
return .nine
case .superTie:
case .superTie, .twoSetsOfSuperTie:
return .superTieBreak
case .megaTie:
return .megaTieBreak
@ -1592,7 +1670,8 @@ enum RoundRule {
}
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 {
@ -1680,6 +1759,113 @@ enum PadelTournamentStructurePreset: Int, Identifiable, CaseIterable {
case manual
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 {
switch self {
@ -1687,6 +1873,22 @@ enum PadelTournamentStructurePreset: Int, Identifiable, CaseIterable {
return "Défaut"
case .doubleGroupStage:
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:
return "24 équipes, 4 poules de 4, 1 qualifié par poule"
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()
}
}
//
// 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() {
// StoreCenter.main.resetLoggingCollections()
StoreCenter.main.resetLoggingCollections()
}
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 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 destinationFileUrl = documentsUrl.appendingPathComponent("\(dateString)")

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

@ -22,12 +22,13 @@ class FederalDataViewModel {
var searchAttemptCount: Int = 0
var dayDuration: Int?
var dayPeriod: DayPeriod = .all
var lastError: NetworkManagerError?
func filterStatus() -> String {
var labels: [String] = []
labels.append(contentsOf: levels.map { $0.localizedLevelLabel() }.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 club: Club? = DataStore.shared.clubs.first(where: { $0.code == codeClub })
return club?.clubTitle(.short)

@ -1,11 +1,12 @@
//
// MatchDescriptor.swift
// swift
// PadelClub
//
// Created by Razmig Sarkissian on 02/04/2024.
//
import Foundation
import SwiftUI
class MatchDescriptor: ObservableObject {
@Published var matchFormat: MatchFormat
@ -16,6 +17,58 @@ class MatchDescriptor: ObservableObject {
var teamLabelTwo: String = ""
var startDate: Date = Date()
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) {
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 print
case share
case restingTime
}

@ -36,8 +36,7 @@ class SearchViewModel: ObservableObject, Identifiable {
@Published var filterSelectionEnabled: Bool = false
@Published var isPresented: Bool = false
@Published var selectedAgeCategory: FederalTournamentAge = .unlisted
var mostRecentDate: Date? = nil
@Published var mostRecentDate: Date? = nil
var selectionIsOver: Bool {
if allowSingleSelection && selectedPlayers.count == 1 {
@ -69,9 +68,6 @@ class SearchViewModel: ObservableObject, Identifiable {
var message = ["Vérifiez l'ortographe ou lancez une nouvelle recherche."]
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.")
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")
}
@ -231,7 +227,7 @@ class SearchViewModel: ObservableObject, Identifiable {
]
if let mostRecentDate {
//predicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg))
predicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg))
}
if hideAssimilation {
@ -344,7 +340,7 @@ class SearchViewModel: ObservableObject, Identifiable {
}
if let mostRecentDate {
//andPredicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg))
andPredicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg))
}
if nameComponents.count > 1 {

@ -14,6 +14,12 @@ struct SetDescriptor: Identifiable, Equatable {
var tieBreakValueTeamOne: Int?
var tieBreakValueTeamTwo: Int?
var setFormat: SetFormat
var showSetInputView: Bool = true
var showTieBreakInputView: Bool = false
var isTeamOneSet: Bool {
return valueTeamOne != nil || tieBreakValueTeamOne != nil
}
var hasEnded: Bool {
if let valueTeamTwo, let valueTeamOne {
@ -30,4 +36,8 @@ struct SetDescriptor: Identifiable, Equatable {
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 {
[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? {
@ -259,7 +267,7 @@ struct CallMessageCustomizationView: View {
}
}.italic().foregroundStyle(.gray)
} header: {
Text("Rendu généré automatiquement")
Text("Exemple généré automatiquement")
}
}

@ -14,6 +14,7 @@ struct CallView: View {
let count: Int
let total: Int
let startDate: Date?
var title: String = "convoquées au bon horaire"
var body: some View {
VStack(spacing: 0) {
@ -32,7 +33,7 @@ struct CallView: View {
Text(startDate.formatted(.dateTime.weekday().day(.twoDigits).month().year()))
}
Spacer()
Text("convoquées au bon horaire")
Text(title)
}
.font(.caption)
.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 networkMonitor: NetworkMonitor
@ -57,6 +50,7 @@ struct CallView: View {
let callDate: Date
let matchFormat: MatchFormat
let roundLabel: String
let displayContext: SummoningDisplayContext
@State private var contactType: ContactType? = nil
@State private var sentError: ContactManagerError? = nil
@ -67,6 +61,49 @@ struct CallView: View {
@State var summonParamByMessage: 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 {
return self.tournament.tournamentStore
}
@ -82,9 +119,15 @@ struct CallView: View {
}
private func _called(_ calledTeams: [TeamRegistration], _ success: Bool) {
if simpleMode {
return
}
if success {
calledTeams.forEach { team in
team.callDate = callDate
if reSummon {
team.confirmationDate = nil
}
}
do {
try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: calledTeams)
@ -94,33 +137,39 @@ struct CallView: View {
}
}
func finalMessage(reSummon: Bool) -> String {
ContactType.callingMessage(tournament: tournament, startDate: callDate, roundLabel: roundLabel, matchFormat: matchFormat, reSummon: reSummon)
func finalMessage(reSummon: Bool, forcedEmptyMessage: Bool) -> String {
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 {
if simpleMode {
return false
}
return self.teams.allSatisfy({ $0.called() })
}
var mainWord: String {
if simpleMode {
return "Contacter"
} else {
return "Convoquer"
}
}
var body: some View {
let callWord : String = (reSummon ? "Reconvoquer" : "Convoquer")
HStack {
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 {
Text("\(callWord) cette paire par")
}
} else {
Text("\(callWord) ces \(self.teams.count) paires par")
Group {
switch displayContext {
case .footer:
_footerStyleView()
case .menu:
_menuStyleView()
}
self._summonMenu(byMessage: true)
Text("ou")
self._summonMenu(byMessage: false)
}
.font(.subheadline)
.buttonStyle(.borderless)
.alert("Un problème est survenu", isPresented: messageSentFailed) {
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
private func _summonMenu(byMessage: Bool) -> some View {
if self.reSummon {
Menu {
Button("Convoquer") {
Button(mainWord) {
self._summon(byMessage: byMessage, reSummon: false)
}
@ -229,6 +326,13 @@ struct CallView: View {
self._summon(byMessage: byMessage, reSummon: true)
}
if simpleMode == false {
Divider()
Button("Contacter") {
self._summon(byMessage: byMessage, reSummon: false, forcedEmptyMessage: true)
}
}
} label: {
Text(byMessage ? "sms" : "mail")
.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.summonParamReSummon = reSummon
self._verifyUser {
self._payTournamentAndExecute {
if byMessage {
self._contactByMessage(reSummon: reSummon)
self._contactByMessage(reSummon: reSummon, forcedEmptyMessage: forcedEmptyMessage)
} 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,
recipients: teams.flatMap { $0.getPhoneNumbers() },
body: finalMessage(reSummon: reSummon),
body: finalMessage(reSummon: reSummon, forcedEmptyMessage: forcedEmptyMessage),
tournamentBuild: nil)
}
fileprivate func _contactByMail(reSummon: Bool) {
fileprivate func _contactByMail(reSummon: Bool, forcedEmptyMessage: Bool) {
self.contactType = .mail(date: callDate,
recipients: tournament.umpireMail(),
bccRecipients: teams.flatMap { $0.getMail() },
body: finalMessage(reSummon: reSummon),
body: finalMessage(reSummon: reSummon, forcedEmptyMessage: forcedEmptyMessage),
subject: tournament.tournamentTitle(),
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
func _teamActionView(_ team: TeamRegistration) -> some View {
Menu(team.name ?? "Toute l'équipe") {
Menu(team.teamNameLabel()) {
let players = team.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 {
ForEach(withoutPhones) { player in
NavigationLink {
@ -45,7 +45,7 @@ struct PlayersWithoutContactView: View {
LabeledContent {
Text(withoutPhones.count.formatted())
} label: {
Text("Joueurs sans téléphone")
Text("Joueurs sans téléphone portable")
}
}
} header: {

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

@ -14,18 +14,21 @@ struct SeedsCallingView: View {
var body: some View {
List {
let tournamentRounds = tournament.rounds()
let uncalledSeeds = tournament.seededTeams().filter({ $0.callDate == nil })
NavigationLink {
TeamsCallingView(teams: tournament.seededTeams().filter({ $0.callDate == nil }))
.environment(tournament)
} label: {
LabeledContent("Équipes non contactées", value: tournament.seededTeams().filter({ $0.callDate == nil }).count.formatted())
if uncalledSeeds.isEmpty == false {
NavigationLink {
TeamsCallingView(teams: uncalledSeeds)
.environment(tournament)
} label: {
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))
ForEach(tournamentRounds) { round in
let seeds = round.seeds()
let seeds = round.teamsOrSeeds()
let callSeeds = seeds.filter({ tournament.isStartDateIsDifferentThanCallDate($0) == false })
if seeds.isEmpty == false {
Section {
@ -63,8 +66,14 @@ struct SeedsCallingView: View {
}
}
NavigationLink("Équipes non contactées") {
TeamsCallingView(teams: round.teams().filter({ $0.callDate == nil }))
let uncalledTeams = 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)
Section {
ForEach(teams) { team in
CallView.TeamView(team: team)
TeamCallView(team: team)
}
} header: {
HStack {

@ -12,77 +12,170 @@ struct TeamsCallingView: View {
@Environment(Tournament.self) var tournament: Tournament
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 {
List {
Section {
ForEach(teams) { team in
Menu {
_menuOptions(team: team)
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 {
LabeledContent {
Text(label).font(.title3)
} label: {
Text("Paire\(justCalled.count.pluralSuffix) convoquée\(justCalled.count.pluralSuffix)")
Text(subtitle)
}
LabeledContent {
Text(confirmedLabel).font(.title3)
} label: {
HStack {
TeamRowView(team: team, displayCallDate: true)
Spacer()
Menu {
_menuOptions(team: team)
} label: {
LabelOptions().labelStyle(.iconOnly)
}
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 = ""
}
}
.buttonStyle(.plain)
.listRowView(isActive: team.confirmed(), color: .green, hideColorVariation: true)
} header: {
HStack {
Text("Paire\(filteredTeams.count.pluralSuffix)")
Spacer()
Text(filteredTeams.count.formatted())
}
} footer: {
CallView(teams: filteredTeams)
}
} else {
ContentUnavailableView("Aucune équipe", systemImage: "person.2.slash")
}
}
.toolbar(content: {
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)
.navigationTitle("Statut des équipes")
.navigationTitle("Statut des convocations")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
}
}
struct CallMenuOptionsView: View {
@Environment(\.dismiss) private var dismiss
@Environment(Tournament.self) var tournament: Tournament
let team: TeamRegistration
let action: (() -> Void)?
@ViewBuilder
func _menuOptions(team: TeamRegistration) -> some View {
Button {
var confirmed: Binding<Bool> {
Binding {
team.confirmed()
} set: { _ in
team.toggleSummonConfirmation()
do {
try self.tournament.tournamentStore.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
}
} label: {
if team.confirmed() {
Label("Confirmation reçue", systemImage: "checkmark.circle.fill").foregroundStyle(.green)
} else {
Label("Confirmation reçue", systemImage: "circle").foregroundStyle(.logoRed)
}
action?()
}
Divider()
}
Button(role: .destructive) {
team.callDate = nil
do {
try self.tournament.tournamentStore.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
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])
}
} label: {
Text("Effacer la date de convocation")
}
Section {
NavigationLink {
EditingTeamView(team: team)
.environment(tournament)
} label: {
Text("Détails de l'équipe")
}
}
Divider()
Section {
RowButtonView("Effacer la date de convocation", role: .destructive) {
team.callDate = nil
do {
try self.tournament.tournamentStore.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
}
action?()
dismiss()
}
}
Button(role: .destructive) {
team.callDate = team.initialMatch()?.startDate ?? tournament.startDate
do {
try self.tournament.tournamentStore.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
Section {
RowButtonView("Indiquer comme convoquée", role: .destructive) {
team.callDate = team.initialMatch()?.startDate ?? tournament.startDate
do {
try self.tournament.tournamentStore.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
}
action?()
dismiss()
}
}
} label: {
Text("Indiquer comme convoquée")
}
.navigationTitle("Options de convocation")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
}
}

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

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

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

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

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

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

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

@ -11,13 +11,29 @@ struct CopyPasteButtonView: View {
let pasteValue: String?
@State private var copied: Bool = false
@ViewBuilder
var body: some View {
Button {
let pasteboard = UIPasteboard.general
pasteboard.string = pasteValue
copied = true
} label: {
Label(copied ? "copié" : "copier", systemImage: "doc.on.doc").symbolVariant(copied ? .fill : .none)
if let pasteValue {
Button {
let pasteboard = UIPasteboard.general
pasteboard.string = pasteValue
copied = true
} label: {
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
protocol SpinDrawable {
func segmentLabel(_ displayStyle: DisplayStyle) -> [String]
func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String]
}
extension String: SpinDrawable {
func segmentLabel(_ displayStyle: DisplayStyle) -> [String] {
func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] {
[self]
}
}
extension Match: SpinDrawable {
func segmentLabel(_ displayStyle: DisplayStyle) -> [String] {
func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] {
let teams = teams()
if teams.count == 1 {
return teams.first!.segmentLabel(displayStyle)
if teams.count == 1, hideNames == false {
return teams.first!.segmentLabel(displayStyle, hideNames: hideNames)
} else {
return [roundTitle(), matchTitle(displayStyle)].compactMap { $0 }
}
@ -29,12 +29,16 @@ extension Match: SpinDrawable {
}
extension TeamRegistration: SpinDrawable {
func segmentLabel(_ displayStyle: DisplayStyle) -> [String] {
func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] {
var strings: [String] = []
let indexLabel = tournamentObject()?.labelIndexOf(team: self)
if let indexLabel {
strings.append(indexLabel)
if hideNames {
return strings
}
}
strings.append(contentsOf: self.players().map { $0.playerLabel(displayStyle) })
return strings
}
@ -51,8 +55,8 @@ struct DrawOption: Identifiable, SpinDrawable {
let initialIndex: Int
let option: SpinDrawable
func segmentLabel(_ displayStyle: DisplayStyle) -> [String] {
option.segmentLabel(displayStyle)
func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] {
option.segmentLabel(displayStyle, hideNames: hideNames)
}
}
@ -62,6 +66,7 @@ struct SpinDrawView: View {
let drawees: [any SpinDrawable]
@State var segments: [any SpinDrawable]
var autoMode: Bool = false
var hideNames: Bool = false
let completion: ([DrawResult]) async -> Void // Completion closure
@State private var drawCount: Int = 0
@ -89,12 +94,12 @@ struct SpinDrawView: View {
}
} else if drawCount < drawees.count {
Section {
_segmentLabelView(segment: drawees[drawCount].segmentLabel(.wide), horizontalAlignment: .center)
_segmentLabelView(segment: drawees[drawCount].segmentLabel(.wide, hideNames: hideNames), horizontalAlignment: .center)
}
Section {
ZStack {
FortuneWheelContainerView(segments: drawOptions, autoMode: autoMode) { index in
FortuneWheelContainerView(segments: drawOptions, autoMode: autoMode, hideNames: hideNames) { index in
self.selectedIndex = index
self.draws.append(DrawResult(drawee: drawCount, drawIndex: drawOptions[index].initialIndex))
self.drawOptions.remove(at: index)
@ -209,8 +214,8 @@ struct SpinDrawView: View {
private func _segmentLabelView(segment: [String], horizontalAlignment: HorizontalAlignment = .leading) -> some View {
VStack(alignment: horizontalAlignment, spacing: 0.0) {
ForEach(segment, id: \.self) { string in
Text(string).font(.title3)
ForEach(segment.indices, id: \.self) { lineIndex in
Text(segment[lineIndex]).font(.title3)
.frame(maxWidth: .infinity)
.lineLimit(1)
}
@ -221,13 +226,13 @@ struct SpinDrawView: View {
private func _validationLabelView(drawee: Int, result: SpinDrawable) -> some View {
VStack(spacing: 0.0) {
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 {
Image(systemName: "flag.2.crossed.fill").font(.largeTitle).foregroundColor(.logoRed)
} else {
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
let segments: [any SpinDrawable]
let autoMode: Bool
let hideNames: Bool
let completion: (Int) -> Void // Completion closure
var body: some View {
FortuneWheelView(segments: segments)
FortuneWheelView(segments: segments, hideNames: hideNames)
.rotationEffect(.degrees(rotation))
.aspectRatio(contentMode: .fill)
.padding(.top, 5)
@ -303,6 +309,7 @@ struct FortuneWheelContainerView: View {
struct FortuneWheelView: View {
let segments: [any SpinDrawable]
let hideNames: Bool
let colors: [Color] = [.yellow, .cyan, .green, .blue, .orange, .purple, .mint, .brown]
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.closeSubpath()
}
.fill(getColor(forIndex:index))
.fill(getColor(forIndex: index))
VStack(alignment: .trailing, spacing: 0.0) {
let strings = segments[index].segmentLabel(.short)
ForEach(strings, id: \.self) { string in
Text(string).bold()
let strings = labels(forIndex: index)
ForEach(strings.indices, id: \.self) { lineIndex in
Text(strings[lineIndex]).bold()
.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
private func arcPosition(index: Int, radius: Double) -> CGPoint {
let segmentAngle = 360.0 / Double(segments.count)

@ -73,7 +73,7 @@ struct GenericDestinationPickerView<T: Identifiable & Selectable & Equatable >:
)
.offset(x: 3, y: 3)
} 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)
.imageScale(.medium)
.background (
@ -93,7 +93,7 @@ struct GenericDestinationPickerView<T: Identifiable & Selectable & Equatable >:
)
.offset(x: 3, y: 3)
} 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)
.imageScale(.medium)
.background (

@ -10,11 +10,10 @@ import SwiftUI
struct MatchListView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament
@Environment(\.matchViewStyle) private var matchViewStyle
let section: String
let matches: [Match]?
var matchViewStyle: MatchViewStyle = .standardStyle
var hideWhenEmpty: Bool = false
@State var isExpanded: Bool = true
@ -30,24 +29,22 @@ struct MatchListView: View {
@ViewBuilder
var body: some View {
if _shouldHide() == false {
Section {
DisclosureGroup(isExpanded: $isExpanded) {
if let matches {
ForEach(matches) { match in
MatchRowView(match: match, matchViewStyle: matchViewStyle)
.listRowInsets(EdgeInsets(top: 0, leading: -2, bottom: 0, trailing: 8))
}
DisclosureGroup(isExpanded: $isExpanded) {
if let matches {
ForEach(matches) { match in
MatchRowView(match: match)
.listRowInsets(EdgeInsets(top: 0, leading: -2, bottom: 0, trailing: 8))
}
} label: {
LabeledContent {
if matches == nil {
ProgressView()
} else {
Text(matches!.count.formatted() + " match" + matches!.count.pluralSuffix)
}
} label: {
Text(section.firstCapitalized)
}
} label: {
LabeledContent {
if matches == nil {
ProgressView()
} else {
Text(matches!.count.formatted() + " match" + matches!.count.pluralSuffix)
}
} label: {
Text(section.firstCapitalized)
}
}
}

@ -42,7 +42,7 @@ struct GroupStageSettingsView: View {
.submitLabel(.done)
.frame(maxWidth: .infinity)
.onSubmit {
groupStageName = groupStageName.trimmed
groupStageName = groupStageName.prefixTrimmed(200)
if groupStageName.isEmpty == false {
groupStage.name = groupStageName
_save()
@ -143,18 +143,60 @@ struct GroupStageSettingsView: View {
Section {
RowButtonView("Recommencer tous les matchs", role: .destructive) {
let isReturnMatchesEnabled = groupStage.isReturnMatchEnabled()
groupStage.buildMatches()
if isReturnMatchesEnabled {
groupStage.addReturnMatches()
}
}
} footer: {
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) {
if size != groupStage.size {
presentConfirmationButton = true
}
}
.onChange(of: groupStage.matchFormat) {
_save()
groupStage.updateAllMatchesFormat()
}
.navigationBarBackButtonHidden(focusedField != nil)
.toolbar(content: {
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")
.toolbarBackground(.visible, for: .navigationBar)

@ -48,12 +48,20 @@ struct GroupStageTeamView: View {
var body: some View {
List {
Section {
if let name = team.name {
if let name = team.name, name.isEmpty == false {
Text(name).foregroundStyle(.secondary)
}
ForEach(team.players()) { player in
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 {
@ -66,24 +74,28 @@ struct GroupStageTeamView: View {
}
}
}
}
Section {
if team.qualified == false {
RowButtonView("Qualifier l'équipe", role: .destructive) {
team.qualified = true
//team.bracketPosition = nil
_save()
}
} else {
RowButtonView("Annuler la qualification", role: .destructive) {
team.qualified = false
groupStage.tournamentObject()?.resetTeamScores(in: team.bracketPosition)
team.bracketPosition = nil
_save()
}
Section {
if team.qualified == false {
RowButtonView("Qualifier l'équipe", role: .destructive) {
team.qualified = true
//team.bracketPosition = nil
_save()
}
} else {
RowButtonView("Annuler la qualification", role: .destructive) {
team.qualified = false
groupStage.tournamentObject()?.resetTeamScores(in: team.bracketPosition)
team.bracketPosition = nil
_save()
}
}
}
if groupStage.tournamentObject()?.hasEnded() == false {
if team.qualified == false {
Section {
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 {
List {
let playedMatches = groupStage.playedMatches()
Section {
GroupStageScoreView(groupStage: groupStage, sortByScore: sortingMode == .score)
} header: {
@ -49,15 +51,31 @@ struct GroupStageView: View {
}
}
.headerProminence(.increased)
.onChange(of: playedMatches) {
if groupStage.hasEnded() {
sortingMode = .score
}
}
let playedMatches = groupStage.playedMatches()
let runningMatches = groupStage.runningMatches(playedMatches: playedMatches)
MatchListView(section: "en cours", matches: groupStage.runningMatches(playedMatches: playedMatches), hideWhenEmpty: true)
Section {
MatchListView(section: "en cours", matches: groupStage.runningMatches(playedMatches: playedMatches), hideWhenEmpty: true)
}
let availableToStart = groupStage.availableToStart(playedMatches: playedMatches, in: runningMatches)
MatchListView(section: "prêt à démarrer", matches: availableToStart, hideWhenEmpty: true)
.listRowView(isActive: availableToStart.isEmpty == false, color: .green, hideColorVariation: true)
MatchListView(section: "à lancer", matches: groupStage.readyMatches(playedMatches: playedMatches), hideWhenEmpty: true)
MatchListView(section: "terminés", matches: groupStage.finishedMatches(playedMatches: playedMatches), hideWhenEmpty: playedMatches.isEmpty || playedMatches.flatMap({ $0.teamScores }).isEmpty, isExpanded: false)
Section {
MatchListView(section: "prêt à démarrer", matches: availableToStart, hideWhenEmpty: true)
.listRowView(isActive: availableToStart.isEmpty == false, color: .green, hideColorVariation: true)
}
Section {
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)
}
if playedMatches.isEmpty {
RowButtonView("Créer les matchs de poules") {
@ -136,17 +154,16 @@ struct GroupStageView: View {
.font(.footnote)
HStack {
VStack(alignment: .leading) {
if let teamName = team.name {
Text(teamName).font(.title3)
} else {
ForEach(team.players()) { player in
Text(player.playerLabel()).lineLimit(1)
.overlay {
if player.hasArrived && team.isHere() == false {
Color.green.opacity(0.6)
}
if let teamName = team.name, teamName.isEmpty == false {
Text(teamName).foregroundStyle(.secondary).font(.footnote)
}
ForEach(team.players()) { player in
Text(player.playerLabel()).lineLimit(1)
.overlay {
if player.hasArrived && team.isHere() == false {
Color.green.opacity(0.6)
}
}
}
}
}
Spacer()
@ -160,13 +177,11 @@ struct GroupStageView: View {
if let setsDifference = score.setsDifference {
HStack(spacing: 4.0) {
Text(setsDifference)
Text("sets")
}.font(.footnote)
}
if let gamesDifference = score.gamesDifference {
HStack(spacing: 4.0) {
Text(gamesDifference)
Text("jeux")
}.font(.footnote)
}
}

@ -12,7 +12,7 @@ struct GroupStagesSettingsView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(\.dismiss) private var dismiss
@Environment(Tournament.self) var tournament: Tournament
@State private var generationDone: Bool = false
@State private var generationDoneMessage: String?
let step: Int
var tournamentStore: TournamentStore {
@ -168,6 +168,40 @@ struct GroupStagesSettingsView: View {
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 {
RowButtonView("Nommer les poules alphabétiquement", role: .destructive) {
let groupStages = tournament.groupStages()
@ -220,25 +254,29 @@ struct GroupStagesSettingsView: View {
}
.overlay(alignment: .bottom) {
if generationDone {
Label("Poules mises à jour", systemImage: "checkmark.circle.fill")
if let generationDoneMessage {
Label(generationDoneMessage, systemImage: "checkmark.circle.fill")
.toastFormatted()
.deferredRendering(for: .seconds(2))
}
}
.toolbar {
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 {
RowButtonView("Refaire les poules", role: .destructive) {
tournament.deleteGroupStages()
tournament.buildGroupStages()
generationDone = true
generationDoneMessage = "Poules mises à jour"
tournament.shouldVerifyGroupStage = false
_save()
}
@ -248,8 +286,8 @@ struct GroupStagesSettingsView: View {
func menuGenerateGroupStage(_ mode: GroupStageOrderingMode) -> some View {
RowButtonView("Poule \(mode.localizedLabel().lowercased())", role: .destructive, systemImage: mode.systemImage) {
tournament.groupStageOrderingMode = mode
tournament.refreshGroupStages()
generationDone = true
tournament.refreshGroupStages(keepExistingMatches: true)
generationDoneMessage = "Poules mises à jour"
tournament.shouldVerifyGroupStage = false
_save()
}

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

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

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

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

@ -25,17 +25,32 @@ struct MatchTeamDetailView: View {
.headerProminence(.increased)
.tint(.master)
}
.presentationDetents([.fraction(0.66)])
}
@ViewBuilder
private func _teamDetailView(_ team: TeamRegistration, inTournament tournament: Tournament?) -> some View {
Section {
if let teamName = team.name, teamName.isEmpty == false {
Text(teamName).foregroundStyle(.secondary).font(.footnote)
}
ForEach(team.players()) { player in
EditablePlayerView(player: player, editingOptions: _editingOptions())
}
if let coachList = team.comment, coachList.isEmpty == false {
Text("Coachs : " + coachList).foregroundStyle(.secondary).font(.footnote)
}
} 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
struct PlayerBlockView: View {
@Environment(\.matchViewStyle) private var matchViewStyle
@State var match: Match
let teamPosition: TeamPosition
let team: TeamRegistration?
let color: Color
let width: CGFloat
let teamScore: TeamScore?
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.teamPosition = teamPosition
let theTeam = match.team(teamPosition)
self.team = theTeam
self.color = color
self.width = width
let theTeamScore = match.teamScore(ofTeam: theTeam)
self.teamScore = theTeamScore
self.isWalkOut = theTeamScore?.isWalkOut() == true
@ -44,39 +49,67 @@ struct PlayerBlockView: View {
teamScore?.score?.components(separatedBy: ",") ?? []
}
private func _defaultLabel() -> String {
teamPosition.localizedLabel()
private func _defaultLabel() -> [String] {
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 {
HStack {
VStack(alignment: .leading) {
if let names {
if let team {
if let teamScore, teamScore.luckyLoser != nil, match.isLoserBracket == false {
Text("Repêchée").italic().font(.caption)
}
if let name = team?.name {
Text(name).font(.title3)
} else {
ForEach(names, id: \.self) { name in
Text(name).lineLimit(1)
}
if let teamName = team.name {
Text(teamName).foregroundStyle(.secondary).font(.footnote)
}
ForEach(team.players()) { player in
Text(player.playerLabel()).lineLimit(1)
.italic(player.isHere() == false)
.foregroundStyle(player.isHere() == false ? .secondary : .primary)
}
} else {
ZStack(alignment: .leading) {
VStack {
if let name = team?.name {
Text(name).font(.title3)
} else {
Text("longLabelPlayerOne").lineLimit(1)
Text("longLabelPlayerTwo").lineLimit(1)
if let teamName = team?.name {
Text(teamName).foregroundStyle(.secondary).font(.footnote)
}
Text("longLabelPlayerOne").lineLimit(1)
Text("longLabelPlayerTwo").lineLimit(1)
}
.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)
Spacer()
@ -92,7 +125,7 @@ struct PlayerBlockView: View {
if width == 1 {
Divider()
} else {
Divider().frame(width: width).overlay(color)
Divider().frame(width: width).overlay(Color(white: 0.9))
}
Text(string)
.font(.title3)

@ -14,7 +14,7 @@ struct MatchDetailView: View {
@EnvironmentObject var networkMonitor: NetworkMonitor
@Environment(\.dismiss) var dismiss
let matchViewStyle: MatchViewStyle
@Environment(\.matchViewStyle) private var matchViewStyle
@State private var showLiveScore: Bool = false
@State private var editScore: Bool = false
@ -33,6 +33,10 @@ struct MatchDetailView: View {
@State var showSubscriptionView: 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 {
return match.tournamentStore
@ -50,9 +54,8 @@ struct MatchDetailView: View {
var match: Match
init(match: Match, matchViewStyle: MatchViewStyle = .standardStyle) {
init(match: Match, updatedField: Int? = nil) {
self.match = match
self.matchViewStyle = matchViewStyle
if match.hasStarted() == false && (match.startDate == nil || match.courtIndex == nil) {
_isEditing = State(wrappedValue: true)
@ -69,7 +72,7 @@ struct MatchDetailView: View {
_endDate = State(wrappedValue: endDate)
}
if let courtIndex = match.courtIndex {
if let courtIndex = updatedField ?? match.courtIndex {
_fieldSetup = State(wrappedValue: .field(courtIndex))
}
}
@ -85,7 +88,8 @@ struct MatchDetailView: View {
}
Section {
MatchSummaryView(match: match, matchViewStyle: .plainStyle)
MatchSummaryView(match: match)
.matchViewStyle(.plainStyle)
} footer: {
if match.isEmpty() == false {
HStack {
@ -153,13 +157,47 @@ struct MatchDetailView: View {
}
}
})
.sheet(item: $scoreType, onDismiss: {
if match.hasEnded() {
.sheet(isPresented: $presentFollowUpMatch, onDismiss: {
if dismissWhenPresentFollowUpMatchIsDismissed {
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
let matchDescriptor = MatchDescriptor(match: match)
EditScoreView(matchDescriptor: matchDescriptor)
EditScoreView(match: match, confirmScoreEdition: $confirmScoreEdition)
.tint(.master)
// switch scoreType {
@ -305,6 +343,7 @@ struct MatchDetailView: View {
match.resetScores()
match.resetMatch()
match.confirmed = false
match.updateFollowingMatchTeamScore()
save()
} label: {
Text("Supprimer les scores")
@ -319,6 +358,26 @@ struct MatchDetailView: View {
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: {
LabelOptions()
}
@ -402,13 +461,13 @@ struct MatchDetailView: View {
Text("Partage sur les réseaux sociaux")
}
// if let followUpMatch = match.followUpMatch {
// Section {
// MatchRowView(match: followUpMatch)
// } header: {
// Text("à suivre terrain \(match.fieldIndex)")
// }
// }
if match.currentTournament()?.hasEnded() == false {
Section {
RowButtonView("Match à suivre") {
presentFollowUpMatch = true
}
}
}
}
var editionView: some View {
@ -429,20 +488,27 @@ struct MatchDetailView: View {
Text("Dans 5 minutes").tag(MatchDateSetup.inMinutes(5))
Text("Dans 15 minutes").tag(MatchDateSetup.inMinutes(15))
}
Text("Précédente rotation").tag(MatchDateSetup.inMinutes(-rotationDuration))
Text("Prochaine rotation").tag(MatchDateSetup.inMinutes(rotationDuration))
Text("Précédente rotation").tag(MatchDateSetup.previousRotation)
Text("Prochaine rotation").tag(MatchDateSetup.nextRotation)
Text("À").tag(MatchDateSetup.customDate)
} label: {
Text("Horaire")
}
.onChange(of: startDateSetup) {
let date = Date().withoutSeconds()
switch startDateSetup {
case .customDate:
break
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):
startDate = Date().addingTimeInterval(Double(minutes) * 60)
startDate = date.addingTimeInterval(Double(minutes) * 60)
}
}
}
@ -484,7 +550,7 @@ struct MatchDetailView: View {
}
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()

@ -6,13 +6,15 @@
//
import SwiftUI
import LeStorage
struct MatchRowView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(\.matchViewStyle) private var matchViewStyle
@State var match: Match
let matchViewStyle: MatchViewStyle
var title: String? = nil
var updatedField: Int? = nil
@Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed
@ -58,10 +60,28 @@ struct MatchRowView: View {
// })
NavigationLink {
MatchDetailView(match: match, matchViewStyle: matchViewStyle)
MatchDetailView(match: match, updatedField: updatedField)
} label: {
MatchSummaryView(match: match, matchViewStyle: matchViewStyle, title: title)
MatchSummaryView(match: match, title: title, updatedField: updatedField)
.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 {
EditSharingView(match: match)
} label: {

@ -9,7 +9,7 @@ import SwiftUI
import LeStorage
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
@ -166,7 +166,7 @@ struct MatchSetupView: View {
Text("Libérer")
.underline()
}
} else {
} else if match.isFromLastRound() == false {
ConfirmButtonView(shouldConfirm: shouldConfirm, message: MatchSetupView.confirmationMessage) {
_ = match.lockAndGetSeedPosition(atTeamPosition: teamPosition)
do {
@ -190,16 +190,22 @@ struct MatchSetupView: View {
func _removeTeam(team: TeamRegistration, teamPosition: TeamPosition) -> some View {
Button(role: .cancel) {
//todo
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
do {
try tournamentStore.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
}
//match.updateTeamScores()
match.previousMatches().forEach { previousMatch in
if let previousMatch = match.previousMatch(teamPosition) {
if previousMatch.disabled {
previousMatch.enableMatch()
do {

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

@ -134,8 +134,12 @@ struct ActivityView: View {
ContentUnavailableView {
Label("Une erreur est survenue", systemImage: "exclamationmark.circle.fill")
} description: {
Text(error.localizedDescription)
Text("Tenup est peut-être en maintenance. " + error.localizedDescription)
} actions: {
Link(destination: URLs.tenup.url) {
Text("Voir si tenup est en maintenance")
}
RowButtonView("D'accord.") {
self.error = nil
}
@ -510,15 +514,32 @@ struct ActivityView: View {
.padding()
}
} else {
ContentUnavailableView {
Label("Aucun tournoi", systemImage: "shield.slash")
} description: {
Text("Aucun tournoi ne correspond aux critères sélectionnés.")
} actions: {
FooterButtonView("modifier vos critères de recherche") {
displaySearchView = true
if federalDataViewModel.lastError == nil {
ContentUnavailableView {
Label("Aucun tournoi", systemImage: "shield.slash")
} description: {
Text("Aucun tournoi ne correspond aux critères sélectionnés.")
} actions: {
FooterButtonView("modifier vos critères de recherche") {
displaySearchView = true
}
.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()
}
.padding()
}
}
}

@ -124,7 +124,7 @@ struct CalendarView: View {
)
.overlay(alignment: .bottomTrailing) {
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)
.imageScale(.medium)
.background (

@ -181,6 +181,7 @@ struct TournamentLookUpView: View {
federalDataViewModel.levels = Set(levels)
federalDataViewModel.categories = Set(categories)
federalDataViewModel.ageCategories = Set(ages)
federalDataViewModel.lastError = nil
Task {
await getNewPage()
@ -223,7 +224,12 @@ struct TournamentLookUpView: View {
await getNewBuildForm()
} 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)
if commands.anySatisfy({ $0.command == "alert" }) {
federalDataViewModel.lastError = .maintenance
}
let resultCommand = commands.first(where: { $0.results != nil })
if let newTournaments = resultCommand?.results?.items {
newTournaments.forEach { ft in
// let isValid = ft.tournaments.anySatisfy({ build in
@ -363,7 +369,7 @@ struct TournamentLookUpView: View {
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
Text(type.localizedLabel())
Text(type.localizedFederalAgeLabel())
}
.navigationTitle("Limites d'âge")
.environment(\.editMode, Binding.constant(EditMode.active))
@ -375,7 +381,7 @@ struct TournamentLookUpView: View {
Text("Tous les âges")
.foregroundStyle(.secondary)
} else {
Text(ages.map({ $0.localizedLabel()}).joined(separator: ", "))
Text(ages.map({ $0.localizedFederalAgeLabel()}).joined(separator: ", "))
.foregroundStyle(.secondary)
}
}

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

@ -79,7 +79,7 @@ struct MainView: View {
TournamentOrganizerView()
.tabItem(for: .tournamentOrganizer)
.toolbarBackground(.visible, for: .tabBar)
OngoingView()
OngoingContainerView()
.tabItem(for: .ongoing)
.badge(self.dataStore.runningMatches().count)
.toolbarBackground(.visible, for: .tabBar)
@ -263,7 +263,7 @@ struct MainView: View {
await _startImporting(importingDate: mostRecentDateImported)
} else if current.dataModelIdentifier != PersistenceController.getModelVersion() && current.fileModelIdentifier != fileURL?.fileModelIdentifier() {
await _startImporting(importingDate: mostRecentDateImported)
} else if current.incompleteMode == false || updated == 0 {
} else if updated == 0 {
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,82 +8,110 @@
import SwiftUI
import LeStorage
extension Int: @retroactive Identifiable {
public var id: Int {
return self
}
}
struct OngoingView: View {
@Environment(NavigationViewModel.self) private var navigation: NavigationViewModel
@EnvironmentObject var dataStore: DataStore
@Environment(OngoingViewModel.self) private var ongoingViewModel: OngoingViewModel
@State private var sortByField: Bool = false
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 filterMode: OngoingDestination {
ongoingViewModel.destination!
}
var body: some View {
@Bindable var navigation = navigation
NavigationStack(path: $navigation.ongoingPath) {
List {
ForEach(matches) { match in
if let tournament = match.currentTournament() {
Section {
MatchRowView(match: match, matchViewStyle: .standardStyle)
} header: {
HStack {
Text(tournament.tournamentTitle(.short))
Spacer()
if let club = tournament.club() {
Text("@" + club.clubTitle(.short))
}
let filteredMatches = filterMode.sortedMatches
List {
ForEach(filteredMatches) { match in
let tournament = match.currentTournament()
Section {
MatchRowView(match: match)
.matchViewStyle(.followUpStyle)
} header: {
if let tournament {
HStack {
Text(tournament.tournamentTitle(.short))
Spacer()
if let club = tournament.club() {
Text("@" + club.clubTitle(.short))
}
} footer: {
HStack {
Text(tournament.eventLabel())
}
}
} footer: {
HStack {
if let tournament {
Text(tournament.eventLabel())
}
#if DEBUG
Spacer()
FooterButtonView("copier l'id") {
let pasteboard = UIPasteboard.general
pasteboard.string = match.id
}
#endif
}
Spacer()
FooterButtonView("copier l'id") {
let pasteboard = UIPasteboard.general
pasteboard.string = match.id
}
#endif
}
}
}
.headerProminence(.increased)
.overlay {
if matches.isEmpty {
ContentUnavailableView("Aucun match en cours", systemImage: "figure.tennis", description: Text("Tous vos matchs en cours seront visibles ici, quelque soit le tournoi."))
}
}
.headerProminence(.increased)
.overlay {
if filteredMatches.isEmpty {
filterMode.contentUnavailable()
}
.navigationTitle("En cours")
.toolbarBackground(.visible, for: .bottomBar)
.toolbar(matches.isEmpty ? .hidden : .visible, for: .navigationBar)
.toolbar {
ToolbarItem(placement: .status) {
Picker(selection: $sortByField) {
Text("tri par date").tag(true)
Text("tri par terrain").tag(false)
} label: {
}
}
}
struct OngoingCourtView: View {
@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 {
@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 {
@Bindable var user = dataStore.user
List {
Section {
Picker(selection: $user.groupStageMatchFormatPreference) {
Text("Automatique").tag(nil as MatchFormat?)
ForEach(MatchFormat.allCases, id: \.self) { format in
Text(format.format).tag(format as MatchFormat?)
}
} label: {
HStack {
Text("Poule")
Spacer()
}
Toggle(isOn: groupStageMatchFormatPreference) {
Text("Automatique")
}
Picker(selection: $user.bracketMatchFormatPreference) {
Text("Automatique").tag(nil as MatchFormat?)
ForEach(MatchFormat.allCases, id: \.self) { format in
Text(format.format).tag(format as MatchFormat?)
}
} label: {
HStack {
Text("Tableau")
Spacer()
}
if groupStageMatchFormatPreference.wrappedValue == false {
MatchTypeSelectionView(selectedFormat: groupStageMatchFormat)
}
} header: {
Text("Poule")
} 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.loserBracketMatchFormatPreference) {
Text("Automatique").tag(nil as MatchFormat?)
ForEach(MatchFormat.allCases, id: \.self) { format in
Text(format.format).tag(format as MatchFormat?)
}
} label: {
HStack {
Text("Match de classement")
Spacer()
}
if bracketMatchFormatPreference.wrappedValue == false {
MatchTypeSelectionView(selectedFormat: bracketMatchFormat)
}
} header: {
Text("Tableau")
} footer: {
Text("À minima, les règles fédérales seront toujours prises en compte par défaut.")
}
Section {
Toggle(isOn: loserBracketMatchFormatPreference) {
Text("Automatique")
}
if loserBracketMatchFormatPreference.wrappedValue == false {
MatchTypeSelectionView(selectedFormat: loserBracketMatchFormat)
}
} header: {
Text("Vos formats préférés")
Text("Match de classement")
} footer: {
Text("À minima, les règles fédérales seront toujours prises en compte par défaut.")
}
}
.headerProminence(.increased)
.onChange(of: [
user.bracketMatchFormatPreference,
user.groupStageMatchFormatPreference,

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

@ -129,7 +129,7 @@ struct ToolboxView: View {
Section {
NavigationLink {
SelectablePlayerListView(isPresented: false)
SelectablePlayerListView(isPresented: false, lastDataSource: true)
} label: {
Label("Rechercher un joueur", systemImage: "person.fill.viewfinder")
}
@ -196,10 +196,17 @@ struct ToolboxView: View {
Text("Contrat d'utilisation")
}
}
Section {
RowButtonView("Effacer les logs", role: .destructive) {
StoreCenter.main.resetLoggingCollections()
didResetApiCalls = true
}
}
}
.overlay(alignment: .bottom) {
if didResetApiCalls {
Label("failed api calls deleted", systemImage: "checkmark")
Label("logs effacés", systemImage: "checkmark")
.toastFormatted()
.deferredRendering(for: .seconds(3))
.onAppear {
@ -221,10 +228,9 @@ struct ToolboxView: View {
ShareLink(item: URLs.appStore.url) {
Label("Lien AppStore", systemImage: "link")
}
if let zip = _getZip() {
ShareLink(item: zip) {
Label("Mes données", systemImage: "server.rack")
}
ShareLink(item: ZipLog(), preview: .init("Mon archive")) {
Label("Mes données", systemImage: "server.rack")
}
} label: {
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? {
do {
let filePath = try Club.storageDirectoryPath()
@ -243,8 +256,19 @@ struct ToolboxView: View {
return nil
}
}
}
//#Preview {
// ToolboxView()
//}
func shareFile() -> URL? {
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 LeStorage
import Foundation
struct PadelClubView: View {
@State private var uuid: UUID = UUID()
@ -74,9 +75,14 @@ struct PadelClubView: View {
print("before anonymousPlayers.count", anonymousPlayers.count)
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)
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 {
Logger.error(error)
}
@ -241,3 +247,71 @@ struct PadelClubView: View {
//#Preview {
// 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.")
}
Section {
NavigationLink {
UmpireStatisticView()
} label: {
Text("Statistiques de participations")
}
}
Section {
@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
//
// Created by Razmig Sarkissian on 17/04/2024.
@ -91,225 +91,3 @@ struct DatePickingView: View {
.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
@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?
var courtsUnavailability: [Int: [DateInterval]] {
@ -45,10 +42,6 @@ struct CourtAvailabilitySettingsView: View {
}
Button("éditer") {
editingSlot = dateInterval
courtIndex = dateInterval.courtIndex
startDate = dateInterval.startDate
endDate = dateInterval.endDate
showingPopover = true
}
Button("effacer", role: .destructive) {
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.")
} actions: {
RowButtonView("Ajouter une indisponibilité", systemImage: "plus.circle.fill") {
startDate = tournament.startDate
endDate = tournament.startDate.addingTimeInterval(5400)
showingPopover = true
}
}
@ -120,8 +111,6 @@ struct CourtAvailabilitySettingsView: View {
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
BarButtonView("Ajouter une indisponibilité", icon: "plus.circle.fill") {
startDate = tournament.startDate
endDate = tournament.startDate.addingTimeInterval(5400)
showingPopover = true
}
}
@ -130,34 +119,99 @@ struct CourtAvailabilitySettingsView: View {
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Créneau indisponible")
.sheet(isPresented: $showingPopover) {
NavigationStack {
Form {
Section {
CourtPicker(title: "Terrain", selection: $courtIndex, maxCourt: tournament.courtCount)
}
CourtAvailabilityEditorView(event: event)
}
.sheet(item: $editingSlot) { editingSlot in
CourtAvailabilityEditorView(editingSlot: editingSlot, event: event)
}
}
}
Section {
DatePicker("Début", selection: $startDate)
.onChange(of: startDate) {
if endDate < startDate {
endDate = startDate.addingTimeInterval(90*60)
}
}
DatePicker("Fin", selection: $endDate)
.onChange(of: endDate) {
if startDate > endDate {
startDate = endDate.addingTimeInterval(-90*60)
}
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 {
Form {
Section {
CourtPicker(title: "Terrain", selection: $courtIndex, maxCourt: tournament.courtCount)
}
Section {
DatePicker("Début", selection: $startDate)
.onChange(of: startDate) {
if endDate < startDate {
endDate = startDate.addingTimeInterval(90*60)
}
} footer: {
FooterButtonView("jour entier") {
startDate = startDate.startOfDay
endDate = startDate.endOfDay()
}
DatePicker("Fin", selection: $endDate)
.onChange(of: endDate) {
if startDate > endDate {
startDate = endDate.addingTimeInterval(-90*60)
}
}
} footer: {
FooterButtonView("jour entier") {
startDate = startDate.startOfDay
endDate = startDate.tomorrowAtNine.startOfDay
}
}
.toolbar {
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 {
ToolbarItem(placement: .topBarTrailing) {
ButtonValidateView {
if editingSlot == nil {
let dateInterval = DateInterval(event: event.id, courtIndex: courtIndex, startDate: startDate, endDate: endDate)
@ -176,40 +230,50 @@ struct CourtAvailabilitySettingsView: View {
Logger.error(error)
}
}
showingPopover = false
dismiss()
}
}
ToolbarItem(placement: .topBarLeading) {
Button("Annuler", role: .cancel) {
dismiss()
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Nouveau créneau")
.tint(.master)
}
.onAppear {
UIDatePicker.appearance().minuteInterval = 5
}
.onDisappear {
UIDatePicker.appearance().minuteInterval = 1
}
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle(_navigationTitle())
.tint(.master)
}
}
}
struct CourtPicker: View {
@Environment(Tournament.self) var tournament: Tournament
private func _navigationTitle() -> String {
editingSlot == nil ? "Nouveau créneau" : "Édition du créneau"
}
}
let title: String
@Binding var selection: Int
let maxCourt: Int
struct DateAdjusterView: View {
@Binding var date: Date
var body: some View {
Picker(title, selection: $selection) {
ForEach(0..<maxCourt, id: \.self) {
Text(tournament.courtName(atIndex: $0))
}
HStack {
_createButton(label: "-1h", timeOffset: -1, component: .hour)
_createButton(label: "-30m", timeOffset: -30, component: .minute)
_createButton(label: "+30m", timeOffset: 30, component: .minute)
_createButton(label: "+1h", timeOffset: 1, component: .hour)
}
.font(.headline)
}
}
//#Preview {
// CourtAvailabilitySettingsView(event: Event.mock())
//}
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)
}
}

@ -15,6 +15,7 @@ struct GroupStageScheduleEditorView: View {
@Bindable var groupStage: GroupStage
var tournament: Tournament
@State private var startDate: Date
@State private var currentDate: Date?
var tournamentStore: TournamentStore {
return self.tournament.tournamentStore
@ -24,14 +25,19 @@ struct GroupStageScheduleEditorView: View {
self.groupStage = groupStage
self.tournament = tournament
self._startDate = State(wrappedValue: groupStage.startDate ?? tournament.startDate)
self._currentDate = State(wrappedValue: groupStage.startDate)
}
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
tournament.matchScheduler()?.updateGroupStageSchedule(tournament: tournament, specificGroupStage: groupStage)
_save()
}
.onChange(of: currentDate) {
groupStage.startDate = currentDate
_save()
}
}
private func _save() {

@ -17,6 +17,7 @@ struct LoserRoundScheduleEditorView: View {
var loserRounds: [Round]
@State private var startDate: Date
@State private var matchFormat: MatchFormat
@State private var currentDate: Date?
var tournamentStore: TournamentStore {
return self.tournament.tournamentStore
@ -27,8 +28,11 @@ struct LoserRoundScheduleEditorView: View {
self.tournament = tournament
let _loserRounds = upperRound.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._currentDate = State(wrappedValue: startDate)
}
var body: some View {
@ -37,9 +41,17 @@ struct LoserRoundScheduleEditorView: View {
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()
}
.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 })
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