merge conflict

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

@ -7,14 +7,48 @@
import Foundation import Foundation
import LeStorage import LeStorage
import SwiftUI
@Observable
class AppSettings: MicroStorable { class AppSettings: MicroStorable {
static var fileName: String { "appsettings.json" } 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() { 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,6 +32,8 @@ class Club : ModelObject, Storable, Hashable {
var zipCode: String? var zipCode: String?
var latitude: Double? var latitude: Double?
var longitude: 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) { 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.name = name
@ -45,12 +47,7 @@ class Club : ModelObject, Storable, Hashable {
self.longitude = longitude self.longitude = longitude
} }
var tournaments: [Tournament] {
return []
}
override func deleteDependencies() throws { override func deleteDependencies() throws {
try Store.main.deleteDependencies(items: self.tournaments)
} }
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
@ -64,6 +61,8 @@ class Club : ModelObject, Storable, Hashable {
case _zipCode = "zipCode" case _zipCode = "zipCode"
case _latitude = "latitude" case _latitude = "latitude"
case _longitude = "longitude" 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 playerRegistrations: StoredCollection<PlayerRegistration>
fileprivate(set) var rounds: StoredCollection<Round> fileprivate(set) var rounds: StoredCollection<Round>
fileprivate(set) var teamScores: StoredCollection<TeamScore> fileprivate(set) var teamScores: StoredCollection<TeamScore>
fileprivate(set) var monthData: StoredCollection<MonthData>
fileprivate var _userStorage: OptionalStorage<User> = OptionalStorage<User>(fileName: "user.json") 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? { var user: User? {
return self._userStorage.item return self._userStorage.item
@ -44,15 +65,17 @@ class DataStore: ObservableObject {
// store.addMigration(Migration<TournamentV1, TournamentV2>(version: 2)) // store.addMigration(Migration<TournamentV1, TournamentV2>(version: 2))
// store.addMigration(Migration<TournamentV2, Tournament>(version: 3)) // store.addMigration(Migration<TournamentV2, Tournament>(version: 3))
self.clubs = store.registerCollection(synchronized: false, indexed: true) let indexed : Bool = true
self.tournaments = store.registerCollection(synchronized: false, indexed: true) self.clubs = store.registerCollection(synchronized: false, indexed: indexed)
self.events = store.registerCollection(synchronized: false, indexed: true) self.tournaments = store.registerCollection(synchronized: false, indexed: indexed)
self.groupStages = store.registerCollection(synchronized: false, indexed: true) self.events = store.registerCollection(synchronized: false, indexed: indexed)
self.teamScores = store.registerCollection(synchronized: false, indexed: true) self.groupStages = store.registerCollection(synchronized: false, indexed: indexed)
self.teamRegistrations = store.registerCollection(synchronized: false, indexed: true) self.teamScores = store.registerCollection(synchronized: false, indexed: indexed)
self.playerRegistrations = store.registerCollection(synchronized: false, indexed: true) self.teamRegistrations = store.registerCollection(synchronized: false, indexed: indexed)
self.rounds = store.registerCollection(synchronized: false, indexed: true) self.playerRegistrations = store.registerCollection(synchronized: false, indexed: indexed)
self.matches = store.registerCollection(synchronized: false, indexed: true) 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.CollectionDidLoad, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(collectionWasUpdated), name: NSNotification.Name.CollectionDidChange, 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 groupStageFormat: Int?
var roundFormat: Int? var roundFormat: Int?
var loserRoundFormat: 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) { 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 self.club = club

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

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

@ -19,6 +19,7 @@ class GroupStage: ModelObject, Storable {
var size: Int var size: Int
var format: Int? var format: Int?
var startDate: Date? var startDate: Date?
var name: String?
var matchFormat: MatchFormat { var matchFormat: MatchFormat {
get { get {
@ -136,7 +137,8 @@ class GroupStage: ModelObject, Storable {
} }
func availableToStart() -> [Match] { 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] { func runningMatches() -> [Match] {
@ -208,6 +210,10 @@ class GroupStage: ModelObject, Storable {
Store.main.filter { $0.groupStage == self.id } Store.main.filter { $0.groupStage == self.id }
} }
func unsortedPlayers() -> [PlayerRegistration] {
unsortedTeams().flatMap({ $0.unsortedPlayers() })
}
fileprivate typealias TeamScoreAreInIncreasingOrder = (TeamGroupStageScore, TeamGroupStageScore) -> Bool fileprivate typealias TeamScoreAreInIncreasingOrder = (TeamGroupStageScore, TeamGroupStageScore) -> Bool
fileprivate typealias TeamGroupStageScore = (team: TeamRegistration, wins: Int, loses: Int, setDifference: Int, gameDifference: Int) 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] { func teams(_ sortedByScore: Bool = false) -> [TeamRegistration] {
let teams: [TeamRegistration] = Store.main.filter { $0.groupStage == self.id && $0.groupStagePosition != nil }
if sortedByScore { 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] = [ let predicates: [TeamScoreAreInIncreasingOrder] = [
{ $0.wins < $1.wins }, { $0.wins < $1.wins },
{ $0.setDifference < $1.setDifference }, { $0.setDifference < $1.setDifference },
@ -244,10 +253,19 @@ class GroupStage: ModelObject, Storable {
return false return false
}.map({ $0.team }).reversed() }.map({ $0.team }).reversed()
} else { } 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 { override func deleteDependencies() throws {
try Store.main.deleteDependencies(items: self._matches()) try Store.main.deleteDependencies(items: self._matches())
} }
@ -261,6 +279,7 @@ extension GroupStage {
case _size = "size" case _size = "size"
case _format = "format" case _format = "format"
case _startDate = "startDate" case _startDate = "startDate"
case _name = "name"
} }
} }
@ -272,4 +291,8 @@ extension GroupStage: Selectable {
func badgeValue() -> Int? { func badgeValue() -> Int? {
runningMatches().count runningMatches().count
} }
func badgeImage() -> Badge? {
hasEnded() ? .checkmark : nil
}
} }

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

@ -35,27 +35,19 @@ extension Tournament {
} }
static func newEmptyInstance() -> Tournament { static func newEmptyInstance() -> Tournament {
let lastDataSource: String? = UserDefaults.standard.string(forKey: "lastDataSource") let lastDataSource: String? = DataStore.shared.appSettings.lastDataSource
let lastDataSourceMaleUnranked: Int = UserDefaults.standard.integer(forKey: "lastDataSourceMaleUnranked")
let lastDataSourceFemaleUnranked: Int = UserDefaults.standard.integer(forKey: "lastDataSourceFemaleUnranked")
var _mostRecentDateAvailable: Date? { var _mostRecentDateAvailable: Date? {
guard let lastDataSource else { return nil } guard let lastDataSource else { return nil }
return URL.importDateFormatter.date(from: lastDataSource) return URL.importDateFormatter.date(from: lastDataSource)
} }
let maleUnrankedValue : Int? = lastDataSourceMaleUnranked == 0 ? nil : lastDataSourceMaleUnranked
let femaleUnrankedValue : Int? = lastDataSourceFemaleUnranked == 0 ? nil : lastDataSourceMaleUnranked
let rankSourceDate = _mostRecentDateAvailable 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 return Tournament(groupStageSortMode: .snake, rankSourceDate: rankSourceDate, teamSorting: tournamentLevel.defaultTeamSortingType, federalCategory: tournamentCategory, federalLevelCategory: tournamentLevel, federalAgeCategory: federalTournamentAge)
/*
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)
} }
} }

@ -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 { func pasteData() -> String {
[firstName.capitalized, lastName.capitalized, licenceId].compactMap({ $0 }).joined(separator: " ") [firstName.capitalized, lastName.capitalized, licenceId].compactMap({ $0 }).joined(separator: " ")
} }
@ -145,7 +165,11 @@ class PlayerRegistration: ModelObject, Storable {
func rankLabel(_ displayStyle: DisplayStyle = .wide) -> String { func rankLabel(_ displayStyle: DisplayStyle = .wide) -> String {
if let rank, rank > 0 { if let rank, rank > 0 {
if rank != weight {
return weight.formatted() + " (" + rank.formatted() + ")"
} else {
return rank.formatted() return rank.formatted()
}
} else { } else {
return "non classé" + (isMalePlayer() ? "" : "e") return "non classé" + (isMalePlayer() ? "" : "e")
} }
@ -319,7 +343,6 @@ class PlayerRegistration: ModelObject, Storable {
return 15000 return 15000
} }
} }
} }
extension PlayerRegistration: Hashable { extension PlayerRegistration: Hashable {
@ -333,6 +356,9 @@ extension PlayerRegistration: Hashable {
} }
extension PlayerRegistration: PlayerHolder { extension PlayerRegistration: PlayerHolder {
func getAssimilatedAsMaleRank() -> Int? {
nil
}
func getFirstName() -> String { func getFirstName() -> String {
firstName firstName

@ -92,26 +92,29 @@ class Round: ModelObject, Storable {
func seed(_ team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? { func seed(_ team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? {
return Store.main.filter(isIncluded: { return Store.main.filter(isIncluded: {
$0.tournament == tournament && $0.bracketPosition != nil $0.tournament == tournament
}).first(where: { && $0.bracketPosition != nil
($0.bracketPosition! / 2) == matchIndex && ($0.bracketPosition! / 2) == matchIndex
&& ($0.bracketPosition! % 2) == team.rawValue && ($0.bracketPosition! % 2) == team.rawValue
}) }).first
} }
func seeds(inMatchIndex matchIndex: Int) -> [TeamRegistration] { func seeds(inMatchIndex matchIndex: Int) -> [TeamRegistration] {
return Store.main.filter(isIncluded: { return Store.main.filter(isIncluded: {
$0.tournament == tournament && $0.bracketPosition != nil $0.tournament == tournament
}).filter({ && $0.bracketPosition != nil
($0.bracketPosition! / 2) == matchIndex && ($0.bracketPosition! / 2) == matchIndex
}) })
} }
func seeds() -> [TeamRegistration] { func seeds() -> [TeamRegistration] {
let initialMatchIndex = RoundRule.matchIndex(fromRoundIndex: index)
let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: index)
return Store.main.filter(isIncluded: { return Store.main.filter(isIncluded: {
$0.tournament == tournament && $0.bracketPosition != nil $0.tournament == tournament
}).filter({ && $0.bracketPosition != nil
($0.bracketPosition! / 2) >= RoundRule.matchIndex(fromRoundIndex: index) && ($0.bracketPosition! / 2) < RoundRule.matchIndex(fromRoundIndex: index) + RoundRule.numberOfMatches(forRoundIndex: index) && ($0.bracketPosition! / 2) >= initialMatchIndex
&& ($0.bracketPosition! / 2) < initialMatchIndex + numberOfMatches
}) })
} }
@ -209,7 +212,7 @@ class Round: ModelObject, Storable {
} }
func loserRounds(forRoundIndex roundIndex: Int) -> [Round] { 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 { func isDisabled() -> Bool {
@ -246,8 +249,8 @@ class Round: ModelObject, Storable {
} }
func getActiveLoserRound() -> Round? { func getActiveLoserRound() -> Round? {
let rounds = loserRounds() let rounds = loserRounds().filter({ $0.isDisabled() == false }).sorted(by: \.index).reversed()
return rounds.filter({ $0.hasStarted() && $0.hasEnded() == false && $0.isDisabled() == false }).sorted(by: \.index).reversed().first ?? rounds.first(where: { $0.isDisabled() == false }) return rounds.first(where: { $0.hasStarted() && $0.hasEnded() == false }) ?? rounds.first
} }
func enableRound() { func enableRound() {
@ -268,39 +271,6 @@ class Round: ModelObject, Storable {
try? DataStore.shared.matches.addOrUpdate(contentOfs: _matches) 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 cumulativeMatchCount: Int {
var totalMatches = playedMatches().count var totalMatches = playedMatches().count
if let parent = parentRound { if let parent = parentRound {
@ -317,13 +287,58 @@ class Round: ModelObject, Storable {
} }
} }
func roundTitle(_ displayStyle: DisplayStyle = .wide) -> String {
if let parentRound, let initialRound = parentRound.initialRound() { func disabledMatches() -> [Match] {
let parentMatchCount = parentRound.cumulativeMatchCount - initialRound.playedMatches().count _matches().filter({ $0.disabled })
// 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) 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 loser != nil {
return seedInterval()?.localizedLabel(displayStyle) ?? "Pas trouvé"
} }
return RoundRule.roundName(fromRoundIndex: index) return RoundRule.roundName(fromRoundIndex: index)
} }
@ -387,6 +402,15 @@ class Round: ModelObject, Storable {
return Store.main.findById(parentRound) 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 { override func deleteDependencies() throws {
try Store.main.deleteDependencies(items: _matches()) try Store.main.deleteDependencies(items: _matches())
try Store.main.deleteDependencies(items: loserRoundsAndChildren()) try Store.main.deleteDependencies(items: loserRoundsAndChildren())
@ -418,4 +442,8 @@ extension Round: Selectable {
return playedMatches().filter({ $0.isRunning() }).count 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 { func teamLabel(_ displayStyle: DisplayStyle = .wide) -> String {
switch displayStyle { switch displayStyle {
case .wide: case .wide:
unsortedPlayers().map { $0.playerLabel(displayStyle) }.joined(separator: " & ") players().map { $0.playerLabel(displayStyle) }.joined(separator: " & ")
case .short: 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) 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? { func tournamentObject() -> Tournament? {
Store.main.findById(tournament) Store.main.findById(tournament)
} }

@ -44,11 +44,14 @@ class Tournament : ModelObject, Storable {
var maleUnrankedValue: Int? var maleUnrankedValue: Int?
var femaleUnrankedValue: Int? var femaleUnrankedValue: Int?
var payment: TournamentPayment? = nil var payment: TournamentPayment? = nil
var additionalEstimationDuration: Int = 0
var courtsUnavailability: [Int: [DateInterval]]? = nil
@ObservationIgnored @ObservationIgnored
var navigationPath: [Screen] = [] 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.event = event
self.creator = creator self.creator = creator
self.name = name self.name = name
@ -77,8 +80,6 @@ class Tournament : ModelObject, Storable {
self.qualifiedPerGroupStage = qualifiedPerGroupStage self.qualifiedPerGroupStage = qualifiedPerGroupStage
self.teamsPerGroupStage = teamsPerGroupStage self.teamsPerGroupStage = teamsPerGroupStage
self.entryFee = entryFee self.entryFee = entryFee
self.maleUnrankedValue = maleUnrankedValue
self.femaleUnrankedValue = femaleUnrankedValue
self.teamSorting = teamSorting ?? federalLevelCategory.defaultTeamSortingType self.teamSorting = teamSorting ?? federalLevelCategory.defaultTeamSortingType
} }
@ -260,14 +261,18 @@ class Tournament : ModelObject, Storable {
let availableSeedSpot = availableSeedSpot(inRoundIndex: roundIndex) let availableSeedSpot = availableSeedSpot(inRoundIndex: roundIndex)
let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex) let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex)
if availableSeeds.count == availableSeedSpot.count { if availableSeeds.count == availableSeedSpot.count && availableSeedGroup.count == availableSeeds.count {
return availableSeedGroup 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 return availableSeedGroup
} else if let chunk = 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) return seedGroupAvailable(atRoundIndex: roundIndex, availableSeedGroup: chunk)
} }
} }
}
return nil return nil
} }
@ -300,11 +305,15 @@ class Tournament : ModelObject, Storable {
for (index, seed) in availableSeeds.enumerated() { for (index, seed) in availableSeeds.enumerated() {
seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: true) seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: true)
} }
} else if let chunk = seedGroup.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) setSeeds(inRoundIndex: roundIndex, inSeedGroup: chunk)
} }
} }
} }
}
func inscriptionClosed() -> Bool { func inscriptionClosed() -> Bool {
@ -320,17 +329,36 @@ class Tournament : ModelObject, Storable {
return groupStages.filter({ $0.hasStarted() && $0.hasEnded() == false }).sorted(by: \.index).first ?? groupStages.first 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() 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] { func allMatches() -> [Match] {
let unsortedGroupStages : [GroupStage] = Store.main.filter { $0.tournament == self.id } 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 }) 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] { func allRounds() -> [Round] {
Store.main.filter { $0.tournament == self.id } 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) _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]))) //print("func selectedSortedTeams", id, tournamentTitle(), duration.formatted(.units(allowed: [.seconds, .milliseconds])))
return _sortedTeams return _sortedTeams
} }
@ -448,7 +476,7 @@ class Tournament : ModelObject, Storable {
//todo //todo
var clubName: String? { var clubName: String? {
nil eventObject?.clubObject?.name
} }
//todo //todo
@ -493,7 +521,14 @@ class Tournament : ModelObject, Storable {
func playersWithoutValidLicense(in players: [PlayerRegistration]) -> [PlayerRegistration] { func playersWithoutValidLicense(in players: [PlayerRegistration]) -> [PlayerRegistration] {
let licenseYearValidity = licenseYearValidity() 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]) { func importTeams(_ teams: [FileImportManager.TeamHolder]) {
@ -503,7 +538,7 @@ class Tournament : ModelObject, Storable {
previousTeam.updatePlayers(team.players) previousTeam.updatePlayers(team.players)
teamsToImport.append(previousTeam) teamsToImport.append(previousTeam)
} else { } else {
let newTeam = addTeam(team.players) let newTeam = addTeam(team.players, registrationDate: team.registrationDate)
teamsToImport.append(newTeam) 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() { func lockRegistration() {
closedRegistrationDate = Date() closedRegistrationDate = Date()
let count = selectedSortedTeams().count let count = selectedSortedTeams().count
@ -541,9 +629,19 @@ class Tournament : ModelObject, Storable {
guard let newDate else { return } guard let newDate else { return }
rankSourceDate = newDate rankSourceDate = newDate
if currentMonthData() == nil {
let lastRankWoman = SourceFileManager.shared.getUnrankValue(forMale: false, rankSourceDate: rankSourceDate) let lastRankWoman = SourceFileManager.shared.getUnrankValue(forMale: false, rankSourceDate: rankSourceDate)
let lastRankMan = SourceFileManager.shared.getUnrankValue(forMale: true, 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 try await unsortedPlayers().concurrentForEach { player in
let dataURLs = SourceFileManager.shared.allFiles.filter({ $0.dateFromPath == newDate }) let dataURLs = SourceFileManager.shared.allFiles.filter({ $0.dateFromPath == newDate })
@ -551,11 +649,6 @@ class Tournament : ModelObject, Storable {
try await player.updateRank(from: sources, lastRank: (player.sex == 0 ? lastRankWoman : lastRankMan) ?? 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 { func missingUnrankedValue() -> Bool {
@ -629,16 +722,11 @@ class Tournament : ModelObject, Storable {
} }
func umpireMail() -> [String]? { func umpireMail() -> [String]? {
if let mail = UserDefaults.standard.string(forKey: "umpireMail"), mail.isEmpty == false { if let email = DataStore.shared.user?.email {
return [mail] return [email]
} else { } else {
return nil return nil
} }
// if let umpireMail = federalTournament?.courrielEngagement {
// return [umpireMail]
// } else {
// }
} }
func earnings() -> Double { func earnings() -> Double {
@ -655,19 +743,32 @@ class Tournament : ModelObject, Storable {
return Double(selectedPlayers.filter { $0.hasPaid() }.count) / Double(selectedPlayers.count) return Double(selectedPlayers.filter { $0.hasPaid() }.count) / Double(selectedPlayers.count)
} }
func cashierStatus() -> String { typealias TournamentStatus = (label:String, completion: String)
//todo func cashierStatus() -> TournamentStatus {
return "todo" 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 { func scheduleStatus() -> TournamentStatus {
//todo let allMatches = allMatches()
return "todo" 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 { func callStatus() -> TournamentStatus {
//todo let selectedSortedTeams = selectedSortedTeams()
return "todo" 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 { func bracketStatus() -> String {
@ -826,8 +927,12 @@ class Tournament : ModelObject, Storable {
entryFee == nil || entryFee == 0 entryFee == nil || entryFee == 0
} }
func addTeam(_ players: Set<PlayerRegistration>) -> TeamRegistration { func indexOf(team: TeamRegistration) -> Int? {
let team = TeamRegistration(tournament: id, registrationDate: Date()) 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.tournamentCategory = tournamentCategory
team.setWeight(from: Array(players)) team.setWeight(from: Array(players))
players.forEach { player in players.forEach { player in
@ -927,6 +1032,13 @@ class Tournament : ModelObject, Storable {
} }
} }
func setupFederalSettings() {
teamSorting = tournamentLevel.defaultTeamSortingType
groupStageMatchFormat = groupStageSmartMatchFormat()
loserBracketMatchFormat = loserBracketSmartMatchFormat(1)
matchFormat = roundSmartMatchFormat(1)
}
func roundSmartMatchFormat(_ roundIndex: Int) -> MatchFormat { func roundSmartMatchFormat(_ roundIndex: Int) -> MatchFormat {
let format = tournamentLevel.federalFormatForBracketRound(roundIndex) let format = tournamentLevel.federalFormatForBracketRound(roundIndex)
if matchFormat.rank > format.rank { 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.groupStages())
try Store.main.deleteDependencies(items: self.rounds()) 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 { extension Tournament {
@ -991,8 +1118,7 @@ extension Tournament {
case _qualifiedPerGroupStage = "qualifiedPerGroupStage" case _qualifiedPerGroupStage = "qualifiedPerGroupStage"
case _teamsPerGroupStage = "teamsPerGroupStage" case _teamsPerGroupStage = "teamsPerGroupStage"
case _entryFee = "entryFee" case _entryFee = "entryFee"
case _maleUnrankedValue = "maleUnrankedValue" case _additionalEstimationDuration = "additionalEstimationDuration"
case _femaleUnrankedValue = "femaleUnrankedValue"
} }
} }
@ -1036,6 +1162,4 @@ extension Tournament: TournamentBuildHolder {
var age: FederalTournamentAge { var age: FederalTournamentAge {
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 firstDayOfWeek = Calendar.current.firstWeekday
static var capitalizedFirstLettersOfWeekdays: [String] { static var capitalizedFirstLettersOfWeekdays: [String] {
let calendar = Calendar.current let calendar = Calendar.current
@ -180,9 +184,15 @@ extension Date {
var dayInt: Int { var dayInt: Int {
Calendar.current.component(.day, from: self) Calendar.current.component(.day, from: self)
} }
var startOfDay: Date { var startOfDay: Date {
Calendar.current.startOfDay(for: self) 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 { 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 Foundation
import SwiftUI import SwiftUI
import MessageUI import MessageUI
import LeStorage
enum ContactManagerError: LocalizedError { enum ContactManagerError: LocalizedError {
case mailFailed case mailFailed
@ -33,7 +34,7 @@ extension ContactType {
static let defaultSignature = "" static let defaultSignature = ""
static func callingGroupStageCustomMessage(tournament: Tournament?, startDate: Date?, roundLabel: String) -> String { 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 ?? "" let clubName = tournament?.clubName ?? ""
var text = tournamentCustomMessage 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: "#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: "#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) text = text.replacingOccurrences(of: "#signature", with: signature)
return text return text
@ -56,7 +57,7 @@ extension ContactType {
static func callingGroupStageMessage(tournament: Tournament?, startDate: Date?, roundLabel: String, matchFormat: MatchFormat?) -> String { 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 { if useFullCustomMessage {
return callingGroupStageCustomMessage(tournament: tournament, startDate: startDate, roundLabel: roundLabel) return callingGroupStageCustomMessage(tournament: tournament, startDate: startDate, roundLabel: roundLabel)
@ -65,17 +66,17 @@ extension ContactType {
let date = startDate ?? tournament?.startDate ?? Date() let date = startDate ?? tournament?.startDate ?? Date()
let clubName = tournament?.clubName ?? "" let clubName = tournament?.clubName ?? ""
let message = UserDefaults.standard.string(forKey: "customMessage") ?? defaultCustomMessage let message = DataStore.shared.appSettings.callMessageBody ?? defaultCustomMessage
let signature = UserDefaults.standard.string(forKey: "mySelf") ?? defaultSignature let signature = DataStore.shared.appSettings.callMessageSignature ?? defaultSignature
let localizedCalled = "convoqué" + (tournament?.tournamentCategory == .women ? "e" : "") + "s" let localizedCalled = "convoqué" + (tournament?.tournamentCategory == .women ? "e" : "") + "s"
var formatMessage: String? { var formatMessage: String? {
UserDefaults.standard.bool(forKey: "displayFormat") ? matchFormat?.computedLongLabel.appending(".") : nil (DataStore.shared.appSettings.callDisplayFormat ?? false) ? matchFormat?.computedLongLabel.appending(".") : nil
} }
var entryFeeMessage: String? { var entryFeeMessage: String? {
UserDefaults.standard.bool(forKey: "displayEntryFee") ? tournament?.entryFeeMessage : nil (DataStore.shared.appSettings.callDisplayEntryFee ?? false) ? tournament?.entryFeeMessage : nil
} }
var computedMessage: String { var computedMessage: String {

@ -44,10 +44,13 @@ class FileImportManager {
var id: Self { self } var id: Self { self }
case frenchFederation case frenchFederation
case padelClub
case unknown case unknown
var localizedLabel: String { var localizedLabel: String {
switch self { switch self {
case .padelClub:
return "Padel Club"
case .frenchFederation: case .frenchFederation:
return "FFT" return "FFT"
case .unknown: case .unknown:
@ -58,28 +61,32 @@ class FileImportManager {
struct TeamHolder: Identifiable { struct TeamHolder: Identifiable {
let id: UUID = UUID() let id: UUID = UUID()
let playerOne: PlayerRegistration let players: Set<PlayerRegistration>
let playerTwo: PlayerRegistration
let weight: Int let weight: Int
let tournamentCategory: TournamentCategory let tournamentCategory: TournamentCategory
let previousTeam: TeamRegistration? let previousTeam: TeamRegistration?
var registrationDate: Date? = nil
init(playerOne: PlayerRegistration, playerTwo: PlayerRegistration, tournamentCategory: TournamentCategory, previousTeam: TeamRegistration?) { init(players: [PlayerRegistration], tournamentCategory: TournamentCategory, previousTeam: TeamRegistration?, registrationDate: Date? = nil) {
self.playerOne = playerOne self.players = Set(players)
self.playerTwo = playerTwo
self.tournamentCategory = tournamentCategory self.tournamentCategory = tournamentCategory
self.previousTeam = previousTeam self.previousTeam = previousTeam
self.weight = playerOne.weight + playerTwo.weight self.weight = players.map { $0.weight }.reduce(0,+)
} self.registrationDate = registrationDate
var players: Set<PlayerRegistration> {
Set([playerOne, playerTwo])
} }
func index(in teams: [TeamHolder]) -> Int? { func index(in teams: [TeamHolder]) -> Int? {
teams.firstIndex(where: { $0.id == id }) 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 { func formattedSeed(in teams: [TeamHolder]) -> String {
if let index = index(in: teams) { if let index = index(in: teams) {
return "#\(index + 1)" return "#\(index + 1)"
@ -92,6 +99,60 @@ class FileImportManager {
static let FFT_ASSIMILATION_WOMAN_IN_MAN = "A calculer selon la pondération en vigueur" 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] { 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") let lines = fileContent.components(separatedBy: "\n")
guard let firstLine = lines.first else { return [] } guard let firstLine = lines.first else { return [] }
var separator = "," var separator = ","
@ -100,58 +161,7 @@ class FileImportManager {
} }
let headerCount = firstLine.components(separatedBy: separator).count let headerCount = firstLine.components(separatedBy: separator).count
var results: [TeamHolder] = [] var results: [TeamHolder] = []
if headerCount == 23 && fileProvider == .unknown { //PBL if headerCount <= 18 {
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 {
Array(lines.dropFirst()).chunked(into: 2).forEach { teamLines in Array(lines.dropFirst()).chunked(into: 2).forEach { teamLines in
if teamLines.count == 2 { if teamLines.count == 2 {
let dataOne = teamLines[0].replacingOccurrences(of: "\"", with: "").components(separatedBy: separator) let dataOne = teamLines[0].replacingOccurrences(of: "\"", with: "").components(separatedBy: separator)
@ -203,13 +213,13 @@ class FileImportManager {
playerOne.setWeight(in: tournament) playerOne.setWeight(in: tournament)
let playerTwo = PlayerRegistration(federalData: Array(resultTwo[0...7]), sex: sexPlayerTwo, sexUnknown: sexUnknown) let playerTwo = PlayerRegistration(federalData: Array(resultTwo[0...7]), sex: sexPlayerTwo, sexUnknown: sexUnknown)
playerTwo.setWeight(in: tournament) 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) results.append(team)
} }
} }
} }
return results return results
} else if headerCount > 18 && fileProvider == .frenchFederation { } else {
lines.dropFirst().forEach { line in lines.dropFirst().forEach { line in
let data = line.components(separatedBy: separator) let data = line.components(separatedBy: separator)
if data.count > 18 { if data.count > 18 {
@ -250,55 +260,107 @@ class FileImportManager {
let playerTwo = PlayerRegistration(federalData: Array(result[8...]), sex: sexPlayerTwo, sexUnknown: sexUnknown) let playerTwo = PlayerRegistration(federalData: Array(result[8...]), sex: sexPlayerTwo, sexUnknown: sexUnknown)
playerTwo.setWeight(in: tournament) 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) results.append(team)
} }
} }
return results return results
} else {
return []
} }
} }
func importDataFromFFT() async -> String? { private func _getPadelClubTeams(from fileContent: String, tournament: Tournament) async -> [TeamHolder] {
if let importingDate = SourceFileManager.shared.mostRecentDateAvailable { let lines = fileContent.components(separatedBy: "\n\n")
for source in SourceFile.allCases { var results: [TeamHolder] = []
for fileURL in source.currentURLs { let fetchRequest = ImportedPlayer.fetchRequest()
let p = readCSV(inputFile: fileURL) let federalContext = PersistenceController.shared.localContainer.viewContext
await importingChunkOfPlayers(p, importingDate: importingDate)
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
} }
return URL.importDateFormatter.string(from: importingDate) let team = TeamHolder(players: registeredPlayers, tournamentCategory: tournament.tournamentCategory, previousTeam: tournament.findTeam(registeredPlayers), registrationDate: registrationDate)
results.append(team)
} }
return nil
} }
return results
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] { private func _getPadelBusinessLeagueTeams(from fileContent: String, tournament: Tournament) async -> [TeamHolder] {
let lines = fileContent.components(separatedBy: "\n") let lines = fileContent.components(separatedBy: "\n")
return lines.compactMap { line in guard let firstLine = lines.first else { return [] }
if line.components(separatedBy: ";").count < 10 { var separator = ","
} else { if firstLine.contains(";") {
let data = line.components(separatedBy: ";").joined(separator: "\n") separator = ";"
return FederalPlayer(data, isMale: isMale)
} }
return nil 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)
// }
} }
} }
func importingChunkOfPlayers(_ players: [FederalPlayer], importingDate: Date) async {
for chunk in players.chunked(into: 1000) { return results
await PersistenceController.shared.batchInsertPlayers(chunk, importingDate: importingDate)
} }
return []
} }
} }

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

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

@ -55,4 +55,8 @@ enum AgendaDestination: CaseIterable, Identifiable, Selectable {
nil 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 teamOne = match?.team(.one)
let teamTwo = match?.team(.two) let teamTwo = match?.team(.two)
self.teamLabelOne = teamOne?.teamLabel() ?? "" self.teamLabelOne = teamOne?.teamLabel(.short) ?? ""
self.teamLabelTwo = teamTwo?.teamLabel() ?? "" self.teamLabelTwo = teamTwo?.teamLabel(.short) ?? ""
if let match, let scoresTeamOne = match.teamScore(ofTeam: teamOne)?.score, let scoresTeamTwo = match.teamScore(ofTeam: teamTwo)?.score { 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 { class MatchScheduler {
static let shared = MatchScheduler() static let shared = MatchScheduler()
var additionalEstimationDuration : Int = 0
var options: Set<MatchSchedulerOption> = Set(arrayLiteral: .accountUpperBracketBreakTime) var options: Set<MatchSchedulerOption> = Set(arrayLiteral: .accountUpperBracketBreakTime)
var timeDifferenceLimit: Double = 300.0 var timeDifferenceLimit: Double = 300.0
var loserBracketRotationDifference: Int = 0 var loserBracketRotationDifference: Int = 0
var upperBracketRotationDifference: Int = 1 var upperBracketRotationDifference: Int = 1
var courtsUnavailability: [Int: [DateInterval]]? = nil
func shouldHandleUpperRoundSlice() -> Bool { func shouldHandleUpperRoundSlice() -> Bool {
options.contains(.shouldHandleUpperRoundSlice) 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 { 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) let previousMatches = roundObject.precedentMatches(ofMatch: match)
if previousMatches.isEmpty { return true } if previousMatches.isEmpty { return true }
@ -254,7 +267,7 @@ class MatchScheduler {
let matchesByCourt = byCourt[court]?.sorted(by: \.startDate!) let matchesByCourt = byCourt[court]?.sorted(by: \.startDate!)
let lastMatch = matchesByCourt?.last let lastMatch = matchesByCourt?.last
var results = [(String, Date)]() var results = [(String, Date)]()
if let courtFreeDate = lastMatch?.estimatedEndDate() { if let courtFreeDate = lastMatch?.estimatedEndDate(additionalEstimationDuration) {
results.append((court, courtFreeDate)) results.append((court, courtFreeDate))
} }
return results return results
@ -276,7 +289,8 @@ class MatchScheduler {
_startDate = match.startDate _startDate = match.startDate
rotationIndex += 1 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) slots.append(timeMatch)
} }
@ -359,34 +373,56 @@ class MatchScheduler {
func dispatchCourts(availableCourts: Int, courts: [Int], availableMatchs: inout [Match], slots: inout [TimeMatch], rotationIndex: Int, rotationStartDate: Date, freeCourtPerRotation: inout [Int: [Int]]) { 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 matchPerRound = [Int: Int]()
var minimumTargetedEndDate: Date = rotationStartDate var minimumTargetedEndDate: Date = rotationStartDate
courts.forEach { courtIndex in print("dispatchCourts", courts.sorted(), rotationStartDate, rotationIndex)
//print(mt.map { ($0.bracket!.index.intValue, counts[$0.bracket!.index.intValue]) }) courts.sorted().forEach { courtIndex in
print("trying to find a match for \(courtIndex) in \(rotationIndex)")
if let first = availableMatchs.first(where: { match in if let first = availableMatchs.first(where: { match in
let roundObject = match.roundObject! 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 canBePlayed = roundMatchCanBePlayed(match, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate)
let currentRotationSameRoundMatches = matchPerRound[roundObject.index] ?? 0 let currentRotationSameRoundMatches = matchPerRound[roundObject.index] ?? 0
if shouldHandleUpperRoundSlice() { if shouldHandleUpperRoundSlice() {
let roundMatchesCount = roundObject.playedMatches().count let roundMatchesCount = roundObject.playedMatches().count
print("shouldHandleUpperRoundSlice \(roundMatchesCount)")
if roundObject.loser == nil && roundMatchesCount > courts.count { 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
} }
} }
}
let indexInRound = match.indexInRound()
if roundObject.loser == nil && roundObject.index > 0, match.indexInRound() == 0, courts.count > 1, let nextMatch = match.next() { 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) { 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 return true
} else { } else {
print("next match and this match can not be played at the same time, returning false")
return 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 { if currentRotationSameRoundMatches%2 == 0 && roundObject.index != 0 && roundObject.loser == nil && courtIndex == courts.count - 1 {
print("we return false")
return false return false
} }
return canBePlayed return canBePlayed
}) { }) {
print(first.roundObject!.roundTitle(), first.matchTitle(), courtIndex, rotationStartDate) print(first.roundObject!.roundTitle(), first.matchTitle(), courtIndex, rotationStartDate)
@ -398,7 +434,7 @@ class MatchScheduler {
matchPerRound[first.roundObject!.index] = 1 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) slots.append(timeMatch)
availableMatchs.removeAll(where: { $0.id == first.id }) availableMatchs.removeAll(where: { $0.id == first.id })
} else { } else {
@ -407,11 +443,17 @@ class MatchScheduler {
} }
if freeCourtPerRotation[rotationIndex]!.count == availableCourts { 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] = [] freeCourtPerRotation[rotationIndex] = []
let courtsUsed = getNextEarliestAvailableDate(from: slots) let courtsUsed = getNextEarliestAvailableDate(from: slots)
let freeCourts = courtsUsed.filter { (courtIndex, availableDate) in var freeCourts: [Int] = []
if courtsUsed.isEmpty {
freeCourts = (0..<availableCourts).map { $0 }
} else {
freeCourts = courtsUsed.filter { (courtIndex, availableDate) in
availableDate <= minimumTargetedEndDate availableDate <= minimumTargetedEndDate
}.sorted(by: \.1).map { $0.0 } }.sorted(by: \.1).map { $0.0 }
}
dispatchCourts(availableCourts: availableCourts, courts: freeCourts, availableMatchs: &availableMatchs, slots: &slots, rotationIndex: rotationIndex, rotationStartDate: minimumTargetedEndDate, freeCourtPerRotation: &freeCourtPerRotation) dispatchCourts(availableCourts: availableCourts, courts: freeCourts, availableMatchs: &availableMatchs, slots: &slots, rotationIndex: rotationIndex, rotationStartDate: minimumTargetedEndDate, freeCourtPerRotation: &freeCourtPerRotation)
} }
@ -422,8 +464,26 @@ class MatchScheduler {
let upperRounds = tournament.rounds() let upperRounds = tournament.rounds()
let allMatches = tournament.allMatches() let allMatches = tournament.allMatches()
// 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
// }
var roundIndex = 0
let rounds = upperRounds.map { let rounds = upperRounds.map {
$0 $0
@ -457,13 +517,15 @@ 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 let initialCourts = usedCourts.filter { (court, availableDate) in
availableDate <= startDate availableDate <= startDate
}.sorted(by: \.1).compactMap { tournament.getCourtIndex($0.0) } }.sorted(by: \.1).compactMap { tournament.getCourtIndex($0.0) }
let courts : [Int]? = initialCourts.isEmpty ? nil : initialCourts 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) let roundDispatch = self.roundDispatcher(numberOfCourtsAvailablePerRotation: tournament.courtCount, flattenedMatches: flattenedMatches, dispatcherStartDate: startDate, initialCourts: courts)
roundDispatch.timedMatches.forEach { matchSchedule in roundDispatch.timedMatches.forEach { matchSchedule in
@ -475,5 +537,21 @@ class MatchScheduler {
try? DataStore.shared.matches.addOrUpdate(contentOfs: allMatches) 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 @Observable
class NavigationViewModel { class NavigationViewModel {
var path = NavigationPath() var path = NavigationPath()
var selectedTab: TabDestination?
var agendaDestination: AgendaDestination? = .activity var agendaDestination: AgendaDestination? = .activity
var tournament: Tournament? var tournament: Tournament?
} }

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

@ -10,27 +10,36 @@ import Foundation
struct SeedInterval: Hashable, Comparable { struct SeedInterval: Hashable, Comparable {
let first: Int let first: Int
let last: Int let last: Int
var reduce: Int = 0
static func <(lhs: SeedInterval, rhs: SeedInterval) -> Bool { static func <(lhs: SeedInterval, rhs: SeedInterval) -> Bool {
return lhs.first < rhs.first return lhs.first < rhs.first
} }
func chunk() -> SeedInterval? { var count: Int {
if (last - first) / 2 > 0 { dimension
if last - (last - first) / 2 > first {
return SeedInterval(first: first, last: last - (last - first) / 2)
} }
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 { func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
if last - first < 2 { if dimension < 2 {
return "#\(first) / #\(last)" return "#\(first - reduce) / #\(last - reduce)"
} else { } else {
return "#\(first) à #\(last)" return "#\(first - reduce) à #\(last - reduce)"
} }
} }
} }

@ -6,8 +6,38 @@
// //
import Foundation import Foundation
import SwiftUI
protocol Selectable { protocol Selectable {
func selectionLabel() -> String func selectionLabel() -> String
func badgeValue() -> Int? 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 Foundation
import SwiftUI import SwiftUI
// Create an environment key
private struct TournamentSeedEditing: EnvironmentKey { private struct TournamentSeedEditing: EnvironmentKey {
static let defaultValue: Bool = false static let defaultValue: Binding<Bool> = .constant(false)
} }
// ## Introduce new value to EnvironmentValues
extension EnvironmentValues { extension EnvironmentValues {
var isEditingTournamentSeed: Bool { var isEditingTournamentSeed: Binding<Bool> {
get { self[TournamentSeedEditing.self] } get { self[TournamentSeedEditing.self] }
set { self[TournamentSeedEditing.self] = newValue } 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 { Section {
NavigationLink { NavigationLink {
CallMessageCustomizationView(tournament: tournament)
} label: { } label: {
Text("Modifier le message de convocation") Text("Personnaliser le message de convocation")
} }
} }
Section { 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() let teams = tournament.unsortedTeams()
teams.forEach { team in teams.forEach { team in
team.callDate = nil team.callDate = nil
@ -32,13 +39,7 @@ struct CallSettingsView: View {
} }
Section { Section {
RowButtonView("Envoyer un message à tout le monde") { RowButtonView("Tout le monde a été convoqué", role: .destructive) {
}
}
Section {
RowButtonView("Tout le monde a été convoqué") {
let teams = tournament.unsortedTeams() let teams = tournament.unsortedTeams()
teams.forEach { team in teams.forEach { team in
team.callDate = Date() team.callDate = Date()

@ -86,7 +86,11 @@ struct CallView: View {
let callWord = teams.allSatisfy({ $0.called() }) ? "Reconvoquer" : "Convoquer" let callWord = teams.allSatisfy({ $0.called() }) ? "Reconvoquer" : "Convoquer"
HStack { HStack {
if teams.count == 1 { if teams.count == 1 {
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") Text(callWord + " cette paire par")
}
} else { } else {
Text(callWord + " ces \(teams.count) paires par") Text(callWord + " ces \(teams.count) paires par")
} }

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

@ -15,7 +15,7 @@ struct SeedsCallingView: View {
List { List {
ForEach(tournament.rounds()) { round in ForEach(tournament.rounds()) { round in
let seeds = round.seeds() let seeds = round.seeds()
let callSeeds = seeds.filter({ $0.callDate != nil }) let callSeeds = seeds.filter({ tournament.isStartDateIsDifferentThanCallDate($0) == false })
if seeds.isEmpty == false { if seeds.isEmpty == false {
Section { Section {
NavigationLink { 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 beach = "BEACH"
case padel = "PADEL" case padel = "PADEL"
case tennis = "TENNIS" case tennis = "TENNIS"
case pickle = "PICKLE"
} }
// MARK: - ClubMarker // 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) .opacity(selectedDestination?.id == destination.id ? 1.0 : 0.4)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.overlay(alignment: .bottomTrailing) { // .overlay(alignment: .bottomTrailing) {
if let count = destination.badgeValue(), count > 0 { // if let badge = destination.badgeImage() {
Image(systemName: count <= 50 ? "\(count).circle.fill" : "plus.circle.fill") // Image(systemName: badge.systemName())
.foregroundColor(.secondary) // .foregroundColor(badge.color())
.imageScale(.medium) // .imageScale(.medium)
.background ( // .background (
Color(.systemBackground) // Color(.systemBackground)
.clipShape(.circle) // .clipShape(.circle)
) // )
.offset(x: 5, y: 5) // .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() .fixedSize()

@ -15,13 +15,13 @@ struct LabelOptions: View {
struct LabelStructure: View { struct LabelStructure: View {
var body: some View { var body: some View {
Label("Structure", systemImage: "hammer") Label("Structure", systemImage: "hammer").labelStyle(.titleOnly)
} }
} }
struct LabelSettings: View { struct LabelSettings: View {
var body: some 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 { var body: some View {
if matches.isEmpty == false { if matches.isEmpty == false {
Section { Section {
if isExpanded { DisclosureGroup(isExpanded: $isExpanded) {
ForEach(matches) { match in ForEach(matches) { match in
MatchRowView(match: match, matchViewStyle: matchViewStyle) MatchRowView(match: match, matchViewStyle: matchViewStyle)
.listRowInsets(EdgeInsets())
} }
}
} header: {
Button {
isExpanded.toggle()
} label: { } label: {
HStack { LabeledContent {
Text(section.capitalized) Text(matches.count.formatted() + " match" + matches.count.pluralSuffix)
Spacer() .foregroundStyle(.master)
Text(matches.count.formatted()) } label: {
Image(systemName: isExpanded ? "chevron.down.circle" : "chevron.right.circle") Text(section.firstCapitalized)
} }
.contentShape(Rectangle())
} }
.buttonStyle(.plain)
.frame(maxWidth: .infinity)
} }
.headerProminence(.increased)
} }
} }
} }

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

@ -13,12 +13,12 @@ struct StepperView: View {
var title: String? = nil var title: String? = nil
@Binding var count: Int @Binding var count: Int
var step: Int = 1
var minimum: Int? = nil var minimum: Int? = nil
var maximum: Int? = nil var maximum: Int? = nil
var body: some View { var body: some View {
VStack(spacing: 0) { VStack {
HStack(spacing: 8) { HStack(spacing: 8) {
Button(action: { Button(action: {
self._subtract() self._subtract()
@ -74,14 +74,14 @@ struct StepperView: View {
if let maximum, self.count + 1 > maximum { if let maximum, self.count + 1 > maximum {
return return
} }
self.count += 1 self.count += step
} }
fileprivate func _subtract() { fileprivate func _subtract() {
if let minimum, self.count - 1 < minimum { if let minimum, self.count - 1 < minimum {
return 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,14 +40,13 @@ struct EventCreationView: View {
} }
if eventType == .approvedTournament { if eventType == .approvedTournament {
Stepper(value: $duration, in: 1...3) { LabeledContent {
HStack { StepperView(count: $duration, minimum: 1, maximum: 3)
} label: {
Text("Durée") Text("Durée")
Spacer()
Text("\(duration) jour" + duration.pluralSuffix) Text("\(duration) jour" + duration.pluralSuffix)
} }
} }
}
NavigationLink { NavigationLink {
ClubsView() { club in ClubsView() { club in
@ -101,6 +100,7 @@ struct EventCreationView: View {
tournaments.forEach { tournament in tournaments.forEach { tournament in
tournament.startDate = startingDate tournament.startDate = startingDate
tournament.dayDuration = duration tournament.dayDuration = duration
tournament.setupFederalSettings()
} }
try? dataStore.tournaments.addOrUpdate(contentOfs: tournaments) try? dataStore.tournaments.addOrUpdate(contentOfs: tournaments)

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

@ -34,25 +34,32 @@ struct GroupStageView: View {
Section { Section {
_groupStageView() _groupStageView()
} header: { } header: {
HStack {
if let startDate = groupStage.startDate { if let startDate = groupStage.startDate {
Text(startDate.formatted(Date.FormatStyle().weekday(.wide)).capitalized + " à partir de " + startDate.formatted(.dateTime.hour().minute())) Text(startDate.formatted(Date.FormatStyle().weekday(.wide)).capitalized + " à partir de " + startDate.formatted(.dateTime.hour().minute()))
} }
} footer: {
HStack {
Spacer() Spacer()
Button { Button {
if sortingMode == .weight { if sortingMode == .auto {
if groupStage.hasEnded() {
sortingMode = .weight
} else {
sortingMode = .score sortingMode = .score
}
} else if sortingMode == .weight {
sortingMode = .weight
} else { } else {
sortingMode = .weight sortingMode = .weight
} }
} label: { } label: {
Label(sortByScore ? "tri par score" : "tri par poids", systemImage: "arrow.up.arrow.down").labelStyle(.titleOnly) 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: "disponible", matches: groupStage.availableToStart()).id(UUID())
MatchListView(section: "en cours", matches: groupStage.runningMatches()).id(UUID()) MatchListView(section: "en cours", matches: groupStage.runningMatches()).id(UUID())

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

@ -8,13 +8,15 @@
import SwiftUI import SwiftUI
struct MatchDateView: View { struct MatchDateView: View {
@Environment(Tournament.self) var tournament: Tournament
@EnvironmentObject var dataStore: DataStore
var match: Match var match: Match
var showPrefix: Bool = false var showPrefix: Bool = false
var body: some View { var body: some View {
Menu { Menu {
if match.startDate == nil { if match.startDate == nil && match.isReady() {
Button("Commencer") { Button("Démarrer") {
match.startDate = Date() match.startDate = Date()
save() save()
} }
@ -23,12 +25,21 @@ struct MatchDateView: View {
save() save()
} }
} else { } else {
Button("Recommencer") { if match.isReady() {
Button("Démarrer maintenant") {
match.startDate = Date() match.startDate = Date()
match.endDate = nil match.endDate = nil
save() save()
} }
Button("Remise à zéro") { } 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("Retirer l'horaire") {
match.startDate = nil match.startDate = nil
match.endDate = nil match.endDate = nil
save() save()
@ -50,8 +61,16 @@ struct MatchDateView: View {
if showPrefix { if showPrefix {
Text("en cours").font(.footnote).foregroundStyle(.secondary) Text("en cours").font(.footnote).foregroundStyle(.secondary)
} }
if match.isReady() {
Text(startDate, style: .timer) Text(startDate, style: .timer)
.monospacedDigit() .monospacedDigit()
.foregroundStyle(Color.master)
.underline()
} else {
Text("en retard")
.foregroundStyle(Color.master)
.underline()
}
} else if startDate.timeIntervalSinceNow <= 7200 && showPrefix { } else if startDate.timeIntervalSinceNow <= 7200 && showPrefix {
if showPrefix { if showPrefix {
Text("démarre dans") Text("démarre dans")
@ -59,15 +78,21 @@ struct MatchDateView: View {
} }
Text(startDate, style: .timer) Text(startDate, style: .timer)
.monospacedDigit() .monospacedDigit()
.foregroundStyle(Color.master)
.underline()
} else { } else {
if showPrefix { if showPrefix {
Text("le " + startDate.formatted(date: .abbreviated, time: .omitted)) Text("le " + startDate.formatted(date: .abbreviated, time: .omitted))
.font(.footnote).foregroundStyle(.secondary) .font(.footnote).foregroundStyle(.secondary)
Text("à " + startDate.formatted(date: .omitted, time: .shortened)) Text("à " + startDate.formatted(date: .omitted, time: .shortened))
.monospacedDigit() .monospacedDigit()
.foregroundStyle(Color.master)
.underline()
} else { } else {
Text(startDate.formatted(date: .abbreviated, time: .shortened)) Text(startDate.formatted(date: .abbreviated, time: .shortened))
.monospacedDigit() .monospacedDigit()
.foregroundStyle(Color.master)
.underline()
} }
} }
} }
@ -81,11 +106,15 @@ struct MatchDateView: View {
} }
Text(duration) Text(duration)
.monospacedDigit() .monospacedDigit()
.foregroundStyle(Color.master)
.underline()
} }
if match.startDate == nil && match.hasEnded() == false { if match.startDate == nil && match.hasEnded() == false {
Text("démarrage").font(.footnote).foregroundStyle(.secondary) Text("démarrage").font(.footnote).foregroundStyle(.secondary)
Text("non défini") Text("non défini")
.foregroundStyle(Color.master)
.underline()
} }
} }
} }
@ -94,9 +123,7 @@ struct MatchDateView: View {
func save() { func save() {
do { do {
// match.currentTournament?.objectWillChange.send() try dataStore.matches.addOrUpdate(instance: match)
// match.objectWillChange.send()
// try viewContext.save()
} catch { } catch {
// Replace this implementation with code to handle the error appropriately. // 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. // 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 { private func _defaultLabel() -> String {
if match.upperBracketMatch(teamPosition)?.disabled == true {
return "Bye"
}
return teamPosition.localizedLabel() return teamPosition.localizedLabel()
} }
@ -79,7 +76,7 @@ struct PlayerBlockView: View {
Text("WO") Text("WO")
} }
if hideScore == false { if hideScore == false && scores.isEmpty == false {
ForEach(scores.indices, id: \.self) { index in ForEach(scores.indices, id: \.self) { index in
let string = scores[index] let string = scores[index]
if string.isEmpty == false { if string.isEmpty == false {
@ -96,6 +93,8 @@ struct PlayerBlockView: View {
.lineLimit(1) .lineLimit(1)
} }
} }
} else if let team {
TeamWeightView(team: team, teamPosition: teamPosition)
} }
} }
} }

@ -49,58 +49,9 @@ struct MatchDetailView: View {
} }
} }
// @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 { var quickLookHeader: some View {
Section { Section {
HStack { HStack {
if match.hasEnded() == false {
Menu { Menu {
Button("Non défini") { Button("Non défini") {
match.removeCourt() match.removeCourt()
@ -117,17 +68,20 @@ struct MatchDetailView: View {
Text("terrain").font(.footnote).foregroundStyle(.secondary) Text("terrain").font(.footnote).foregroundStyle(.secondary)
if let court = match.court { if let court = match.court {
Text("#" + court) Text("#" + court)
.foregroundStyle(Color.master)
.underline()
} else { } else {
Text("Choisir") Text("Choisir")
.foregroundStyle(Color.master)
.underline()
} }
} }
} }
.buttonStyle(.plain)
}
Spacer() Spacer()
MatchDateView(match: match, showPrefix: true) MatchDateView(match: match, showPrefix: true)
} }
.font(.title) .font(.title)
.buttonStyle(.plain)
} footer: { } footer: {
// if match.hasWalkoutTeam() == false { // if match.hasWalkoutTeam() == false {
// if let weatherData = match.weatherData { // if let weatherData = match.weatherData {
@ -151,7 +105,6 @@ struct MatchDetailView: View {
Section { Section {
MatchSummaryView(match: match, matchViewStyle: .plainStyle) MatchSummaryView(match: match, matchViewStyle: .plainStyle)
} header: {
} footer: { } footer: {
if match.isEmpty() == false { if match.isEmpty() == false {
HStack { HStack {
@ -171,34 +124,34 @@ struct MatchDetailView: View {
} }
} }
let players = match.teams().flatMap { $0.players() }
let unpaid = players.filter({ $0.hasPaid() == false })
if unpaid.isEmpty == false {
Section { Section {
ForEach(match.teams()) { team in DisclosureGroup {
ForEach(team.players().filter({ $0.hasPaid() == false })) { player in ForEach(unpaid) { player in
HStack { LabeledContent {
PlayerPayView(player: player)
} label: {
Text(player.playerLabel()) Text(player.playerLabel())
Spacer() }
//PlayerPayView(player: player) }
} label: {
LabeledContent {
Text(unpaid.count.formatted() + " / " + players.count.formatted())
} label: {
Text("Encaissement manquant")
} }
} }
} }
} }
menuView menuView
} }
// .sheet(isPresented: $showDetails) { .sheet(isPresented: $showDetails) {
// NavigationStack { MatchTeamDetailView(match: match)
// List { }
// if let entrantOne = match.entrantOne() {
// entrantView(entrantOne)
// }
// if let entrantTwo = match.entrantTwo() {
// entrantView(entrantTwo)
// }
// }
// }
// .presentationDetents([.fraction(0.66)])
// }
.sheet(item: $scoreType, onDismiss: { .sheet(item: $scoreType, onDismiss: {
if match.hasEnded() { if match.hasEnded() {
dismiss() dismiss()
@ -206,6 +159,7 @@ struct MatchDetailView: View {
}) { scoreType in }) { scoreType in
let matchDescriptor = MatchDescriptor(match: match) let matchDescriptor = MatchDescriptor(match: match)
EditScoreView(matchDescriptor: matchDescriptor) EditScoreView(matchDescriptor: matchDescriptor)
.tint(.master)
// switch scoreType { // switch scoreType {
// case .edition: // case .edition:
@ -305,7 +259,8 @@ struct MatchDetailView: View {
// } // }
// } // }
.navigationTitle(match.matchTitle()) .navigationTitle(match.matchTitle())
.navigationBarTitleDisplayMode(.large) .navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
} }
enum ScoreType: Int, Identifiable, Hashable { enum ScoreType: Int, Identifiable, Hashable {
@ -365,14 +320,15 @@ struct MatchDetailView: View {
Section { Section {
if match.hasEnded() == false { if match.hasEnded() == false {
let rotationDuration = match.getDuration()
Picker(selection: $startDateSetup) { Picker(selection: $startDateSetup) {
if match.isReady() { if match.isReady() {
Text("Dans 5 minutes").tag(MatchDateSetup.inMinutes(5)) Text("Dans 5 minutes").tag(MatchDateSetup.inMinutes(5))
Text("Dans 15 minutes").tag(MatchDateSetup.inMinutes(15)) Text("Dans 15 minutes").tag(MatchDateSetup.inMinutes(15))
Text("Tout de suite").tag(MatchDateSetup.now) Text("Tout de suite").tag(MatchDateSetup.now)
} }
Text("Précédente rotation").tag(MatchDateSetup.inMinutes(-match.matchFormat.estimatedDuration)) Text("Précédente rotation").tag(MatchDateSetup.inMinutes(-rotationDuration))
Text("Prochaine rotation").tag(MatchDateSetup.inMinutes(match.matchFormat.estimatedDuration)) Text("Prochaine rotation").tag(MatchDateSetup.inMinutes(rotationDuration))
Text("À").tag(MatchDateSetup.customDate) Text("À").tag(MatchDateSetup.customDate)
} label: { } label: {
Text("Horaire") Text("Horaire")

@ -10,13 +10,49 @@ import SwiftUI
struct MatchRowView: View { struct MatchRowView: View {
var match: Match var match: Match
let matchViewStyle: MatchViewStyle let matchViewStyle: MatchViewStyle
@Environment(\.editMode) private var editMode @Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed
@ViewBuilder @ViewBuilder
var body: some View { 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) MatchSetupView(match: match)
} else { } 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 { NavigationLink {
MatchDetailView(match: match, matchViewStyle: matchViewStyle) MatchDetailView(match: match, matchViewStyle: matchViewStyle)
} label: { } label: {

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

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

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

@ -13,7 +13,12 @@ struct PadelClubView: View {
@State private var checkingFiles: Bool = false @State private var checkingFiles: Bool = false
@State private var importingFiles: 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 @Environment(\.managedObjectContext) private var viewContext
@FetchRequest( @FetchRequest(
@ -36,17 +41,38 @@ struct PadelClubView: View {
List { List {
if let _lastDataSourceDate { if let _lastDataSourceDate {
Section { Section {
HStack { LabeledContent {
VStack(alignment: .leading) {
Text("Classement mensuel utilisé").font(.caption).foregroundStyle(.secondary)
Text(_lastDataSourceDate.monthYearFormatted)
}
Spacer()
Image(systemName: "checkmark") 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 { // if players.isEmpty {
// ContentUnavailableView { // ContentUnavailableView {
@ -60,6 +86,7 @@ struct PadelClubView: View {
// } // }
// } // }
} }
.headerProminence(.increased)
.navigationTitle(TabDestination.padelClub.title) .navigationTitle(TabDestination.padelClub.title)
// .task { // .task {
// await self._checkSourceFileAvailability() // await self._checkSourceFileAvailability()
@ -101,7 +128,12 @@ struct PadelClubView: View {
private func _startImporting() { private func _startImporting() {
importingFiles = true importingFiles = true
Task { 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 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 { var body: some View {
NavigationStack { NavigationStack {
List { List {
Section {
NavigationLink { NavigationLink {
SelectablePlayerListView() SelectablePlayerListView()
} label: { } label: {
Label("Rechercher un joueur", systemImage: "person.fill.viewfinder") Label("Rechercher un joueur", systemImage: "person.fill.viewfinder")
} }
}
Section {
NavigationLink { NavigationLink {
RankCalculatorView() RankCalculatorView()
} label: { } label: {
Label("Calculateur de points", systemImage: "scalemass") 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) .navigationTitle(TabDestination.toolbox.title)
} }

@ -64,7 +64,6 @@ struct UmpireView: View {
user.licenceId = nil user.licenceId = nil
dataStore.setUser(user) dataStore.setUser(user)
} }
.font(.caption)
} }
} }
@ -83,7 +82,6 @@ struct UmpireView: View {
user.club = nil user.club = nil
dataStore.setUser(user) 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 { struct GroupStageScheduleEditorView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
var groupStage: GroupStage 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 { var body: some View {
@Bindable var groupStage = groupStage @Bindable var groupStage = groupStage
List { List {
Section { Section {
MatchFormatPickerView(headerLabel: "Format", matchFormat: $groupStage.matchFormat) MatchFormatPickerView(headerLabel: "Format", matchFormat: $groupStage.matchFormat)
DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline)
} }
.onChange(of: startDate) {
Section { dateUpdated = true
Text("Modifier l'horaire") }
} header: {
Text(groupStage.groupStageTitle())
} footer: {
DateUpdateManagerView(startDate: $startDate) {
groupStage.startDate = startDate
_save()
} }
RowButtonView("Convoquer") {
} }
NavigationLink { NavigationLink {
@ -35,10 +48,15 @@ struct GroupStageScheduleEditorView: View {
.onChange(of: groupStage.matchFormat) { .onChange(of: groupStage.matchFormat) {
_save() _save()
} }
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
} }
private func _save() { 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) try? dataStore.groupStages.addOrUpdate(instance: groupStage)
} }
} }

@ -6,74 +6,6 @@
// //
import SwiftUI 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 { struct LoserRoundScheduleEditorView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@ -97,13 +29,14 @@ struct LoserRoundScheduleEditorView: View {
Section { Section {
MatchFormatPickerView(headerLabel: "Format", matchFormat: $matchFormat) MatchFormatPickerView(headerLabel: "Format", matchFormat: $matchFormat)
DatePicker(selection: $startDate) { DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday())) Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline)
}
RowButtonView("Valider la modification") {
_updateSchedule()
} }
} header: { } header: {
Text("Classement " + upperRound.roundTitle()) Text("Classement " + upperRound.roundTitle())
} footer: {
DateUpdateManagerView(startDate: $startDate) {
_updateSchedule()
}
} }
@ -132,6 +65,8 @@ struct LoserRoundScheduleEditorView: View {
MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: upperRound.loserRounds().first?.id, fromMatchId: nil, startDate: startDate) MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: upperRound.loserRounds().first?.id, fromMatchId: nil, startDate: startDate)
_save() _save()
upperRound.loserRounds().first?.startDate = startDate
} }
private func _save() { 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 { var body: some View {
Section { Section {
DatePicker(selection: $startDate) { DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday())) Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline)
}
RowButtonView("Valider la modification") {
_updateSchedule()
} }
} header: { } header: {
if let round = match.roundObject { if let round = match.roundObject {
@ -31,6 +28,10 @@ struct MatchScheduleEditorView: View {
} else { } else {
Text(match.matchTitle()) Text(match.matchTitle())
} }
} footer: {
DateUpdateManagerView(startDate: $startDate) {
_updateSchedule()
}
} }
.headerProminence(.increased) .headerProminence(.increased)
} }

@ -10,7 +10,6 @@ import SwiftUI
struct PlanningSettingsView: View { struct PlanningSettingsView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
var tournament: Tournament var tournament: Tournament
@State private var scheduleSetup: Bool = false
@State private var randomCourtDistribution: Bool @State private var randomCourtDistribution: Bool
@State private var groupStageCourtCount: Int @State private var groupStageCourtCount: Int
@State private var upperBracketBreakTime: Bool @State private var upperBracketBreakTime: Bool
@ -20,6 +19,8 @@ struct PlanningSettingsView: View {
@State private var upperBracketRotationDifference: Int @State private var upperBracketRotationDifference: Int
@State private var timeDifferenceLimit: Double @State private var timeDifferenceLimit: Double
@State private var shouldHandleUpperRoundSlice: Bool @State private var shouldHandleUpperRoundSlice: Bool
@State private var isScheduling: Bool = false
@State private var schedulingDone: Bool = false
init(tournament: Tournament) { init(tournament: Tournament) {
self.tournament = tournament self.tournament = tournament
@ -39,13 +40,12 @@ struct PlanningSettingsView: View {
List { List {
Section { Section {
DatePicker(tournament.startDate.formatted(.dateTime.weekday()), selection: $tournament.startDate) DatePicker(tournament.startDate.formatted(.dateTime.weekday()), selection: $tournament.startDate)
Stepper(value: $tournament.dayDuration, in: 1...1_000) { LabeledContent {
HStack { StepperView(count: $tournament.dayDuration, minimum: 1, maximum: 1_000)
} label: {
Text("Durée") Text("Durée")
Spacer()
Text("\(tournament.dayDuration) jour" + tournament.dayDuration.pluralSuffix) Text("\(tournament.dayDuration) jour" + tournament.dayDuration.pluralSuffix)
} }
}
} header: { } header: {
Text("Démarrage et durée du tournoi") Text("Démarrage et durée du tournoi")
} footer: { } footer: {
@ -53,17 +53,35 @@ struct PlanningSettingsView: View {
} }
Section { Section {
TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount) TournamentFieldsManagerView(localizedStringKey: "Terrains maximum", count: $tournament.courtCount, max: 100)
if tournament.groupStages().isEmpty == false { if tournament.groupStages().isEmpty == false {
TournamentFieldsManagerView(localizedStringKey: "Terrains par poule", count: $groupStageCourtCount) TournamentFieldsManagerView(localizedStringKey: "Terrains par poule", count: $groupStageCourtCount, max: tournament.maximumCourtsPerGroupSage())
} }
NavigationLink { NavigationLink {
CourtAvailabilitySettingsView()
.environment(tournament)
} label: { } 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 { Section {
@ -103,25 +121,35 @@ struct PlanningSettingsView: View {
.disabled(rotationDifferenceIsImportant == false) .disabled(rotationDifferenceIsImportant == false)
//timeDifferenceLimit //timeDifferenceLimit
RowButtonView("Horaire intelligent", role: .destructive) { RowButtonView("Horaire intelligent", role: .destructive) {
_setupSchedule() schedulingDone = false
} await _setupSchedule()
schedulingDone = true
if scheduleSetup {
HStack {
Image(systemName: "checkmark")
}
} }
} }
Section { Section {
NavigationLink { RowButtonView("Supprimer tous les horaires", role: .destructive) {
let allMatches = tournament.allMatches()
allMatches.forEach({ $0.startDate = nil })
try? dataStore.matches.addOrUpdate(contentOfs: allMatches)
} label: { let allGroupStages = tournament.groupStages()
Text("Modifier le message de convocation") 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) { .onChange(of: groupStageCourtCount) {
tournament.groupStageCourtCount = groupStageCourtCount tournament.groupStageCourtCount = groupStageCourtCount
@ -141,11 +169,12 @@ struct PlanningSettingsView: View {
} }
} }
private func _setupSchedule() { private func _setupSchedule() async {
let groupStageCourtCount = tournament.groupStageCourtCount ?? 1 let groupStageCourtCount = tournament.groupStageCourtCount ?? 1
let groupStages = tournament.groupStages() let groupStages = tournament.groupStages()
let numberOfCourtsAvailablePerRotation: Int = tournament.courtCount let numberOfCourtsAvailablePerRotation: Int = tournament.courtCount
let matchScheduler = MatchScheduler.shared let matchScheduler = MatchScheduler.shared
matchScheduler.courtsUnavailability = tournament.courtsUnavailability
matchScheduler.options.removeAll() matchScheduler.options.removeAll()
if randomCourtDistribution { if randomCourtDistribution {
@ -175,13 +204,6 @@ struct PlanningSettingsView: View {
let matches = tournament.groupStages().flatMap({ $0._matches() }) let matches = tournament.groupStages().flatMap({ $0._matches() })
matches.forEach({ $0.startDate = nil }) 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 var lastDate : Date = tournament.startDate
groupStages.chunked(into: groupStageCourtCount).forEach { groups in groupStages.chunked(into: groupStageCourtCount).forEach { groups in
groups.forEach({ $0.startDate = lastDate }) groups.forEach({ $0.startDate = lastDate })
@ -191,11 +213,12 @@ struct PlanningSettingsView: View {
dispatch.timedMatches.forEach { matchSchedule in dispatch.timedMatches.forEach { matchSchedule in
if let match = matches.first(where: { $0.id == matchSchedule.matchID }) { if let match = matches.first(where: { $0.id == matchSchedule.matchID }) {
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 { if let startDate = match.groupStageObject?.startDate {
let matchStartDate = startDate.addingTimeInterval(timeIntervalToAdd) let matchStartDate = startDate.addingTimeInterval(timeIntervalToAdd)
match.startDate = matchStartDate match.startDate = matchStartDate
lastDate = matchStartDate.addingTimeInterval(Double(match.matchFormat.estimatedDuration) * 60) lastDate = matchStartDate.addingTimeInterval(Double(estimatedDuration) * 60)
} }
match.setCourt(matchSchedule.courtIndex + 1) match.setCourt(matchSchedule.courtIndex + 1)
} }
@ -204,9 +227,6 @@ struct PlanningSettingsView: View {
try? dataStore.matches.addOrUpdate(contentOfs: matches) try? dataStore.matches.addOrUpdate(contentOfs: matches)
matchScheduler.updateSchedule(tournament: tournament, fromRoundId: nil, fromMatchId: nil, startDate: lastDate) matchScheduler.updateSchedule(tournament: tournament, fromRoundId: nil, fromMatchId: nil, startDate: lastDate)
scheduleSetup = true
} }
private func _save() { private func _save() {

@ -9,7 +9,7 @@ import SwiftUI
struct PlanningView: View { struct PlanningView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(\.editMode) private var editMode @Environment(Tournament.self) var tournament: Tournament
let matches: [Match] let matches: [Match]
@State private var timeSlots: [Date:[Match]] @State private var timeSlots: [Date:[Match]]
@ -30,63 +30,6 @@ struct PlanningView: View {
Section { Section {
ForEach(keys.filter({ $0.dayInt == day.dayInt }), id: \.self) { key in ForEach(keys.filter({ $0.dayInt == day.dayInt }), id: \.self) { key in
if let _matches = timeSlots[key] { 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) {
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())
}
}
}
}
} else {
DisclosureGroup { DisclosureGroup {
ForEach(_matches) { match in ForEach(_matches) { match in
NavigationLink { NavigationLink {
@ -111,36 +54,15 @@ struct PlanningView: View {
} }
} }
} }
}
} header: { } header: {
Text(day.formatted(.dateTime.day().weekday().month().year())) Text(day.formatted(.dateTime.day().weekday().month().year()))
} }
.headerProminence(.increased) .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") .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 { private func _timeSlotView(key: Date, matches: [Match]) -> some View {
LabeledContent { LabeledContent {
Text(matches.count.formatted() + " match" + matches.count.pluralSuffix) Text(matches.count.formatted() + " match" + matches.count.pluralSuffix)

@ -25,17 +25,32 @@ struct RoundScheduleEditorView: View {
Section { Section {
MatchFormatPickerView(headerLabel: "Format", matchFormat: $round.matchFormat) MatchFormatPickerView(headerLabel: "Format", matchFormat: $round.matchFormat)
DatePicker(selection: $startDate) { DatePicker(selection: $startDate) {
Text(startDate.formatted(.dateTime.weekday())) Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline)
} }
RowButtonView("Valider la modification") { } footer: {
HStack {
DateUpdateManagerView(startDate: $startDate) {
_updateSchedule() _updateSchedule()
} }
Spacer()
if let roundStartDate = round.startDate {
Button("horaire automatique") {
round.startDate = nil
}
.underline()
.buttonStyle(.borderless)
}
}
} }
ForEach(round.playedMatches()) { match in ForEach(round.playedMatches()) { match in
MatchScheduleEditorView(match: match) MatchScheduleEditorView(match: match)
} }
} }
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
} }
private func _updateSchedule() { private func _updateSchedule() {
@ -47,6 +62,7 @@ struct RoundScheduleEditorView: View {
_save() _save()
MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate) MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate)
round.startDate = startDate
_save() _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 import SwiftUI
struct LoserRoundsView: View { struct LoserRound: Identifiable, Selectable {
var upperBracketRound: Round let turnIndex: Int
@State private var selectedRound: Round? let rounds: [Round]
let loserRounds: [Round]
init(upperBracketRound: Round) { var id: Int {
self.upperBracketRound = upperBracketRound return turnIndex
self.loserRounds = upperBracketRound.loserRounds()
_selectedRound = State(wrappedValue: upperBracketRound.getActiveLoserRound())
} }
var body: some View {
VStack(spacing: 0) {
GenericDestinationPickerView(selectedDestination: $selectedRound, destinations: loserRounds, nilDestinationIsValid: true)
switch selectedRound {
case .none:
List {
RowButtonView("Effacer", role: .destructive) {
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
} }
case .some(let selectedRound):
LoserRoundView(loserRounds: upperBracketRound.loserRounds(forRoundIndex: selectedRound.index)) static func enabledLoserRounds(inLoserRounds loserRounds: [Round], inUpperBracketRound upperBracketRound: Round) -> [Round] {
return loserRounds.filter { loserRound in
upperBracketRound.loserRounds(forRoundIndex: loserRound.index).anySatisfy({ $0.isDisabled() == false })
} }
} }
.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 { extension LoserRound {
List { func selectionLabel() -> String {
if editMode?.wrappedValue.isEditing == true { return "Tour #\(turnIndex + 1)"
_editingView()
} }
ForEach(loserRounds) { loserRound in func badgeValue() -> Int? {
Section { return rounds.flatMap { $0.playedMatches() }.filter({ $0.isRunning() }).count
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()
}
} }
private func _editingView() -> some View { func badgeImage() -> Badge? {
if _roundDisabled() { return rounds.allSatisfy({ $0.hasEnded() }) ? .checkmark : nil
RowButtonView("Jouer ce tour", role: .destructive) {
loserRounds.forEach { round in
round.enableRound()
round.handleLoserRoundState()
} }
}
struct LoserRoundsView: View {
@Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed
var upperBracketRound: Round
@State private var selectedRound: LoserRound?
let loserRounds: [Round]
@State private var allDestinations: [LoserRound]
init(upperBracketRound: Round) {
self.upperBracketRound = upperBracketRound
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)
} }
} else {
RowButtonView("Ne pas jouer ce tour", role: .destructive) { var body: some View {
loserRounds.forEach { round in VStack(spacing: 0) {
round.disableRound() GenericDestinationPickerView(selectedDestination: $selectedRound, destinations: allDestinations, nilDestinationIsValid: false)
LoserRoundView(loserRounds: selectedRound!.rounds)
} }
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.onChange(of: isEditingTournamentSeed.wrappedValue) {
_updateDestinations()
} }
} }
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 { struct RoundSettingsView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(\.editMode) private var editMode @Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed
@Environment(Tournament.self) var tournament: Tournament @Environment(Tournament.self) var tournament: Tournament
var body: some View { var body: some View {
List { 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 { Section {
RowButtonView("Retirer toutes les têtes de séries", role: .destructive) { RowButtonView("Retirer toutes les têtes de séries", role: .destructive) {
tournament.unsortedTeams().forEach({ $0.bracketPosition = nil }) tournament.unsortedTeams().forEach({ $0.bracketPosition = nil })
@ -21,7 +31,7 @@ struct RoundSettingsView: View {
tournament.allRounds().forEach({ round in tournament.allRounds().forEach({ round in
round.enableRound() round.enableRound()
}) })
editMode?.wrappedValue = .active self.isEditingTournamentSeed.wrappedValue = true
} }
} }

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

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

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

@ -13,7 +13,7 @@ struct PointSelectionView: View {
var possibleValues: [Int] var possibleValues: [Int]
var disableValues: [Int] = [] var disableValues: [Int] = []
var deleteAction: () -> () 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) { init(valueSelected: Binding<Int?>, values: [Int], possibleValues: [Int], disableValues: [Int], deleteAction: @escaping () -> Void) {
@ -26,12 +26,13 @@ struct PointSelectionView: View {
var body: some 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 ForEach(possibleValues, id: \.self) { value in
Button { Button {
valueSelected = value valueSelected = value
} label: { } label: {
PointView(value: "\(value).circle.fill") PointView(value: "\(value).circle.fill")
.frame(maxWidth: .infinity)
} }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
.controlSize(.large) .controlSize(.large)
@ -41,10 +42,11 @@ struct PointSelectionView: View {
deleteAction() deleteAction()
} label: { } label: {
PointView(value: "delete.left.fill") PointView(value: "delete.left.fill")
.frame(maxWidth: .infinity)
} }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
.controlSize(.large) .controlSize(.large)
} }
.padding() .padding(8)
} }
} }

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

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

@ -10,6 +10,7 @@ import SwiftUI
struct ImportedPlayerView: View { struct ImportedPlayerView: View {
let player: PlayerHolder let player: PlayerHolder
var index: Int? = nil var index: Int? = nil
var showFemaleInMaleAssimilation: Bool = false
var body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
@ -58,9 +59,26 @@ struct ImportedPlayerView: View {
} }
} }
Text(player.formattedLicense()) 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) .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 { if let clubName = player.clubName {
Text(clubName) Text(clubName)
.font(.caption) .font(.caption)

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

@ -18,9 +18,14 @@ struct SelectablePlayerListView: View {
let playerSelectionAction: PlayerSelectionAction? let playerSelectionAction: PlayerSelectionAction?
let contentUnavailableAction: ContentUnavailableAction? let contentUnavailableAction: ContentUnavailableAction?
@EnvironmentObject var dataStore: DataStore
@StateObject private var searchViewModel: SearchViewModel @StateObject private var searchViewModel: SearchViewModel
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
@AppStorage("lastDataSource") var lastDataSource: String?
var lastDataSource: String? {
dataStore.appSettings.lastDataSource
}
@AppStorage("importingFiles") var importingFiles: Bool = false @AppStorage("importingFiles") var importingFiles: Bool = false
@State private var searchText: String = "" @State private var searchText: String = ""
@ -29,12 +34,13 @@ struct SelectablePlayerListView: View {
return URL.importDateFormatter.date(from: lastDataSource) 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.allowSelection = allowSelection
// self.searchText = searchField ?? "" // self.searchText = searchField ?? ""
self.playerSelectionAction = playerSelectionAction self.playerSelectionAction = playerSelectionAction
self.contentUnavailableAction = contentUnavailableAction self.contentUnavailableAction = contentUnavailableAction
let searchViewModel = SearchViewModel() let searchViewModel = SearchViewModel()
searchViewModel.showFemaleInMaleAssimilation = showFemaleInMaleAssimilation
searchViewModel.searchText = searchField ?? "" searchViewModel.searchText = searchField ?? ""
searchViewModel.isPresented = allowSelection != 0 searchViewModel.isPresented = allowSelection != 0
searchViewModel.user = user searchViewModel.user = user
@ -287,7 +293,7 @@ struct MySearchView: View {
let array = Array(searchViewModel.selectedPlayers) let array = Array(searchViewModel.selectedPlayers)
Section { Section {
ForEach(array) { player in ForEach(array) { player in
ImportedPlayerView(player: player) ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation)
} }
.onDelete { indexSet in .onDelete { indexSet in
for index in indexSet { for index in indexSet {
@ -302,7 +308,7 @@ struct MySearchView: View {
} else { } else {
Section { Section {
ForEach(players, id: \.self) { player in ForEach(players, id: \.self) { player in
ImportedPlayerView(player: player, index: nil) ImportedPlayerView(player: player, index: nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation)
} }
} header: { } header: {
if players.isEmpty == false { if players.isEmpty == false {
@ -321,7 +327,7 @@ struct MySearchView: View {
Button { Button {
searchViewModel.selectedPlayers.insert(player) searchViewModel.selectedPlayers.insert(player)
} label: { } label: {
ImportedPlayerView(player: player) ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
} }
@ -334,7 +340,7 @@ struct MySearchView: View {
} else { } else {
Section { Section {
ForEach(players) { player in ForEach(players) { player in
ImportedPlayerView(player: player) ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation)
} }
} header: { } header: {
if players.isEmpty == false { if players.isEmpty == false {
@ -351,13 +357,13 @@ struct MySearchView: View {
Button { Button {
searchViewModel.selectedPlayers.insert(player) searchViewModel.selectedPlayers.insert(player)
} label: { } label: {
ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil) ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation)
.contentShape(Rectangle()) .contentShape(Rectangle())
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.buttonStyle(.plain) .buttonStyle(.plain)
} else { } else {
ImportedPlayerView(player: player) ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation)
} }
} }
} header: { } 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 import SwiftUI
struct TeamDetailView: View { struct TeamDetailView: View {
@Environment(Tournament.self) var tournament: Tournament
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
var team: TeamRegistration var team: TeamRegistration
@ -16,10 +17,15 @@ struct TeamDetailView: View {
Text("Aucun joueur, espace réservé") Text("Aucun joueur, espace réservé")
} else { } else {
ForEach(team.players()) { player in ForEach(team.players()) { player in
NavigationLink {
PlayerDetailView(player: player)
.environment(tournament)
} label: {
PlayerView(player: player) PlayerView(player: player)
} }
} }
} }
}
} }
#Preview { #Preview {

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

@ -14,24 +14,10 @@ struct TeamRowView: View {
var body: some View { var body: some View {
LabeledContent { LabeledContent {
VStack(alignment: .trailing, spacing: 0) { TeamWeightView(team: team, teamPosition: teamPosition)
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)
}
}
} label: { } label: {
Text(team.teamLabel(.short)) Text(team.teamLabel(.short))
if let callDate = team.callDate { if let callDate = team.callDate, displayCallDate {
Text("Déjà convoquée \(callDate.localizedDate())") Text("Déjà convoquée \(callDate.localizedDate())")
.foregroundStyle(.red) .foregroundStyle(.red)
.italic() .italic()

@ -40,6 +40,14 @@ struct FileImportView: View {
Label("beach-padel.app.fft.fr", systemImage: "tennisball") 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 { Button {
convertingFile = false convertingFile = false
isShowing.toggle() isShowing.toggle()
@ -141,29 +149,16 @@ struct FileImportView: View {
} }
} }
Section { Section {
ForEach(_filteredTeams) { team in
LabeledContent { LabeledContent {
HStack { Text(_filteredTeams.count.formatted())
if let previousTeam = team.previousTeam {
Text(previousTeam.formattedSeed(in: previousTeams))
Image(systemName: "arrowshape.forward.fill")
}
Text(team.formattedSeed(in: _filteredTeams))
}
} label: { } 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)") Text("Équipe\(_filteredTeams.count.pluralSuffix) \(tournament.tournamentCategory.importingRawValue) détectée\(_filteredTeams.count.pluralSuffix)")
Spacer()
Text(_filteredTeams.count.formatted())
} }
} }
ForEach(_filteredTeams) { team in
_teamView(team: team, inTeams: _filteredTeams, previousTeams: previousTeams)
}
} }
} }
.onAppear { .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 { switch results {
case .success(let fileurls): case .success(let fileurls):
@ -273,6 +268,35 @@ struct FileImportView: View {
} }
} }
@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() { private func _save() {
try? dataStore.tournaments.addOrUpdate(instance: tournament) 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