merge conflict

multistore
Laurent 2 years ago
commit 3e01aeb7ff
  1. 150
      PadelClub.xcodeproj/project.pbxproj
  2. 36
      PadelClub/Data/AppSettings.swift
  3. 11
      PadelClub/Data/Club.swift
  4. 43
      PadelClub/Data/DataStore.swift
  5. 1
      PadelClub/Data/Event.swift
  6. 26
      PadelClub/Data/Federal/FederalPlayer.swift
  7. 1
      PadelClub/Data/Federal/FederalTournament.swift
  8. 31
      PadelClub/Data/GroupStage.swift
  9. 139
      PadelClub/Data/Match.swift
  10. 20
      PadelClub/Data/MockData.swift
  11. 53
      PadelClub/Data/MonthData.swift
  12. 30
      PadelClub/Data/PlayerRegistration.swift
  13. 132
      PadelClub/Data/Round.swift
  14. 17
      PadelClub/Data/TeamRegistration.swift
  15. 222
      PadelClub/Data/Tournament.swift
  16. 19
      PadelClub/Extensions/Date+Extensions.swift
  17. 5
      PadelClub/Extensions/String+Extensions.swift
  18. 15
      PadelClub/Manager/ContactManager.swift
  19. 270
      PadelClub/Manager/FileImportManager.swift
  20. 111
      PadelClub/Manager/PadelRule.swift
  21. 2
      PadelClub/Manager/SourceFileManager.swift
  22. 4
      PadelClub/ViewModel/AgendaDestination.swift
  23. 13
      PadelClub/ViewModel/AppScreen.swift
  24. 32
      PadelClub/ViewModel/DateInterval.swift
  25. 4
      PadelClub/ViewModel/MatchDescriptor.swift
  26. 112
      PadelClub/ViewModel/MatchScheduler.swift
  27. 1
      PadelClub/ViewModel/NavigationViewModel.swift
  28. 2
      PadelClub/ViewModel/SearchViewModel.swift
  29. 31
      PadelClub/ViewModel/SeedInterval.swift
  30. 30
      PadelClub/ViewModel/Selectable.swift
  31. 13
      PadelClub/ViewModel/TournamentSeedEditing.swift
  32. 186
      PadelClub/Views/Calling/CallMessageCustomizationView.swift
  33. 19
      PadelClub/Views/Calling/CallSettingsView.swift
  34. 6
      PadelClub/Views/Calling/CallView.swift
  35. 26
      PadelClub/Views/Calling/GroupStageCallingView.swift
  36. 2
      PadelClub/Views/Calling/SeedsCallingView.swift
  37. 76
      PadelClub/Views/Cashier/CashierDetailView.swift
  38. 56
      PadelClub/Views/Cashier/CashierSettingsView.swift
  39. 311
      PadelClub/Views/Cashier/CashierView.swift
  40. 18
      PadelClub/Views/Cashier/PlayerListView.swift
  41. 1
      PadelClub/Views/Club/ClubSearchView.swift
  42. 23
      PadelClub/Views/ClubView.swift
  43. 18
      PadelClub/Views/Components/FooterButtonView.swift
  44. 33
      PadelClub/Views/Components/GenericDestinationPickerView.swift
  45. 4
      PadelClub/Views/Components/Labels.swift
  46. 21
      PadelClub/Views/Components/MatchListView.swift
  47. 83
      PadelClub/Views/Components/RowButtonView.swift
  48. 8
      PadelClub/Views/Components/StepperView.swift
  49. 82
      PadelClub/Views/ContentView.swift
  50. 12
      PadelClub/Views/Event/EventCreationView.swift
  51. 12
      PadelClub/Views/Event/TournamentConfiguratorView.swift
  52. 25
      PadelClub/Views/GroupStage/GroupStageView.swift
  53. 4
      PadelClub/Views/GroupStage/GroupStagesView.swift
  54. 51
      PadelClub/Views/Match/Components/MatchDateView.swift
  55. 45
      PadelClub/Views/Match/Components/MatchTeamDetailView.swift
  56. 7
      PadelClub/Views/Match/Components/PlayerBlockView.swift
  57. 140
      PadelClub/Views/Match/MatchDetailView.swift
  58. 40
      PadelClub/Views/Match/MatchRowView.swift
  59. 21
      PadelClub/Views/Match/MatchSetupView.swift
  60. 6
      PadelClub/Views/Navigation/Agenda/ActivityView.swift
  61. 43
      PadelClub/Views/Navigation/MainView.swift
  62. 1
      PadelClub/Views/Navigation/Organizer/TournamentOrganizerView.swift
  63. 48
      PadelClub/Views/Navigation/PadelClubView.swift
  64. 25
      PadelClub/Views/Navigation/Toolbox/DurationSettingsView.swift
  65. 67
      PadelClub/Views/Navigation/Toolbox/GlobalSettingsView.swift
  66. 50
      PadelClub/Views/Navigation/Toolbox/MatchFormatStorageView.swift
  67. 33
      PadelClub/Views/Navigation/Toolbox/ToolboxView.swift
  68. 2
      PadelClub/Views/Navigation/Umpire/UmpireView.swift
  69. 54
      PadelClub/Views/Planning/Components/DateUpdateManagerView.swift
  70. 157
      PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift
  71. 34
      PadelClub/Views/Planning/GroupStageScheduleEditorView.swift
  72. 85
      PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift
  73. 79
      PadelClub/Views/Planning/LoserRoundStepScheduleEditorView.swift
  74. 9
      PadelClub/Views/Planning/MatchScheduleEditorView.swift
  75. 94
      PadelClub/Views/Planning/PlanningSettingsView.swift
  76. 112
      PadelClub/Views/Planning/PlanningView.swift
  77. 24
      PadelClub/Views/Planning/RoundScheduleEditorView.swift
  78. 104
      PadelClub/Views/Player/Components/EditablePlayerView.swift
  79. 114
      PadelClub/Views/Player/PlayerDetailView.swift
  80. 53
      PadelClub/Views/Round/LoserBracketView.swift
  81. 72
      PadelClub/Views/Round/LoserRoundView.swift
  82. 131
      PadelClub/Views/Round/LoserRoundsView.swift
  83. 14
      PadelClub/Views/Round/RoundSettingsView.swift
  84. 30
      PadelClub/Views/Round/RoundView.swift
  85. 6
      PadelClub/Views/Round/RoundsView.swift
  86. 8
      PadelClub/Views/Score/EditScoreView.swift
  87. 10
      PadelClub/Views/Score/PointSelectionView.swift
  88. 2
      PadelClub/Views/Score/PointView.swift
  89. 2
      PadelClub/Views/Score/SetInputView.swift
  90. 24
      PadelClub/Views/Shared/ImportedPlayerView.swift
  91. 3
      PadelClub/Views/Shared/MatchFormatPickerView.swift
  92. 24
      PadelClub/Views/Shared/SelectablePlayerListView.swift
  93. 43
      PadelClub/Views/Team/Components/TeamHeaderView.swift
  94. 38
      PadelClub/Views/Team/Components/TeamWeightView.swift
  95. 45
      PadelClub/Views/Team/EditingTeamView.swift
  96. 8
      PadelClub/Views/Team/TeamDetailView.swift
  97. 2
      PadelClub/Views/Team/TeamPickerView.swift
  98. 18
      PadelClub/Views/Team/TeamRowView.swift
  99. 66
      PadelClub/Views/Tournament/FileImportView.swift
  100. 41
      PadelClub/Views/Tournament/Screen/CashierDetailView.swift
  101. Some files were not shown because too many files have changed in this diff Show More

@ -8,7 +8,6 @@
/* Begin PBXBuildFile section */
C425D4012B6D249D002A7B48 /* PadelClubApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C425D4002B6D249D002A7B48 /* PadelClubApp.swift */; };
C425D4032B6D249D002A7B48 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C425D4022B6D249D002A7B48 /* ContentView.swift */; };
C425D4052B6D249E002A7B48 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C425D4042B6D249E002A7B48 /* Assets.xcassets */; };
C425D4082B6D249E002A7B48 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C425D4072B6D249E002A7B48 /* Preview Assets.xcassets */; };
C425D4122B6D249E002A7B48 /* PadelClubTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C425D4112B6D249E002A7B48 /* PadelClubTests.swift */; };
@ -22,7 +21,6 @@
C4A47D5A2B6D383C00ADC637 /* Tournament.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D592B6D383C00ADC637 /* Tournament.swift */; };
C4A47D5E2B6D38EC00ADC637 /* DataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D5D2B6D38EC00ADC637 /* DataStore.swift */; };
C4A47D632B6D3D6500ADC637 /* Club.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D622B6D3D6500ADC637 /* Club.swift */; };
C4A47D742B72881F00ADC637 /* ClubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D732B72881F00ADC637 /* ClubView.swift */; };
C4A47D772B73789100ADC637 /* TournamentV1.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D762B73789100ADC637 /* TournamentV1.swift */; };
C4A47D7B2B73C0F900ADC637 /* TournamentV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D7A2B73C0F900ADC637 /* TournamentV2.swift */; };
C4A47D7D2B73CDC300ADC637 /* ClubV1.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47D7C2B73CDC300ADC637 /* ClubV1.swift */; };
@ -37,6 +35,18 @@
C4A47DAD2B85FCCD00ADC637 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47DAC2B85FCCD00ADC637 /* User.swift */; };
C4A47DB12B86375E00ADC637 /* MainUserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47DB02B86375E00ADC637 /* MainUserView.swift */; };
C4A47DB32B86387500ADC637 /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47DB22B86387500ADC637 /* AccountView.swift */; };
FF025AD82BD0C10F00A86CF8 /* TeamHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AD72BD0C10F00A86CF8 /* TeamHeaderView.swift */; };
FF025ADB2BD0C2D000A86CF8 /* MatchTeamDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025ADA2BD0C2D000A86CF8 /* MatchTeamDetailView.swift */; };
FF025ADD2BD0C94300A86CF8 /* FooterButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025ADC2BD0C94300A86CF8 /* FooterButtonView.swift */; };
FF025ADF2BD0CE0A00A86CF8 /* TeamWeightView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025ADE2BD0CE0A00A86CF8 /* TeamWeightView.swift */; };
FF025AE12BD0EB9000A86CF8 /* TournamentClubSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE02BD0EB9000A86CF8 /* TournamentClubSettingsView.swift */; };
FF025AE32BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE22BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift */; };
FF025AE52BD0EBB800A86CF8 /* TournamentGeneralSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE42BD0EBB800A86CF8 /* TournamentGeneralSettingsView.swift */; };
FF025AE72BD1111000A86CF8 /* GlobalSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE62BD1111000A86CF8 /* GlobalSettingsView.swift */; };
FF025AE92BD1307F00A86CF8 /* MonthData.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE82BD1307E00A86CF8 /* MonthData.swift */; };
FF025AED2BD1513700A86CF8 /* AppScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AEC2BD1513700A86CF8 /* AppScreen.swift */; };
FF025AEF2BD1AE9400A86CF8 /* DurationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AEE2BD1AE9400A86CF8 /* DurationSettingsView.swift */; };
FF025AF12BD1AEBD00A86CF8 /* MatchFormatStorageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AF02BD1AEBD00A86CF8 /* MatchFormatStorageView.swift */; };
FF089EB42BB0020000F0AEC7 /* PlayerSexPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF089EB32BB0020000F0AEC7 /* PlayerSexPickerView.swift */; };
FF089EB62BB00A3800F0AEC7 /* TeamRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF089EB52BB00A3800F0AEC7 /* TeamRowView.swift */; };
FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF089EBA2BB0120700F0AEC7 /* PlayerPopoverView.swift */; };
@ -87,6 +97,15 @@
FF0EC5752BB195E20056B6D1 /* CLASSEMENT-PADEL-DAMES-11-2022.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5342BB195CA0056B6D1 /* CLASSEMENT-PADEL-DAMES-11-2022.csv */; };
FF0EC5762BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-04-2023.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC5452BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-04-2023.csv */; };
FF0EC5772BB195E20056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-10-2022.csv in Resources */ = {isa = PBXBuildFile; fileRef = FF0EC54A2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-10-2022.csv */; };
FF11627A2BCF8109000C4809 /* CallMessageCustomizationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162792BCF8109000C4809 /* CallMessageCustomizationView.swift */; };
FF11627D2BCF941A000C4809 /* CashierSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF11627C2BCF941A000C4809 /* CashierSettingsView.swift */; };
FF11627F2BCF9432000C4809 /* PlayerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF11627E2BCF9432000C4809 /* PlayerListView.swift */; };
FF1162812BCF945C000C4809 /* TournamentCashierView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162802BCF945C000C4809 /* TournamentCashierView.swift */; };
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 */; };
FF11628C2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.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 */; };
@ -195,7 +214,7 @@
FFC1E1082BAC29FC008D6F59 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */; };
FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */; };
FFC1E10C2BAC7FB0008D6F59 /* ClubImportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E10B2BAC7FB0008D6F59 /* ClubImportView.swift */; };
FFC2DCB22BBE75D40046DB9F /* LoserBracketView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DCB12BBE75D40046DB9F /* LoserBracketView.swift */; };
FFC2DCB22BBE75D40046DB9F /* LoserRoundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DCB12BBE75D40046DB9F /* LoserRoundView.swift */; };
FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */; };
FFC83D4F2BB807D100750834 /* RoundsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC83D4E2BB807D100750834 /* RoundsView.swift */; };
FFC83D512BB8087E00750834 /* RoundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC83D502BB8087E00750834 /* RoundView.swift */; };
@ -214,6 +233,8 @@
FFDB1C6D2BB2A02000F1E467 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */; };
FFDB1C732BB2CFE900F1E467 /* MySortDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDB1C722BB2CFE900F1E467 /* MySortDescriptor.swift */; };
FFDDD40C2B93B2BB00C91A49 /* DeferredViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */; };
FFF116E12BD2A9B600A33B06 /* DateInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF116E02BD2A9B600A33B06 /* DateInterval.swift */; };
FFF116E32BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF116E22BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift */; };
FFF527D62BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */; };
FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACCC2B92367B008466FA /* FederalPlayer.swift */; };
FFF8ACD42B92392C008466FA /* SourceFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACD32B92392C008466FA /* SourceFileManager.swift */; };
@ -276,7 +297,6 @@
/* Begin PBXFileReference section */
C425D3FD2B6D249D002A7B48 /* PadelClub.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PadelClub.app; sourceTree = BUILT_PRODUCTS_DIR; };
C425D4002B6D249D002A7B48 /* PadelClubApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PadelClubApp.swift; sourceTree = "<group>"; };
C425D4022B6D249D002A7B48 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
C425D4042B6D249E002A7B48 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
C425D4072B6D249E002A7B48 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
C425D40D2B6D249E002A7B48 /* PadelClubTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PadelClubTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@ -293,7 +313,6 @@
C4A47D592B6D383C00ADC637 /* Tournament.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tournament.swift; sourceTree = "<group>"; };
C4A47D5D2B6D38EC00ADC637 /* DataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStore.swift; sourceTree = "<group>"; };
C4A47D622B6D3D6500ADC637 /* Club.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Club.swift; sourceTree = "<group>"; };
C4A47D732B72881F00ADC637 /* ClubView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubView.swift; sourceTree = "<group>"; };
C4A47D762B73789100ADC637 /* TournamentV1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentV1.swift; sourceTree = "<group>"; };
C4A47D7A2B73C0F900ADC637 /* TournamentV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentV2.swift; sourceTree = "<group>"; };
C4A47D7C2B73CDC300ADC637 /* ClubV1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubV1.swift; sourceTree = "<group>"; };
@ -308,6 +327,18 @@
C4A47DAC2B85FCCD00ADC637 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = "<group>"; };
C4A47DB02B86375E00ADC637 /* MainUserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainUserView.swift; sourceTree = "<group>"; };
C4A47DB22B86387500ADC637 /* AccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountView.swift; sourceTree = "<group>"; };
FF025AD72BD0C10F00A86CF8 /* TeamHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamHeaderView.swift; sourceTree = "<group>"; };
FF025ADA2BD0C2D000A86CF8 /* MatchTeamDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchTeamDetailView.swift; sourceTree = "<group>"; };
FF025ADC2BD0C94300A86CF8 /* FooterButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FooterButtonView.swift; sourceTree = "<group>"; };
FF025ADE2BD0CE0A00A86CF8 /* TeamWeightView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamWeightView.swift; sourceTree = "<group>"; };
FF025AE02BD0EB9000A86CF8 /* TournamentClubSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentClubSettingsView.swift; sourceTree = "<group>"; };
FF025AE22BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentMatchFormatsSettingsView.swift; sourceTree = "<group>"; };
FF025AE42BD0EBB800A86CF8 /* TournamentGeneralSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentGeneralSettingsView.swift; sourceTree = "<group>"; };
FF025AE62BD1111000A86CF8 /* GlobalSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSettingsView.swift; sourceTree = "<group>"; };
FF025AE82BD1307E00A86CF8 /* MonthData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonthData.swift; sourceTree = "<group>"; };
FF025AEC2BD1513700A86CF8 /* AppScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppScreen.swift; sourceTree = "<group>"; };
FF025AEE2BD1AE9400A86CF8 /* DurationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DurationSettingsView.swift; sourceTree = "<group>"; };
FF025AF02BD1AEBD00A86CF8 /* MatchFormatStorageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchFormatStorageView.swift; sourceTree = "<group>"; };
FF089EB32BB0020000F0AEC7 /* PlayerSexPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerSexPickerView.swift; sourceTree = "<group>"; };
FF089EB52BB00A3800F0AEC7 /* TeamRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamRowView.swift; sourceTree = "<group>"; };
FF089EBA2BB0120700F0AEC7 /* PlayerPopoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerPopoverView.swift; sourceTree = "<group>"; };
@ -358,6 +389,15 @@
FF0EC54A2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-10-2022.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-10-2022.csv"; sourceTree = "<group>"; };
FF0EC54B2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-11-2022.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-11-2022.csv"; sourceTree = "<group>"; };
FF0EC54C2BB195CA0056B6D1 /* CLASSEMENT-PADEL-MESSIEURS-12-2022.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CLASSEMENT-PADEL-MESSIEURS-12-2022.csv"; sourceTree = "<group>"; };
FF1162792BCF8109000C4809 /* CallMessageCustomizationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMessageCustomizationView.swift; sourceTree = "<group>"; };
FF11627C2BCF941A000C4809 /* CashierSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CashierSettingsView.swift; sourceTree = "<group>"; };
FF11627E2BCF9432000C4809 /* PlayerListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerListView.swift; sourceTree = "<group>"; };
FF1162802BCF945C000C4809 /* TournamentCashierView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentCashierView.swift; sourceTree = "<group>"; };
FF1162822BCFBE4E000C4809 /* EditablePlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditablePlayerView.swift; sourceTree = "<group>"; };
FF1162842BD00279000C4809 /* PlayerDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerDetailView.swift; sourceTree = "<group>"; };
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>"; };
FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundStepScheduleEditorView.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>"; };
@ -465,7 +505,7 @@
FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = "<group>"; };
FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkFederalService.swift; sourceTree = "<group>"; };
FFC1E10B2BAC7FB0008D6F59 /* ClubImportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubImportView.swift; sourceTree = "<group>"; };
FFC2DCB12BBE75D40046DB9F /* LoserBracketView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserBracketView.swift; sourceTree = "<group>"; };
FFC2DCB12BBE75D40046DB9F /* LoserRoundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundView.swift; sourceTree = "<group>"; };
FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundsView.swift; sourceTree = "<group>"; };
FFC83D4E2BB807D100750834 /* RoundsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundsView.swift; sourceTree = "<group>"; };
FFC83D502BB8087E00750834 /* RoundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundView.swift; sourceTree = "<group>"; };
@ -484,6 +524,8 @@
FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
FFDB1C722BB2CFE900F1E467 /* MySortDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MySortDescriptor.swift; sourceTree = "<group>"; };
FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeferredViewModifier.swift; sourceTree = "<group>"; };
FFF116E02BD2A9B600A33B06 /* DateInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateInterval.swift; sourceTree = "<group>"; };
FFF116E22BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourtAvailabilitySettingsView.swift; sourceTree = "<group>"; };
FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchScheduleEditorView.swift; sourceTree = "<group>"; };
FFF8ACCC2B92367B008466FA /* FederalPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalPlayer.swift; sourceTree = "<group>"; };
FFF8ACD32B92392C008466FA /* SourceFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceFileManager.swift; sourceTree = "<group>"; };
@ -622,6 +664,7 @@
FF967CEF2BAECC0A00A9A3BD /* TeamScore.swift */,
C4A47D622B6D3D6500ADC637 /* Club.swift */,
FF8F263E2BAD7D5C00650388 /* Event.swift */,
FF025AE82BD1307E00A86CF8 /* MonthData.swift */,
FF1DC5522BAB354A00FD8220 /* MockData.swift */,
FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */,
FF6EC9012B94799200EA7F5A /* Coredata */,
@ -633,8 +676,6 @@
C4A47D722B72881500ADC637 /* Views */ = {
isa = PBXGroup;
children = (
C425D4022B6D249D002A7B48 /* ContentView.swift */,
C4A47D732B72881F00ADC637 /* ClubView.swift */,
FF39719B2B8DE04B004C4E75 /* Navigation */,
FF8F26392BAD526A00650388 /* Event */,
FF1DC54D2BAB34FA00FD8220 /* Club */,
@ -646,6 +687,7 @@
FF089EB92BB011EE00F0AEC7 /* Player */,
FF9267FD2BCE94520080F940 /* Calling */,
FFF964512BC2628600EEF017 /* Planning */,
FF11627B2BCF937F000C4809 /* Cashier */,
FF3F74F72B919F96004CFE0E /* Tournament */,
C4A47D882B7BBB5000ADC637 /* Subscription */,
C4A47D852B7BA33F00ADC637 /* User */,
@ -697,18 +739,39 @@
C4A47D9E2B7D0BCE00ADC637 /* StepperView.swift */,
FF5DA1942BB927E800A33061 /* GenericDestinationPickerView.swift */,
FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */,
FF025ADC2BD0C94300A86CF8 /* FooterButtonView.swift */,
FFBF065D2BBD8040009D6715 /* MatchListView.swift */,
FF967CF72BAEDF0000A9A3BD /* Labels.swift */,
);
path = Components;
sourceTree = "<group>";
};
FF025AD62BD0C0FB00A86CF8 /* Components */ = {
isa = PBXGroup;
children = (
FF025AD72BD0C10F00A86CF8 /* TeamHeaderView.swift */,
FF025ADE2BD0CE0A00A86CF8 /* TeamWeightView.swift */,
);
path = Components;
sourceTree = "<group>";
};
FF025AD92BD0C2BD00A86CF8 /* Components */ = {
isa = PBXGroup;
children = (
FF967D0C2BAF3EB200A9A3BD /* MatchDateView.swift */,
FF967D0E2BAF63B000A9A3BD /* PlayerBlockView.swift */,
FF025ADA2BD0C2D000A86CF8 /* MatchTeamDetailView.swift */,
);
path = Components;
sourceTree = "<group>";
};
FF089EB02BB001EA00F0AEC7 /* Components */ = {
isa = PBXGroup;
children = (
FF089EB32BB0020000F0AEC7 /* PlayerSexPickerView.swift */,
FF089EBA2BB0120700F0AEC7 /* PlayerPopoverView.swift */,
FF9267FB2BCE84870080F940 /* PlayerPayView.swift */,
FF1162822BCFBE4E000C4809 /* EditablePlayerView.swift */,
);
path = Components;
sourceTree = "<group>";
@ -717,6 +780,7 @@
isa = PBXGroup;
children = (
FF089EBC2BB0287D00F0AEC7 /* PlayerView.swift */,
FF1162842BD00279000C4809 /* PlayerDetailView.swift */,
FF089EB02BB001EA00F0AEC7 /* Components */,
);
path = Player;
@ -771,6 +835,25 @@
path = CSV;
sourceTree = "<group>";
};
FF11627B2BCF937F000C4809 /* Cashier */ = {
isa = PBXGroup;
children = (
FF9267F92BCE78EB0080F940 /* CashierDetailView.swift */,
FF9267F72BCE78C70080F940 /* CashierView.swift */,
FF11627E2BCF9432000C4809 /* PlayerListView.swift */,
FF11627C2BCF941A000C4809 /* CashierSettingsView.swift */,
);
path = Cashier;
sourceTree = "<group>";
};
FF1162882BD0523B000C4809 /* Components */ = {
isa = PBXGroup;
children = (
FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */,
);
path = Components;
sourceTree = "<group>";
};
FF1DC54D2BAB34FA00FD8220 /* Club */ = {
isa = PBXGroup;
children = (
@ -830,8 +913,7 @@
FF8F26532BAE1E4400650388 /* TableStructureView.swift */,
FF0E0B6C2BC254C6005F00A9 /* TournamentScheduleView.swift */,
FF9268062BCE94D90080F940 /* TournamentCallView.swift */,
FF9267F72BCE78C70080F940 /* CashierView.swift */,
FF9267F92BCE78EB0080F940 /* CashierDetailView.swift */,
FF1162802BCF945C000C4809 /* TournamentCashierView.swift */,
FF8F26522BAE0E4E00650388 /* Components */,
);
path = Screen;
@ -852,6 +934,9 @@
children = (
FF59FFB82B90EFD70061EFF9 /* ToolboxView.swift */,
FF5D0D822BB48997005CB568 /* RankCalculatorView.swift */,
FF025AE62BD1111000A86CF8 /* GlobalSettingsView.swift */,
FF025AEE2BD1AE9400A86CF8 /* DurationSettingsView.swift */,
FF025AF02BD1AEBD00A86CF8 /* MatchFormatStorageView.swift */,
);
path = Toolbox;
sourceTree = "<group>";
@ -868,6 +953,7 @@
isa = PBXGroup;
children = (
FF7091652B90F0B000AB08DA /* TabDestination.swift */,
FF025AEC2BD1513700A86CF8 /* AppScreen.swift */,
FF3F74FE2B91A2D4004CFE0E /* AgendaDestination.swift */,
FF4AB6BA2B9256D50002987F /* SearchViewModel.swift */,
FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */,
@ -878,6 +964,7 @@
FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */,
FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */,
FF3B60A22BC49BBC008C2E66 /* MatchScheduler.swift */,
FFF116E02BD2A9B600A33B06 /* DateInterval.swift */,
);
path = ViewModel;
sourceTree = "<group>";
@ -944,6 +1031,9 @@
FF8F26492BAE0B4100650388 /* TournamentLevelPickerView.swift */,
FF0EC5212BB173E70056B6D1 /* UpdateSourceRankDateView.swift */,
FF5D0D772BB42C5B005CB568 /* InscriptionInfoView.swift */,
FF025AE02BD0EB9000A86CF8 /* TournamentClubSettingsView.swift */,
FF025AE22BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift */,
FF025AE42BD0EBB800A86CF8 /* TournamentGeneralSettingsView.swift */,
);
path = Components;
sourceTree = "<group>";
@ -955,6 +1045,7 @@
FF9268002BCE94920080F940 /* SeedsCallingView.swift */,
FF9268022BCE94A30080F940 /* GroupStageCallingView.swift */,
FF9268082BCEDC2C0080F940 /* CallView.swift */,
FF1162792BCF8109000C4809 /* CallMessageCustomizationView.swift */,
);
path = Calling;
sourceTree = "<group>";
@ -977,8 +1068,7 @@
FF967D052BAF3C4200A9A3BD /* MatchSetupView.swift */,
FF967D002BAEF0B400A9A3BD /* MatchSummaryView.swift */,
FF967D022BAEF0C000A9A3BD /* MatchDetailView.swift */,
FF967D0C2BAF3EB200A9A3BD /* MatchDateView.swift */,
FF967D0E2BAF63B000A9A3BD /* PlayerBlockView.swift */,
FF025AD92BD0C2BD00A86CF8 /* Components */,
);
path = Match;
sourceTree = "<group>";
@ -989,6 +1079,8 @@
FF967D082BAF3D4000A9A3BD /* TeamDetailView.swift */,
FF967D0A2BAF3D4C00A9A3BD /* TeamPickerView.swift */,
FF089EB52BB00A3800F0AEC7 /* TeamRowView.swift */,
FF1162862BD004AD000C4809 /* EditingTeamView.swift */,
FF025AD62BD0C0FB00A86CF8 /* Components */,
);
path = Team;
sourceTree = "<group>";
@ -999,7 +1091,7 @@
FFC83D4E2BB807D100750834 /* RoundsView.swift */,
FFC83D502BB8087E00750834 /* RoundView.swift */,
FF5DA1922BB9279B00A33061 /* RoundSettingsView.swift */,
FFC2DCB12BBE75D40046DB9F /* LoserBracketView.swift */,
FFC2DCB12BBE75D40046DB9F /* LoserRoundView.swift */,
FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */,
);
path = Round;
@ -1084,8 +1176,11 @@
FFF964542BC266CF00EEF017 /* SchedulerView.swift */,
FFF964562BC26B3400EEF017 /* RoundScheduleEditorView.swift */,
FFFCDE0D2BCC833600317DEF /* LoserRoundScheduleEditorView.swift */,
FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */,
FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */,
FFF9645A2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift */,
FFF116E22BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift */,
FF1162882BD0523B000C4809 /* Components */,
);
path = Planning;
sourceTree = "<group>";
@ -1297,6 +1392,7 @@
FF7091662B90F0B000AB08DA /* TabDestination.swift in Sources */,
FF9267F82BCE78C70080F940 /* CashierView.swift in Sources */,
FF8F263F2BAD7D5C00650388 /* Event.swift in Sources */,
FF11628C2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift in Sources */,
FF089EBF2BB0B14600F0AEC7 /* FileImportView.swift in Sources */,
C4A47D9F2B7D0BCE00ADC637 /* StepperView.swift in Sources */,
FFC83D4F2BB807D100750834 /* RoundsView.swift in Sources */,
@ -1308,8 +1404,10 @@
FF967D062BAF3C4200A9A3BD /* MatchSetupView.swift in Sources */,
FF4AB6B52B9248200002987F /* NetworkManager.swift in Sources */,
FFB9C8752BBADDF700A0EF4F /* SeedInterval.swift in Sources */,
FF025AE12BD0EB9000A86CF8 /* TournamentClubSettingsView.swift in Sources */,
FFBF065C2BBD2657009D6715 /* GroupStageTeamView.swift in Sources */,
FF5DA1932BB9279B00A33061 /* RoundSettingsView.swift in Sources */,
FF025ADF2BD0CE0A00A86CF8 /* TeamWeightView.swift in Sources */,
FF9268012BCE94920080F940 /* SeedsCallingView.swift in Sources */,
FF9268092BCEDC2C0080F940 /* CallView.swift in Sources */,
FF5D0D742BB41DF8005CB568 /* Color+Extensions.swift in Sources */,
@ -1320,6 +1418,8 @@
C4A47DB32B86387500ADC637 /* AccountView.swift in Sources */,
FF1CBC1D2BB53DC10036DAAB /* Calendar+Extensions.swift in Sources */,
FF967CF22BAECC0B00A9A3BD /* TeamScore.swift in Sources */,
FF1162832BCFBE4E000C4809 /* EditablePlayerView.swift in Sources */,
FF1162852BD00279000C4809 /* PlayerDetailView.swift in Sources */,
FF5D0D762BB428B2005CB568 /* ListRowViewModifier.swift in Sources */,
FF6EC9002B94794700EA7F5A /* PresentationContext.swift in Sources */,
FFDB1C6D2BB2A02000F1E467 /* AppSettings.swift in Sources */,
@ -1335,6 +1435,8 @@
FF70916C2B91005400AB08DA /* TournamentView.swift in Sources */,
FF1DC5552BAB36DD00FD8220 /* CreateClubView.swift in Sources */,
FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */,
FF025AEF2BD1AE9400A86CF8 /* DurationSettingsView.swift in Sources */,
FF025AED2BD1513700A86CF8 /* AppScreen.swift in Sources */,
FFCFC00E2BBC3D4600B82851 /* PointSelectionView.swift in Sources */,
FF089EB62BB00A3800F0AEC7 /* TeamRowView.swift in Sources */,
FF92680B2BCEE3E10080F940 /* ContactManager.swift in Sources */,
@ -1343,14 +1445,17 @@
FF92680D2BCEE5EA0080F940 /* NetworkMonitor.swift in Sources */,
FF967CF62BAED51600A9A3BD /* TournamentRunningView.swift in Sources */,
FF8F264D2BAE0B4100650388 /* TournamentDatePickerView.swift in Sources */,
FFF116E32BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift in Sources */,
FF967D042BAEF1C300A9A3BD /* MatchRowView.swift in Sources */,
C44B79112BBDA63A00906534 /* Locale+Extensions.swift in Sources */,
FF967CEA2BAEC70100A9A3BD /* GroupStage.swift in Sources */,
C4A47D742B72881F00ADC637 /* ClubView.swift in Sources */,
FF1162812BCF945C000C4809 /* TournamentCashierView.swift in Sources */,
C4A47D902B7BBBEC00ADC637 /* StoreManager.swift in Sources */,
FF11627F2BCF9432000C4809 /* PlayerListView.swift in Sources */,
FF4AB6BB2B9256D50002987F /* SearchViewModel.swift in Sources */,
FF967CF32BAECC0B00A9A3BD /* PlayerRegistration.swift in Sources */,
FF4AB6BF2B92577A0002987F /* ImportedPlayerView.swift in Sources */,
FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */,
FF6EC9062B947A1000EA7F5A /* NetworkManagerError.swift in Sources */,
C4A47D5A2B6D383C00ADC637 /* Tournament.swift in Sources */,
C4A47D7B2B73C0F900ADC637 /* TournamentV2.swift in Sources */,
@ -1361,6 +1466,7 @@
FF1CBC222BB53E590036DAAB /* FederalTournamentHolder.swift in Sources */,
C4A47D5E2B6D38EC00ADC637 /* DataStore.swift in Sources */,
FFCFC01C2BBC5AAA00B82851 /* SetDescriptor.swift in Sources */,
FF025AD82BD0C10F00A86CF8 /* TeamHeaderView.swift in Sources */,
FF82CFC52B911F5B00B0CAF2 /* OrganizedTournamentView.swift in Sources */,
FFF964572BC26B3400EEF017 /* RoundScheduleEditorView.swift in Sources */,
FF59FFB32B90EFAC0061EFF9 /* EventListView.swift in Sources */,
@ -1368,7 +1474,10 @@
FF8F263D2BAD627A00650388 /* TournamentConfiguratorView.swift in Sources */,
FFC1E10C2BAC7FB0008D6F59 /* ClubImportView.swift in Sources */,
FF3B60A32BC49BBC008C2E66 /* MatchScheduler.swift in Sources */,
FF11627A2BCF8109000C4809 /* CallMessageCustomizationView.swift in Sources */,
FF025ADB2BD0C2D000A86CF8 /* MatchTeamDetailView.swift in Sources */,
FF5DA1952BB927E800A33061 /* GenericDestinationPickerView.swift in Sources */,
FFF116E12BD2A9B600A33B06 /* DateInterval.swift in Sources */,
FF8F26542BAE1E4400650388 /* TableStructureView.swift in Sources */,
C45BAE442BCA753E002EEC8A /* Purchase.swift in Sources */,
FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */,
@ -1379,12 +1488,14 @@
FF5D0D892BB4935C005CB568 /* ClubRowView.swift in Sources */,
FF1DC5512BAB351300FD8220 /* ClubDetailView.swift in Sources */,
FF9268032BCE94A30080F940 /* GroupStageCallingView.swift in Sources */,
FF11627D2BCF941A000C4809 /* CashierSettingsView.swift in Sources */,
FFFCDE0E2BCC833600317DEF /* LoserRoundScheduleEditorView.swift in Sources */,
C4A47D632B6D3D6500ADC637 /* Club.swift in Sources */,
FF6EC90B2B947AC000EA7F5A /* Array+Extensions.swift in Sources */,
FF59FFB92B90EFD70061EFF9 /* ToolboxView.swift in Sources */,
FFF8ACD92B923F3C008466FA /* String+Extensions.swift in Sources */,
FFC2DCB22BBE75D40046DB9F /* LoserBracketView.swift in Sources */,
FF025AE52BD0EBB800A86CF8 /* TournamentGeneralSettingsView.swift in Sources */,
FFC2DCB22BBE75D40046DB9F /* LoserRoundView.swift in Sources */,
FF9267FC2BCE84870080F940 /* PlayerPayView.swift in Sources */,
FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */,
FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */,
@ -1399,12 +1510,14 @@
FF5D0D8B2BB4D1E3005CB568 /* CalendarView.swift in Sources */,
FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */,
FF8F26472BAE0ACB00650388 /* TournamentFieldsManagerView.swift in Sources */,
FF025AE32BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift in Sources */,
FF11628A2BD05247000C4809 /* DateUpdateManagerView.swift in Sources */,
FFCFC01A2BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift in Sources */,
FF025AE92BD1307F00A86CF8 /* MonthData.swift in Sources */,
FF967D0B2BAF3D4C00A9A3BD /* TeamPickerView.swift in Sources */,
FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */,
FF1DC55B2BAB80C400FD8220 /* DisplayContext.swift in Sources */,
FF9268072BCE94D90080F940 /* TournamentCallView.swift in Sources */,
C425D4032B6D249D002A7B48 /* ContentView.swift in Sources */,
FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */,
FF967CFC2BAEE52E00A9A3BD /* GroupStagesView.swift in Sources */,
FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */,
@ -1418,6 +1531,7 @@
FFDDD40C2B93B2BB00C91A49 /* DeferredViewModifier.swift in Sources */,
FFD784042B91C280000F62A6 /* EmptyActivityView.swift in Sources */,
FF0E0B6D2BC254C6005F00A9 /* TournamentScheduleView.swift in Sources */,
FF025AF12BD1AEBD00A86CF8 /* MatchFormatStorageView.swift in Sources */,
FF3F74F62B919E45004CFE0E /* UmpireView.swift in Sources */,
C4A47D772B73789100ADC637 /* TournamentV1.swift in Sources */,
C4A47DAD2B85FCCD00ADC637 /* User.swift in Sources */,
@ -1454,11 +1568,13 @@
FF967CF82BAEDF0000A9A3BD /* Labels.swift in Sources */,
FF089EB42BB0020000F0AEC7 /* PlayerSexPickerView.swift in Sources */,
FF9267FF2BCE94830080F940 /* CallSettingsView.swift in Sources */,
FF025ADD2BD0C94300A86CF8 /* FooterButtonView.swift in Sources */,
FF5D0D852BB48997005CB568 /* RankCalculatorView.swift in Sources */,
FF70916A2B90F95E00AB08DA /* DateBoxView.swift in Sources */,
FF5D0D722BB3EFA5005CB568 /* LearnMoreSheetView.swift in Sources */,
FFF8ACD42B92392C008466FA /* SourceFileManager.swift in Sources */,
FF0EC5222BB173E70056B6D1 /* UpdateSourceRankDateView.swift in Sources */,
FF025AE72BD1111000A86CF8 /* GlobalSettingsView.swift in Sources */,
C4A47D912B7BBBEC00ADC637 /* Guard.swift in Sources */,
C49EF0192BD694290077B5AA /* PurchaseListView.swift in Sources */,
);
@ -1623,6 +1739,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6;
ENABLE_PREVIEWS = YES;
@ -1654,6 +1771,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6;
ENABLE_PREVIEWS = YES;

@ -7,14 +7,48 @@
import Foundation
import LeStorage
import SwiftUI
@Observable
class AppSettings: MicroStorable {
static var fileName: String { "appsettings.json" }
var lastDataSource: String? = nil
var callMessageBody : String? = nil
var callMessageSignature: String? = nil
var callDisplayFormat: Bool = false
var callDisplayEntryFee: Bool = false
var callUseFullCustomMessage: Bool = false
var matchFormatsDefaultDuration: [MatchFormat: Int]? = nil
var bracketMatchFormatPreference: Int?
var groupStageMatchFormatPreference: Int?
var loserBracketMatchFormatPreference: Int?
required init() {
}
// var id: String = Store.randomId()
func saveMatchFormatsDefaultDuration(_ matchFormat: MatchFormat, estimatedDuration: Int) {
if estimatedDuration == matchFormat.defaultEstimatedDuration {
matchFormatsDefaultDuration?.removeValue(forKey: matchFormat)
} else {
matchFormatsDefaultDuration = matchFormatsDefaultDuration ?? [MatchFormat: Int]()
matchFormatsDefaultDuration?[matchFormat] = estimatedDuration
}
}
enum CodingKeys: String, CodingKey {
case _lastDataSource = "lastDataSource"
case _callMessageBody = "callMessageBody"
case _callMessageSignature = "callMessageSignature"
case _callDisplayFormat = "callDisplayFormat"
case _callDisplayEntryFee = "callDisplayEntryFee"
case _callUseFullCustomMessage = "callUseFullCustomMessage"
case _matchFormatsDefaultDuration = "matchFormatsDefaultDuration"
case _bracketMatchFormatPreference = "bracketMatchFormatPreference"
case _groupStageMatchFormatPreference = "groupStageMatchFormatPreference"
case _loserBracketMatchFormatPreference = "loserBracketMatchFormatPreference"
}
}

@ -32,7 +32,9 @@ class Club : ModelObject, Storable, Hashable {
var zipCode: String?
var latitude: Double?
var longitude: Double?
var courtCount: Int?
var courtNames: [String]? = nil
internal init(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) {
self.name = name
self.acronym = acronym ?? name.acronym()
@ -45,12 +47,7 @@ class Club : ModelObject, Storable, Hashable {
self.longitude = longitude
}
var tournaments: [Tournament] {
return []
}
override func deleteDependencies() throws {
try Store.main.deleteDependencies(items: self.tournaments)
}
enum CodingKeys: String, CodingKey {
@ -64,6 +61,8 @@ class Club : ModelObject, Storable, Hashable {
case _zipCode = "zipCode"
case _latitude = "latitude"
case _longitude = "longitude"
case _courtCount = "courtCount"
case _courtNames = "courtNames"
}
}

@ -24,8 +24,29 @@ class DataStore: ObservableObject {
fileprivate(set) var playerRegistrations: StoredCollection<PlayerRegistration>
fileprivate(set) var rounds: StoredCollection<Round>
fileprivate(set) var teamScores: StoredCollection<TeamScore>
fileprivate(set) var monthData: StoredCollection<MonthData>
fileprivate var _userStorage: OptionalStorage<User> = OptionalStorage<User>(fileName: "user.json")
fileprivate var _appSettingsStorage: MicroStorage<AppSettings> = MicroStorage()
var appSettings: AppSettings {
_appSettingsStorage.item
}
func updateSettings() {
_appSettingsStorage.update { settings in
settings.lastDataSource = appSettings.lastDataSource
settings.callMessageBody = appSettings.callMessageBody
settings.callDisplayFormat = appSettings.callDisplayFormat
settings.callMessageSignature = appSettings.callMessageSignature
settings.callDisplayEntryFee = appSettings.callDisplayEntryFee
settings.callUseFullCustomMessage = appSettings.callUseFullCustomMessage
settings.matchFormatsDefaultDuration = appSettings.matchFormatsDefaultDuration
settings.bracketMatchFormatPreference = appSettings.bracketMatchFormatPreference
settings.groupStageMatchFormatPreference = appSettings.groupStageMatchFormatPreference
settings.loserBracketMatchFormatPreference = appSettings.loserBracketMatchFormatPreference
}
}
var user: User? {
return self._userStorage.item
@ -44,16 +65,18 @@ class DataStore: ObservableObject {
// store.addMigration(Migration<TournamentV1, TournamentV2>(version: 2))
// store.addMigration(Migration<TournamentV2, Tournament>(version: 3))
self.clubs = store.registerCollection(synchronized: false, indexed: true)
self.tournaments = store.registerCollection(synchronized: false, indexed: true)
self.events = store.registerCollection(synchronized: false, indexed: true)
self.groupStages = store.registerCollection(synchronized: false, indexed: true)
self.teamScores = store.registerCollection(synchronized: false, indexed: true)
self.teamRegistrations = store.registerCollection(synchronized: false, indexed: true)
self.playerRegistrations = store.registerCollection(synchronized: false, indexed: true)
self.rounds = store.registerCollection(synchronized: false, indexed: true)
self.matches = store.registerCollection(synchronized: false, indexed: true)
let indexed : Bool = true
self.clubs = store.registerCollection(synchronized: false, indexed: indexed)
self.tournaments = store.registerCollection(synchronized: false, indexed: indexed)
self.events = store.registerCollection(synchronized: false, indexed: indexed)
self.groupStages = store.registerCollection(synchronized: false, indexed: indexed)
self.teamScores = store.registerCollection(synchronized: false, indexed: indexed)
self.teamRegistrations = store.registerCollection(synchronized: false, indexed: indexed)
self.playerRegistrations = store.registerCollection(synchronized: false, indexed: indexed)
self.rounds = store.registerCollection(synchronized: false, indexed: indexed)
self.matches = store.registerCollection(synchronized: false, indexed: indexed)
self.monthData = store.registerCollection(synchronized: false, indexed: indexed)
NotificationCenter.default.addObserver(self, selector: #selector(collectionWasUpdated), name: NSNotification.Name.CollectionDidLoad, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(collectionWasUpdated), name: NSNotification.Name.CollectionDidChange, object: nil)
}

@ -22,6 +22,7 @@ class Event: ModelObject, Storable {
var groupStageFormat: Int?
var roundFormat: Int?
var loserRoundFormat: Int?
//var timeslots ?
internal init(club: String? = nil, name: String? = nil, courtCount: Int? = nil, tenupId: String? = nil, groupStageFormat: Int? = nil, roundFormat: Int? = nil, loserRoundFormat: Int? = nil) {
self.club = club

@ -21,6 +21,8 @@ protocol PlayerHolder {
var clubName: String? { get }
var ligueName: String? { get }
var assimilation: String? { get }
var computedAge: Int? { get }
func getAssimilatedAsMaleRank() -> Int?
}
extension PlayerHolder {
@ -29,8 +31,32 @@ extension PlayerHolder {
}
}
fileprivate extension Int {
var femaleInMaleAssimilation: Int {
self + femaleInMaleAssimilationAddition
}
var femaleInMaleAssimilationAddition: Int {
switch self {
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
}
}
}
extension ImportedPlayer: PlayerHolder {
func getAssimilatedAsMaleRank() -> Int? {
guard male == false else { return nil }
return getRank()?.femaleInMaleAssimilation
}
var computedAge: Int? { nil }
var tournamentPlayed: Int? {
Int(tournamentCount)

@ -203,6 +203,7 @@ enum TypePratique: String, Codable {
case beach = "BEACH"
case padel = "PADEL"
case tennis = "TENNIS"
case pickle = "PICKLE"
}
// MARK: - CategorieTournoi

@ -19,6 +19,7 @@ class GroupStage: ModelObject, Storable {
var size: Int
var format: Int?
var startDate: Date?
var name: String?
var matchFormat: MatchFormat {
get {
@ -136,7 +137,8 @@ class GroupStage: ModelObject, Storable {
}
func availableToStart() -> [Match] {
playedMatches().filter({ $0.canBeStarted() && $0.isRunning() == false })
let runningMatches = runningMatches()
return playedMatches().filter({ $0.canBeStarted(inMatches: runningMatches) && $0.isRunning() == false })
}
func runningMatches() -> [Match] {
@ -208,6 +210,10 @@ class GroupStage: ModelObject, Storable {
Store.main.filter { $0.groupStage == self.id }
}
func unsortedPlayers() -> [PlayerRegistration] {
unsortedTeams().flatMap({ $0.unsortedPlayers() })
}
fileprivate typealias TeamScoreAreInIncreasingOrder = (TeamGroupStageScore, TeamGroupStageScore) -> Bool
fileprivate typealias TeamGroupStageScore = (team: TeamRegistration, wins: Int, loses: Int, setDifference: Int, gameDifference: Int)
@ -221,10 +227,13 @@ class GroupStage: ModelObject, Storable {
}
}
func unsortedTeams() -> [TeamRegistration] {
Store.main.filter { $0.groupStage == self.id && $0.groupStagePosition != nil }
}
func teams(_ sortedByScore: Bool = false) -> [TeamRegistration] {
let teams: [TeamRegistration] = Store.main.filter { $0.groupStage == self.id && $0.groupStagePosition != nil }
if sortedByScore {
return teams.compactMap({ _score(forGroupStagePosition: $0.groupStagePosition!) }).sorted { (lhs, rhs) in
return unsortedTeams().compactMap({ _score(forGroupStagePosition: $0.groupStagePosition!) }).sorted { (lhs, rhs) in
let predicates: [TeamScoreAreInIncreasingOrder] = [
{ $0.wins < $1.wins },
{ $0.setDifference < $1.setDifference },
@ -244,9 +253,18 @@ class GroupStage: ModelObject, Storable {
return false
}.map({ $0.team }).reversed()
} else {
return teams.sorted(by: \TeamRegistration.groupStagePosition!)
return unsortedTeams().sorted(by: \TeamRegistration.groupStagePosition!)
}
}
func updateMatchFormat(_ matchFormat: MatchFormat) {
self.matchFormat = matchFormat
let playedMatches = playedMatches()
playedMatches.forEach { match in
match.matchFormat = matchFormat
}
try? DataStore.shared.matches.addOrUpdate(contentOfs: playedMatches)
}
override func deleteDependencies() throws {
try Store.main.deleteDependencies(items: self._matches())
@ -261,6 +279,7 @@ extension GroupStage {
case _size = "size"
case _format = "format"
case _startDate = "startDate"
case _name = "name"
}
}
@ -272,4 +291,8 @@ extension GroupStage: Selectable {
func badgeValue() -> Int? {
runningMatches().count
}
func badgeImage() -> Badge? {
hasEnded() ? .checkmark : nil
}
}

@ -11,6 +11,7 @@ import LeStorage
@Observable
class Match: ModelObject, Storable {
static func resourceName() -> String { "matches" }
var byeState: Bool = false
var id: String = Store.randomId()
var round: String?
@ -27,6 +28,7 @@ class Match: ModelObject, Storable {
var name: String?
var order: Int
var disabled: Bool = false
var courtIndex: Int?
internal init(round: String? = nil, groupStage: String? = nil, startDate: Date? = nil, endDate: Date? = nil, index: Int, matchFormat: MatchFormat? = nil, court: String? = nil, servingTeamId: String? = nil, winningTeamId: String? = nil, losingTeamId: String? = nil, broadcasted: Bool = false, name: String? = nil, order: Int = 0) {
self.round = round
@ -100,8 +102,8 @@ class Match: ModelObject, Storable {
return index * 2 + teamPosition.rawValue == bracketPosition
}
func estimatedEndDate() -> Date? {
let minutesToAdd = Double(matchFormat.estimatedDuration)
func estimatedEndDate(_ additionalEstimationDuration: Int) -> Date? {
let minutesToAdd = Double(matchFormat.getEstimatedDuration(additionalEstimationDuration))
return startDate?.addingTimeInterval(minutesToAdd * 60.0)
}
@ -159,31 +161,99 @@ class Match: ModelObject, Storable {
_toggleMatchDisableState(false)
}
private func _toggleLoserMatchDisableState(_ state: Bool) {
if isLoserBracket == false {
let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: index)
if let loserMatch = roundObject?.loserRounds().first?.getMatch(atMatchIndexInRound: indexInRound / 2) {
loserMatch.disabled = state
try? DataStore.shared.matches.addOrUpdate(instance: loserMatch)
loserMatch._toggleLoserMatchDisableState(state)
}
} else {
roundObject?.loserRounds().forEach({ round in
round.handleLoserRoundState()
})
private func _loserMatch() -> Match? {
let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: index)
return roundObject?.loserRounds().first?.getMatch(atMatchIndexInRound: indexInRound / 2)
}
func _toggleLoserMatchDisableState(_ state: Bool) {
guard let loserMatch = _loserMatch() else { return }
guard let next = _otherMatch() else { return }
loserMatch.byeState = true
if next.disabled {
loserMatch.byeState = false
}
loserMatch._toggleMatchDisableState(state, forward: true)
}
fileprivate func _toggleMatchDisableState(_ state: Bool) {
fileprivate func _otherMatch() -> Match? {
guard let round else { return nil }
guard index > 0 else { return nil }
let nextIndex = (index - 1) / 2
let topMatchIndex = (nextIndex * 2) + 1
let bottomMatchIndex = (nextIndex + 1) * 2
let isTopMatch = topMatchIndex + 1 == index
let lookingForIndex = isTopMatch ? topMatchIndex : bottomMatchIndex
return Store.main.filter(isIncluded: { $0.round == round && $0.index == lookingForIndex }).first
}
private func _forwardMatch(inRound round: Round) -> Match? {
guard let roundObjectNextRound = round.nextRound() else { return nil }
let nextIndex = (index - 1) / 2
return Store.main.filter(isIncluded: { $0.round == roundObjectNextRound.id && $0.index == nextIndex }).first
}
func _toggleForwardMatchDisableState(_ state: Bool) {
guard let roundObject else { return }
guard roundObject.loser != nil else { return }
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)
} else if byeState && next.byeState {
print("don't disable forward match")
forwardMatch.byeState = false
forwardMatch._toggleMatchDisableState(false, forward: true)
} else {
forwardMatch.byeState = true
forwardMatch._toggleMatchDisableState(state, forward: true)
}
// if next.disabled == false {
// forwardMatch.byeState = state
// }
//
// if next.disabled == state {
// if next.byeState != byeState {
// //forwardMatch.byeState = state
// forwardMatch._toggleMatchDisableState(state)
// } else {
// forwardMatch._toggleByeState(state)
// }
// } else {
// }
// forwardMatch._toggleByeState(state)
}
func _toggleMatchDisableState(_ state: Bool, forward: Bool = false) {
//if disabled == state { return }
disabled = state
//byeState = false
//try? DataStore.shared.matches.addOrUpdate(instance: self)
_toggleLoserMatchDisableState(state)
topPreviousRoundMatch()?._toggleMatchDisableState(state)
bottomPreviousRoundMatch()?._toggleMatchDisableState(state)
try? DataStore.shared.matches.addOrUpdate(instance: self)
if forward {
_toggleForwardMatchDisableState(state)
} else {
topPreviousRoundMatch()?._toggleMatchDisableState(state)
bottomPreviousRoundMatch()?._toggleMatchDisableState(state)
}
}
func next() -> Match? {
Store.main.filter(isIncluded: { $0.round == round && $0.index == index + 1 }).first
Store.main.filter(isIncluded: { $0.round == round && $0.index > index }).sorted(by: \.index).first
}
func getDuration() -> Int {
if let tournament = currentTournament() {
matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)
} else {
matchFormat.getEstimatedDuration()
}
}
func roundTitle() -> String? {
@ -203,14 +273,14 @@ class Match: ModelObject, Storable {
func topPreviousRoundMatch() -> Match? {
guard let roundObject else { return nil }
return Store.main.filter { match in
match.index == topPreviousRoundMatchIndex() && match.round == roundObject.previousRound()?.id
match.index == topPreviousRoundMatchIndex() && match.round != nil && match.round == roundObject.previousRound()?.id
}.sorted(by: \.index).first
}
func bottomPreviousRoundMatch() -> Match? {
guard let roundObject else { return nil }
return Store.main.filter { match in
match.index == bottomPreviousRoundMatchIndex() && match.round == roundObject.previousRound()?.id
match.index == bottomPreviousRoundMatchIndex() && match.round != nil && match.round == roundObject.previousRound()?.id
}.sorted(by: \.index).first
}
@ -315,7 +385,7 @@ class Match: ModelObject, Storable {
}
}
func courtIndex() -> Int? {
func getCourtIndex() -> Int? {
guard let court else { return nil }
if let courtIndex = Int(court) { return courtIndex - 1 }
return nil
@ -349,24 +419,26 @@ class Match: ModelObject, Storable {
court = String(courtIndex)
}
func canBeStarted() -> Bool {
func canBeStarted(inMatches matches: [Match]) -> Bool {
let teams = teams()
guard teams.count == 2 else { return false }
guard hasEnded() == false else { return false }
guard hasStarted() == false else { return false }
return teams.allSatisfy({ $0.canPlay() && isTeamPlaying($0) == false })
return teams.allSatisfy({ $0.canPlay() && isTeamPlaying($0, inMatches: matches) == false })
}
func isTeamPlaying(_ team: TeamRegistration) -> Bool {
if isGroupStage() {
let isPlaying = groupStageObject?.runningMatches().filter({ $0.teams().contains(team) }).isEmpty == false
return isPlaying
} else {
//todo
return false
}
func isTeamPlaying(_ team: TeamRegistration, inMatches matches: [Match]) -> Bool {
matches.filter({ $0.teams().contains(team) }).isEmpty == false
}
var computedStartDateForSorting: Date {
startDate ?? .distantFuture
}
var computedEndDateForSorting: Date {
endDate ?? .distantFuture
}
func isReady() -> Bool {
teams().count == 2
}
@ -534,6 +606,7 @@ class Match: ModelObject, Storable {
case _index = "index"
case _format = "format"
case _court = "court"
case _courtIndex = "courtIndex"
case _servingTeamId = "servingTeamId"
case _winningTeamId = "winningTeamId"
case _losingTeamId = "losingTeamId"

@ -35,27 +35,19 @@ extension Tournament {
}
static func newEmptyInstance() -> Tournament {
let lastDataSource: String? = UserDefaults.standard.string(forKey: "lastDataSource")
let lastDataSourceMaleUnranked: Int = UserDefaults.standard.integer(forKey: "lastDataSourceMaleUnranked")
let lastDataSourceFemaleUnranked: Int = UserDefaults.standard.integer(forKey: "lastDataSourceFemaleUnranked")
let lastDataSource: String? = DataStore.shared.appSettings.lastDataSource
var _mostRecentDateAvailable: Date? {
guard let lastDataSource else { return nil }
return URL.importDateFormatter.date(from: lastDataSource)
}
let maleUnrankedValue : Int? = lastDataSourceMaleUnranked == 0 ? nil : lastDataSourceMaleUnranked
let femaleUnrankedValue : Int? = lastDataSourceFemaleUnranked == 0 ? nil : lastDataSourceMaleUnranked
let rankSourceDate = _mostRecentDateAvailable
let tournaments : [Tournament] = DataStore.shared.tournaments.filter { $0.endDate != nil }.sorted(by: \.startDate).reversed()
let tournamentLevel = TournamentLevel.mostUsed(inTournaments: tournaments)
let tournamentCategory = TournamentCategory.mostUsed(inTournaments: tournaments)
let federalTournamentAge = FederalTournamentAge.mostUsed(inTournaments: tournaments)
//todo
/*
tournament.tournamentLevel = TournamentLevel.mostUsed(tournaments: tournaments)
tournament.tournamentCategory = TournamentCategory.mostUsed(tournaments: tournaments)
tournament.federalTournamentAge = FederalTournamentAge.mostUsed(tournaments: tournaments)
*/
return Tournament(groupStageSortMode: .snake, rankSourceDate: rankSourceDate, teamSorting: .inscriptionDate, federalCategory: .men, federalLevelCategory: .p100, federalAgeCategory: .senior, maleUnrankedValue: maleUnrankedValue, femaleUnrankedValue: femaleUnrankedValue)
return Tournament(groupStageSortMode: .snake, rankSourceDate: rankSourceDate, teamSorting: tournamentLevel.defaultTeamSortingType, federalCategory: tournamentCategory, federalLevelCategory: tournamentLevel, federalAgeCategory: federalTournamentAge)
}
}

@ -0,0 +1,53 @@
//
// MonthData.swift
// PadelClub
//
// Created by Razmig Sarkissian on 18/04/2024.
//
import Foundation
import SwiftUI
import LeStorage
@Observable
class MonthData : ModelObject, Storable {
static func resourceName() -> String { return "month-data" }
private(set) var id: String = Store.randomId()
private(set) var monthKey: String
private(set) var creationDate: Date
var maleUnrankedValue: Int? = nil
var femaleUnrankedValue: Int? = nil
init(monthKey: String) {
self.monthKey = monthKey
self.creationDate = Date()
}
static func calculateCurrentUnrankedValues(mostRecentDateAvailable: Date) async {
let lastDataSourceMaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: mostRecentDateAvailable, man: true)
let lastDataSourceFemaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: mostRecentDateAvailable, man: false)
await MainActor.run {
if let lastDataSource = DataStore.shared.appSettings.lastDataSource {
let currentMonthData : MonthData = Store.main.filter(isIncluded: { $0.monthKey == lastDataSource }).first ?? MonthData(monthKey: lastDataSource)
currentMonthData.maleUnrankedValue = lastDataSourceMaleUnranked
currentMonthData.femaleUnrankedValue = lastDataSourceFemaleUnranked
try? DataStore.shared.monthData.addOrUpdate(instance: currentMonthData)
}
}
}
override func deleteDependencies() throws {
}
enum CodingKeys: String, CodingKey {
case _id = "id"
case _monthKey = "monthKey"
case _creationDate = "creationDate"
case _maleUnrankedValue = "maleUnrankedValue"
case _femaleUnrankedValue = "femaleUnrankedValue"
}
}

@ -87,6 +87,26 @@ class PlayerRegistration: ModelObject, Storable {
}
}
var computedAge: Int? {
if let birthdate {
let components = birthdate.components(separatedBy: "/")
if components.count == 3 {
if let year = Calendar.current.dateComponents([.year], from: Date()).year, let age = components.last, let ageInt = Int(age) {
if age.count == 2 { //si l'année est sur 2 chiffres dans le fichier
if ageInt < 23 {
return year - 2000 - ageInt
} else {
return year - 2000 + 100 - ageInt
}
} else { //si l'année est représenté sur 4 chiffres
return year - ageInt
}
}
}
}
return nil
}
func pasteData() -> String {
[firstName.capitalized, lastName.capitalized, licenceId].compactMap({ $0 }).joined(separator: " ")
}
@ -145,7 +165,11 @@ class PlayerRegistration: ModelObject, Storable {
func rankLabel(_ displayStyle: DisplayStyle = .wide) -> String {
if let rank, rank > 0 {
return rank.formatted()
if rank != weight {
return weight.formatted() + " (" + rank.formatted() + ")"
} else {
return rank.formatted()
}
} else {
return "non classé" + (isMalePlayer() ? "" : "e")
}
@ -319,7 +343,6 @@ class PlayerRegistration: ModelObject, Storable {
return 15000
}
}
}
extension PlayerRegistration: Hashable {
@ -333,6 +356,9 @@ extension PlayerRegistration: Hashable {
}
extension PlayerRegistration: PlayerHolder {
func getAssimilatedAsMaleRank() -> Int? {
nil
}
func getFirstName() -> String {
firstName

@ -92,26 +92,29 @@ class Round: ModelObject, Storable {
func seed(_ team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? {
return Store.main.filter(isIncluded: {
$0.tournament == tournament && $0.bracketPosition != nil
}).first(where: {
($0.bracketPosition! / 2) == matchIndex
$0.tournament == tournament
&& $0.bracketPosition != nil
&& ($0.bracketPosition! / 2) == matchIndex
&& ($0.bracketPosition! % 2) == team.rawValue
})
}).first
}
func seeds(inMatchIndex matchIndex: Int) -> [TeamRegistration] {
return Store.main.filter(isIncluded: {
$0.tournament == tournament && $0.bracketPosition != nil
}).filter({
($0.bracketPosition! / 2) == matchIndex
$0.tournament == tournament
&& $0.bracketPosition != nil
&& ($0.bracketPosition! / 2) == matchIndex
})
}
func seeds() -> [TeamRegistration] {
let initialMatchIndex = RoundRule.matchIndex(fromRoundIndex: index)
let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: index)
return Store.main.filter(isIncluded: {
$0.tournament == tournament && $0.bracketPosition != nil
}).filter({
($0.bracketPosition! / 2) >= RoundRule.matchIndex(fromRoundIndex: index) && ($0.bracketPosition! / 2) < RoundRule.matchIndex(fromRoundIndex: index) + RoundRule.numberOfMatches(forRoundIndex: index)
$0.tournament == tournament
&& $0.bracketPosition != nil
&& ($0.bracketPosition! / 2) >= initialMatchIndex
&& ($0.bracketPosition! / 2) < initialMatchIndex + numberOfMatches
})
}
@ -209,7 +212,7 @@ class Round: ModelObject, Storable {
}
func loserRounds(forRoundIndex roundIndex: Int) -> [Round] {
return loserRoundsAndChildren().filter({ $0.index == roundIndex }).sorted(by: \.cumulativeMatchCount)
return loserRoundsAndChildren().filter({ $0.index == roundIndex }).sorted(by: \.theoryCumulativeMatchCount)
}
func isDisabled() -> Bool {
@ -246,8 +249,8 @@ class Round: ModelObject, Storable {
}
func getActiveLoserRound() -> Round? {
let rounds = loserRounds()
return rounds.filter({ $0.hasStarted() && $0.hasEnded() == false && $0.isDisabled() == false }).sorted(by: \.index).reversed().first ?? rounds.first(where: { $0.isDisabled() == false })
let rounds = loserRounds().filter({ $0.isDisabled() == false }).sorted(by: \.index).reversed()
return rounds.first(where: { $0.hasStarted() && $0.hasEnded() == false }) ?? rounds.first
}
func enableRound() {
@ -268,39 +271,6 @@ class Round: ModelObject, Storable {
try? DataStore.shared.matches.addOrUpdate(contentOfs: _matches)
}
func handleLoserRoundState() {
let _matches = _matches()
_matches.forEach { match in
let previousRound = self.previousRound()
let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: match.index)
var parentMatches = [Match]()
if isLoserBracket(), previousRound == nil, let parentRound = parentRound {
let upperBracketTopMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2)
let upperBracketBottomMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2 + 1)
parentMatches = [upperBracketTopMatch, upperBracketBottomMatch].compactMap({ $0 })
} else if let previousRound {
let previousRoundTopMatch : Match? = Store.main.filter {
$0.round == previousRound.id && $0.index == match.topPreviousRoundMatchIndex()
}.first
let previousRoundBottomMatch : Match? = Store.main.filter {
$0.round == previousRound.id && $0.index == match.bottomPreviousRoundMatchIndex()
}.first
parentMatches = [previousRoundTopMatch, previousRoundBottomMatch].compactMap({ $0 })
}
if parentMatches.anySatisfy({ $0.disabled }) {
match.disabled = true
} else if parentMatches.allSatisfy({ $0.disabled == false }) {
match.disabled = false
}
}
try? DataStore.shared.matches.addOrUpdate(contentOfs: _matches)
loserRounds().forEach { round in
round.handleLoserRoundState()
}
}
var cumulativeMatchCount: Int {
var totalMatches = playedMatches().count
if let parent = parentRound {
@ -317,13 +287,58 @@ class Round: ModelObject, Storable {
}
}
func disabledMatches() -> [Match] {
_matches().filter({ $0.disabled })
}
var theoryCumulativeMatchCount: Int {
var totalMatches = RoundRule.numberOfMatches(forRoundIndex: index)
if let parent = parentRound {
totalMatches += parent.theoryCumulativeMatchCount
}
return totalMatches
}
func correspondingLoserRoundTitle(_ displayStyle: DisplayStyle = .wide) -> String {
let initialMatchIndexFromRoundIndex = RoundRule.matchIndex(fromRoundIndex: index)
let seedsAfterThisRound : [TeamRegistration] = Store.main.filter(isIncluded: {
$0.tournament == tournament
&& $0.bracketPosition != nil
&& ($0.bracketPosition! / 2) < initialMatchIndexFromRoundIndex
})
let playedMatches = playedMatches()
let seedInterval = SeedInterval(first: playedMatches.count + seedsAfterThisRound.count + 1, last: playedMatches.count * 2 + seedsAfterThisRound.count)
return seedInterval.localizedLabel(displayStyle)
}
func seedInterval() -> SeedInterval? {
if loser == nil {
let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: index + 1)
let initialMatchIndexFromRoundIndex = RoundRule.matchIndex(fromRoundIndex: index)
let seedsAfterThisRound : [TeamRegistration] = Store.main.filter(isIncluded: {
$0.tournament == tournament
&& $0.bracketPosition != nil
&& ($0.bracketPosition! / 2) < initialMatchIndexFromRoundIndex
})
let playedMatches = playedMatches()
let reduce = numberOfMatches / 2 - (playedMatches.count + seedsAfterThisRound.count)
return SeedInterval(first: 1, last: numberOfMatches, reduce: reduce)
}
if let previousRound = previousRound() {
return previousRound.seedInterval()?.chunks()?.first
} else if let parentRound = parentRound {
return parentRound.seedInterval()?.chunks()?.last
}
return nil
}
func roundTitle(_ displayStyle: DisplayStyle = .wide) -> String {
if let parentRound, let initialRound = parentRound.initialRound() {
let parentMatchCount = parentRound.cumulativeMatchCount - initialRound.playedMatches().count
// print("initialRound", initialRound.roundTitle())
if let initialRoundNextRound = initialRound.nextRound()?.playedMatches() {
return SeedInterval(first: parentMatchCount + initialRoundNextRound.count * 2 + 1, last: parentMatchCount + initialRoundNextRound.count * 2 + (previousRound() ?? parentRound).playedMatches().count).localizedLabel(displayStyle)
}
if loser != nil {
return seedInterval()?.localizedLabel(displayStyle) ?? "Pas trouvé"
}
return RoundRule.roundName(fromRoundIndex: index)
}
@ -387,6 +402,15 @@ class Round: ModelObject, Storable {
return Store.main.findById(parentRound)
}
func updateMatchFormat(_ matchFormat: MatchFormat) {
self.matchFormat = matchFormat
let playedMatches = _matches()
playedMatches.forEach { match in
match.matchFormat = matchFormat
}
try? DataStore.shared.matches.addOrUpdate(contentOfs: playedMatches)
}
override func deleteDependencies() throws {
try Store.main.deleteDependencies(items: _matches())
try Store.main.deleteDependencies(items: loserRoundsAndChildren())
@ -418,4 +442,8 @@ extension Round: Selectable {
return playedMatches().filter({ $0.isRunning() }).count
}
}
func badgeImage() -> Badge? {
hasEnded() ? .checkmark : nil
}
}

@ -128,9 +128,9 @@ class TeamRegistration: ModelObject, Storable {
func teamLabel(_ displayStyle: DisplayStyle = .wide) -> String {
switch displayStyle {
case .wide:
unsortedPlayers().map { $0.playerLabel(displayStyle) }.joined(separator: " & ")
players().map { $0.playerLabel(displayStyle) }.joined(separator: " & ")
case .short:
unsortedPlayers().map { $0.playerLabel(.wide) }.joined(separator: "\n")
players().map { $0.playerLabel(.wide) }.joined(separator: "\n")
}
}
@ -283,6 +283,19 @@ class TeamRegistration: ModelObject, Storable {
return Store.main.findById(groupStage)
}
func initialRound() -> Round? {
guard let bracketPosition else { return nil }
let roundIndex = RoundRule.roundIndex(fromMatchIndex: bracketPosition / 2)
return Store.main.filter(isIncluded: { $0.tournament == tournament && $0.index == roundIndex }).first
}
func initialMatch() -> Match? {
guard let bracketPosition else { return nil }
guard let initialRoundObject = initialRound() else { return nil }
return Store.main.filter(isIncluded: { $0.round == initialRoundObject.id && $0.index == bracketPosition / 2 }).first
}
func tournamentObject() -> Tournament? {
Store.main.findById(tournament)
}

@ -44,11 +44,14 @@ class Tournament : ModelObject, Storable {
var maleUnrankedValue: Int?
var femaleUnrankedValue: Int?
var payment: TournamentPayment? = nil
var additionalEstimationDuration: Int = 0
var courtsUnavailability: [Int: [DateInterval]]? = nil
@ObservationIgnored
var navigationPath: [Screen] = []
internal init(event: String? = nil, creator: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = true, groupStageFormat: Int? = nil, roundFormat: Int? = nil, loserRoundFormat: Int? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, groupStageCourtCount: Int? = nil, seedCount: Int = 8, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, maleUnrankedValue: Int? = nil, femaleUnrankedValue: Int? = nil) {
internal init(event: String? = nil, creator: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = true, groupStageFormat: Int? = nil, roundFormat: Int? = nil, loserRoundFormat: Int? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, groupStageCourtCount: Int? = nil, seedCount: Int = 8, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil) {
self.event = event
self.creator = creator
self.name = name
@ -77,8 +80,6 @@ class Tournament : ModelObject, Storable {
self.qualifiedPerGroupStage = qualifiedPerGroupStage
self.teamsPerGroupStage = teamsPerGroupStage
self.entryFee = entryFee
self.maleUnrankedValue = maleUnrankedValue
self.femaleUnrankedValue = femaleUnrankedValue
self.teamSorting = teamSorting ?? federalLevelCategory.defaultTeamSortingType
}
@ -260,12 +261,16 @@ class Tournament : ModelObject, Storable {
let availableSeedSpot = availableSeedSpot(inRoundIndex: roundIndex)
let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex)
if availableSeeds.count == availableSeedSpot.count {
if availableSeeds.count == availableSeedSpot.count && availableSeedGroup.count == availableSeeds.count {
return availableSeedGroup
} else if (availableSeeds.count == availableSeedOpponentSpot.count && availableSeeds.count == self.availableSeeds().count) {
} else if (availableSeeds.count == availableSeedOpponentSpot.count && availableSeeds.count == self.availableSeeds().count) && availableSeedGroup.count == availableSeedOpponentSpot.count {
return availableSeedGroup
} else if let chunk = availableSeedGroup.chunk() {
return seedGroupAvailable(atRoundIndex: roundIndex, availableSeedGroup: chunk)
} else if let chunks = availableSeedGroup.chunks() {
if let chunk = chunks.first(where: { seedInterval in
seedInterval.first >= self.seededTeams().count
}) {
return seedGroupAvailable(atRoundIndex: roundIndex, availableSeedGroup: chunk)
}
}
}
@ -300,8 +305,12 @@ class Tournament : ModelObject, Storable {
for (index, seed) in availableSeeds.enumerated() {
seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: true)
}
} else if let chunk = seedGroup.chunk() {
setSeeds(inRoundIndex: roundIndex, inSeedGroup: chunk)
} else if let chunks = seedGroup.chunks() {
if let chunk = chunks.first(where: { seedInterval in
seedInterval.first >= self.seededTeams().count
}) {
setSeeds(inRoundIndex: roundIndex, inSeedGroup: chunk)
}
}
}
}
@ -320,17 +329,36 @@ class Tournament : ModelObject, Storable {
return groupStages.filter({ $0.hasStarted() && $0.hasEnded() == false }).sorted(by: \.index).first ?? groupStages.first
}
func getActiveRound() -> Round? {
func getActiveRound(withSeeds: Bool = false) -> Round? {
let rounds = rounds()
return rounds.filter({ $0.hasStarted() && $0.hasEnded() == false }).sorted(by: \.index).reversed().first ?? rounds.first
let round = rounds.filter({ $0.hasStarted() && $0.hasEnded() == false }).sorted(by: \.index).reversed().first ?? rounds.first
if withSeeds {
if round?.seeds().isEmpty == false {
return round
} else {
return nil
}
} else {
return round
}
}
func allRoundMatches() -> [Match] {
allRounds().flatMap { $0._matches() }
}
func allMatches() -> [Match] {
let unsortedGroupStages : [GroupStage] = Store.main.filter { $0.tournament == self.id }
let matches: [Match] = unsortedGroupStages.flatMap { $0._matches() } + allRounds().flatMap { $0._matches() }
let matches: [Match] = unsortedGroupStages.flatMap { $0._matches() } + allRoundMatches()
return matches.filter({ $0.disabled == false })
}
func _allMatchesIncludingDisabled() -> [Match] {
let unsortedGroupStages : [GroupStage] = Store.main.filter { $0.tournament == self.id }
return unsortedGroupStages.flatMap { $0._matches() } + allRounds().flatMap { $0._matches() }
}
func allRounds() -> [Round] {
Store.main.filter { $0.tournament == self.id }
}
@ -376,7 +404,7 @@ class Tournament : ModelObject, Storable {
_sortedTeams = bracketTeams.sorted(using: _currentSelectionSorting, order: .ascending) + groupStageTeams.sorted(using: _currentSelectionSorting, order: .ascending)
}
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
//let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
//print("func selectedSortedTeams", id, tournamentTitle(), duration.formatted(.units(allowed: [.seconds, .milliseconds])))
return _sortedTeams
}
@ -448,7 +476,7 @@ class Tournament : ModelObject, Storable {
//todo
var clubName: String? {
nil
eventObject?.clubObject?.name
}
//todo
@ -493,7 +521,14 @@ class Tournament : ModelObject, Storable {
func playersWithoutValidLicense(in players: [PlayerRegistration]) -> [PlayerRegistration] {
let licenseYearValidity = licenseYearValidity()
return players.filter({ ($0.isImported() && $0.isValidLicenseNumber(year: licenseYearValidity) == false) || ($0.isImported() == false && ($0.licenceId == nil || $0.licenceId?.isLicenseNumber == false || $0.licenceId?.isEmpty == true)) })
return players.filter({
($0.isImported() && $0.isValidLicenseNumber(year: licenseYearValidity) == false) || ($0.isImported() == false && ($0.licenceId == nil || $0.formattedLicense().isLicenseNumber == false || $0.licenceId?.isEmpty == true))
})
}
func getStartDate(ofSeedIndex seedIndex: Int?) -> Date? {
guard let seedIndex else { return nil }
return selectedSortedTeams()[safe: seedIndex]?.callDate
}
func importTeams(_ teams: [FileImportManager.TeamHolder]) {
@ -503,7 +538,7 @@ class Tournament : ModelObject, Storable {
previousTeam.updatePlayers(team.players)
teamsToImport.append(previousTeam)
} else {
let newTeam = addTeam(team.players)
let newTeam = addTeam(team.players, registrationDate: team.registrationDate)
teamsToImport.append(newTeam)
}
}
@ -513,6 +548,59 @@ class Tournament : ModelObject, Storable {
}
func maximumCourtsPerGroupSage() -> Int {
if teamsPerGroupStage > 1 {
return min(teamsPerGroupStage / 2, courtCount)
} else {
return max(1, courtCount)
}
}
func registrationIssues() -> Int {
let players : [PlayerRegistration] = unsortedPlayers()
let selectedTeams : [TeamRegistration] = selectedSortedTeams()
let callDateIssue : [TeamRegistration] = selectedTeams.filter { isStartDateIsDifferentThanCallDate($0) }
let duplicates : [PlayerRegistration] = duplicates(in: players)
let problematicPlayers : [PlayerRegistration] = players.filter({ $0.sex == -1 })
let inadequatePlayers : [PlayerRegistration] = inadequatePlayers(in: players)
let playersWithoutValidLicense : [PlayerRegistration] = playersWithoutValidLicense(in: players)
let playersMissing : [TeamRegistration] = selectedTeams.filter({ $0.unsortedPlayers().count < 2 })
let waitingList : [TeamRegistration] = waitingListTeams(in: selectedTeams)
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
}
func isStartDateIsDifferentThanCallDate(_ team: TeamRegistration) -> Bool {
guard let callDate = team.callDate else { return false }
if let groupStageStartDate = team.groupStageObject()?.startDate {
return Calendar.current.compare(callDate, to: groupStageStartDate, toGranularity: .minute) != ComparisonResult.orderedSame
} else if let roundMatchStartDate = team.initialMatch()?.startDate {
return Calendar.current.compare(callDate, to: roundMatchStartDate, toGranularity: .minute) != ComparisonResult.orderedSame
}
return false
}
func availableToStart(_ allMatches: [Match]) -> [Match] {
let runningMatches = allMatches.filter({ $0.isRunning() && $0.isReady() })
return allMatches.filter({ $0.canBeStarted(inMatches: runningMatches) && $0.isRunning() == false }).sorted(by: \.computedStartDateForSorting)
}
func runningMatches(_ allMatches: [Match]) -> [Match] {
allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(by: \.computedStartDateForSorting)
}
func readyMatches(_ allMatches: [Match]) -> [Match] {
return allMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }).sorted(by: \.computedStartDateForSorting)
}
func finishedMatches(_ allMatches: [Match], limit: Int? = nil) -> [Match] {
let _limit = limit ?? courtCount
return Array(allMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed().prefix(_limit))
}
func lockRegistration() {
closedRegistrationDate = Date()
let count = selectedSortedTeams().count
@ -540,22 +628,27 @@ class Tournament : ModelObject, Storable {
func updateRank(to newDate: Date?) async throws {
guard let newDate else { return }
rankSourceDate = newDate
let lastRankWoman = SourceFileManager.shared.getUnrankValue(forMale: false, rankSourceDate: rankSourceDate)
let lastRankMan = SourceFileManager.shared.getUnrankValue(forMale: true, rankSourceDate: rankSourceDate)
if currentMonthData() == nil {
let lastRankWoman = SourceFileManager.shared.getUnrankValue(forMale: false, rankSourceDate: rankSourceDate)
let lastRankMan = SourceFileManager.shared.getUnrankValue(forMale: true, rankSourceDate: rankSourceDate)
await MainActor.run {
let monthData = MonthData(monthKey: URL.importDateFormatter.string(from: newDate))
monthData.maleUnrankedValue = lastRankMan
monthData.femaleUnrankedValue = lastRankWoman
try? DataStore.shared.monthData.addOrUpdate(instance: monthData)
}
}
let lastRankMan = currentMonthData()?.maleUnrankedValue
let lastRankWoman = currentMonthData()?.femaleUnrankedValue
try await unsortedPlayers().concurrentForEach { player in
let dataURLs = SourceFileManager.shared.allFiles.filter({ $0.dateFromPath == newDate })
let sources = dataURLs.map { CSVParser(url: $0) }
try await player.updateRank(from: sources, lastRank: (player.sex == 0 ? lastRankWoman : lastRankMan) ?? 0)
}
await MainActor.run {
self.maleUnrankedValue = lastRankMan
self.femaleUnrankedValue = lastRankWoman
}
}
func missingUnrankedValue() -> Bool {
@ -629,16 +722,11 @@ class Tournament : ModelObject, Storable {
}
func umpireMail() -> [String]? {
if let mail = UserDefaults.standard.string(forKey: "umpireMail"), mail.isEmpty == false {
return [mail]
if let email = DataStore.shared.user?.email {
return [email]
} else {
return nil
}
// if let umpireMail = federalTournament?.courrielEngagement {
// return [umpireMail]
// } else {
// }
}
func earnings() -> Double {
@ -655,19 +743,32 @@ class Tournament : ModelObject, Storable {
return Double(selectedPlayers.filter { $0.hasPaid() }.count) / Double(selectedPlayers.count)
}
func cashierStatus() -> String {
//todo
return "todo"
typealias TournamentStatus = (label:String, completion: String)
func cashierStatus() -> TournamentStatus {
let selectedPlayers = selectedPlayers()
let paid = selectedPlayers.filter({ $0.hasPaid() })
let label = paid.count.formatted() + " / " + selectedPlayers.count.formatted() + " joueurs encaissés"
let completion = (Double(paid.count) / Double(selectedPlayers.count))
let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0)))
return TournamentStatus(label: label, completion: completionLabel)
}
func scheduleStatus() -> String {
//todo
return "todo"
func scheduleStatus() -> TournamentStatus {
let allMatches = allMatches()
let ready = allMatches.filter({ $0.startDate != nil })
let label = ready.count.formatted() + " / " + allMatches.count.formatted() + " matchs programmés"
let completion = (Double(ready.count) / Double(allMatches.count))
let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0)))
return TournamentStatus(label: label, completion: completionLabel)
}
func callStatus() -> String {
//todo
return "todo"
func callStatus() -> TournamentStatus {
let selectedSortedTeams = selectedSortedTeams()
let called = selectedSortedTeams.filter{ $0.called() }
let label = called.count.formatted() + " / " + selectedSortedTeams.count.formatted() + " paires convoquées"
let completion = (Double(called.count) / Double(selectedSortedTeams.count))
let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0)))
return TournamentStatus(label: label, completion: completionLabel)
}
func bracketStatus() -> String {
@ -826,8 +927,12 @@ class Tournament : ModelObject, Storable {
entryFee == nil || entryFee == 0
}
func addTeam(_ players: Set<PlayerRegistration>) -> TeamRegistration {
let team = TeamRegistration(tournament: id, registrationDate: Date())
func indexOf(team: TeamRegistration) -> Int? {
selectedSortedTeams().firstIndex(where: { $0.id == team.id })
}
func addTeam(_ players: Set<PlayerRegistration>, registrationDate: Date? = nil) -> TeamRegistration {
let team = TeamRegistration(tournament: id, registrationDate: registrationDate ?? Date())
team.tournamentCategory = tournamentCategory
team.setWeight(from: Array(players))
players.forEach { player in
@ -926,7 +1031,14 @@ class Tournament : ModelObject, Storable {
return groupStageMatchFormat
}
}
func setupFederalSettings() {
teamSorting = tournamentLevel.defaultTeamSortingType
groupStageMatchFormat = groupStageSmartMatchFormat()
loserBracketMatchFormat = loserBracketSmartMatchFormat(1)
matchFormat = roundSmartMatchFormat(1)
}
func roundSmartMatchFormat(_ roundIndex: Int) -> MatchFormat {
let format = tournamentLevel.federalFormatForBracketRound(roundIndex)
if matchFormat.rank > format.rank {
@ -958,6 +1070,21 @@ class Tournament : ModelObject, Storable {
try Store.main.deleteDependencies(items: self.groupStages())
try Store.main.deleteDependencies(items: self.rounds())
}
func currentMonthData() -> MonthData? {
guard let rankSourceDate else { return nil }
let dateString = URL.importDateFormatter.string(from: rankSourceDate)
return Store.main.filter(isIncluded: { $0.monthKey == dateString }).first
}
var maleUnrankedValue: Int? {
currentMonthData()?.maleUnrankedValue
}
var femaleUnrankedValue: Int? {
currentMonthData()?.femaleUnrankedValue
}
}
extension Tournament {
@ -991,8 +1118,7 @@ extension Tournament {
case _qualifiedPerGroupStage = "qualifiedPerGroupStage"
case _teamsPerGroupStage = "teamsPerGroupStage"
case _entryFee = "entryFee"
case _maleUnrankedValue = "maleUnrankedValue"
case _femaleUnrankedValue = "femaleUnrankedValue"
case _additionalEstimationDuration = "additionalEstimationDuration"
}
}
@ -1036,6 +1162,4 @@ extension Tournament: TournamentBuildHolder {
var age: FederalTournamentAge {
federalTournamentAge
}
}

@ -89,6 +89,10 @@ extension Date {
}
}
func atBeginningOfDay(hourInt: Int = 9) -> Date {
Calendar.current.date(byAdding: .hour, value: hourInt, to: self.startOfDay)!
}
static var firstDayOfWeek = Calendar.current.firstWeekday
static var capitalizedFirstLettersOfWeekdays: [String] {
let calendar = Calendar.current
@ -180,9 +184,15 @@ extension Date {
var dayInt: Int {
Calendar.current.component(.day, from: self)
}
var startOfDay: Date {
Calendar.current.startOfDay(for: self)
}
func endOfDay() -> Date {
let calendar = Calendar.current
return calendar.date(bySettingHour: 23, minute: 59, second: 59, of: self)!
}
}
extension Date {
@ -191,3 +201,12 @@ extension Date {
}
}
extension Date {
func localizedTime() -> String {
self.formatted(.dateTime.hour().minute())
}
func localizedDay() -> String {
self.formatted(.dateTime.weekday(.wide).day())
}
}

@ -159,3 +159,8 @@ extension String {
}
}
}
extension StringProtocol {
var firstUppercased: String { prefix(1).uppercased() + dropFirst() }
var firstCapitalized: String { prefix(1).capitalized + dropFirst() }
}

@ -8,6 +8,7 @@
import Foundation
import SwiftUI
import MessageUI
import LeStorage
enum ContactManagerError: LocalizedError {
case mailFailed
@ -33,7 +34,7 @@ extension ContactType {
static let defaultSignature = ""
static func callingGroupStageCustomMessage(tournament: Tournament?, startDate: Date?, roundLabel: String) -> String {
let tournamentCustomMessage = UserDefaults.standard.string(forKey: "customMessage") ?? defaultCustomMessage
let tournamentCustomMessage = DataStore.shared.appSettings.callMessageBody ?? defaultCustomMessage
let clubName = tournament?.clubName ?? ""
var text = tournamentCustomMessage
@ -48,7 +49,7 @@ extension ContactType {
text = text.replacingOccurrences(of: "#jour", with: "\(date.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide)))")
text = text.replacingOccurrences(of: "#horaire", with: "\(date.formatted(Date.FormatStyle().hour().minute()))")
let signature = UserDefaults.standard.string(forKey: "mySelf") ?? defaultSignature
let signature = DataStore.shared.appSettings.callMessageSignature ?? defaultSignature
text = text.replacingOccurrences(of: "#signature", with: signature)
return text
@ -56,7 +57,7 @@ extension ContactType {
static func callingGroupStageMessage(tournament: Tournament?, startDate: Date?, roundLabel: String, matchFormat: MatchFormat?) -> String {
let useFullCustomMessage = UserDefaults.standard.bool(forKey: "useFullCustomMessage")
let useFullCustomMessage = DataStore.shared.appSettings.callUseFullCustomMessage ?? false
if useFullCustomMessage {
return callingGroupStageCustomMessage(tournament: tournament, startDate: startDate, roundLabel: roundLabel)
@ -65,17 +66,17 @@ extension ContactType {
let date = startDate ?? tournament?.startDate ?? Date()
let clubName = tournament?.clubName ?? ""
let message = UserDefaults.standard.string(forKey: "customMessage") ?? defaultCustomMessage
let signature = UserDefaults.standard.string(forKey: "mySelf") ?? defaultSignature
let message = DataStore.shared.appSettings.callMessageBody ?? defaultCustomMessage
let signature = DataStore.shared.appSettings.callMessageSignature ?? defaultSignature
let localizedCalled = "convoqué" + (tournament?.tournamentCategory == .women ? "e" : "") + "s"
var formatMessage: String? {
UserDefaults.standard.bool(forKey: "displayFormat") ? matchFormat?.computedLongLabel.appending(".") : nil
(DataStore.shared.appSettings.callDisplayFormat ?? false) ? matchFormat?.computedLongLabel.appending(".") : nil
}
var entryFeeMessage: String? {
UserDefaults.standard.bool(forKey: "displayEntryFee") ? tournament?.entryFeeMessage : nil
(DataStore.shared.appSettings.callDisplayEntryFee ?? false) ? tournament?.entryFeeMessage : nil
}
var computedMessage: String {

@ -44,10 +44,13 @@ class FileImportManager {
var id: Self { self }
case frenchFederation
case padelClub
case unknown
var localizedLabel: String {
switch self {
case .padelClub:
return "Padel Club"
case .frenchFederation:
return "FFT"
case .unknown:
@ -58,28 +61,32 @@ class FileImportManager {
struct TeamHolder: Identifiable {
let id: UUID = UUID()
let playerOne: PlayerRegistration
let playerTwo: PlayerRegistration
let players: Set<PlayerRegistration>
let weight: Int
let tournamentCategory: TournamentCategory
let previousTeam: TeamRegistration?
init(playerOne: PlayerRegistration, playerTwo: PlayerRegistration, tournamentCategory: TournamentCategory, previousTeam: TeamRegistration?) {
self.playerOne = playerOne
self.playerTwo = playerTwo
var registrationDate: Date? = nil
init(players: [PlayerRegistration], tournamentCategory: TournamentCategory, previousTeam: TeamRegistration?, registrationDate: Date? = nil) {
self.players = Set(players)
self.tournamentCategory = tournamentCategory
self.previousTeam = previousTeam
self.weight = playerOne.weight + playerTwo.weight
}
var players: Set<PlayerRegistration> {
Set([playerOne, playerTwo])
self.weight = players.map { $0.weight }.reduce(0,+)
self.registrationDate = registrationDate
}
func index(in teams: [TeamHolder]) -> Int? {
teams.firstIndex(where: { $0.id == id })
}
func formattedSeedIndex(index: Int?) -> String {
if let index {
return "#\(index + 1)"
} else {
return "###"
}
}
func formattedSeed(in teams: [TeamHolder]) -> String {
if let index = index(in: teams) {
return "#\(index + 1)"
@ -92,6 +99,60 @@ class FileImportManager {
static let FFT_ASSIMILATION_WOMAN_IN_MAN = "A calculer selon la pondération en vigueur"
func createTeams(from fileContent: String, tournament: Tournament, fileProvider: FileProvider = .frenchFederation) async -> [TeamHolder] {
switch fileProvider {
case .frenchFederation:
return await _getFederalTeams(from: fileContent, tournament: tournament)
case .padelClub:
return await _getPadelClubTeams(from: fileContent, tournament: tournament)
case .unknown:
return await _getPadelBusinessLeagueTeams(from: fileContent, tournament: tournament)
}
}
func importDataFromFFT() async -> String? {
if let importingDate = SourceFileManager.shared.mostRecentDateAvailable {
for source in SourceFile.allCases {
for fileURL in source.currentURLs {
let p = readCSV(inputFile: fileURL)
await importingChunkOfPlayers(p, importingDate: importingDate)
}
}
return URL.importDateFormatter.string(from: importingDate)
}
return nil
}
func readCSV(inputFile: URL) -> [FederalPlayer] {
do {
let fileContent = try String(contentsOf: inputFile)
return loadFromCSV(fileContent: fileContent, isMale: inputFile.manData)
} catch {
print("error: \(error)") // to do deal with errors
}
return []
}
func loadFromCSV(fileContent: String, isMale: Bool) -> [FederalPlayer] {
let lines = fileContent.components(separatedBy: "\n")
return lines.compactMap { line in
if line.components(separatedBy: ";").count < 10 {
} else {
let data = line.components(separatedBy: ";").joined(separator: "\n")
return FederalPlayer(data, isMale: isMale)
}
return nil
}
}
func importingChunkOfPlayers(_ players: [FederalPlayer], importingDate: Date) async {
for chunk in players.chunked(into: 1000) {
await PersistenceController.shared.batchInsertPlayers(chunk, importingDate: importingDate)
}
}
private func _getFederalTeams(from fileContent: String, tournament: Tournament) async -> [TeamHolder] {
let lines = fileContent.components(separatedBy: "\n")
guard let firstLine = lines.first else { return [] }
var separator = ","
@ -100,58 +161,7 @@ class FileImportManager {
}
let headerCount = firstLine.components(separatedBy: separator).count
var results: [TeamHolder] = []
if headerCount == 23 && fileProvider == .unknown { //PBL
let fetchRequest = ImportedPlayer.fetchRequest()
let federalContext = PersistenceController.shared.localContainer.viewContext
lines.dropFirst().forEach { line in
let data = line.components(separatedBy: separator)
if data.count == 23 {
// let team = Team(context: context)
// let brand = Brand(context: context)
// brand.title = data[2].trimmed
// brand.qualifier = data[0].trimmed
// brand.country = data[1].trimmed
// brand.lineOfBusiness = data[3].trimmed
// if brand.lineOfBusiness == "Bâtiment / Immo" { //quick fix
// brand.lineOfBusiness = "Bâtiment / Immo / Transport"
// }
// brand.name = data[4].trimmed
// team.brand = brand
//
// for i in 0...5 {
// let sex = data[i*3+5]
// let lastName = data[i*3+6].trimmed
// let firstName = data[i*3+7].trimmed
// if lastName.isEmpty == false {
// let playerOne = Player(context: context)
// let predicate = NSPredicate(format: "(canonicalLastName matches[cd] %@ OR canonicalLastName matches[cd] %@) AND (canonicalFirstName matches[cd] %@ OR canonicalFirstName matches[cd] %@)", lastName, lastName.removePunctuationAndHyphens, firstName, firstName.removePunctuationAndHyphens)
// fetchRequest.predicate = predicate
// if let playerFound = try? federalContext.fetch(fetchRequest).first {
// playerOne.updateWithImportedPlayer(playerFound)
// } else {
// playerOne.lastName = lastName
// playerOne.firstName = firstName
// playerOne.sex = sex == "H" ? 1 : sex == "F" ? 0 : -1
// playerOne.currentRank = tournament?.lastRankMan ?? 0
// }
// team.addToPlayers(playerOne)
// }
// }
// team.category = TournamentCategory.men.importingRawValue
//
// if let players = team.players, players.count > 0 {
// results.append(team)
// } else {
// context.delete(team)
// }
}
}
return results
} else if headerCount <= 18 && fileProvider == .frenchFederation {
if headerCount <= 18 {
Array(lines.dropFirst()).chunked(into: 2).forEach { teamLines in
if teamLines.count == 2 {
let dataOne = teamLines[0].replacingOccurrences(of: "\"", with: "").components(separatedBy: separator)
@ -203,13 +213,13 @@ class FileImportManager {
playerOne.setWeight(in: tournament)
let playerTwo = PlayerRegistration(federalData: Array(resultTwo[0...7]), sex: sexPlayerTwo, sexUnknown: sexUnknown)
playerTwo.setWeight(in: tournament)
let team = TeamHolder(playerOne: playerOne, playerTwo: playerTwo, tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo]))
let team = TeamHolder(players: [playerOne, playerTwo], tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo]))
results.append(team)
}
}
}
return results
} else if headerCount > 18 && fileProvider == .frenchFederation {
} else {
lines.dropFirst().forEach { line in
let data = line.components(separatedBy: separator)
if data.count > 18 {
@ -236,7 +246,7 @@ class FileImportManager {
case .mix: return 1
}
}
var sexPlayerTwo : Int {
switch tournamentCategory {
case .men: return 1
@ -249,56 +259,108 @@ class FileImportManager {
playerOne.setWeight(in: tournament)
let playerTwo = PlayerRegistration(federalData: Array(result[8...]), sex: sexPlayerTwo, sexUnknown: sexUnknown)
playerTwo.setWeight(in: tournament)
let team = TeamHolder(playerOne: playerOne, playerTwo: playerTwo, tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo]))
let team = TeamHolder(players: [playerOne, playerTwo], tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo]))
results.append(team)
}
}
return results
} else {
return []
}
}
func importDataFromFFT() async -> String? {
if let importingDate = SourceFileManager.shared.mostRecentDateAvailable {
for source in SourceFile.allCases {
for fileURL in source.currentURLs {
let p = readCSV(inputFile: fileURL)
await importingChunkOfPlayers(p, importingDate: importingDate)
private func _getPadelClubTeams(from fileContent: String, tournament: Tournament) async -> [TeamHolder] {
let lines = fileContent.components(separatedBy: "\n\n")
var results: [TeamHolder] = []
let fetchRequest = ImportedPlayer.fetchRequest()
let federalContext = PersistenceController.shared.localContainer.viewContext
lines.forEach { team in
let data = team.components(separatedBy: "\n")
let players = team.licencesFound()
fetchRequest.predicate = NSPredicate(format: "license IN %@", players)
let found = try? federalContext.fetch(fetchRequest)
let registeredPlayers = found?.map({ importedPlayer in
let player = PlayerRegistration(importedPlayer: importedPlayer)
player.setWeight(in: tournament)
return player
})
if let registeredPlayers, registeredPlayers.isEmpty == false {
var registrationDate: Date? {
if let registrationDateData = data[safe:2]?.replacingOccurrences(of: "inscrit le ", with: "") {
return try? Date(registrationDateData, strategy: .dateTime.weekday().day().month().hour().minute())
}
return nil
}
let team = TeamHolder(players: registeredPlayers, tournamentCategory: tournament.tournamentCategory, previousTeam: tournament.findTeam(registeredPlayers), registrationDate: registrationDate)
results.append(team)
}
return URL.importDateFormatter.string(from: importingDate)
}
return nil
}
func readCSV(inputFile: URL) -> [FederalPlayer] {
do {
let fileContent = try String(contentsOf: inputFile)
return loadFromCSV(fileContent: fileContent, isMale: inputFile.manData)
} catch {
print("error: \(error)") // to do deal with errors
}
return []
return results
}
func loadFromCSV(fileContent: String, isMale: Bool) -> [FederalPlayer] {
private func _getPadelBusinessLeagueTeams(from fileContent: String, tournament: Tournament) async -> [TeamHolder] {
let lines = fileContent.components(separatedBy: "\n")
return lines.compactMap { line in
if line.components(separatedBy: ";").count < 10 {
} else {
let data = line.components(separatedBy: ";").joined(separator: "\n")
return FederalPlayer(data, isMale: isMale)
}
return nil
guard let firstLine = lines.first else { return [] }
var separator = ","
if firstLine.contains(";") {
separator = ";"
}
}
func importingChunkOfPlayers(_ players: [FederalPlayer], importingDate: Date) async {
for chunk in players.chunked(into: 1000) {
await PersistenceController.shared.batchInsertPlayers(chunk, importingDate: importingDate)
let headerCount = firstLine.components(separatedBy: separator).count
var results: [TeamHolder] = []
if headerCount == 23 {
//todo
let fetchRequest = ImportedPlayer.fetchRequest()
let federalContext = PersistenceController.shared.localContainer.viewContext
lines.dropFirst().forEach { line in
let data = line.components(separatedBy: separator)
if data.count == 23 {
// let team = Team(context: context)
// let brand = Brand(context: context)
// brand.title = data[2].trimmed
// brand.qualifier = data[0].trimmed
// brand.country = data[1].trimmed
// brand.lineOfBusiness = data[3].trimmed
// if brand.lineOfBusiness == "Bâtiment / Immo" { //quick fix
// brand.lineOfBusiness = "Bâtiment / Immo / Transport"
// }
// brand.name = data[4].trimmed
// team.brand = brand
//
// for i in 0...5 {
// let sex = data[i*3+5]
// let lastName = data[i*3+6].trimmed
// let firstName = data[i*3+7].trimmed
// if lastName.isEmpty == false {
// let playerOne = Player(context: context)
// let predicate = NSPredicate(format: "(canonicalLastName matches[cd] %@ OR canonicalLastName matches[cd] %@) AND (canonicalFirstName matches[cd] %@ OR canonicalFirstName matches[cd] %@)", lastName, lastName.removePunctuationAndHyphens, firstName, firstName.removePunctuationAndHyphens)
// fetchRequest.predicate = predicate
// if let playerFound = try? federalContext.fetch(fetchRequest).first {
// playerOne.updateWithImportedPlayer(playerFound)
// } else {
// playerOne.lastName = lastName
// playerOne.firstName = firstName
// playerOne.sex = sex == "H" ? 1 : sex == "F" ? 0 : -1
// playerOne.currentRank = tournament?.lastRankMan ?? 0
// }
// team.addToPlayers(playerOne)
// }
// }
// team.category = TournamentCategory.men.importingRawValue
//
// if let players = team.players, players.count > 0 {
// results.append(team)
// } else {
// context.delete(team)
// }
}
}
return results
}
return []
}
}

@ -159,20 +159,18 @@ enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifiable {
case a45 = 450
case a55 = 550
static func mostRecent(tournaments: [Tournament] = []) -> Self {
.senior
// return tournaments.first?.federalTournamentAge ?? .a11_12
static func mostRecent(inTournaments tournaments: [Tournament]) -> Self {
return tournaments.first?.federalTournamentAge ?? .senior
}
static func mostUsed(tournaments: [Tournament] = []) -> Self {
// let countedSet = NSCountedSet(array: tournaments.map { $0.federalTournamentAge })
// let mostFrequent = countedSet.max { countedSet.count(for: $0) < countedSet.count(for: $1) }
// if mostFrequent != nil {
// return mostFrequent as! FederalTournamentAge
// } else {
// return mostRecent(tournaments: tournaments)
// }
.senior
static func mostUsed(inTournaments tournaments: [Tournament]) -> Self {
let countedSet = NSCountedSet(array: tournaments.map { $0.federalTournamentAge })
let mostFrequent = countedSet.max { countedSet.count(for: $0) < countedSet.count(for: $1) }
if mostFrequent != nil {
return mostFrequent as! FederalTournamentAge
} else {
return mostRecent(inTournaments: tournaments)
}
}
var id: Int { self.rawValue }
@ -236,22 +234,20 @@ enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable {
case p1500 = 1500
case p2000 = 2000
static func mostRecent(tournaments: [Tournament] = []) -> Self {
//return tournaments.first?.tournamentLevel ?? .p25
.p100
static func mostRecent(inTournaments tournaments: [Tournament]) -> Self {
return tournaments.first?.tournamentLevel ?? .p100
}
static func mostUsed(tournaments: [Tournament] = []) -> Self {
// let countedSet = NSCountedSet(array: tournaments.map { $0.tournamentLevel })
// let mostFrequent = countedSet.max { countedSet.count(for: $0) < countedSet.count(for: $1) }
// if mostFrequent != nil {
// return mostFrequent as! TournamentLevel
// } else {
// return mostRecent(tournaments: tournaments)
// }
.p100
static func mostUsed(inTournaments tournaments: [Tournament]) -> Self {
let countedSet = NSCountedSet(array: tournaments.map { $0.tournamentLevel })
let mostFrequent = countedSet.max { countedSet.count(for: $0) < countedSet.count(for: $1) }
if mostFrequent != nil {
return mostFrequent as! TournamentLevel
} else {
return mostRecent(inTournaments: tournaments)
}
}
var id: Int { self.rawValue }
func maximumDuration() -> Double {
@ -631,20 +627,27 @@ enum TournamentCategory: Int, Hashable, Codable, CaseIterable, Identifiable {
}
}
static func mostRecent(tournaments: [Tournament] = []) -> Self {
//return tournaments.first?.tournamentCategory ?? .mix
.men
var showFemaleInMaleAssimilation: Bool {
switch self {
case .men:
return true
default:
return false
}
}
static func mostRecent(inTournaments tournaments: [Tournament]) -> Self {
return tournaments.first?.tournamentCategory ?? .men
}
static func mostUsed(tournaments: [Tournament] = []) -> Self {
// let countedSet = NSCountedSet(array: tournaments.map { $0.tournamentCategory })
// let mostFrequent = countedSet.max { countedSet.count(for: $0) < countedSet.count(for: $1) }
// if mostFrequent != nil {
// return mostFrequent as! TournamentCategory
// } else {
// return mostRecent(tournaments: tournaments)
// }
.men
static func mostUsed(inTournaments tournaments: [Tournament]) -> Self {
let countedSet = NSCountedSet(array: tournaments.map { $0.tournamentCategory })
let mostFrequent = countedSet.max { countedSet.count(for: $0) < countedSet.count(for: $1) }
if mostFrequent != nil {
return mostFrequent as! TournamentCategory
} else {
return mostRecent(inTournaments: tournaments)
}
}
var next: TournamentCategory {
@ -794,10 +797,12 @@ enum TournamentType: Int, Hashable, Codable, CaseIterable, Identifiable {
}
}
enum TeamPosition: Int, Hashable, Codable, CaseIterable {
enum TeamPosition: Int, Identifiable, Hashable, Codable, CaseIterable {
case one
case two
var id: Int { self.rawValue }
var otherTeam: TeamPosition {
switch self {
case .one:
@ -1019,10 +1024,14 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
}
static func defaultFormatForMatchType(_ matchType: MatchType) -> MatchFormat {
if UserDefaults.standard.object(forKey: matchType.rawValue + "MatchFormatPreference") == nil {
return .nineGamesDecisivePoint
switch matchType {
case .bracket:
MatchFormat(rawValue: DataStore.shared.appSettings.bracketMatchFormatPreference) ?? .nineGamesDecisivePoint
case .groupStage:
MatchFormat(rawValue: DataStore.shared.appSettings.groupStageMatchFormatPreference) ?? .nineGamesDecisivePoint
case .loserBracket:
MatchFormat(rawValue: DataStore.shared.appSettings.loserBracketMatchFormatPreference) ?? .nineGamesDecisivePoint
}
return MatchFormat(rawValue: UserDefaults.standard.integer(forKey: matchType.rawValue + "MatchFormatPreference")) ?? .nineGamesDecisivePoint
}
static var allCases: [MatchFormat] {
@ -1046,16 +1055,16 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
}
}
var estimatedDuration: Int {
if UserDefaults.standard.object(forKey: format) != nil {
return UserDefaults.standard.integer(forKey: format)
} else {
return defaultEstimatedDuration
}
func getEstimatedDuration(_ additionalDuration: Int = 0) -> Int {
estimatedDuration + additionalDuration
}
func formattedEstimatedDuration() -> String {
Duration.seconds(estimatedDuration * 60).formatted(.units(allowed: [.minutes]))
private var estimatedDuration: Int {
DataStore.shared.appSettings.matchFormatsDefaultDuration?[self] ?? defaultEstimatedDuration
}
func formattedEstimatedDuration(_ additionalDuration: Int = 0) -> String {
Duration.seconds((estimatedDuration + additionalDuration) * 60).formatted(.units(allowed: [.minutes]))
}
func formattedEstimatedBreakDuration() -> String {
@ -1419,6 +1428,10 @@ enum RoundRule {
return (1 << roundIndex) - 1
}
static func matchIndex(fromBracketPosition: Int) -> Int {
roundIndex(fromMatchIndex: fromBracketPosition / 2) + fromBracketPosition%2
}
static func roundIndex(fromMatchIndex matchIndex: Int) -> Int {
Int(log2(Double(matchIndex + 1)))
}

@ -12,7 +12,7 @@ class SourceFileManager {
static let beachPadel = URL(string: "https://beach-padel.app.fft.fr/beachja/index/")!
var lastDataSource: String? {
UserDefaults.standard.string(forKey: "lastDataSource")
DataStore.shared.appSettings.lastDataSource
}
func lastDataSourceDate() -> Date? {

@ -55,4 +55,8 @@ enum AgendaDestination: CaseIterable, Identifiable, Selectable {
nil
}
}
func badgeImage() -> Badge? {
nil
}
}

@ -0,0 +1,13 @@
//
// AppScreen.swift
// PadelClub
//
// Created by Razmig Sarkissian on 18/04/2024.
//
import Foundation
enum AppScreen: CaseIterable, Identifiable {
var id: Self { self }
case matchFormatSettings
}

@ -0,0 +1,32 @@
//
// DateInterval.swift
// PadelClub
//
// Created by Razmig Sarkissian on 19/04/2024.
//
import Foundation
import LeStorage
struct DateInterval: Identifiable, Codable {
var id: String = Store.randomId()
let startDate: Date
let endDate: Date
var range: Range<Date> {
startDate..<endDate
}
func isSingleDay() -> Bool {
Calendar.current.isDate(startDate, inSameDayAs: endDate)
}
func isDateInside(_ date: Date) -> Bool {
date >= startDate && date <= endDate
}
func isDateOutside(_ date: Date) -> Bool {
date <= startDate && date <= endDate && date >= startDate && date >= endDate
}
}

@ -29,8 +29,8 @@ class MatchDescriptor: ObservableObject {
}
let teamOne = match?.team(.one)
let teamTwo = match?.team(.two)
self.teamLabelOne = teamOne?.teamLabel() ?? ""
self.teamLabelTwo = teamTwo?.teamLabel() ?? ""
self.teamLabelOne = teamOne?.teamLabel(.short) ?? ""
self.teamLabelTwo = teamTwo?.teamLabel(.short) ?? ""
if let match, let scoresTeamOne = match.teamScore(ofTeam: teamOne)?.score, let scoresTeamTwo = match.teamScore(ofTeam: teamTwo)?.score {

@ -62,10 +62,12 @@ enum MatchSchedulerOption: Hashable {
class MatchScheduler {
static let shared = MatchScheduler()
var additionalEstimationDuration : Int = 0
var options: Set<MatchSchedulerOption> = Set(arrayLiteral: .accountUpperBracketBreakTime)
var timeDifferenceLimit: Double = 300.0
var loserBracketRotationDifference: Int = 0
var upperBracketRotationDifference: Int = 1
var courtsUnavailability: [Int: [DateInterval]]? = nil
func shouldHandleUpperRoundSlice() -> Bool {
options.contains(.shouldHandleUpperRoundSlice)
@ -175,7 +177,18 @@ class MatchScheduler {
}
func roundMatchCanBePlayed(_ match: Match, roundObject: Round, slots: [TimeMatch], rotationIndex: Int, targetedStartDate: Date, minimumTargetedEndDate: inout Date) -> Bool {
//print(roundObject.roundTitle(), match.matchTitle())
print(roundObject.roundTitle(), match.matchTitle())
if let roundStartDate = roundObject.startDate, targetedStartDate < roundStartDate {
print("can't start \(targetedStartDate) earlier than \(roundStartDate)")
if targetedStartDate == minimumTargetedEndDate {
minimumTargetedEndDate = roundStartDate
} else {
minimumTargetedEndDate = min(roundStartDate, minimumTargetedEndDate)
}
return false
}
let previousMatches = roundObject.precedentMatches(ofMatch: match)
if previousMatches.isEmpty { return true }
@ -254,7 +267,7 @@ class MatchScheduler {
let matchesByCourt = byCourt[court]?.sorted(by: \.startDate!)
let lastMatch = matchesByCourt?.last
var results = [(String, Date)]()
if let courtFreeDate = lastMatch?.estimatedEndDate() {
if let courtFreeDate = lastMatch?.estimatedEndDate(additionalEstimationDuration) {
results.append((court, courtFreeDate))
}
return results
@ -276,7 +289,8 @@ class MatchScheduler {
_startDate = match.startDate
rotationIndex += 1
}
let timeMatch = TimeMatch(matchID: match.id, rotationIndex: rotationIndex, courtIndex: match.courtIndex() ?? 0, startDate: match.startDate!, durationLeft: match.matchFormat.estimatedDuration, minimumBreakTime: match.matchFormat.breakTime.breakTime)
let timeMatch = TimeMatch(matchID: match.id, rotationIndex: rotationIndex, courtIndex: match.getCourtIndex() ?? 0, startDate: match.startDate!, durationLeft: match.matchFormat.getEstimatedDuration(additionalEstimationDuration), minimumBreakTime: match.matchFormat.breakTime.breakTime)
slots.append(timeMatch)
}
@ -359,33 +373,55 @@ class MatchScheduler {
func dispatchCourts(availableCourts: Int, courts: [Int], availableMatchs: inout [Match], slots: inout [TimeMatch], rotationIndex: Int, rotationStartDate: Date, freeCourtPerRotation: inout [Int: [Int]]) {
var matchPerRound = [Int: Int]()
var minimumTargetedEndDate: Date = rotationStartDate
courts.forEach { courtIndex in
//print(mt.map { ($0.bracket!.index.intValue, counts[$0.bracket!.index.intValue]) })
print("dispatchCourts", courts.sorted(), rotationStartDate, rotationIndex)
courts.sorted().forEach { courtIndex in
print("trying to find a match for \(courtIndex) in \(rotationIndex)")
if let first = availableMatchs.first(where: { match in
let roundObject = match.roundObject!
let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: match.matchFormat.getEstimatedDuration(additionalEstimationDuration))
print("courtsUnavailable \(courtsUnavailable)")
if courtIndex >= availableCourts - courtsUnavailable {
return false
}
let canBePlayed = roundMatchCanBePlayed(match, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate)
let currentRotationSameRoundMatches = matchPerRound[roundObject.index] ?? 0
if shouldHandleUpperRoundSlice() {
let roundMatchesCount = roundObject.playedMatches().count
print("shouldHandleUpperRoundSlice \(roundMatchesCount)")
if roundObject.loser == nil && roundMatchesCount > courts.count {
if currentRotationSameRoundMatches >= min(roundMatchesCount / 2, courts.count) { return false }
print("roundMatchesCount \(roundMatchesCount) > \(courts.count)")
if currentRotationSameRoundMatches >= min(roundMatchesCount / 2, courts.count) {
print("return false, \(currentRotationSameRoundMatches) >= \(min(roundMatchesCount / 2, courts.count))")
return false
}
}
}
if roundObject.loser == nil && roundObject.index > 0, match.indexInRound() == 0, courts.count > 1, let nextMatch = match.next() {
let indexInRound = match.indexInRound()
print("Upper Round, index > 0, first Match of round \(indexInRound) and more than one court available; looking for next match (same round) \(indexInRound + 1)")
if roundObject.loser == nil && roundObject.index > 0, indexInRound == 0, courts.count > 1, let nextMatch = match.next() {
if canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate) {
print("next match and this match can be played, returning true")
return true
} else {
print("next match and this match can not be played at the same time, returning false")
return false
}
}
print("\(currentRotationSameRoundMatches) modulo \(currentRotationSameRoundMatches%2) same round match is even, index of round is not 0 and upper bracket. If it's not the last court available \(courtIndex) == \(courts.count - 1)")
if currentRotationSameRoundMatches%2 == 0 && roundObject.index != 0 && roundObject.loser == nil && courtIndex == courts.count - 1 {
print("we return false")
return false
}
return canBePlayed
}) {
@ -398,7 +434,7 @@ class MatchScheduler {
matchPerRound[first.roundObject!.index] = 1
}
}
let timeMatch = TimeMatch(matchID: first.id, rotationIndex: rotationIndex, courtIndex: courtIndex, startDate: rotationStartDate, durationLeft: first.matchFormat.estimatedDuration, minimumBreakTime: first.matchFormat.breakTime.breakTime)
let timeMatch = TimeMatch(matchID: first.id, rotationIndex: rotationIndex, courtIndex: courtIndex, startDate: rotationStartDate, durationLeft: first.matchFormat.getEstimatedDuration(additionalEstimationDuration), minimumBreakTime: first.matchFormat.breakTime.breakTime)
slots.append(timeMatch)
availableMatchs.removeAll(where: { $0.id == first.id })
} else {
@ -407,11 +443,17 @@ class MatchScheduler {
}
if freeCourtPerRotation[rotationIndex]!.count == availableCourts {
print("no match found to be put in this rotation, check if we can put anything to another date")
freeCourtPerRotation[rotationIndex] = []
let courtsUsed = getNextEarliestAvailableDate(from: slots)
let freeCourts = courtsUsed.filter { (courtIndex, availableDate) in
availableDate <= minimumTargetedEndDate
}.sorted(by: \.1).map { $0.0 }
var freeCourts: [Int] = []
if courtsUsed.isEmpty {
freeCourts = (0..<availableCourts).map { $0 }
} else {
freeCourts = courtsUsed.filter { (courtIndex, availableDate) in
availableDate <= minimumTargetedEndDate
}.sorted(by: \.1).map { $0.0 }
}
dispatchCourts(availableCourts: availableCourts, courts: freeCourts, availableMatchs: &availableMatchs, slots: &slots, rotationIndex: rotationIndex, rotationStartDate: minimumTargetedEndDate, freeCourtPerRotation: &freeCourtPerRotation)
}
@ -422,9 +464,27 @@ class MatchScheduler {
let upperRounds = tournament.rounds()
let allMatches = tournament.allMatches()
var roundIndex = 0
// if dayOne < tournament.courtCount {
// let startOfDay = startDate.startOfDay
// let endOfday = Calendar.current.dateInterval(of: .day, for: startOfDay)!.end
// let dateInterval = DateInterval(startDate: startOfDay, endDate: endOfday)
// let _courtsUnavailability = courtsUnavailability ?? [:]()
// let data = _courtsUnavailability[courtCount - 1] ?? [DateInterval]()
// data.append(dateInterval)
// courtsUnavailability = _courtsUnavailability
// }
//
// if dayTwo < tournament.courtCount {
// let startOfDay = startDate.startOfDay
// let endOfday = Calendar.current.dateInterval(of: .day, for: startOfDay)!.end
// let dateInterval = DateInterval(startDate: startOfDay, endDate: endOfday)
// let _courtsUnavailability = courtsUnavailability ?? [:]()
// let data = _courtsUnavailability[courtCount - 1] ?? [DateInterval]()
// data.append(dateInterval)
// courtsUnavailability = _courtsUnavailability
// }
let rounds = upperRounds.map {
$0
} + upperRounds.flatMap {
@ -457,12 +517,14 @@ class MatchScheduler {
}
}
let usedCourts = getAvailableCourts(from: allMatches.filter({ $0.startDate?.isEarlierThan(startDate) == true }))
let usedCourts = getAvailableCourts(from: allMatches.filter({ $0.startDate?.isEarlierThan(startDate) == true && $0.startDate?.dayInt == startDate.dayInt }))
let initialCourts = usedCourts.filter { (court, availableDate) in
availableDate <= startDate
}.sorted(by: \.1).compactMap { tournament.getCourtIndex($0.0) }
let courts : [Int]? = initialCourts.isEmpty ? nil : initialCourts
print("initial available courts at beginning: \(courts)")
let roundDispatch = self.roundDispatcher(numberOfCourtsAvailablePerRotation: tournament.courtCount, flattenedMatches: flattenedMatches, dispatcherStartDate: startDate, initialCourts: courts)
@ -475,5 +537,21 @@ class MatchScheduler {
try? DataStore.shared.matches.addOrUpdate(contentOfs: allMatches)
}
}
func courtsUnavailable(startDate: Date, duration: Int) -> Int {
let endDate = startDate.addingTimeInterval(Double(duration) * 60)
guard let courtsUnavailability else { return 0 }
let courts = courtsUnavailability.keys
return courts.filter {
courtUnavailable(courtIndex: $0, from: startDate, to: endDate)
}.count
}
func courtUnavailable(courtIndex: Int, from startDate: Date, to endDate: Date) -> Bool {
guard let courtLockedSchedule = courtsUnavailability?[courtIndex] else { return true }
return courtLockedSchedule.anySatisfy({ dateInterval in
dateInterval.isDateInside(startDate) || dateInterval.isDateInside(endDate)
})
}
}

@ -10,6 +10,7 @@ import SwiftUI
@Observable
class NavigationViewModel {
var path = NavigationPath()
var selectedTab: TabDestination?
var agendaDestination: AgendaDestination? = .activity
var tournament: Tournament?
}

@ -14,6 +14,8 @@ class SearchViewModel: ObservableObject, Identifiable {
var codeClub: String? = nil
var clubName: String? = nil
var ligueName: String? = nil
var showFemaleInMaleAssimilation: Bool = false
@Published var debouncableText: String = ""
@Published var searchText: String = ""
@Published var task: DispatchWorkItem?

@ -10,27 +10,36 @@ import Foundation
struct SeedInterval: Hashable, Comparable {
let first: Int
let last: Int
var reduce: Int = 0
static func <(lhs: SeedInterval, rhs: SeedInterval) -> Bool {
return lhs.first < rhs.first
}
func chunk() -> SeedInterval? {
if (last - first) / 2 > 0 {
if last - (last - first) / 2 > first {
return SeedInterval(first: first, last: last - (last - first) / 2)
}
var count: Int {
dimension
}
private var dimension: Int {
(last - (first - 1))
}
func chunks() -> [SeedInterval]? {
if dimension > 3 {
let split = dimension / 2
let firstHalf = SeedInterval(first: first, last: first + split - 1, reduce: reduce)
let secondHalf = SeedInterval(first: first + split, last: last, reduce: reduce)
return [firstHalf, secondHalf]
} else {
return nil
}
return nil
}
}
extension SeedInterval {
func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
if last - first < 2 {
return "#\(first) / #\(last)"
if dimension < 2 {
return "#\(first - reduce) / #\(last - reduce)"
} else {
return "#\(first) à #\(last)"
return "#\(first - reduce) à #\(last - reduce)"
}
}
}

@ -6,8 +6,38 @@
//
import Foundation
import SwiftUI
protocol Selectable {
func selectionLabel() -> String
func badgeValue() -> Int?
func badgeImage() -> Badge?
}
enum Badge {
case checkmark
case xmark
case custom(systemName: String, color: Color)
func systemName() -> String {
switch self {
case .checkmark:
return "checkmark.circle.fill"
case .xmark:
return "xmark.circle.fill"
case .custom(let systemName, _):
return systemName
}
}
func color() -> Color {
switch self {
case .checkmark:
.green
case .xmark:
.red
case .custom(_, let color):
color
}
}
}

@ -8,22 +8,13 @@
import Foundation
import SwiftUI
// Create an environment key
private struct TournamentSeedEditing: EnvironmentKey {
static let defaultValue: Bool = false
static let defaultValue: Binding<Bool> = .constant(false)
}
// ## Introduce new value to EnvironmentValues
extension EnvironmentValues {
var isEditingTournamentSeed: Bool {
var isEditingTournamentSeed: Binding<Bool> {
get { self[TournamentSeedEditing.self] }
set { self[TournamentSeedEditing.self] = newValue }
}
}
// Add a dedicated modifier (Optional)
extension View {
func editTournamentSeed(_ value: Bool) -> some View {
environment(\.isEditingTournamentSeed, value)
}
}

@ -0,0 +1,186 @@
//
// CallMessageCustomizationView.swift
// Padel Tournament
//
// Created by Razmig Sarkissian on 02/11/2023.
//
import SwiftUI
struct CallMessageCustomizationView: View {
@EnvironmentObject var dataStore: DataStore
var tournament: Tournament
@FocusState private var textEditor: Bool
@State private var customClubName: String = ""
@State private var customCallMessageBody: String = ""
@State private var customCallMessageSignature: String = ""
init(tournament: Tournament) {
self.tournament = tournament
_customCallMessageBody = State(wrappedValue: DataStore.shared.appSettings.callMessageBody ?? "")
_customCallMessageSignature = State(wrappedValue: DataStore.shared.appSettings.callMessageSignature ?? "")
_customClubName = State(wrappedValue: tournament.clubName ?? "")
}
var clubName: String {
customClubName
}
var formatMessage: String? {
dataStore.appSettings.callDisplayFormat ? tournament.matchFormat.computedLongLabel + "." : nil
}
var entryFeeMessage: String? {
dataStore.appSettings.callDisplayEntryFee ? tournament.entryFeeMessage : nil
}
var computedMessage: String {
[formatMessage, entryFeeMessage, customCallMessageBody].compacted().map { $0.trimmed }.joined(separator: "\n")
}
var finalMessage: String? {
let localizedCalled = "convoqué" + (tournament.tournamentCategory == .women ? "e" : "") + "s"
return "Bonjour,\n\nVous êtes \(localizedCalled) pour jouer en \(RoundRule.roundName(fromRoundIndex: 2).lowercased()) du \(tournament.tournamentTitle()) au \(clubName) le \(Date().formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(Date().formatted(Date.FormatStyle().hour().minute())).\n\n" + computedMessage + "\n\n\(customCallMessageSignature)"
}
var body: some View {
@Bindable var appSettings = dataStore.appSettings
List {
Section {
ZStack {
Text(customCallMessageBody).hidden()
.padding(.vertical, 20)
TextEditor(text: $customCallMessageBody)
.autocorrectionDisabled()
.focused($textEditor)
}
} header: {
Text("Personnalisation du message de convocation")
}
Section {
ZStack {
Text(customCallMessageSignature).hidden()
TextEditor(text: $customCallMessageSignature)
.autocorrectionDisabled()
.focused($textEditor)
}
} header: {
Text("Signature du message")
}
Section {
TextField("Nom du club", text: $customClubName)
.autocorrectionDisabled()
.onSubmit {
if let eventClub = tournament.eventObject?.clubObject {
eventClub.name = customClubName
try? dataStore.clubs.addOrUpdate(instance: eventClub)
}
}
} header: {
Text("Nom du club")
}
Section {
if appSettings.callUseFullCustomMessage {
Text(self.computedFullCustomMessage())
.contextMenu {
Button("Coller dans le presse-papier") {
UIPasteboard.general.string = self.computedFullCustomMessage()
}
}
}
else if let finalMessage {
Text(finalMessage)
.contextMenu {
Button("Coller dans le presse-papier") {
UIPasteboard.general.string = finalMessage
}
}
}
} header: {
Text("Exemple")
}
Section {
LabeledContent {
Toggle(isOn: $appSettings.callUseFullCustomMessage) {
}
} label: {
Text("contrôle complet du message")
}
} header: {
Text("Personnalisation complète")
} footer: {
Text("Utilisez ces balises dans votre texte : #titre, #jour, #horaire, #club, #signature")
}
}
.navigationTitle("Message de convocation")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Menu {
Picker(selection: $appSettings.callDisplayFormat) {
Text("Afficher le format").tag(true)
Text("Masquer le format").tag(false)
} label: {
}
Picker(selection: $appSettings.callDisplayEntryFee) {
Text("Afficher le prix d'inscription").tag(true)
Text("Masquer le prix d'inscription").tag(false)
} label: {
}
} label: {
LabelOptions()
}
}
ToolbarItemGroup(placement: .keyboard) {
if textEditor {
Spacer()
Button {
textEditor = false
} label: {
Label("Fermer", systemImage: "xmark")
}
}
}
}
.onChange(of: appSettings.callUseFullCustomMessage) {
if appSettings.callUseFullCustomMessage == false {
appSettings.callMessageBody = ContactType.defaultCustomMessage
}
_save()
}
.onChange(of: customCallMessageBody) {
appSettings.callMessageBody = customCallMessageBody
_save()
}
.onChange(of: customCallMessageSignature) {
appSettings.callMessageSignature = customCallMessageSignature
_save()
}
.onChange(of: appSettings.callDisplayEntryFee) {
_save()
}
.onChange(of: appSettings.callDisplayFormat) {
_save()
}
}
private func _save() {
dataStore.updateSettings()
}
func computedFullCustomMessage() -> String {
var text = customCallMessageBody.replacingOccurrences(of: "#titre", with: tournament.tournamentTitle())
text = text.replacingOccurrences(of: "#club", with: clubName)
text = text.replacingOccurrences(of: "#jour", with: "\(Date().formatted(Date.FormatStyle().weekday(.wide).day().month(.wide)))")
text = text.replacingOccurrences(of: "#horaire", with: "\(Date().formatted(Date.FormatStyle().hour().minute()))")
text = text.replacingOccurrences(of: "#signature", with: customCallMessageSignature)
return text
}
}

@ -16,13 +16,20 @@ struct CallSettingsView: View {
Section {
NavigationLink {
CallMessageCustomizationView(tournament: tournament)
} label: {
Text("Modifier le message de convocation")
Text("Personnaliser le message de convocation")
}
}
Section {
RowButtonView("Annuler toutes les convocations") {
RowButtonView("Envoyer un message à tout le monde") {
}
}
Section {
RowButtonView("Annuler toutes les convocations", role: .destructive) {
let teams = tournament.unsortedTeams()
teams.forEach { team in
team.callDate = nil
@ -32,13 +39,7 @@ struct CallSettingsView: View {
}
Section {
RowButtonView("Envoyer un message à tout le monde") {
}
}
Section {
RowButtonView("Tout le monde a été convoqué") {
RowButtonView("Tout le monde a été convoqué", role: .destructive) {
let teams = tournament.unsortedTeams()
teams.forEach { team in
team.callDate = Date()

@ -86,7 +86,11 @@ struct CallView: View {
let callWord = teams.allSatisfy({ $0.called() }) ? "Reconvoquer" : "Convoquer"
HStack {
if teams.count == 1 {
Text(callWord + " cette paire par")
if let previousCallDate = teams.first?.callDate, Calendar.current.compare(previousCallDate, to: callDate, toGranularity: .minute) != .orderedSame {
Text("Reconvoquer " + callDate.localizedDate() + " par")
} else {
Text(callWord + " cette paire par")
}
} else {
Text(callWord + " ces \(teams.count) paires par")
}

@ -19,8 +19,8 @@ struct GroupStageCallingView: View {
ForEach(groupStages) { groupStage in
let seeds = groupStage.teams()
let callSeeds = seeds.filter({ $0.callDate != nil })
let callSeeds = seeds.filter({ tournament.isStartDateIsDifferentThanCallDate($0) == false })
if seeds.isEmpty == false {
Section {
NavigationLink {
@ -48,15 +48,19 @@ struct GroupStageCallingView: View {
groupStage.startDate
}
let keys = times.keys.compactMap { $0 }.sorted()
ForEach(keys, id: \.self) { key in
if let _groupStages = times[key], _groupStages.count > 1 {
let teams = _groupStages.flatMap { $0.teams() }
Section {
CallView.CallStatusView(count: teams.filter({ $0.callDate != nil }).count, total: teams.count, startDate: key)
} header: {
Text(groupStages.map { $0.groupStageTitle() }.joined(separator: ", "))
} footer: {
CallView(teams: teams, callDate: key, matchFormat: tournament.groupStageMatchFormat, roundLabel: "poule")
if keys.count != groupStages.count {
ForEach(keys, id: \.self) { key in
if let _groupStages = times[key] {
let teams = _groupStages.flatMap { $0.teams() }
let callSeeds = teams.filter({ tournament.isStartDateIsDifferentThanCallDate($0) == false })
Section {
CallView.CallStatusView(count: callSeeds.count, total: teams.count, startDate: key)
} header: {
Text(_groupStages.map { $0.groupStageTitle() }.joined(separator: ", "))
} footer: {
CallView(teams: teams, callDate: key, matchFormat: tournament.groupStageMatchFormat, roundLabel: "poule")
}
}
}
}

@ -15,7 +15,7 @@ struct SeedsCallingView: View {
List {
ForEach(tournament.rounds()) { round in
let seeds = round.seeds()
let callSeeds = seeds.filter({ $0.callDate != nil })
let callSeeds = seeds.filter({ tournament.isStartDateIsDifferentThanCallDate($0) == false })
if seeds.isEmpty == false {
Section {
NavigationLink {

@ -0,0 +1,76 @@
//
// CashierDetailView.swift
// Padel Tournament
//
// Created by Razmig Sarkissian on 31/03/2024.
//
import SwiftUI
struct CashierDetailView: View {
var tournaments : [Tournament]
init(tournaments: [Tournament]) {
self.tournaments = tournaments
}
init(tournament: Tournament) {
self.tournaments = [tournament]
}
var body: some View {
List {
ForEach(tournaments) { tournament in
Section {
LabeledContent {
Text(tournament.earnings().formatted(.currency(code: "EUR").precision(.fractionLength(0))))
} label: {
Text("Encaissement")
Text(tournament.paidCompletion().formatted(.percent.precision(.fractionLength(0)))).foregroundStyle(.secondary)
}
_tournamentCashierDetailView(tournament)
} header: {
if tournaments.count > 1 {
Text(tournament.tournamentTitle())
}
}
}
}
.headerProminence(.increased)
.navigationTitle("Bilan")
}
private func _tournamentCashierDetailView(_ tournament: Tournament) -> some View {
DisclosureGroup {
ForEach(PlayerRegistration.PaymentType.allCases) { type in
let count = tournament.selectedPlayers().filter({ $0.registrationType == type }).count
LabeledContent {
if let entryFee = tournament.entryFee {
let sum = Double(count) * entryFee
Text(sum.formatted(.currency(code: "EUR")))
}
} label: {
Text(type.localizedLabel())
Text(count.formatted())
}
}
} label: {
Text("Voir le détail")
}
//
// Section {
// ForEach(tournaments) { tournament in
// }
//// HStack {
//// Text("Total")
//// Spacer()
//// Text(event.earnings.formatted(.currency(code: "EUR").precision(.fractionLength(0))))
//// Text(event.paidCompletion.formatted(.percent.precision(.fractionLength(0)))).foregroundStyle(.secondary)
//// }
// } header: {
// Text("Encaissement")
// }
}
}

@ -0,0 +1,56 @@
//
// CashierSettingsView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 17/04/2024.
//
import SwiftUI
struct CashierSettingsView: View {
@EnvironmentObject var dataStore: DataStore
var tournaments: [Tournament]
init(tournaments: [Tournament]) {
self.tournaments = tournaments
}
init(tournament: Tournament) {
self.tournaments = [tournament]
}
var body: some View {
List {
Section {
RowButtonView("Tout le monde a réglé", role: .destructive) {
let players = tournaments.flatMap({ $0.selectedPlayers() })
players.forEach { player in
if player.hasPaid() == false {
player.registrationType = .gift
}
}
try? dataStore.playerRegistrations.addOrUpdate(contentOfs: players)
}
} footer: {
Text("Passe tous les joueurs qui n'ont pas réglé en offert")
}
Section {
RowButtonView("Personne n'a réglé", role: .destructive) {
let players = tournaments.flatMap({ $0.selectedPlayers() })
players.forEach { player in
player.registrationType = nil
}
try? dataStore.playerRegistrations.addOrUpdate(contentOfs: players)
}
} footer: {
Text("Remet à zéro le type d'encaissement de tous les joueurs")
}
}
}
}
#Preview {
CashierSettingsView(tournaments: [])
}

@ -0,0 +1,311 @@
//
// CashierView.swift
// Padel Tournament
//
// Created by Razmig Sarkissian on 04/03/2023.
//
import SwiftUI
import Combine
struct CashierView: View {
@EnvironmentObject var dataStore: DataStore
var tournaments : [Tournament]
var teams: [TeamRegistration]
@State private var sortOption: SortOption = .callDate
@State private var filterOption: FilterOption = .all
@State private var sortOrder: SortOrder = .ascending
@State private var searchText = ""
@State private var isSearching: Bool = false
init(event: Event) {
self.tournaments = event.tournaments
self.teams = []
}
init(tournament: Tournament, teams: [TeamRegistration]) {
self.tournaments = [tournament]
self.teams = teams
}
private func _sharedData() -> String {
let players = teams
.flatMap({ $0.players() })
.map {
[$0.pasteData()]
.compacted()
.joined(separator: "\n")
}
.joined(separator: "\n\n")
return players
}
enum SortOption: Int, Identifiable, CaseIterable {
case teamRank
case alphabeticalLastName
case alphabeticalFirstName
case playerRank
case age
case callDate
var id: Int { self.rawValue }
func localizedLabel() -> String {
switch self {
case .callDate:
return "Convocation"
case .teamRank:
return "Poids d'équipe"
case .alphabeticalLastName:
return "Nom"
case .alphabeticalFirstName:
return "Prénom"
case .playerRank:
return "Rang"
case .age:
return "Âge"
}
}
}
enum FilterOption: Int, Identifiable, CaseIterable {
case all
case didPay
case didNotPay
var id: Int { self.rawValue }
func localizedLabel() -> String {
switch self {
case .all:
return "Tous"
case .didPay:
return "Réglé"
case .didNotPay:
return "Non réglé"
}
}
func shouldDisplayPlayer(_ player: PlayerRegistration) -> Bool {
switch self {
case .all:
return true
case .didPay:
return player.hasPaid()
case .didNotPay:
return player.hasPaid() == false
}
}
}
var body: some View {
List {
if isSearching == false {
Section {
Picker(selection: $filterOption) {
ForEach(FilterOption.allCases) { filterOption in
Text(filterOption.localizedLabel()).tag(filterOption)
}
} label: {
Text("Statut du règlement")
}
Picker(selection: $sortOption) {
ForEach(SortOption.allCases) { sortOption in
Text(sortOption.localizedLabel()).tag(sortOption)
}
} label: {
Text("Affichage par")
}
Picker(selection: $sortOrder) {
Text("Croissant").tag(SortOrder.ascending)
Text("Décroissant").tag(SortOrder.descending)
} label: {
Text("Trier par ordre")
}
} header: {
Text("Options d'affichage")
}
}
if _isContentUnavailable() {
_contentUnavailableView()
}
switch sortOption {
case .teamRank:
_byTeamRankView()
case .alphabeticalLastName:
_byPlayerLastName()
case .alphabeticalFirstName:
_byPlayerFirstName()
case .playerRank:
_byPlayerRank()
case .age:
_byPlayerAge()
case .callDate:
_byCallDateView()
}
}
.headerProminence(.increased)
.searchable(text: $searchText, isPresented: $isSearching, prompt: Text("Chercher un joueur"))
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
ShareLink(item: _sharedData())
}
}
}
@ViewBuilder
func computedPlayerView(_ player: PlayerRegistration) -> some View {
EditablePlayerView(player: player, editingOptions: [.licenceId, .payment])
}
private func _shouldDisplayTeam(_ team: TeamRegistration) -> Bool {
team.players().allSatisfy({
_shouldDisplayPlayer($0)
})
}
private func _shouldDisplayPlayer(_ player: PlayerRegistration) -> Bool {
if searchText.isEmpty == false {
filterOption.shouldDisplayPlayer(player) && player.contains(searchText)
} else {
filterOption.shouldDisplayPlayer(player)
}
}
@ViewBuilder
private func _byPlayer(_ players: [PlayerRegistration]) -> some View {
let _players = sortOrder == .ascending ? players : players.reversed()
ForEach(_players) { player in
Section {
computedPlayerView(player)
} header: {
HStack {
if let teamCallDate = player.team()?.callDate {
Text(teamCallDate.localizedDate())
}
Spacer()
Text(player.weight.formatted())
}
} footer: {
if tournaments.count > 1, let tournamentTitle = player.tournament()?.tournamentTitle() {
Text(tournamentTitle)
}
}
}
}
@ViewBuilder
private func _byPlayerRank() -> some View {
let players = teams.flatMap({ $0.players() }).sorted(using: .keyPath(\.weight)).filter({ _shouldDisplayPlayer($0) })
_byPlayer(players)
}
@ViewBuilder
private func _byPlayerAge() -> some View {
let players = teams.flatMap({ $0.players() }).filter({ $0.computedAge != nil }).sorted(using: .keyPath(\.computedAge!)).filter({ _shouldDisplayPlayer($0) })
_byPlayer(players)
}
@ViewBuilder
private func _byPlayerLastName() -> some View {
let players = teams.flatMap({ $0.players() }).sorted(using: .keyPath(\.lastName)).filter({ _shouldDisplayPlayer($0) })
_byPlayer(players)
}
@ViewBuilder
private func _byPlayerFirstName() -> some View {
let players = teams.flatMap({ $0.players() }).sorted(using: .keyPath(\.firstName)).filter({ _shouldDisplayPlayer($0) })
_byPlayer(players)
}
@ViewBuilder
private func _byTeamRankView() -> some View {
let _teams = sortOrder == .ascending ? teams : teams.reversed()
ForEach(_teams) { team in
if _shouldDisplayTeam(team) {
Section {
_cashierPlayersView(team.players())
} header: {
HStack {
if let callDate = team.callDate {
Text(callDate.localizedDate())
}
Spacer()
Text(team.weight.formatted())
}
} footer: {
if tournaments.count > 1, let tournamentTitle = team.tournamentObject()?.tournamentTitle() {
Text(tournamentTitle)
}
}
}
}
}
@ViewBuilder
private func _byCallDateView() -> some View {
let groupedTeams = Dictionary(grouping: teams) { team in
team.callDate
}
let keys = 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
if _shouldDisplayTeam(team) {
Section {
_cashierPlayersView(team.players())
} header: {
Text(key.localizedDate())
} footer: {
if tournaments.count > 1, let tournamentTitle = team.tournamentObject()?.tournamentTitle() {
Text(tournamentTitle)
}
}
}
}
}
}
}
@ViewBuilder
private func _cashierPlayersView(_ players: [PlayerRegistration]) -> some View {
ForEach(players) { player in
if _shouldDisplayPlayer(player) {
computedPlayerView(player)
}
}
}
private func _isContentUnavailable() -> Bool {
switch sortOption {
case .teamRank, .callDate:
return teams.filter({ _shouldDisplayTeam($0) }).isEmpty
default:
return teams.flatMap({ $0.players() }).filter({ _shouldDisplayPlayer($0) }).isEmpty
}
}
private func _unavailableIcon() -> String {
switch sortOption {
case .teamRank, .callDate:
return "person.2.slash.fill"
default:
return "person.slash.fill"
}
}
@ViewBuilder
private func _contentUnavailableView() -> some View {
if isSearching {
ContentUnavailableView.search(text: searchText)
} else {
ContentUnavailableView("Aucun résultat", systemImage: _unavailableIcon())
}
}
}

@ -0,0 +1,18 @@
//
// PlayerListView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 17/04/2024.
//
import SwiftUI
struct PlayerListView: View {
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
}
}
#Preview {
PlayerListView()
}

@ -339,6 +339,7 @@ enum Pratique: String, Codable {
case beach = "BEACH"
case padel = "PADEL"
case tennis = "TENNIS"
case pickle = "PICKLE"
}
// MARK: - ClubMarker

@ -1,23 +0,0 @@
//
// ClubView.swift
// PadelClub
//
// Created by Laurent Morvillier on 06/02/2024.
//
import SwiftUI
struct ClubView: View {
var club: Club
var body: some View {
List(club.tournaments) { tournament in
Text(tournament.tournamentTitle())
}.navigationTitle(club.name)
}
}
#Preview {
ClubView(club: Club(name: "AUC", acronym: "test", address: ""))
}

@ -0,0 +1,18 @@
//
// FooterButtonView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 18/04/2024.
//
import SwiftUI
struct FooterButtonView: View {
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
}
}
#Preview {
FooterButtonView()
}

@ -44,18 +44,27 @@ struct GenericDestinationPickerView<T: Identifiable & Selectable>: View {
.opacity(selectedDestination?.id == destination.id ? 1.0 : 0.4)
}
.buttonStyle(.plain)
.overlay(alignment: .bottomTrailing) {
if let count = destination.badgeValue(), count > 0 {
Image(systemName: count <= 50 ? "\(count).circle.fill" : "plus.circle.fill")
.foregroundColor(.secondary)
.imageScale(.medium)
.background (
Color(.systemBackground)
.clipShape(.circle)
)
.offset(x: 5, y: 5)
}
}
// .overlay(alignment: .bottomTrailing) {
// if let badge = destination.badgeImage() {
// Image(systemName: badge.systemName())
// .foregroundColor(badge.color())
// .imageScale(.medium)
// .background (
// Color(.systemBackground)
// .clipShape(.circle)
// )
// .offset(x: 3, y: 3)
// } else if let count = destination.badgeValue(), count > 0 {
// Image(systemName: count <= 50 ? "\(count).circle.fill" : "plus.circle.fill")
// .foregroundColor(.red)
// .imageScale(.medium)
// .background (
// Color(.systemBackground)
// .clipShape(.circle)
// )
// .offset(x: 3, y: 3)
// }
// }
}
}
.fixedSize()

@ -15,13 +15,13 @@ struct LabelOptions: View {
struct LabelStructure: View {
var body: some View {
Label("Structure", systemImage: "hammer")
Label("Structure", systemImage: "hammer").labelStyle(.titleOnly)
}
}
struct LabelSettings: View {
var body: some View {
Label("Réglages", systemImage: "slider.horizontal.3")
Label("Réglages", systemImage: "slider.horizontal.3").labelStyle(.titleOnly)
}
}

@ -19,27 +19,20 @@ struct MatchListView: View {
var body: some View {
if matches.isEmpty == false {
Section {
if isExpanded {
DisclosureGroup(isExpanded: $isExpanded) {
ForEach(matches) { match in
MatchRowView(match: match, matchViewStyle: matchViewStyle)
.listRowInsets(EdgeInsets())
}
}
} header: {
Button {
isExpanded.toggle()
} label: {
HStack {
Text(section.capitalized)
Spacer()
Text(matches.count.formatted())
Image(systemName: isExpanded ? "chevron.down.circle" : "chevron.right.circle")
LabeledContent {
Text(matches.count.formatted() + " match" + matches.count.pluralSuffix)
.foregroundStyle(.master)
} label: {
Text(section.firstCapitalized)
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.frame(maxWidth: .infinity)
}
.headerProminence(.increased)
}
}
}

@ -14,66 +14,91 @@ struct RowButtonView: View {
let title: String
var systemImage: String? = nil
var image: String? = nil
var animatedProgress: Bool = false
let confirmationMessage: String
let action: () -> ()
var action: (() -> ())? = nil
var asyncAction: (() async -> ())? = nil
@State private var askConfirmation: Bool = false
@State private var isLoading = false
init(_ title: String, role: ButtonRole? = nil, systemImage: String? = nil, image: String? = nil, animatedProgress: Bool = false, confirmationMessage: String? = nil, action: @escaping () -> Void) {
init(_ title: String, role: ButtonRole? = nil, systemImage: String? = nil, image: String? = nil, confirmationMessage: String? = nil, action: @escaping (() -> ())) {
self.role = role
self.title = title
self.systemImage = systemImage
self.image = image
self.animatedProgress = animatedProgress
self.confirmationMessage = confirmationMessage ?? defaultConfirmationMessage
self.action = action
}
init(_ title: String, role: ButtonRole? = nil, systemImage: String? = nil, image: String? = nil, confirmationMessage: String? = nil, asyncAction: @escaping (() async -> ())) {
self.role = role
self.title = title
self.systemImage = systemImage
self.image = image
self.confirmationMessage = confirmationMessage ?? defaultConfirmationMessage
self.asyncAction = asyncAction
}
var body: some View {
Button(role: role) {
if role == .destructive {
askConfirmation = true
} else {
} else if let action {
action()
} else if let asyncAction {
isLoading = true
Task {
await asyncAction()
isLoading = false
}
}
} label: {
HStack {
if animatedProgress {
Spacer()
ProgressView()
} else {
if let systemImage {
Image(systemName: systemImage)
.resizable()
.scaledToFit()
.frame(height: 24)
}
if let image {
Image(image)
.resizable()
.scaledToFit()
.frame(width: 32, height: 32)
}
Spacer()
Text(title)
.foregroundColor(.white)
.frame(height: 32)
if let systemImage {
Image(systemName: systemImage)
.resizable()
.scaledToFit()
.frame(height: 24)
}
if let image {
Image(image)
.resizable()
.scaledToFit()
.frame(width: 32, height: 32)
}
Spacer()
Text(title)
.opacity(isLoading ? 0.0 : 1.0)
.foregroundColor(.white)
.frame(height: 32)
Spacer()
}
.font(.headline)
}
.disabled(animatedProgress)
.overlay {
if isLoading {
ProgressView()
}
}
.disabled(isLoading)
.frame(maxWidth: .infinity)
.buttonStyle(.borderedProminent)
.tint(role == .destructive ? Color.red : Color.launchScreenBackground)
.tint(role == .destructive ? Color.red : Color.master)
.listRowBackground(Color.clear)
.listRowInsets(EdgeInsets(.zero))
.confirmationDialog("Confirmation",
isPresented: $askConfirmation,
titleVisibility: .visible) {
Button("OK") {
action()
if let action {
action()
} else if let asyncAction {
isLoading = true
Task {
await asyncAction()
isLoading = false
}
}
}
Button("Annuler", role: .cancel) {}
} message: {

@ -13,12 +13,12 @@ struct StepperView: View {
var title: String? = nil
@Binding var count: Int
var step: Int = 1
var minimum: Int? = nil
var maximum: Int? = nil
var body: some View {
VStack(spacing: 0) {
VStack {
HStack(spacing: 8) {
Button(action: {
self._subtract()
@ -74,14 +74,14 @@ struct StepperView: View {
if let maximum, self.count + 1 > maximum {
return
}
self.count += 1
self.count += step
}
fileprivate func _subtract() {
if let minimum, self.count - 1 < minimum {
return
}
self.count -= 1
self.count -= step
}
}

@ -1,82 +0,0 @@
//
// ContentView.swift
// PadelClub
//
// Created by Laurent Morvillier on 02/02/2024.
//
import SwiftUI
import LeStorage
struct ContentView: View {
@StateObject var dataStore = DataStore()
var body: some View {
NavigationStack {
VStack {
List(self.dataStore.clubs) { club in
NavigationLink {
ClubView(club: club)
} label: {
Text(club.name)
}
}
Button("add") {
self._add()
}
.padding()
.buttonStyle(.bordered)
}
.toolbar(content: {
ToolbarItem {
NavigationLink {
MainUserView()
.environmentObject(self.dataStore)
} label: {
Image(systemName: "person.circle.fill")
}
}
ToolbarItem {
NavigationLink {
SubscriptionView()
} label: {
Image(systemName: "tennisball.circle.fill")
}
}
})
.navigationTitle("Home")
}
}
func _add() {
// let id = (0...1000000).randomElement()!
// let club: Club = Club(name: "test\(id)", address: "some address")
// self.dataStore.clubs.addOrUpdate(instance: club)
// for _ in 0...20 {
// var clubs: [Club] = []
// for _ in 0...20 {
// let id = (0...1000000).randomElement()!
// let club: Club = Club(name: "test\(id)", acronym: "test", address: "some address")
// clubs.append(club)
// }
// do {
// try self.dataStore.clubs.append(contentOfs: clubs)
// } catch {
// Logger.error(error)
// }
// }
}
}
#Preview {
ContentView()
}

@ -40,12 +40,11 @@ struct EventCreationView: View {
}
if eventType == .approvedTournament {
Stepper(value: $duration, in: 1...3) {
HStack {
Text("Durée")
Spacer()
Text("\(duration) jour" + duration.pluralSuffix)
}
LabeledContent {
StepperView(count: $duration, minimum: 1, maximum: 3)
} label: {
Text("Durée")
Text("\(duration) jour" + duration.pluralSuffix)
}
}
@ -101,6 +100,7 @@ struct EventCreationView: View {
tournaments.forEach { tournament in
tournament.startDate = startingDate
tournament.dayDuration = duration
tournament.setupFederalSettings()
}
try? dataStore.tournaments.addOrUpdate(contentOfs: tournaments)

@ -35,13 +35,11 @@ struct TournamentConfigurationView: View {
Text(type.localizedLabel()).tag(type.rawValue)
}
}
Stepper(value: $tournament.teamCount, in: minimumTeamsCount...maximumTeamsCount) {
HStack {
Text("Équipes souhaitées")
Spacer()
Text(tournament.teamCount.formatted())
}
LabeledContent {
StepperView(count: $tournament.teamCount, minimum: minimumTeamsCount, maximum: maximumTeamsCount)
} label: {
Text("Équipes souhaitées")
Text(tournament.teamCount.formatted())
}
}
}

@ -34,26 +34,33 @@ struct GroupStageView: View {
Section {
_groupStageView()
} header: {
if let startDate = groupStage.startDate {
Text(startDate.formatted(Date.FormatStyle().weekday(.wide)).capitalized + " à partir de " + startDate.formatted(.dateTime.hour().minute()))
}
} footer: {
HStack {
if let startDate = groupStage.startDate {
Text(startDate.formatted(Date.FormatStyle().weekday(.wide)).capitalized + " à partir de " + startDate.formatted(.dateTime.hour().minute()))
}
Spacer()
Button {
if sortingMode == .weight {
sortingMode = .score
if sortingMode == .auto {
if groupStage.hasEnded() {
sortingMode = .weight
} else {
sortingMode = .score
}
} else if sortingMode == .weight {
sortingMode = .weight
} else {
sortingMode = .weight
}
} label: {
Label(sortByScore ? "tri par score" : "tri par poids", systemImage: "arrow.up.arrow.down").labelStyle(.titleOnly)
.underline()
}
.buttonStyle(.borderless)
}
.buttonStyle(.plain)
}
.headerProminence(.increased)
MatchListView(section: "disponible", matches: groupStage.availableToStart()).id(UUID())
MatchListView(section: "en cours", matches: groupStage.runningMatches()).id(UUID())
MatchListView(section: "à lancer", matches: groupStage.readyMatches()).id(UUID())

@ -41,6 +41,10 @@ struct GroupStagesView: View {
return groupStage.badgeValue()
}
}
func badgeImage() -> Badge? {
nil
}
}
init(tournament: Tournament) {

@ -8,13 +8,15 @@
import SwiftUI
struct MatchDateView: View {
@Environment(Tournament.self) var tournament: Tournament
@EnvironmentObject var dataStore: DataStore
var match: Match
var showPrefix: Bool = false
var body: some View {
Menu {
if match.startDate == nil {
Button("Commencer") {
if match.startDate == nil && match.isReady() {
Button("Démarrer") {
match.startDate = Date()
save()
}
@ -23,12 +25,21 @@ struct MatchDateView: View {
save()
}
} else {
Button("Recommencer") {
match.startDate = Date()
match.endDate = nil
save()
if match.isReady() {
Button("Démarrer maintenant") {
match.startDate = Date()
match.endDate = nil
save()
}
} else {
let estimatedDuration = match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)
Button("Décaler de \(estimatedDuration) minutes") {
match.startDate = match.startDate?.addingTimeInterval(Double(estimatedDuration) * 60.0)
match.endDate = nil
save()
}
}
Button("Remise à zéro") {
Button("Retirer l'horaire") {
match.startDate = nil
match.endDate = nil
save()
@ -50,8 +61,16 @@ struct MatchDateView: View {
if showPrefix {
Text("en cours").font(.footnote).foregroundStyle(.secondary)
}
Text(startDate, style: .timer)
.monospacedDigit()
if match.isReady() {
Text(startDate, style: .timer)
.monospacedDigit()
.foregroundStyle(Color.master)
.underline()
} else {
Text("en retard")
.foregroundStyle(Color.master)
.underline()
}
} else if startDate.timeIntervalSinceNow <= 7200 && showPrefix {
if showPrefix {
Text("démarre dans")
@ -59,15 +78,21 @@ struct MatchDateView: View {
}
Text(startDate, style: .timer)
.monospacedDigit()
.foregroundStyle(Color.master)
.underline()
} else {
if showPrefix {
Text("le " + startDate.formatted(date: .abbreviated, time: .omitted))
.font(.footnote).foregroundStyle(.secondary)
Text("à " + startDate.formatted(date: .omitted, time: .shortened))
.monospacedDigit()
.foregroundStyle(Color.master)
.underline()
} else {
Text(startDate.formatted(date: .abbreviated, time: .shortened))
.monospacedDigit()
.foregroundStyle(Color.master)
.underline()
}
}
}
@ -81,11 +106,15 @@ struct MatchDateView: View {
}
Text(duration)
.monospacedDigit()
.foregroundStyle(Color.master)
.underline()
}
if match.startDate == nil && match.hasEnded() == false {
Text("démarrage").font(.footnote).foregroundStyle(.secondary)
Text("non défini")
.foregroundStyle(Color.master)
.underline()
}
}
}
@ -94,9 +123,7 @@ struct MatchDateView: View {
func save() {
do {
// match.currentTournament?.objectWillChange.send()
// match.objectWillChange.send()
// try viewContext.save()
try dataStore.matches.addOrUpdate(instance: match)
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.

@ -0,0 +1,45 @@
//
// MatchTeamDetailView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 18/04/2024.
//
import SwiftUI
struct MatchTeamDetailView: View {
let match: Match
var body: some View {
NavigationStack {
let tournament = match.currentTournament()
List {
if let teamOne = match.team(.one) {
_teamDetailView(teamOne, inTournament: tournament)
}
if let teamTwo = match.team(.two) {
_teamDetailView(teamTwo, inTournament: tournament)
}
}
.headerProminence(.increased)
.tint(.master)
}
.presentationDetents([.fraction(0.66)])
}
@ViewBuilder
private func _teamDetailView(_ team: TeamRegistration, inTournament tournament: Tournament?) -> some View {
Section {
ForEach(team.players()) { player in
EditablePlayerView(player: player, editingOptions: [.licenceId, .payment])
}
} header: {
TeamHeaderView(team: team, teamIndex: tournament?.indexOf(team: team), tournament: nil)
}
}
}
#Preview {
MatchTeamDetailView(match: Match.mock())
}

@ -43,9 +43,6 @@ struct PlayerBlockView: View {
}
private func _defaultLabel() -> String {
if match.upperBracketMatch(teamPosition)?.disabled == true {
return "Bye"
}
return teamPosition.localizedLabel()
}
@ -79,7 +76,7 @@ struct PlayerBlockView: View {
Text("WO")
}
if hideScore == false {
if hideScore == false && scores.isEmpty == false {
ForEach(scores.indices, id: \.self) { index in
let string = scores[index]
if string.isEmpty == false {
@ -96,6 +93,8 @@ struct PlayerBlockView: View {
.lineLimit(1)
}
}
} else if let team {
TeamWeightView(team: team, teamPosition: teamPosition)
}
}
}

@ -48,86 +48,40 @@ struct MatchDetailView: View {
_fieldSetup = State(wrappedValue: .field(court))
}
}
// @ViewBuilder
// func entrantView(_ entrant: Entrant) -> some View {
// Section {
// ForEach(entrant.orderedPlayers) { player in
// if player.isPlaying(in: match) {
// playerView(player)
// }
// }
// } header: {
// LabeledContent {
// if let tournament = match.currentTournament, let index = tournament.indexOfEntrant(entrant) {
// Text("#\(index + 1)")
// }
// } label: {
// if let title = entrant.brand?.title {
// Text(title)
// }
// }
// } footer: {
// LabeledContent {
// let weight = entrant.orderedPlayers.filter { $0.isPlaying(in: match) }.map { $0.tournamentRank }.reduce(0, +)
// Text(weight.formatted())
// } label: {
// Text("Poids de la paire")
// }
// }
// .headerProminence(.increased)
// }
// @ViewBuilder
// func playerView(_ player: Player) -> some View {
// VStack(alignment: .leading) {
// HStack {
// Text(player.longLabel)
// Text(player.localizedAge)
// Spacer()
// Text(player.formattedRank)
// }
//
// if let computedClubName = player.computedClubName {
// Text(computedClubName).foregroundStyle(.secondary).font(.caption)
// }
// if let computedLicense = player.computedLicense {
// Text(computedLicense).foregroundStyle(.secondary).font(.caption)
// }
// }
// }
var quickLookHeader: some View {
Section {
HStack {
if match.hasEnded() == false {
Menu {
Button("Non défini") {
match.removeCourt()
Menu {
Button("Non défini") {
match.removeCourt()
save()
}
ForEach(1...match.courtCount(), id: \.self) { courtIndex in
Button("Terrain #\(courtIndex.formatted())") {
match.setCourt(courtIndex)
save()
}
ForEach(1...match.courtCount(), id: \.self) { courtIndex in
Button("Terrain #\(courtIndex.formatted())") {
match.setCourt(courtIndex)
save()
}
}
} label: {
VStack(alignment: .leading) {
Text("terrain").font(.footnote).foregroundStyle(.secondary)
if let court = match.court {
Text("#" + court)
} else {
Text("Choisir")
}
}
} label: {
VStack(alignment: .leading) {
Text("terrain").font(.footnote).foregroundStyle(.secondary)
if let court = match.court {
Text("#" + court)
.foregroundStyle(Color.master)
.underline()
} else {
Text("Choisir")
.foregroundStyle(Color.master)
.underline()
}
}
.buttonStyle(.plain)
}
Spacer()
MatchDateView(match: match, showPrefix: true)
}
.font(.title)
.buttonStyle(.plain)
} footer: {
// if match.hasWalkoutTeam() == false {
// if let weatherData = match.weatherData {
@ -151,7 +105,6 @@ struct MatchDetailView: View {
Section {
MatchSummaryView(match: match, matchViewStyle: .plainStyle)
} header: {
} footer: {
if match.isEmpty() == false {
HStack {
@ -171,34 +124,34 @@ struct MatchDetailView: View {
}
}
Section {
ForEach(match.teams()) { team in
ForEach(team.players().filter({ $0.hasPaid() == false })) { player in
HStack {
Text(player.playerLabel())
Spacer()
//PlayerPayView(player: player)
let players = match.teams().flatMap { $0.players() }
let unpaid = players.filter({ $0.hasPaid() == false })
if unpaid.isEmpty == false {
Section {
DisclosureGroup {
ForEach(unpaid) { player in
LabeledContent {
PlayerPayView(player: player)
} label: {
Text(player.playerLabel())
}
}
} label: {
LabeledContent {
Text(unpaid.count.formatted() + " / " + players.count.formatted())
} label: {
Text("Encaissement manquant")
}
}
}
}
menuView
}
// .sheet(isPresented: $showDetails) {
// NavigationStack {
// List {
// if let entrantOne = match.entrantOne() {
// entrantView(entrantOne)
// }
// if let entrantTwo = match.entrantTwo() {
// entrantView(entrantTwo)
// }
// }
// }
// .presentationDetents([.fraction(0.66)])
// }
.sheet(isPresented: $showDetails) {
MatchTeamDetailView(match: match)
}
.sheet(item: $scoreType, onDismiss: {
if match.hasEnded() {
dismiss()
@ -206,6 +159,7 @@ struct MatchDetailView: View {
}) { scoreType in
let matchDescriptor = MatchDescriptor(match: match)
EditScoreView(matchDescriptor: matchDescriptor)
.tint(.master)
// switch scoreType {
// case .edition:
@ -305,7 +259,8 @@ struct MatchDetailView: View {
// }
// }
.navigationTitle(match.matchTitle())
.navigationBarTitleDisplayMode(.large)
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
}
enum ScoreType: Int, Identifiable, Hashable {
@ -365,14 +320,15 @@ struct MatchDetailView: View {
Section {
if match.hasEnded() == false {
let rotationDuration = match.getDuration()
Picker(selection: $startDateSetup) {
if match.isReady() {
Text("Dans 5 minutes").tag(MatchDateSetup.inMinutes(5))
Text("Dans 15 minutes").tag(MatchDateSetup.inMinutes(15))
Text("Tout de suite").tag(MatchDateSetup.now)
}
Text("Précédente rotation").tag(MatchDateSetup.inMinutes(-match.matchFormat.estimatedDuration))
Text("Prochaine rotation").tag(MatchDateSetup.inMinutes(match.matchFormat.estimatedDuration))
Text("Précédente rotation").tag(MatchDateSetup.inMinutes(-rotationDuration))
Text("Prochaine rotation").tag(MatchDateSetup.inMinutes(rotationDuration))
Text("À").tag(MatchDateSetup.customDate)
} label: {
Text("Horaire")

@ -10,13 +10,49 @@ import SwiftUI
struct MatchRowView: View {
var match: Match
let matchViewStyle: MatchViewStyle
@Environment(\.editMode) private var editMode
@Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed
@ViewBuilder
var body: some View {
if editMode?.wrappedValue.isEditing == true && match.isGroupStage() == false && match.isLoserBracket == false {
if isEditingTournamentSeed.wrappedValue == true && match.isGroupStage() == false && match.isLoserBracket == false {
MatchSetupView(match: match)
} else {
// MatchSummaryView(match: match, matchViewStyle: matchViewStyle)
// .overlay {
// if match.disabled {
// Image(systemName: "xmark")
// .resizable()
// .scaledToFit()
// .opacity(0.8)
// }
// }
// .contextMenu(menuItems: {
// Text("index: \(match.index)")
// Text("bye state : \(match.byeState)")
// Text("disable state : \(match.disabled)")
// Button("enable") {
// match._toggleMatchDisableState(false)
// }
// Button("disable") {
// match._toggleMatchDisableState(true)
// }
// Button("bye") {
// match.byeState = true
// }
// Button("not bye") {
// match.byeState = false
// }
// Button("solo toggle") {
// match.disabled.toggle()
// }
// Button("toggle fwrd match") {
// match._toggleForwardMatchDisableState(true)
// }
// Button("toggle loser match") {
// match._toggleLoserMatchDisableState(true)
// }
// })
NavigationLink {
MatchDetailView(match: match, matchViewStyle: matchViewStyle)
} label: {

@ -14,8 +14,17 @@ struct MatchSetupView: View {
@ViewBuilder
var body: some View {
_teamView(inTeamPosition: .one)
_teamView(inTeamPosition: .two)
ForEach(TeamPosition.allCases) { teamPosition in
VStack(alignment: .leading) {
if teamPosition == .one {
Text("Branche du haut")
}
_teamView(inTeamPosition: teamPosition)
if teamPosition == .two {
Text("Branche du bas")
}
}
}
}
@ViewBuilder
@ -33,6 +42,7 @@ struct MatchSetupView: View {
if match.isSeededBy(team: team, inTeamPosition: teamPosition) {
team.bracketPosition = nil
match.enableMatch()
try? dataStore.matches.addOrUpdate(instance: match)
try? dataStore.teamRegistrations.addOrUpdate(instance: team)
} else {
match.teamWillBeWalkOut(team)
@ -88,7 +98,7 @@ struct MatchSetupView: View {
}
}
} label: {
Text("Tirage").tag(nil as SeedInterval?)
Text("Tirer au sort").tag(nil as SeedInterval?)
}
.disabled(availableSeedGroups.isEmpty && walkOutSpot == false)
@ -106,9 +116,8 @@ struct MatchSetupView: View {
}
}
.fixedSize(horizontal: false, vertical: true)
.buttonBorderShape(.capsule)
.buttonStyle(.borderedProminent)
.buttonStyle(.borderless)
.underline()
}
}
}

@ -19,7 +19,6 @@ struct ActivityView: View {
@State private var viewStyle: AgendaDestination.ViewStyle = .list
@State private var federalTournaments: [FederalTournament] = []
@State private var isGatheringFederalTournaments: Bool = false
@Binding var selectedTab: TabDestination?
@State private var error: Error?
var runningTournaments: [FederalTournamentHolder] {
@ -257,7 +256,7 @@ struct ActivityView: View {
Text("Pour voir vos tournois tenup ici, indiquez vos clubs préférés.")
} actions: {
RowButtonView("Choisir mes clubs préférés") {
selectedTab = .umpire
navigation.selectedTab = .umpire
}
}
} else {
@ -276,6 +275,5 @@ struct ActivityView: View {
}
#Preview {
ActivityView(selectedTab: .constant(.activity))
.environmentObject(DataStore.shared)
ActivityView()
}

@ -6,18 +6,20 @@
//
import SwiftUI
import LeStorage
struct MainView: View {
@StateObject var dataStore = DataStore.shared
@AppStorage("importingFiles") var importingFiles: Bool = false
@Environment(NavigationViewModel.self) private var navigation: NavigationViewModel
@State private var checkingFilesAttempt: Int = 0
@State private var checkingFiles: Bool = false
@AppStorage("lastDataSource") var lastDataSource: String?
@AppStorage("lastDataSourceMaleUnranked") var lastDataSourceMaleUnranked: Int?
@AppStorage("lastDataSourceFemaleUnranked") var lastDataSourceFemaleUnranked: Int?
var lastDataSource: String? {
dataStore.appSettings.lastDataSource
}
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
@ -25,10 +27,10 @@ struct MainView: View {
animation: .default)
private var players: FetchedResults<ImportedPlayer>
@State private var selectedTab: TabDestination?
var body: some View {
TabView(selection: $selectedTab) {
ActivityView(selectedTab: $selectedTab)
@Bindable var navigation = navigation
TabView(selection: $navigation.selectedTab) {
ActivityView()
.tabItem(for: .activity)
TournamentOrganizerView()
.tabItem(for: .tournamentOrganizer)
@ -61,15 +63,7 @@ struct MainView: View {
func _activityStatusBoxView() -> some View {
_activityStatus()
.font(.title3)
.frame(height: 28)
.padding()
.background {
RoundedRectangle(cornerRadius: 20, style: .continuous)
.fill(.white)
}
.shadow(radius: 2)
.offset(y: -64)
.toastFormatted()
}
@ViewBuilder
@ -95,7 +89,7 @@ struct MainView: View {
}
private func _checkSourceFileAvailability() async {
print(dataStore.appSettings.lastDataSource)
print("check internet")
print("check files on internet")
print("check if any files on internet are more recent than here")
@ -112,21 +106,18 @@ struct MainView: View {
private func _startImporting() {
importingFiles = true
Task {
lastDataSource = await FileImportManager.shared.importDataFromFFT()
let lastDataSource = await FileImportManager.shared.importDataFromFFT()
dataStore.appSettings.lastDataSource = lastDataSource
dataStore.updateSettings()
if let lastDataSource, let mostRecentDate = URL.importDateFormatter.date(from: lastDataSource) {
await _calculateCurrentUnrankedValues(mostRecentDateAvailable: mostRecentDate)
await MonthData.calculateCurrentUnrankedValues(mostRecentDateAvailable: mostRecentDate)
}
importingFiles = false
await _downloadPreviousDate()
}
}
private func _calculateCurrentUnrankedValues(mostRecentDateAvailable: Date) async {
lastDataSourceMaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: mostRecentDateAvailable, man: true)
lastDataSourceFemaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: mostRecentDateAvailable, man: false)
}
private func _downloadPreviousDate() async {
await SourceFileManager.shared.getAllFiles()
}

@ -24,6 +24,7 @@ struct TournamentOrganizerView: View {
ContentUnavailableView("Aucun tournoi sélectionné", systemImage: "rectangle.slash", description: Text("Utilisez l'accès rapide ci-dessous pour éditer un tournoi et passer rapidement d'un tournoi à l'autre."))
.navigationTitle("Gestionnaire de tournois")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
}
}
Divider()

@ -13,7 +13,12 @@ struct PadelClubView: View {
@State private var checkingFiles: Bool = false
@State private var importingFiles: Bool = false
@AppStorage("lastDataSource") var lastDataSource: String?
@EnvironmentObject var dataStore: DataStore
var lastDataSource: String? {
dataStore.appSettings.lastDataSource
}
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
@ -36,17 +41,38 @@ struct PadelClubView: View {
List {
if let _lastDataSourceDate {
Section {
HStack {
VStack(alignment: .leading) {
Text("Classement mensuel utilisé").font(.caption).foregroundStyle(.secondary)
Text(_lastDataSourceDate.monthYearFormatted)
}
Spacer()
LabeledContent {
Image(systemName: "checkmark")
} label: {
Text(_lastDataSourceDate.monthYearFormatted)
Text("Classement mensuel utilisé")
}
}
}
let monthData = dataStore.monthData.sorted(by: \.creationDate).reversed()
ForEach(monthData) { monthData in
Section {
LabeledContent {
if let maleUnrankedValue = monthData.maleUnrankedValue {
Text(maleUnrankedValue.formatted())
}
} label: {
Text("Messieurs")
Text("Rang d'un non classé")
}
LabeledContent {
if let femaleUnrankedValue = monthData.femaleUnrankedValue {
Text(femaleUnrankedValue.formatted())
}
} label: {
Text("Dames")
Text("Rang d'une non classée")
}
} header: {
Text(monthData.monthKey)
}
}
//
// if players.isEmpty {
// ContentUnavailableView {
@ -60,6 +86,7 @@ struct PadelClubView: View {
// }
// }
}
.headerProminence(.increased)
.navigationTitle(TabDestination.padelClub.title)
// .task {
// await self._checkSourceFileAvailability()
@ -101,7 +128,12 @@ struct PadelClubView: View {
private func _startImporting() {
importingFiles = true
Task {
lastDataSource = await FileImportManager.shared.importDataFromFFT()
let lastDataSource = await FileImportManager.shared.importDataFromFFT()
dataStore.appSettings.lastDataSource = lastDataSource
dataStore.updateSettings()
if let lastDataSource, let mostRecentDate = URL.importDateFormatter.date(from: lastDataSource) {
await MonthData.calculateCurrentUnrankedValues(mostRecentDateAvailable: mostRecentDate)
}
importingFiles = false
}
}

@ -0,0 +1,25 @@
//
// DurationSettingsView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 18/04/2024.
//
import SwiftUI
struct DurationSettingsView: View {
var body: some View {
List {
ForEach(MatchFormat.allCases, id: \.self) { matchFormat in
MatchFormatStorageView(matchFormat: matchFormat)
}
}
.navigationTitle("Durées moyennes")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
}
}
#Preview {
DurationSettingsView()
}

@ -0,0 +1,67 @@
//
// GlobalSettingsView.swift
// Padel Tournament
//
// Created by Razmig Sarkissian on 16/10/2023.
//
import SwiftUI
struct GlobalSettingsView: View {
@EnvironmentObject var dataStore : DataStore
var body: some View {
@Bindable var appSettings = dataStore.appSettings
List {
Section {
Picker(selection: $appSettings.groupStageMatchFormatPreference) {
Text("Automatique").tag(nil as Int?)
ForEach(MatchFormat.allCases, id: \.self) { format in
Text(format.format).tag(format.rawValue as Int?)
}
} label: {
HStack {
Text("Poule")
Spacer()
}
}
Picker(selection: $appSettings.bracketMatchFormatPreference) {
Text("Automatique").tag(nil as Int?)
ForEach(MatchFormat.allCases, id: \.self) { format in
Text(format.format).tag(format.rawValue as Int?)
}
} label: {
HStack {
Text("Tableau")
Spacer()
}
}
Picker(selection: $appSettings.loserBracketMatchFormatPreference) {
Text("Automatique").tag(nil as Int?)
ForEach(MatchFormat.allCases, id: \.self) { format in
Text(format.format).tag(format.rawValue as Int?)
}
} label: {
HStack {
Text("Match de classement")
Spacer()
}
}
} header: {
Text("Vos formats préférés")
} footer: {
Text("À minima, les règles fédérales seront toujours prises en compte par défaut.")
}
}
.onChange(of: [
appSettings.bracketMatchFormatPreference,
appSettings.groupStageMatchFormatPreference,
appSettings.loserBracketMatchFormatPreference
]) {
dataStore.updateSettings()
}
.navigationTitle("Formats par défaut")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
}
}

@ -0,0 +1,50 @@
//
// MatchFormatStorageView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 18/04/2024.
//
import SwiftUI
struct MatchFormatStorageView: View {
@State private var estimatedDuration: Int
@EnvironmentObject var dataStore: DataStore
let matchFormat: MatchFormat
init(matchFormat: MatchFormat) {
self.matchFormat = matchFormat
_estimatedDuration = State(wrappedValue: matchFormat.getEstimatedDuration())
}
var body: some View {
Section {
LabeledContent {
StepperView(title: "minutes", count: $estimatedDuration, step: 5)
} label: {
Text("Durée \(matchFormat.format)")
Text(matchFormat.computedShortLabelWithoutPrefix)
}
} footer: {
if estimatedDuration != matchFormat.defaultEstimatedDuration {
HStack {
Spacer()
Button {
self.estimatedDuration = matchFormat.defaultEstimatedDuration
} label: {
Text("remettre la durée par défault")
.underline()
}
.buttonStyle(.borderless)
}
}
}
.onChange(of: estimatedDuration) {
dataStore.appSettings.saveMatchFormatsDefaultDuration(matchFormat, estimatedDuration: estimatedDuration)
dataStore.updateSettings()
}
}
}

@ -11,17 +11,34 @@ struct ToolboxView: View {
var body: some View {
NavigationStack {
List {
NavigationLink {
SelectablePlayerListView()
} label: {
Label("Rechercher un joueur", systemImage: "person.fill.viewfinder")
Section {
NavigationLink {
SelectablePlayerListView()
} label: {
Label("Rechercher un joueur", systemImage: "person.fill.viewfinder")
}
}
NavigationLink {
RankCalculatorView()
} label: {
Label("Calculateur de points", systemImage: "scalemass")
Section {
NavigationLink {
RankCalculatorView()
} label: {
Label("Calculateur de points", systemImage: "scalemass")
}
}
Section {
NavigationLink {
GlobalSettingsView()
} label: {
Label("Formats de jeu par défaut", systemImage: "megaphone")
}
NavigationLink {
DurationSettingsView()
} label: {
Label("Estimation des durées moyennes", systemImage: "deskclock")
}
}
}
.navigationTitle(TabDestination.toolbox.title)
}

@ -64,7 +64,6 @@ struct UmpireView: View {
user.licenceId = nil
dataStore.setUser(user)
}
.font(.caption)
}
}
@ -83,7 +82,6 @@ struct UmpireView: View {
user.club = nil
dataStore.setUser(user)
}
.font(.caption)
}
}
}

@ -0,0 +1,54 @@
//
// DateUpdateManagerView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 17/04/2024.
//
import SwiftUI
enum DateUpdate {
case nextRotation
case previousRotation
case tomorrowAtNine
case inMinutes(Int)
case afterRound(Round)
case afterGroupStage(GroupStage)
}
struct DateUpdateManagerView: View {
@Binding var startDate: Date
@State private var dateUpdated: Bool = false
var validateAction: () -> Void
var body: some View {
HStack {
Menu {
Text("à demain 9h")
Text("à la prochaine rotation")
Text("à la précédente rotation")
} label: {
Text("décaler")
.underline()
}
Spacer()
if dateUpdated {
Button {
validateAction()
dateUpdated = false
} label: {
Text("valider la modification")
.underline()
}
}
}
.font(.subheadline)
.buttonStyle(.borderless)
.onChange(of: startDate) {
dateUpdated = true
}
}
}

@ -0,0 +1,157 @@
//
// CourtAvailabilitySettingsView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 19/04/2024.
//
import SwiftUI
struct CourtAvailabilitySettingsView: View {
@Environment(Tournament.self) var tournament: Tournament
@State private var courtsUnavailability: [Int: [DateInterval]] = [Int:[DateInterval]]()
@State private var showingPopover: Bool = false
@State private var courtIndex: Int = 0
@State private var startDate: Date = Date()
@State private var endDate: Date = Date()
var body: some View {
List {
let keys = courtsUnavailability.keys.sorted(by: \.self)
ForEach(keys, id: \.self) { key in
if let dates = courtsUnavailability[key] {
Section {
ForEach(dates) { dateInterval in
HStack {
VStack(alignment: .leading, spacing: 0) {
Text(dateInterval.startDate.localizedTime()).font(.largeTitle)
Text(dateInterval.startDate.localizedDay()).font(.caption)
}
Spacer()
Image(systemName: "arrowshape.forward.fill")
.tint(.master)
Spacer()
VStack(alignment: .trailing, spacing: 0) {
Text(dateInterval.endDate.localizedTime()).font(.largeTitle)
Text(dateInterval.endDate.localizedDay()).font(.caption)
}
}
.contextMenu(menuItems: {
Button("dupliquer") {
}
Button("éditer") {
}
Button("effacer") {
}
})
.swipeActions {
Button(role: .destructive) {
courtsUnavailability[key]?.removeAll(where: { $0.id == dateInterval.id })
} label: {
LabelDelete()
}
}
}
} header: {
Text("Terrain #\(key + 1)")
}
.headerProminence(.increased)
}
}
}
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
showingPopover = true
} label: {
Image(systemName: "plus.circle.fill")
.resizable()
.scaledToFit()
.frame(minHeight: 28)
}
}
}
.onDisappear {
tournament.courtsUnavailability = courtsUnavailability
}
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Créneaux")
.popover(isPresented: $showingPopover) {
NavigationStack {
Form {
Section {
CourtPicker(title: "Terrain", selection: $courtIndex, maxCourt: 3)
}
Section {
DatePicker("Début", selection: $startDate)
DatePicker("Fin", selection: $endDate)
} footer: {
Button("jour entier") {
startDate = startDate.startOfDay
endDate = endDate.endOfDay()
}
.buttonStyle(.borderless)
.underline()
}
}
.toolbar {
Button("Valider") {
let dateInterval = DateInterval(startDate: startDate, endDate: endDate)
var courtUnavailability = courtsUnavailability[courtIndex] ?? [DateInterval]()
courtUnavailability.append(dateInterval)
courtsUnavailability[courtIndex] = courtUnavailability
showingPopover = false
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Nouveau créneau")
}
.onAppear {
UIDatePicker.appearance().minuteInterval = 5
}
.onDisappear {
UIDatePicker.appearance().minuteInterval = 1
}
}
}
}
struct CourtPicker: View {
let title: String
@Binding var selection: Int
let maxCourt: Int
var body: some View {
Picker(title, selection: $selection) {
ForEach(0..<maxCourt, id: \.self) {
Text("Terrain #\($0 + 1)")
}
}
}
}
#Preview {
CourtAvailabilitySettingsView()
}
/*
LabeledContent {
// switch dayIndex {
// case 1:
// StepperView(count: $dayTwo, maximum: tournament.courtCount)
// case 2:
// StepperView(count: $dayThree, maximum: tournament.courtCount)
// default:
// StepperView(count: $dayOne, maximum: tournament.courtCount)
// }
// } label: {
// Text("Terrains maximum")
// Text(tournament.startDate.formatted(.dateTime.weekday(.wide)) + " + \(dayIndex)")
// }
*/

@ -10,20 +10,33 @@ import SwiftUI
struct GroupStageScheduleEditorView: View {
@EnvironmentObject var dataStore: DataStore
var groupStage: GroupStage
@State private var startDate: Date
@State private var dateUpdated: Bool = false
init(groupStage: GroupStage) {
self.groupStage = groupStage
self._startDate = State(wrappedValue: groupStage.startDate ?? Date())
}
var body: some View {
@Bindable var groupStage = groupStage
List {
Section {
MatchFormatPickerView(headerLabel: "Format", matchFormat: $groupStage.matchFormat)
}
Section {
Text("Modifier l'horaire")
}
RowButtonView("Convoquer") {
DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline)
}
.onChange(of: startDate) {
dateUpdated = true
}
} header: {
Text(groupStage.groupStageTitle())
} footer: {
DateUpdateManagerView(startDate: $startDate) {
groupStage.startDate = startDate
_save()
}
}
NavigationLink {
@ -35,10 +48,15 @@ struct GroupStageScheduleEditorView: View {
.onChange(of: groupStage.matchFormat) {
_save()
}
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
}
private func _save() {
let matches = groupStage._matches()
matches.forEach({ $0.matchFormat = groupStage.matchFormat })
try? dataStore.matches.addOrUpdate(contentOfs: matches)
try? dataStore.groupStages.addOrUpdate(instance: groupStage)
}
}

@ -6,74 +6,6 @@
//
import SwiftUI
struct LoserRoundStepScheduleEditorView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
var round: Round
var upperRound: Round
var matches: [Match]
@State private var startDate: Date
@State private var matchFormat: MatchFormat
init(round: Round, upperRound: Round) {
self.upperRound = upperRound
self.round = round
let _matches = upperRound.loserRounds(forRoundIndex: round.index).flatMap({ $0.playedMatches() })
self.matches = _matches
self._startDate = State(wrappedValue: round.startDate ?? _matches.first?.startDate ?? Date())
self._matchFormat = State(wrappedValue: round.matchFormat)
}
var body: some View {
@Bindable var round = round
Section {
MatchFormatPickerView(headerLabel: "Format", matchFormat: $round.matchFormat)
DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday()))
}
RowButtonView("Valider la modification") {
_updateSchedule()
}
} header: {
Text(round.selectionLabel())
} footer: {
NavigationLink {
List {
ForEach(matches) { match in
if match.disabled == false {
MatchScheduleEditorView(match: match)
}
}
}
.headerProminence(.increased)
.navigationTitle(round.selectionLabel())
.environment(tournament)
} label: {
Text("voir tous les matchs")
}
}
.headerProminence(.increased)
}
private func _updateSchedule() {
upperRound.loserRounds(forRoundIndex: round.index).forEach({ round in
round.resetRound(updateMatchFormat: round.matchFormat)
})
try? dataStore.matches.addOrUpdate(contentOfs: matches)
_save()
MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate)
_save()
}
private func _save() {
try? dataStore.rounds.addOrUpdate(contentOfs: upperRound.loserRounds(forRoundIndex: round.index))
}
}
struct LoserRoundScheduleEditorView: View {
@EnvironmentObject var dataStore: DataStore
@ -83,7 +15,7 @@ struct LoserRoundScheduleEditorView: View {
var loserRounds: [Round]
@State private var startDate: Date
@State private var matchFormat: MatchFormat
init(upperRound: Round) {
self.upperRound = upperRound
let _loserRounds = upperRound.loserRounds()
@ -97,13 +29,14 @@ struct LoserRoundScheduleEditorView: View {
Section {
MatchFormatPickerView(headerLabel: "Format", matchFormat: $matchFormat)
DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday()))
}
RowButtonView("Valider la modification") {
_updateSchedule()
Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline)
}
} header: {
Text("Classement " + upperRound.roundTitle())
} footer: {
DateUpdateManagerView(startDate: $startDate) {
_updateSchedule()
}
}
@ -126,12 +59,14 @@ struct LoserRoundScheduleEditorView: View {
upperRound.loserRounds().forEach({ round in
round.resetRound(updateMatchFormat: matchFormat)
})
try? dataStore.matches.addOrUpdate(contentOfs: matches)
_save()
MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: upperRound.loserRounds().first?.id, fromMatchId: nil, startDate: startDate)
_save()
upperRound.loserRounds().first?.startDate = startDate
}
private func _save() {

@ -0,0 +1,79 @@
//
// LoserRoundStepScheduleEditorView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 17/04/2024.
//
import SwiftUI
struct LoserRoundStepScheduleEditorView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
var round: Round
var upperRound: Round
var matches: [Match]
@State private var startDate: Date
@State private var matchFormat: MatchFormat
init(round: Round, upperRound: Round) {
self.upperRound = upperRound
self.round = round
let _matches = upperRound.loserRounds(forRoundIndex: round.index).flatMap({ $0.playedMatches() })
self.matches = _matches
self._startDate = State(wrappedValue: round.startDate ?? _matches.first?.startDate ?? Date())
self._matchFormat = State(wrappedValue: round.matchFormat)
}
var body: some View {
@Bindable var round = round
Section {
MatchFormatPickerView(headerLabel: "Format", matchFormat: $round.matchFormat)
DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline)
}
NavigationLink {
List {
ForEach(matches) { match in
if match.disabled == false {
MatchScheduleEditorView(match: match)
}
}
}
.headerProminence(.increased)
.navigationTitle(round.selectionLabel())
.environment(tournament)
} label: {
Text("Voir tous les matchs")
}
} header: {
Text(round.selectionLabel())
} footer: {
DateUpdateManagerView(startDate: $startDate) {
_updateSchedule()
}
}
.headerProminence(.increased)
}
private func _updateSchedule() {
upperRound.loserRounds(forRoundIndex: round.index).forEach({ round in
round.resetRound(updateMatchFormat: round.matchFormat)
})
try? dataStore.matches.addOrUpdate(contentOfs: matches)
_save()
MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate)
upperRound.loserRounds(forRoundIndex: round.index).forEach({ round in
round.startDate = startDate
})
_save()
}
private func _save() {
try? dataStore.rounds.addOrUpdate(contentOfs: upperRound.loserRounds(forRoundIndex: round.index))
}
}

@ -20,10 +20,7 @@ struct MatchScheduleEditorView: View {
var body: some View {
Section {
DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday()))
}
RowButtonView("Valider la modification") {
_updateSchedule()
Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline)
}
} header: {
if let round = match.roundObject {
@ -31,6 +28,10 @@ struct MatchScheduleEditorView: View {
} else {
Text(match.matchTitle())
}
} footer: {
DateUpdateManagerView(startDate: $startDate) {
_updateSchedule()
}
}
.headerProminence(.increased)
}

@ -10,7 +10,6 @@ import SwiftUI
struct PlanningSettingsView: View {
@EnvironmentObject var dataStore: DataStore
var tournament: Tournament
@State private var scheduleSetup: Bool = false
@State private var randomCourtDistribution: Bool
@State private var groupStageCourtCount: Int
@State private var upperBracketBreakTime: Bool
@ -20,6 +19,8 @@ struct PlanningSettingsView: View {
@State private var upperBracketRotationDifference: Int
@State private var timeDifferenceLimit: Double
@State private var shouldHandleUpperRoundSlice: Bool
@State private var isScheduling: Bool = false
@State private var schedulingDone: Bool = false
init(tournament: Tournament) {
self.tournament = tournament
@ -39,12 +40,11 @@ struct PlanningSettingsView: View {
List {
Section {
DatePicker(tournament.startDate.formatted(.dateTime.weekday()), selection: $tournament.startDate)
Stepper(value: $tournament.dayDuration, in: 1...1_000) {
HStack {
Text("Durée")
Spacer()
Text("\(tournament.dayDuration) jour" + tournament.dayDuration.pluralSuffix)
}
LabeledContent {
StepperView(count: $tournament.dayDuration, minimum: 1, maximum: 1_000)
} label: {
Text("Durée")
Text("\(tournament.dayDuration) jour" + tournament.dayDuration.pluralSuffix)
}
} header: {
Text("Démarrage et durée du tournoi")
@ -53,17 +53,35 @@ struct PlanningSettingsView: View {
}
Section {
TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount)
TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount, max: 100)
if tournament.groupStages().isEmpty == false {
TournamentFieldsManagerView(localizedStringKey: "Terrains par poule", count: $groupStageCourtCount)
TournamentFieldsManagerView(localizedStringKey: "Terrains par poule", count: $groupStageCourtCount, max: tournament.maximumCourtsPerGroupSage())
}
NavigationLink {
CourtAvailabilitySettingsView()
.environment(tournament)
} label: {
Text("Disponibilité des terrains")
Text("Préciser la disponibilité des terrains")
}
// if tournament.dayDuration > 1 {
// ForEach(0..<tournament.dayDuration, id: \.self) { dayIndex in
// LabeledContent {
// switch dayIndex {
// case 1:
// StepperView(count: $dayTwo, maximum: tournament.courtCount)
// case 2:
// StepperView(count: $dayThree, maximum: tournament.courtCount)
// default:
// StepperView(count: $dayOne, maximum: tournament.courtCount)
// }
// } label: {
// Text("Terrains maximum")
// Text(tournament.startDate.formatted(.dateTime.weekday(.wide)) + " + \(dayIndex)")
// }
// }
// }
}
Section {
@ -103,26 +121,36 @@ struct PlanningSettingsView: View {
.disabled(rotationDifferenceIsImportant == false)
//timeDifferenceLimit
RowButtonView("Horaire intelligent", role: .destructive) {
_setupSchedule()
}
if scheduleSetup {
HStack {
Image(systemName: "checkmark")
}
schedulingDone = false
await _setupSchedule()
schedulingDone = true
}
}
Section {
NavigationLink {
} label: {
Text("Modifier le message de convocation")
RowButtonView("Supprimer tous les horaires", role: .destructive) {
let allMatches = tournament.allMatches()
allMatches.forEach({ $0.startDate = nil })
try? dataStore.matches.addOrUpdate(contentOfs: allMatches)
let allGroupStages = tournament.groupStages()
allGroupStages.forEach({ $0.startDate = nil })
try? dataStore.groupStages.addOrUpdate(contentOfs: allGroupStages)
let allRounds = tournament.allRounds()
allRounds.forEach({ $0.startDate = nil })
try? dataStore.rounds.addOrUpdate(contentOfs: allRounds)
}
}
}
.overlay(alignment: .bottom) {
if schedulingDone {
Label("Horaires mis à jour", systemImage: "checkmark.circle.fill")
.toastFormatted()
.deferredRendering(for: .seconds(2))
}
}
.onChange(of: groupStageCourtCount) {
tournament.groupStageCourtCount = groupStageCourtCount
_save()
@ -141,11 +169,12 @@ struct PlanningSettingsView: View {
}
}
private func _setupSchedule() {
private func _setupSchedule() async {
let groupStageCourtCount = tournament.groupStageCourtCount ?? 1
let groupStages = tournament.groupStages()
let numberOfCourtsAvailablePerRotation: Int = tournament.courtCount
let matchScheduler = MatchScheduler.shared
matchScheduler.courtsUnavailability = tournament.courtsUnavailability
matchScheduler.options.removeAll()
if randomCourtDistribution {
@ -175,13 +204,6 @@ struct PlanningSettingsView: View {
let matches = tournament.groupStages().flatMap({ $0._matches() })
matches.forEach({ $0.startDate = nil })
// var times = Set(groupStages.compactMap { $0.startDate }.filter { $0 >= tournament.startDate } )
// if times.isEmpty {
// groupStages.forEach({ $0.startDate = tournament.startDate })
// times.insert(tournament.startDate)
// try? dataStore.groupStages.addOrUpdate(contentOfs: groupStages)
// }
var lastDate : Date = tournament.startDate
groupStages.chunked(into: groupStageCourtCount).forEach { groups in
groups.forEach({ $0.startDate = lastDate })
@ -191,11 +213,12 @@ struct PlanningSettingsView: View {
dispatch.timedMatches.forEach { matchSchedule in
if let match = matches.first(where: { $0.id == matchSchedule.matchID }) {
let timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(match.matchFormat.estimatedDuration) * 60
let estimatedDuration = match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)
let timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(estimatedDuration) * 60
if let startDate = match.groupStageObject?.startDate {
let matchStartDate = startDate.addingTimeInterval(timeIntervalToAdd)
match.startDate = matchStartDate
lastDate = matchStartDate.addingTimeInterval(Double(match.matchFormat.estimatedDuration) * 60)
lastDate = matchStartDate.addingTimeInterval(Double(estimatedDuration) * 60)
}
match.setCourt(matchSchedule.courtIndex + 1)
}
@ -204,9 +227,6 @@ struct PlanningSettingsView: View {
try? dataStore.matches.addOrUpdate(contentOfs: matches)
matchScheduler.updateSchedule(tournament: tournament, fromRoundId: nil, fromMatchId: nil, startDate: lastDate)
scheduleSetup = true
}
private func _save() {

@ -9,8 +9,8 @@ import SwiftUI
struct PlanningView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(\.editMode) private var editMode
@Environment(Tournament.self) var tournament: Tournament
let matches: [Match]
@State private var timeSlots: [Date:[Match]]
@State private var days: [Date]
@ -30,85 +30,27 @@ struct PlanningView: View {
Section {
ForEach(keys.filter({ $0.dayInt == day.dayInt }), id: \.self) { key in
if let _matches = timeSlots[key] {
if editMode?.wrappedValue.isEditing == true {
HStack {
VStack(alignment: .leading) {
let index = keys.firstIndex(of: key)
Button {
let previousKey = keys[index! - 1]
let previousMatches = timeSlots[previousKey]
previousMatches?.forEach { match in
match.startDate = key
}
_matches.forEach { match in
match.startDate = previousKey
}
_update()
} label: {
Image(systemName: "arrow.up")
}
.buttonStyle(.bordered)
.disabled(index == 0)
Button {
let nextKey = keys[index! + 1]
let nextMatches = timeSlots[nextKey]
nextMatches?.forEach { match in
match.startDate = key
}
_matches.forEach { match in
match.startDate = nextKey
}
_update()
} label: {
Image(systemName: "arrow.down")
}
.buttonStyle(.bordered)
.disabled(index == keys.count - 1)
}
VStack(alignment: .leading) {
DisclosureGroup {
ForEach(_matches) { match in
NavigationLink {
MatchDetailView(match: match, matchViewStyle: .sectionedStandardStyle)
} label: {
LabeledContent {
Text(_matches.count.formatted() + " match" + _matches.count.pluralSuffix)
} label: {
Text(key.formatted(date: .omitted, time: .shortened)).font(.largeTitle)
}
ForEach(_matches) { match in
LabeledContent {
Text(match.matchFormat.format)
} label: {
if let groupStage = match.groupStageObject {
Text(groupStage.groupStageTitle())
} else if let round = match.roundObject {
Text(round.roundTitle())
}
Text(match.matchTitle())
if let court = match.court {
Text(court)
}
}
}
}
} else {
DisclosureGroup {
ForEach(_matches) { match in
NavigationLink {
MatchDetailView(match: match, matchViewStyle: .sectionedStandardStyle)
} label: {
LabeledContent {
if let court = match.court {
Text(court)
}
} label: {
if let groupStage = match.groupStageObject {
Text(groupStage.groupStageTitle())
} else if let round = match.roundObject {
Text(round.roundTitle())
}
Text(match.matchTitle())
if let groupStage = match.groupStageObject {
Text(groupStage.groupStageTitle())
} else if let round = match.roundObject {
Text(round.roundTitle())
}
Text(match.matchTitle())
}
}
} label: {
_timeSlotView(key: key, matches: _matches)
}
} label: {
_timeSlotView(key: key, matches: _matches)
}
}
}
@ -118,29 +60,9 @@ struct PlanningView: View {
.headerProminence(.increased)
}
}
.toolbar {
EditButton()
}
.onChange(of: isEditing) { old, new in
if old == true && new == false {
print("save")
try? dataStore.matches.addOrUpdate(contentOfs: matches)
}
}
.navigationTitle("Programmation")
}
private func _update() {
let timeSlots = Dictionary(grouping: matches) { $0.startDate ?? .distantFuture }
self.timeSlots = timeSlots
self.days = Set(timeSlots.keys.map { $0.startOfDay }).sorted()
self.keys = timeSlots.keys.sorted()
}
private var isEditing: Bool {
editMode?.wrappedValue.isEditing == true
}
private func _timeSlotView(key: Date, matches: [Match]) -> some View {
LabeledContent {
Text(matches.count.formatted() + " match" + matches.count.pluralSuffix)

@ -13,7 +13,7 @@ struct RoundScheduleEditorView: View {
var round: Round
@State private var startDate: Date
init(round: Round) {
self.round = round
self._startDate = State(wrappedValue: round.startDate ?? round.playedMatches().first?.startDate ?? Date())
@ -25,10 +25,23 @@ struct RoundScheduleEditorView: View {
Section {
MatchFormatPickerView(headerLabel: "Format", matchFormat: $round.matchFormat)
DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday()))
Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline)
}
RowButtonView("Valider la modification") {
_updateSchedule()
} footer: {
HStack {
DateUpdateManagerView(startDate: $startDate) {
_updateSchedule()
}
Spacer()
if let roundStartDate = round.startDate {
Button("horaire automatique") {
round.startDate = nil
}
.underline()
.buttonStyle(.borderless)
}
}
}
@ -36,6 +49,8 @@ struct RoundScheduleEditorView: View {
MatchScheduleEditorView(match: match)
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
}
private func _updateSchedule() {
@ -47,6 +62,7 @@ struct RoundScheduleEditorView: View {
_save()
MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate)
round.startDate = startDate
_save()
}

@ -0,0 +1,104 @@
//
// EditablePlayerView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 17/04/2024.
//
import SwiftUI
struct EditablePlayerView: View {
enum PlayerEditingOption {
case payment
case licenceId
}
@EnvironmentObject var dataStore: DataStore
var player: PlayerRegistration
var editingOptions: [PlayerEditingOption]
@State private var editedLicenceId = ""
@State private var shouldPresentLicenceIdEdition: Bool = false
var body: some View {
computedPlayerView(player)
.alert("Numéro de licence", isPresented: $shouldPresentLicenceIdEdition) {
TextField("Numéro de licence", text: $editedLicenceId)
.onSubmit {
player.licenceId = editedLicenceId
editedLicenceId = ""
try? dataStore.playerRegistrations.addOrUpdate(instance: player)
}
}
}
@ViewBuilder
func computedPlayerView(_ player: PlayerRegistration) -> some View {
VStack(alignment: .leading) {
ImportedPlayerView(player: player)
HStack {
Text(player.isImported() ? "importé" : "non importé")
Text(player.formattedLicense().isLicenseNumber ? "valide" : "non valide")
}
HStack {
Menu {
if let number = player.phoneNumber?.replacingOccurrences(of: " ", with: ""), let url = URL(string: "tel:\(number)") {
Link(destination: url) {
Label("Appeler", systemImage: "phone")
}
}
if let number = player.phoneNumber?.replacingOccurrences(of: " ", with: ""), let url = URL(string: "sms:\(number)") {
Link(destination: url) {
Label("SMS", systemImage: "message")
}
}
if editingOptions.contains(.licenceId) {
Divider()
if let licenseYearValidity = player.tournament()?.licenseYearValidity(), player.isValidLicenseNumber(year: licenseYearValidity) == false, player.licenceId != nil {
Button {
player.validateLicenceId(licenseYearValidity)
} label: {
Text("Valider la licence \(licenseYearValidity)")
}
}
}
if let license = player.licenceId?.strippedLicense {
Button {
let pasteboard = UIPasteboard.general
pasteboard.string = license
} label: {
Label("Copier la licence", systemImage: "doc.on.doc")
}
}
Section {
Button {
editedLicenceId = player.licenceId ?? ""
shouldPresentLicenceIdEdition = true
} label: {
if player.licenceId == nil {
Text("Ajouter la licence")
} else {
Text("Modifier la licence")
}
}
PasteButton(payloadType: String.self) { strings in
guard let first = strings.first else { return }
player.licenceId = first
}
} header: {
Text("Modification de licence")
}
} label: {
Text("Options")
}
if editingOptions.contains(.payment) {
Spacer()
PlayerPayView(player: player)
}
}
}
}
}

@ -0,0 +1,114 @@
//
// PlayerDetailView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 17/04/2024.
//
import SwiftUI
struct PlayerDetailView: View {
@Environment(Tournament.self) var tournament: Tournament
@EnvironmentObject var dataStore: DataStore
@Bindable var player: PlayerRegistration
@FocusState private var textFieldIsFocus: Bool
var body: some View {
Form {
Section {
LabeledContent {
TextField("Nom", text: $player.lastName)
.keyboardType(.alphabet)
.multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity)
} label: {
Text("Nom")
}
LabeledContent {
TextField("Prénom", text: $player.firstName)
.keyboardType(.alphabet)
.multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity)
} label: {
Text("Prénom")
}
PlayerSexPickerView(player: player)
}
Section {
LabeledContent {
TextField("Rang", value: $player.rank, format: .number)
.keyboardType(.decimalPad)
.multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity)
.focused($textFieldIsFocus)
} label: {
Text("Rang")
}
} header: {
Text("Classement actuel")
}
if player.isMalePlayer() == false && tournament.tournamentCategory == .men, let rank = player.rank {
Section {
let value = PlayerRegistration.addon(for: rank, manMax: tournament.maleUnrankedValue ?? 0, womanMax: tournament.femaleUnrankedValue ?? 0)
LabeledContent {
Text(value.formatted())
} label: {
Text("Valeur à rajouter")
}
LabeledContent {
TextField("Rang", value: $player.weight, format: .number)
.keyboardType(.decimalPad)
.multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity)
.focused($textFieldIsFocus)
} label: {
Text("Poids re-calculé")
}
} header: {
Text("Ré-assimilation")
} footer: {
Text("Calculé en fonction du sexe")
}
}
}
.scrollDismissesKeyboard(.immediately)
.onChange(of: player.sex) {
_save()
}
.onChange(of: player.weight) {
player.team()?.updateWeight()
_save()
}
.onChange(of: player.rank) {
player.setWeight(in: tournament)
player.team()?.updateWeight()
_save()
}
.headerProminence(.increased)
.navigationTitle("Édition")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.toolbar {
ToolbarItem(placement: .keyboard) {
Button("Valider") {
textFieldIsFocus = false
}
}
}
}
private func _save() {
try? dataStore.playerRegistrations.addOrUpdate(instance: player)
if let team = player.team() {
try? dataStore.teamRegistrations.addOrUpdate(instance: team)
}
}
}
#Preview {
PlayerDetailView(player: PlayerRegistration.mock())
}

@ -1,53 +0,0 @@
//
// LoserBracketView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 04/04/2024.
//
import SwiftUI
struct LoserBracketView: View {
@EnvironmentObject var dataStore: DataStore
let loserRounds: [Round]
@ViewBuilder
var body: some View {
if let first = loserRounds.first {
List {
ForEach(loserRounds) { loserRound in
_loserRoundView(loserRound)
let childLoserRounds = loserRound.loserRounds()
if childLoserRounds.isEmpty == false {
let uniqueChildRound = childLoserRounds.first
if childLoserRounds.count == 1, let uniqueChildRound {
_loserRoundView(uniqueChildRound)
} else if let uniqueChildRound {
NavigationLink {
LoserBracketView(loserRounds: childLoserRounds)
} label: {
Text(uniqueChildRound.roundTitle())
}
}
}
}
}
.navigationTitle(first.roundTitle())
}
}
private func _loserRoundView(_ loserRound: Round) -> some View {
Section {
ForEach(loserRound.playedMatches()) { match in
MatchRowView(match: match, matchViewStyle: .standardStyle)
}
} header: {
Text(loserRound.roundTitle())
}
}
}
#Preview {
LoserBracketView(loserRounds: [Round.mock()])
.environmentObject(DataStore.shared)
}

@ -0,0 +1,72 @@
//
// LoserRoundView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 04/04/2024.
//
import SwiftUI
struct LoserRoundView: View {
@EnvironmentObject var dataStore: DataStore
let loserRounds: [Round]
@State private var isEditingTournamentSeed: Bool = false
private func _roundDisabled() -> Bool {
loserRounds.allSatisfy({ $0.isDisabled() })
}
var body: some View {
List {
if isEditingTournamentSeed == true {
_editingView()
}
ForEach(loserRounds) { loserRound in
if isEditingTournamentSeed || loserRound.isDisabled() == false {
Section {
let matches = isEditingTournamentSeed ? loserRound.playedMatches() : loserRound.playedMatches().filter({ $0.disabled == false })
ForEach(matches) { match in
MatchRowView(match: match, matchViewStyle: .standardStyle)
.overlay {
if match.disabled /*&& isEditingTournamentSeed*/ {
Image(systemName: "xmark")
.resizable()
.scaledToFit()
.opacity(0.8)
}
}
.disabled(match.disabled)
}
} header: {
Text(loserRound.roundTitle(.wide))
}
}
}
}
.headerProminence(.increased)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button(isEditingTournamentSeed == true ? "Valider" : "Modifier") {
isEditingTournamentSeed.toggle()
}
}
}
}
private func _editingView() -> some View {
if _roundDisabled() {
RowButtonView("Jouer ce tour", role: .destructive) {
loserRounds.forEach { round in
round.enableRound()
}
}
} else {
RowButtonView("Ne pas jouer ce tour", role: .destructive) {
loserRounds.forEach { round in
round.disableRound()
}
}
}
}
}

@ -7,90 +7,81 @@
import SwiftUI
struct LoserRound: Identifiable, Selectable {
let turnIndex: Int
let rounds: [Round]
var id: Int {
return turnIndex
}
static func updateDestinations(fromLoserRounds loserRounds: [Round], inUpperBracketRound upperBracketRound: Round) -> [LoserRound] {
var rounds = [LoserRound]()
for (index, round) in loserRounds.enumerated() {
rounds.append(LoserRound(turnIndex: index, rounds: upperBracketRound.loserRounds(forRoundIndex: round.index)))
}
return rounds
}
static func enabledLoserRounds(inLoserRounds loserRounds: [Round], inUpperBracketRound upperBracketRound: Round) -> [Round] {
return loserRounds.filter { loserRound in
upperBracketRound.loserRounds(forRoundIndex: loserRound.index).anySatisfy({ $0.isDisabled() == false })
}
}
}
extension LoserRound {
func selectionLabel() -> String {
return "Tour #\(turnIndex + 1)"
}
func badgeValue() -> Int? {
return rounds.flatMap { $0.playedMatches() }.filter({ $0.isRunning() }).count
}
func badgeImage() -> Badge? {
return rounds.allSatisfy({ $0.hasEnded() }) ? .checkmark : nil
}
}
struct LoserRoundsView: View {
@Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed
var upperBracketRound: Round
@State private var selectedRound: Round?
@State private var selectedRound: LoserRound?
let loserRounds: [Round]
@State private var allDestinations: [LoserRound]
init(upperBracketRound: Round) {
self.upperBracketRound = upperBracketRound
self.loserRounds = upperBracketRound.loserRounds()
_selectedRound = State(wrappedValue: upperBracketRound.getActiveLoserRound())
let _loserRounds = upperBracketRound.loserRounds()
self.loserRounds = _loserRounds
let enabledLoserRounds = LoserRound.enabledLoserRounds(inLoserRounds: _loserRounds, inUpperBracketRound: upperBracketRound)
let rounds = LoserRound.updateDestinations(fromLoserRounds: enabledLoserRounds, inUpperBracketRound: upperBracketRound)
_allDestinations = State(wrappedValue: rounds)
_selectedRound = State(wrappedValue: rounds.first(where: { $0.rounds.anySatisfy({ $0.getActiveLoserRound() != nil }) }) ?? rounds.first)
}
var body: some View {
VStack(spacing: 0) {
GenericDestinationPickerView(selectedDestination: $selectedRound, destinations: loserRounds, nilDestinationIsValid: true)
switch selectedRound {
case .none:
List {
RowButtonView("Effacer", role: .destructive) {
}
}
case .some(let selectedRound):
LoserRoundView(loserRounds: upperBracketRound.loserRounds(forRoundIndex: selectedRound.index))
}
GenericDestinationPickerView(selectedDestination: $selectedRound, destinations: allDestinations, nilDestinationIsValid: false)
LoserRoundView(loserRounds: selectedRound!.rounds)
}
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
}
}
struct LoserRoundView: View {
@EnvironmentObject var dataStore: DataStore
let loserRounds: [Round]
@Environment(\.editMode) private var editMode
private func _roundDisabled() -> Bool {
loserRounds.allSatisfy({ $0.isDisabled() })
}
var body: some View {
List {
if editMode?.wrappedValue.isEditing == true {
_editingView()
}
ForEach(loserRounds) { loserRound in
Section {
ForEach(loserRound.playedMatches()) { match in
MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle)
.overlay {
if match.disabled {
Image(systemName: "xmark")
.resizable()
.scaledToFit()
.opacity(0.8)
}
}
.disabled(match.disabled)
}
} header: {
Text(loserRound.roundTitle(.wide))
}
}
}
.headerProminence(.increased)
.toolbar {
EditButton()
.onChange(of: isEditingTournamentSeed.wrappedValue) {
_updateDestinations()
}
}
private func _editingView() -> some View {
if _roundDisabled() {
RowButtonView("Jouer ce tour", role: .destructive) {
loserRounds.forEach { round in
round.enableRound()
round.handleLoserRoundState()
}
}
} else {
RowButtonView("Ne pas jouer ce tour", role: .destructive) {
loserRounds.forEach { round in
round.disableRound()
}
}
}
private func _updateDestinations() {
let enabledLoserRounds = isEditingTournamentSeed.wrappedValue ? loserRounds : LoserRound.enabledLoserRounds(inLoserRounds: loserRounds, inUpperBracketRound: upperBracketRound)
self.allDestinations = LoserRound.updateDestinations(fromLoserRounds: enabledLoserRounds, inUpperBracketRound: upperBracketRound)
}
}

@ -9,11 +9,21 @@ import SwiftUI
struct RoundSettingsView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(\.editMode) private var editMode
@Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed
@Environment(Tournament.self) var tournament: Tournament
var body: some View {
List {
Section {
RowButtonView("Enabled", role: .destructive) {
let allMatches = tournament._allMatchesIncludingDisabled()
allMatches.forEach({
$0.disabled = false
$0.byeState = false
})
try? dataStore.matches.addOrUpdate(contentOfs: allMatches)
}
}
Section {
RowButtonView("Retirer toutes les têtes de séries", role: .destructive) {
tournament.unsortedTeams().forEach({ $0.bracketPosition = nil })
@ -21,7 +31,7 @@ struct RoundSettingsView: View {
tournament.allRounds().forEach({ round in
round.enableRound()
})
editMode?.wrappedValue = .active
self.isEditingTournamentSeed.wrappedValue = true
}
}

@ -8,7 +8,7 @@
import SwiftUI
struct RoundView: View {
@Environment(\.editMode) private var editMode
@Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed
@Environment(Tournament.self) var tournament: Tournament
@EnvironmentObject var dataStore: DataStore
@ -17,16 +17,18 @@ struct RoundView: View {
var body: some View {
List {
if editMode?.wrappedValue.isEditing == false {
if isEditingTournamentSeed.wrappedValue == false {
let loserRounds = round.loserRounds()
if loserRounds.isEmpty == false, let first = loserRounds.first(where: { $0.isDisabled() == false }) {
//(where: { $0.isDisabled() == false || isEditingTournamentSeed.wrappedValue })
if loserRounds.isEmpty == false, let first = loserRounds.first {
let correspondingLoserRoundTitle = round.correspondingLoserRoundTitle()
Section {
NavigationLink {
LoserRoundsView(upperBracketRound: round)
.environment(tournament)
.navigationTitle(first.roundTitle())
.navigationTitle(correspondingLoserRoundTitle)
} label: {
Text(first.roundTitle())
Text(correspondingLoserRoundTitle)
}
}
}
@ -34,10 +36,9 @@ struct RoundView: View {
RowButtonView("Placer \(availableSeedGroup.localizedLabel())") {
tournament.setSeeds(inRoundIndex: round.index, inSeedGroup: availableSeedGroup)
try? dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.seeds())
if tournament.availableSeeds().isEmpty {
editMode?.wrappedValue = .inactive
_save()
self.isEditingTournamentSeed.wrappedValue = false
}
}
}
@ -53,10 +54,21 @@ struct RoundView: View {
.headerProminence(.increased)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
EditButton()
Button(isEditingTournamentSeed.wrappedValue == true ? "Valider" : "Modifier") {
if isEditingTournamentSeed.wrappedValue {
_save()
}
isEditingTournamentSeed.wrappedValue.toggle()
}
}
}
}
private func _save() {
try? dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.seeds())
let allRoundMatches = tournament.allRoundMatches()
try? DataStore.shared.matches.addOrUpdate(contentOfs: allRoundMatches)
}
}
#Preview {

@ -10,13 +10,13 @@ import SwiftUI
struct RoundsView: View {
var tournament: Tournament
@State private var selectedRound: Round?
@State var editMode: EditMode = .inactive
@State private var isEditingTournamentSeed = false
init(tournament: Tournament) {
self.tournament = tournament
_selectedRound = State(wrappedValue: tournament.getActiveRound())
if tournament.availableSeeds().isEmpty == false {
_editMode = .init(wrappedValue: .active)
_isEditingTournamentSeed = State(wrappedValue: true)
}
}
@ -32,7 +32,7 @@ struct RoundsView: View {
.navigationTitle(selectedRound.roundTitle())
}
}
.environment(\.editMode, $editMode)
.environment(\.isEditingTournamentSeed, $isEditingTournamentSeed)
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
}

@ -22,7 +22,10 @@ struct EditScoreView: View {
Form {
Section {
Text(matchDescriptor.teamLabelOne)
Text(matchDescriptor.teamLabelTwo)
HStack {
Spacer()
Text(matchDescriptor.teamLabelTwo).multilineTextAlignment(.trailing)
}
} footer: {
HStack {
Menu {
@ -37,7 +40,8 @@ struct EditScoreView: View {
Text(matchDescriptor.teamLabelTwo)
}
} label: {
Text("Forfait")
Text("Forfait d'une équipe ?")
.underline()
}
Spacer()

@ -13,8 +13,8 @@ struct PointSelectionView: View {
var possibleValues: [Int]
var disableValues: [Int] = []
var deleteAction: () -> ()
let gridItems: [GridItem] = [GridItem(.adaptive(minimum: 65), spacing: 20)]
let columns = Array(repeating: GridItem(.flexible()), count: 3)
init(valueSelected: Binding<Int?>, values: [Int], possibleValues: [Int], disableValues: [Int], deleteAction: @escaping () -> Void) {
_valueSelected = valueSelected
@ -26,12 +26,13 @@ struct PointSelectionView: View {
var body: some View {
LazyVGrid(columns: gridItems, alignment: .center, spacing: 20) {
LazyVGrid(columns: columns, alignment: .center, spacing: 8) {
ForEach(possibleValues, id: \.self) { value in
Button {
valueSelected = value
} label: {
PointView(value: "\(value).circle.fill")
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
@ -41,10 +42,11 @@ struct PointSelectionView: View {
deleteAction()
} label: {
PointView(value: "delete.left.fill")
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
}
.padding()
.padding(8)
}
}

@ -15,7 +15,7 @@ struct PointView: View {
.resizable()
.aspectRatio(contentMode: .fit)
.font(.largeTitle)
.frame(height: 40)
.frame(height: 36)
}
}

@ -124,12 +124,14 @@ struct SetInputView: View {
Section {
DisclosureGroup(isExpanded: $showSetInputView) {
PointSelectionView(valueSelected: currentValue, values: possibleValues(), possibleValues: setFormat.possibleValues, disableValues: disableValues, deleteAction: deleteLastValue)
.listRowInsets(EdgeInsets(top: -8, leading: -20, bottom: 0, trailing: 0))
} label: {
SetLabelView(initialValueLeft: $setDescriptor.valueTeamOne, initialValueRight: $setDescriptor.valueTeamTwo, shouldDisplaySteppers: isMainViewTieBreakView)
}
if showTieBreakView {
DisclosureGroup(isExpanded: $showTieBreakInputView) {
PointSelectionView(valueSelected: currentTiebreakValue, values: tieBreakPossibleValues(), possibleValues: SetFormat.six.possibleValues, disableValues: disableTieBreakValues, deleteAction: deleteLastTiebreakValue)
.listRowInsets(EdgeInsets(top: -8, leading: -20, bottom: 0, trailing: 0))
} label: {
SetLabelView(initialValueLeft: $setDescriptor.tieBreakValueTeamOne, initialValueRight: $setDescriptor.tieBreakValueTeamTwo, shouldDisplaySteppers: showTieBreakInputView, isTieBreak: true)
}

@ -10,6 +10,7 @@ import SwiftUI
struct ImportedPlayerView: View {
let player: PlayerHolder
var index: Int? = nil
var showFemaleInMaleAssimilation: Bool = false
var body: some View {
VStack(alignment: .leading) {
@ -39,7 +40,7 @@ struct ImportedPlayerView: View {
.font(.title3)
if let rank = player.getRank() {
Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated)
.font(.caption)
.font(.caption)
}
}
@ -58,9 +59,26 @@ struct ImportedPlayerView: View {
}
}
Text(player.formattedLicense())
.font(.caption)
if let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank(), showFemaleInMaleAssimilation {
HStack(alignment: .top, spacing: 2) {
Text("(")
Text(assimilatedAsMaleRank.formatted())
VStack(alignment: .leading, spacing: 0) {
Text("équivalence")
Text("messieurs")
}
.font(.caption)
}
Text(")").font(.title3)
}
HStack {
Text(player.formattedLicense())
if let computedAge = player.computedAge {
Text(computedAge.formatted() + " ans")
}
}
.font(.caption)
if let clubName = player.clubName {
Text(clubName)
.font(.caption)

@ -8,6 +8,7 @@
import SwiftUI
struct MatchFormatPickerView: View {
@Environment(Tournament.self) var tournament: Tournament
let headerLabel: String
@Binding var matchFormat: MatchFormat
@State private var isExpanded: Bool = false
@ -40,7 +41,7 @@ struct MatchFormatPickerView: View {
Text(matchFormat.format).font(.largeTitle)
Spacer()
VStack(alignment: .trailing) {
Text("~" + matchFormat.formattedEstimatedDuration())
Text("~" + matchFormat.formattedEstimatedDuration(tournament.additionalEstimationDuration))
Text(matchFormat.formattedEstimatedBreakDuration() + " de pause").foregroundStyle(.secondary).font(.subheadline)
}
}

@ -17,10 +17,15 @@ struct SelectablePlayerListView: View {
let allowSelection: Int
let playerSelectionAction: PlayerSelectionAction?
let contentUnavailableAction: ContentUnavailableAction?
@EnvironmentObject var dataStore: DataStore
@StateObject private var searchViewModel: SearchViewModel
@Environment(\.dismiss) var dismiss
@AppStorage("lastDataSource") var lastDataSource: String?
var lastDataSource: String? {
dataStore.appSettings.lastDataSource
}
@AppStorage("importingFiles") var importingFiles: Bool = false
@State private var searchText: String = ""
@ -29,12 +34,13 @@ struct SelectablePlayerListView: View {
return URL.importDateFormatter.date(from: lastDataSource)
}
init(allowSelection: Int = 0, searchField: String? = nil, user: User? = nil, dataSet: DataSet = .national, filterOption: PlayerFilterOption = .all, hideAssimilation: Bool = false, ascending: Bool = true, sortOption: SortOption = .rank, fromPlayer: FederalPlayer? = nil, codeClub: String? = nil, ligue: String? = nil, playerSelectionAction: PlayerSelectionAction? = nil, contentUnavailableAction: ContentUnavailableAction? = nil) {
init(allowSelection: Int = 0, searchField: String? = nil, user: User? = nil, dataSet: DataSet = .national, filterOption: PlayerFilterOption = .all, hideAssimilation: Bool = false, ascending: Bool = true, sortOption: SortOption = .rank, fromPlayer: FederalPlayer? = nil, codeClub: String? = nil, ligue: String? = nil, showFemaleInMaleAssimilation: Bool = false, playerSelectionAction: PlayerSelectionAction? = nil, contentUnavailableAction: ContentUnavailableAction? = nil) {
self.allowSelection = allowSelection
// self.searchText = searchField ?? ""
self.playerSelectionAction = playerSelectionAction
self.contentUnavailableAction = contentUnavailableAction
let searchViewModel = SearchViewModel()
searchViewModel.showFemaleInMaleAssimilation = showFemaleInMaleAssimilation
searchViewModel.searchText = searchField ?? ""
searchViewModel.isPresented = allowSelection != 0
searchViewModel.user = user
@ -287,7 +293,7 @@ struct MySearchView: View {
let array = Array(searchViewModel.selectedPlayers)
Section {
ForEach(array) { player in
ImportedPlayerView(player: player)
ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation)
}
.onDelete { indexSet in
for index in indexSet {
@ -302,7 +308,7 @@ struct MySearchView: View {
} else {
Section {
ForEach(players, id: \.self) { player in
ImportedPlayerView(player: player, index: nil)
ImportedPlayerView(player: player, index: nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation)
}
} header: {
if players.isEmpty == false {
@ -321,7 +327,7 @@ struct MySearchView: View {
Button {
searchViewModel.selectedPlayers.insert(player)
} label: {
ImportedPlayerView(player: player)
ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation)
}
.buttonStyle(.plain)
}
@ -334,7 +340,7 @@ struct MySearchView: View {
} else {
Section {
ForEach(players) { player in
ImportedPlayerView(player: player)
ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation)
}
} header: {
if players.isEmpty == false {
@ -351,13 +357,13 @@ struct MySearchView: View {
Button {
searchViewModel.selectedPlayers.insert(player)
} label: {
ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil)
ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation)
.contentShape(Rectangle())
}
.frame(maxWidth: .infinity)
.buttonStyle(.plain)
} else {
ImportedPlayerView(player: player)
ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation)
}
}
} header: {

@ -0,0 +1,43 @@
//
// TeamHeaderView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 18/04/2024.
//
import SwiftUI
struct TeamHeaderView: View {
var team: TeamRegistration
var teamIndex: Int?
var tournament: Tournament?
var body: some View {
_teamHeaderView(team, teamIndex: teamIndex)
}
private func _teamHeaderView(_ team: TeamRegistration, teamIndex: Int?) -> some View {
HStack {
if let teamIndex {
Text("#" + (teamIndex + 1).formatted())
}
if team.unsortedPlayers().isEmpty == false {
Text(team.weight.formatted())
}
if team.isWildCard() {
Text("wildcard").italic().font(.caption)
}
Spacer()
if team.walkOut {
Text("WO")
} else if let teamIndex, let tournament {
Text(tournament.cutLabel(index: teamIndex))
}
}
}
}
#Preview {
TeamHeaderView(team: TeamRegistration.mock(), teamIndex: 1, tournament: nil)
}

@ -0,0 +1,38 @@
//
// TeamWeightView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 18/04/2024.
//
import SwiftUI
struct TeamWeightView: View {
var team: TeamRegistration
var teamPosition: TeamPosition? = nil
var body: some View {
VStack(alignment: .trailing, spacing: 0) {
if teamPosition == .one || teamPosition == nil {
Text(team.weight.formatted())
.monospacedDigit()
.font(.caption)
}
if let teams = team.tournamentObject()?.selectedSortedTeams(), let index = team.index(in: teams) {
Text("#" + (index + 1).formatted(.number.precision(.integerLength(2...3))))
.monospacedDigit()
.font(.title)
}
if teamPosition == .two {
Text(team.weight.formatted())
.monospacedDigit()
.font(.caption)
}
}
}
}
#Preview {
TeamWeightView(team: TeamRegistration.mock(), teamPosition: .one)
}

@ -0,0 +1,45 @@
//
// EditingTeamView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 17/04/2024.
//
import SwiftUI
struct EditingTeamView: View {
@EnvironmentObject var dataStore: DataStore
var team: TeamRegistration
@State private var registrationDate : Date
init(team: TeamRegistration) {
self.team = team
_registrationDate = State(wrappedValue: team.registrationDate ?? Date())
}
var body: some View {
List {
Section {
DatePicker(registrationDate.formatted(.dateTime.weekday()), selection: $registrationDate)
} header: {
Text("Date d'inscription")
}
}
.onChange(of: registrationDate) {
team.registrationDate = registrationDate
_save()
}
.headerProminence(.increased)
.navigationTitle("Édition")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
}
private func _save() {
try? dataStore.teamRegistrations.addOrUpdate(instance: team)
}
}
#Preview {
EditingTeamView(team: TeamRegistration.mock())
}

@ -8,6 +8,7 @@
import SwiftUI
struct TeamDetailView: View {
@Environment(Tournament.self) var tournament: Tournament
@EnvironmentObject var dataStore: DataStore
var team: TeamRegistration
@ -16,7 +17,12 @@ struct TeamDetailView: View {
Text("Aucun joueur, espace réservé")
} else {
ForEach(team.players()) { player in
PlayerView(player: player)
NavigationLink {
PlayerDetailView(player: player)
.environment(tournament)
} label: {
PlayerView(player: player)
}
}
}
}

@ -23,7 +23,7 @@ struct TeamPickerView: View {
.sheet(isPresented: $presentTeamPickerView) {
NavigationStack {
List {
let teams = tournament.sortedTeams()
let teams = tournament.selectedSortedTeams()
if luckyLosers.isEmpty == false {
Section {
_teamListView(luckyLosers.sorted(by: \.weight))

@ -14,24 +14,10 @@ struct TeamRowView: View {
var body: some View {
LabeledContent {
VStack(alignment: .trailing, spacing: 0) {
if teamPosition == .one || teamPosition == nil {
Text(team.weight.formatted())
.font(.caption)
}
if let teams = team.tournamentObject()?.selectedSortedTeams(), let index = team.index(in: teams) {
Text("#" + (index + 1).formatted())
.font(.title)
}
if teamPosition == .two {
Text(team.weight.formatted())
.font(.caption)
}
}
TeamWeightView(team: team, teamPosition: teamPosition)
} label: {
Text(team.teamLabel(.short))
if let callDate = team.callDate {
if let callDate = team.callDate, displayCallDate {
Text("Déjà convoquée \(callDate.localizedDate())")
.foregroundStyle(.red)
.italic()

@ -40,6 +40,14 @@ struct FileImportView: View {
Label("beach-padel.app.fft.fr", systemImage: "tennisball")
}
Picker(selection: $fileProvider) {
ForEach(FileImportManager.FileProvider.allCases) {
Text($0.localizedLabel).tag($0)
}
} label: {
Text("Source du fichier")
}
Button {
convertingFile = false
isShowing.toggle()
@ -141,29 +149,16 @@ struct FileImportView: View {
}
}
Section {
ForEach(_filteredTeams) { team in
LabeledContent {
HStack {
if let previousTeam = team.previousTeam {
Text(previousTeam.formattedSeed(in: previousTeams))
Image(systemName: "arrowshape.forward.fill")
}
Text(team.formattedSeed(in: _filteredTeams))
}
} label: {
VStack(alignment: .leading) {
Text(team.playerOne.playerLabel())
Text(team.playerTwo.playerLabel())
}
}
}
} header: {
HStack {
Text("Équipe\(_filteredTeams.count.pluralSuffix) \(tournament.tournamentCategory.importingRawValue) détectée\(_filteredTeams.count.pluralSuffix)")
Spacer()
LabeledContent {
Text(_filteredTeams.count.formatted())
} label: {
Text("Équipe\(_filteredTeams.count.pluralSuffix) \(tournament.tournamentCategory.importingRawValue) détectée\(_filteredTeams.count.pluralSuffix)")
}
}
ForEach(_filteredTeams) { team in
_teamView(team: team, inTeams: _filteredTeams, previousTeams: previousTeams)
}
}
}
.onAppear {
@ -173,7 +168,7 @@ struct FileImportView: View {
}
}
}
.fileImporter(isPresented: $isShowing, allowedContentTypes: [.spreadsheet, .commaSeparatedText], allowsMultipleSelection: false, onCompletion: { results in
.fileImporter(isPresented: $isShowing, allowedContentTypes: [.spreadsheet, .commaSeparatedText, .text], allowsMultipleSelection: false, onCompletion: { results in
switch results {
case .success(let fileurls):
@ -272,6 +267,35 @@ struct FileImportView: View {
didImport = true
}
}
@ViewBuilder
private func _teamView(team: FileImportManager.TeamHolder, inTeams teams: [FileImportManager.TeamHolder], previousTeams: [TeamRegistration]) -> some View {
let newIndex = team.index(in: teams)
Section {
HStack {
VStack(alignment: .leading) {
ForEach(team.players.sorted(by: \.weight)) {
Text($0.playerLabel())
}
}
Spacer()
HStack {
if let previousTeam = team.previousTeam {
Text(previousTeam.formattedSeed(in: previousTeams))
Image(systemName: "arrowshape.forward.fill")
}
Text(team.formattedSeedIndex(index: newIndex))
}
}
if let callDate = team.previousTeam?.callDate, let newDate = tournament.getStartDate(ofSeedIndex: newIndex), callDate != newDate {
Text("Attention, cette paire a déjà été convoquée à \(callDate.localizedDate())")
.foregroundStyle(.red)
.italic()
.font(.caption)
}
}
}
private func _save() {
try? dataStore.tournaments.addOrUpdate(instance: tournament)

@ -1,41 +0,0 @@
//
// CashierDetailView.swift
// Padel Tournament
//
// Created by Razmig Sarkissian on 31/03/2024.
//
import SwiftUI
struct CashierDetailView: View {
var tournaments : [Tournament]
var body: some View {
List {
ForEach(tournaments) { tournament in
_tournamentCashierDetailView(tournament)
}
}
.headerProminence(.increased)
.navigationTitle("Résumé")
}
private func _tournamentCashierDetailView(_ tournament: Tournament) -> some View {
Section {
ForEach(PlayerRegistration.PaymentType.allCases) { type in
let count = tournament.selectedPlayers().filter({ $0.registrationType == type }).count
LabeledContent {
if let entryFee = tournament.entryFee {
let sum = Double(count) * entryFee
Text(sum.formatted(.currency(code: "EUR")))
}
} label: {
Text(type.localizedLabel())
Text(count.formatted())
}
}
} header: {
Text(tournament.tournamentTitle())
}
}
}

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

Loading…
Cancel
Save