diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index 5cc3b3c..6fbc30a 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -25,6 +25,12 @@ C45BAE3B2BC6DF10002EEC8A /* SyncedProducts.storekit in Resources */ = {isa = PBXBuildFile; fileRef = C45BAE3A2BC6DF10002EEC8A /* SyncedProducts.storekit */; }; C45BAE442BCA753E002EEC8A /* Purchase.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45BAE432BCA753E002EEC8A /* Purchase.swift */; }; C4607A7D2C04DDE2004CB781 /* APICallsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4607A7C2C04DDE2004CB781 /* APICallsListView.swift */; }; + C471D1542D0C8FED0068091F /* Drawlog.json in Resources */ = {isa = PBXBuildFile; fileRef = C471D1532D0C8FE80068091F /* Drawlog.json */; }; + C471D1552D0C8FED0068091F /* Drawlog.json in Resources */ = {isa = PBXBuildFile; fileRef = C471D1532D0C8FE80068091F /* Drawlog.json */; }; + C471D1562D0C8FED0068091F /* Drawlog.json in Resources */ = {isa = PBXBuildFile; fileRef = C471D1532D0C8FE80068091F /* Drawlog.json */; }; + C471D1582D0C91FE0068091F /* BaseDrawLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = C471D1572D0C91FE0068091F /* BaseDrawLog.swift */; }; + C471D1592D0C91FE0068091F /* BaseDrawLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = C471D1572D0C91FE0068091F /* BaseDrawLog.swift */; }; + C471D15A2D0C91FF0068091F /* BaseDrawLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = C471D1572D0C91FE0068091F /* BaseDrawLog.swift */; }; C488C7E92CC7D16F0082001F /* generator.py in Resources */ = {isa = PBXBuildFile; fileRef = C488C7E52CC7D1660082001F /* generator.py */; }; C488C7EA2CC7D16F0082001F /* generator.py in Resources */ = {isa = PBXBuildFile; fileRef = C488C7E52CC7D1660082001F /* generator.py */; }; C488C7EB2CC7D16F0082001F /* generator.py in Resources */ = {isa = PBXBuildFile; fileRef = C488C7E52CC7D1660082001F /* generator.py */; }; @@ -181,8 +187,20 @@ FF1162832BCFBE4E000C4809 /* EditablePlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162822BCFBE4E000C4809 /* EditablePlayerView.swift */; }; FF1162852BD00279000C4809 /* PlayerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162842BD00279000C4809 /* PlayerDetailView.swift */; }; FF1162872BD004AD000C4809 /* EditingTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162862BD004AD000C4809 /* EditingTeamView.swift */; }; - FF11628A2BD05247000C4809 /* DateUpdateManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */; }; + FF11628A2BD05247000C4809 /* DatePickingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DatePickingView.swift */; }; FF11628C2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */; }; + FF17CA492CB915A1003C7323 /* MultiCourtPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA482CB915A1003C7323 /* MultiCourtPickerView.swift */; }; + FF17CA4A2CB915A1003C7323 /* MultiCourtPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA482CB915A1003C7323 /* MultiCourtPickerView.swift */; }; + FF17CA4B2CB915A1003C7323 /* MultiCourtPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA482CB915A1003C7323 /* MultiCourtPickerView.swift */; }; + FF17CA4D2CB9243E003C7323 /* FollowUpMatchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA4C2CB9243E003C7323 /* FollowUpMatchView.swift */; }; + FF17CA4E2CB9243E003C7323 /* FollowUpMatchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA4C2CB9243E003C7323 /* FollowUpMatchView.swift */; }; + FF17CA4F2CB9243E003C7323 /* FollowUpMatchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA4C2CB9243E003C7323 /* FollowUpMatchView.swift */; }; + FF17CA532CBE4788003C7323 /* BracketCallingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA522CBE4788003C7323 /* BracketCallingView.swift */; }; + FF17CA542CBE4788003C7323 /* BracketCallingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA522CBE4788003C7323 /* BracketCallingView.swift */; }; + FF17CA552CBE4788003C7323 /* BracketCallingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA522CBE4788003C7323 /* BracketCallingView.swift */; }; + FF17CA572CC02FEA003C7323 /* CoachListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA562CC02FEA003C7323 /* CoachListView.swift */; }; + FF17CA582CC02FEB003C7323 /* CoachListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA562CC02FEA003C7323 /* CoachListView.swift */; }; + FF17CA592CC02FEB003C7323 /* CoachListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA562CC02FEA003C7323 /* CoachListView.swift */; }; FF1CBC1B2BB53D1F0036DAAB /* FederalTournament.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */; }; FF1CBC1D2BB53DC10036DAAB /* Calendar+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */; }; FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */; }; @@ -384,8 +402,8 @@ FF4CBFE02C996C0600151637 /* TournamentFieldsManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26462BAE0ACB00650388 /* TournamentFieldsManagerView.swift */; }; FF4CBFE12C996C0600151637 /* PrintSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1F4B812BFA0124000B4573 /* PrintSettingsView.swift */; }; FF4CBFE22C996C0600151637 /* TournamentMatchFormatsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE22BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift */; }; - FF4CBFE32C996C0600151637 /* DateUpdateManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */; }; - FF4CBFE42C996C0600151637 /* MatchTypeSmallSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0192BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift */; }; + FF4CBFE32C996C0600151637 /* DatePickingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DatePickingView.swift */; }; + FF4CBFE42C996C0600151637 /* MatchFormatRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0192BBC5A8500B82851 /* MatchFormatRowView.swift */; }; FF4CBFE52C996C0600151637 /* MonthData.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE82BD1307E00A86CF8 /* MonthData.swift */; }; FF4CBFE62C996C0600151637 /* MenuWarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFEF7F4D2BDE69130033D0F0 /* MenuWarningView.swift */; }; FF4CBFE72C996C0600151637 /* TournamentBuildView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1F4B6C2BF9E60B000B4573 /* TournamentBuildView.swift */; }; @@ -441,7 +459,7 @@ FF4CC0192C996C0600151637 /* EventSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF41852BF75FDA001B24CB /* EventSettingsView.swift */; }; FF4CC01A2C996C0600151637 /* InscriptionInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5D0D772BB42C5B005CB568 /* InscriptionInfoView.swift */; }; FF4CC01B2C996C0600151637 /* SelectablePlayerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */; }; - FF4CC01C2C996C0600151637 /* MatchFormatPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26502BAE0BAD00650388 /* MatchFormatPickerView.swift */; }; + FF4CC01C2C996C0600151637 /* MatchFormatSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26502BAE0BAD00650388 /* MatchFormatSelectionView.swift */; }; FF4CC01D2C996C0600151637 /* TournamentRankView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5BAF712BE19274008B4B7E /* TournamentRankView.swift */; }; FF4CC01E2C996C0600151637 /* NumberFormatter+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5D0D862BB48AFD005CB568 /* NumberFormatter+Extensions.swift */; }; FF4CC01F2C996C0600151637 /* SetLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0172BBC5A6800B82851 /* SetLabelView.swift */; }; @@ -514,6 +532,15 @@ FF6087EC2BE26A2F004E1E47 /* BroadcastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6087EB2BE26A2F004E1E47 /* BroadcastView.swift */; }; FF6525C32C8C61B400B9498E /* LoserBracketFromGroupStageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6525C22C8C61B400B9498E /* LoserBracketFromGroupStageView.swift */; }; FF663FBE2BE019EC0031AE83 /* TournamentFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */; }; + FF6761532CC77D2100CC9BF2 /* DrawLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761522CC77D1900CC9BF2 /* DrawLog.swift */; }; + FF6761542CC77D2100CC9BF2 /* DrawLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761522CC77D1900CC9BF2 /* DrawLog.swift */; }; + FF6761552CC77D2100CC9BF2 /* DrawLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761522CC77D1900CC9BF2 /* DrawLog.swift */; }; + FF6761572CC7803600CC9BF2 /* DrawLogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */; }; + FF6761582CC7803600CC9BF2 /* DrawLogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */; }; + FF6761592CC7803600CC9BF2 /* DrawLogsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */; }; + FF67615B2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */; }; + FF67615C2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */; }; + FF67615D2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */; }; FF6EC8F72B94773200EA7F5A /* RowButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */; }; FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */; }; FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FD2B94792300EA7F5A /* Screen.swift */; }; @@ -687,8 +714,8 @@ FF70FB5F2C90584900129CC2 /* TournamentFieldsManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26462BAE0ACB00650388 /* TournamentFieldsManagerView.swift */; }; FF70FB602C90584900129CC2 /* PrintSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1F4B812BFA0124000B4573 /* PrintSettingsView.swift */; }; FF70FB612C90584900129CC2 /* TournamentMatchFormatsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE22BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift */; }; - FF70FB622C90584900129CC2 /* DateUpdateManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */; }; - FF70FB632C90584900129CC2 /* MatchTypeSmallSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0192BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift */; }; + FF70FB622C90584900129CC2 /* DatePickingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1162892BD05247000C4809 /* DatePickingView.swift */; }; + FF70FB632C90584900129CC2 /* MatchFormatRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0192BBC5A8500B82851 /* MatchFormatRowView.swift */; }; FF70FB642C90584900129CC2 /* MonthData.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF025AE82BD1307E00A86CF8 /* MonthData.swift */; }; FF70FB652C90584900129CC2 /* MenuWarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFEF7F4D2BDE69130033D0F0 /* MenuWarningView.swift */; }; FF70FB662C90584900129CC2 /* TournamentBuildView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1F4B6C2BF9E60B000B4573 /* TournamentBuildView.swift */; }; @@ -744,7 +771,7 @@ FF70FB982C90584900129CC2 /* EventSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF41852BF75FDA001B24CB /* EventSettingsView.swift */; }; FF70FB992C90584900129CC2 /* InscriptionInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5D0D772BB42C5B005CB568 /* InscriptionInfoView.swift */; }; FF70FB9A2C90584900129CC2 /* SelectablePlayerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */; }; - FF70FB9B2C90584900129CC2 /* MatchFormatPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26502BAE0BAD00650388 /* MatchFormatPickerView.swift */; }; + FF70FB9B2C90584900129CC2 /* MatchFormatSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26502BAE0BAD00650388 /* MatchFormatSelectionView.swift */; }; FF70FB9C2C90584900129CC2 /* TournamentRankView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5BAF712BE19274008B4B7E /* TournamentRankView.swift */; }; FF70FB9D2C90584900129CC2 /* NumberFormatter+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5D0D862BB48AFD005CB568 /* NumberFormatter+Extensions.swift */; }; FF70FB9E2C90584900129CC2 /* SetLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0172BBC5A6800B82851 /* SetLabelView.swift */; }; @@ -790,6 +817,18 @@ FF70FBC82C90584900129CC2 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = FF0CA5742BDA4AE10080E843 /* PrivacyInfo.xcprivacy */; }; FF70FBC92C90584900129CC2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C425D4042B6D249E002A7B48 /* Assets.xcassets */; }; FF70FBCB2C90584900129CC2 /* LeStorage.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C49EF0372BDFF3000077B5AA /* LeStorage.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + FF77CE522CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF77CE512CCCD1AF00CBCBB4 /* MatchFormatPickingView.swift */; }; + FF77CE532CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF77CE512CCCD1AF00CBCBB4 /* MatchFormatPickingView.swift */; }; + FF77CE542CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF77CE512CCCD1AF00CBCBB4 /* MatchFormatPickingView.swift */; }; + FF77CE562CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF77CE552CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift */; }; + FF77CE572CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF77CE552CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift */; }; + FF77CE582CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF77CE552CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift */; }; + FF77CE5A2CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF77CE592CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift */; }; + FF77CE5B2CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF77CE592CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift */; }; + FF77CE5C2CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF77CE592CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift */; }; + FF7DCD392CC330270041110C /* TeamRestingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF7DCD382CC330260041110C /* TeamRestingView.swift */; }; + FF7DCD3A2CC330270041110C /* TeamRestingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF7DCD382CC330260041110C /* TeamRestingView.swift */; }; + FF7DCD3B2CC330270041110C /* TeamRestingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF7DCD382CC330260041110C /* TeamRestingView.swift */; }; FF8044AC2C8F676D00A49A52 /* TournamentSubscriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8044AB2C8F676D00A49A52 /* TournamentSubscriptionView.swift */; }; FF82CFC52B911F5B00B0CAF2 /* OrganizedTournamentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF82CFC42B911F5B00B0CAF2 /* OrganizedTournamentView.swift */; }; FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF82CFC82B9132AF00B0CAF2 /* ActivityView.swift */; }; @@ -806,7 +845,7 @@ FF8F264C2BAE0B4100650388 /* TournamentFormatSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26482BAE0B4100650388 /* TournamentFormatSelectionView.swift */; }; FF8F264D2BAE0B4100650388 /* TournamentDatePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F264A2BAE0B4100650388 /* TournamentDatePickerView.swift */; }; FF8F264F2BAE0B9600650388 /* MatchTypeSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F264E2BAE0B9600650388 /* MatchTypeSelectionView.swift */; }; - FF8F26512BAE0BAD00650388 /* MatchFormatPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26502BAE0BAD00650388 /* MatchFormatPickerView.swift */; }; + FF8F26512BAE0BAD00650388 /* MatchFormatSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26502BAE0BAD00650388 /* MatchFormatSelectionView.swift */; }; FF8F26542BAE1E4400650388 /* TableStructureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26532BAE1E4400650388 /* TableStructureView.swift */; }; FF90FC1D2C44FB3E009339B2 /* AddTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF90FC1C2C44FB3E009339B2 /* AddTeamView.swift */; }; FF92660D2C241CE0002361A4 /* Zip in Frameworks */ = {isa = PBXBuildFile; productRef = FF92660C2C241CE0002361A4 /* Zip */; }; @@ -840,12 +879,27 @@ FF967D0F2BAF63B000A9A3BD /* PlayerBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967D0E2BAF63B000A9A3BD /* PlayerBlockView.swift */; }; FF9AC3952BE3627B00C2E883 /* GroupStageTeamReplacementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF9AC3942BE3627B00C2E883 /* GroupStageTeamReplacementView.swift */; }; FFA1B1292BB71773006CE248 /* PadelClubButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA1B1282BB71773006CE248 /* PadelClubButtonView.swift */; }; + FFA252A92CDB70520074E63F /* PlayerStatisticView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252A82CDB70520074E63F /* PlayerStatisticView.swift */; }; + FFA252AA2CDB70520074E63F /* PlayerStatisticView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252A82CDB70520074E63F /* PlayerStatisticView.swift */; }; + FFA252AB2CDB70520074E63F /* PlayerStatisticView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252A82CDB70520074E63F /* PlayerStatisticView.swift */; }; + FFA252AD2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */; }; + FFA252AE2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */; }; + FFA252AF2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */; }; + FFA252B12CDD2C080074E63F /* OngoingContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B02CDD2C080074E63F /* OngoingContainerView.swift */; }; + FFA252B22CDD2C080074E63F /* OngoingContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B02CDD2C080074E63F /* OngoingContainerView.swift */; }; + FFA252B32CDD2C080074E63F /* OngoingContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B02CDD2C080074E63F /* OngoingContainerView.swift */; }; + FFA252B52CDD2C6C0074E63F /* OngoingDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B42CDD2C630074E63F /* OngoingDestination.swift */; }; + FFA252B62CDD2C6C0074E63F /* OngoingDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B42CDD2C630074E63F /* OngoingDestination.swift */; }; + FFA252B72CDD2C6C0074E63F /* OngoingDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B42CDD2C630074E63F /* OngoingDestination.swift */; }; FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */; }; FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */; }; FFB1C98B2C10255100B154A7 /* TournamentBroadcastRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */; }; FFB9C8712BBADDE200A0EF4F /* Selectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB9C8702BBADDE200A0EF4F /* Selectable.swift */; }; FFB9C8752BBADDF700A0EF4F /* SeedInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */; }; FFBA2D2D2CA2CE9E00D5BBDD /* CodingContainer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C33F752C9B1EC5006316DE /* CodingContainer+Extensions.swift */; }; + FFBE62052CE9DA0900815D33 /* MatchViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBE62042CE9DA0900815D33 /* MatchViewStyle.swift */; }; + FFBE62062CE9DA0900815D33 /* MatchViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBE62042CE9DA0900815D33 /* MatchViewStyle.swift */; }; + FFBE62072CE9DA0900815D33 /* MatchViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBE62042CE9DA0900815D33 /* MatchViewStyle.swift */; }; FFBF065C2BBD2657009D6715 /* GroupStageTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF065B2BBD2657009D6715 /* GroupStageTeamView.swift */; }; FFBF065E2BBD8040009D6715 /* MatchListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF065D2BBD8040009D6715 /* MatchListView.swift */; }; FFBF06602BBD9F6D009D6715 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */; }; @@ -875,7 +929,7 @@ FFCFC0142BBC59FC00B82851 /* MatchDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0132BBC59FC00B82851 /* MatchDescriptor.swift */; }; FFCFC0162BBC5A4C00B82851 /* SetInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0152BBC5A4C00B82851 /* SetInputView.swift */; }; FFCFC0182BBC5A6800B82851 /* SetLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0172BBC5A6800B82851 /* SetLabelView.swift */; }; - FFCFC01A2BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0192BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift */; }; + FFCFC01A2BBC5A8500B82851 /* MatchFormatRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0192BBC5A8500B82851 /* MatchFormatRowView.swift */; }; FFCFC01C2BBC5AAA00B82851 /* SetDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */; }; FFD655D82C8DE27400E5B35E /* TournamentLookUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD655D72C8DE27400E5B35E /* TournamentLookUpView.swift */; }; FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */; }; @@ -981,6 +1035,8 @@ C45BAE3A2BC6DF10002EEC8A /* SyncedProducts.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = SyncedProducts.storekit; sourceTree = ""; }; C45BAE432BCA753E002EEC8A /* Purchase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Purchase.swift; sourceTree = ""; }; C4607A7C2C04DDE2004CB781 /* APICallsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APICallsListView.swift; sourceTree = ""; }; + C471D1532D0C8FE80068091F /* Drawlog.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Drawlog.json; sourceTree = ""; }; + C471D1572D0C91FE0068091F /* BaseDrawLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseDrawLog.swift; sourceTree = ""; }; C488C7E52CC7D1660082001F /* generator.py */ = {isa = PBXFileReference; lastKnownFileType = text.script.python; path = generator.py; sourceTree = ""; }; C488C7EC2CC7D2290082001F /* Club.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Club.json; sourceTree = ""; }; C488C7FE2CC7DCB80082001F /* BaseClub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseClub.swift; sourceTree = ""; }; @@ -1112,8 +1168,12 @@ FF1162822BCFBE4E000C4809 /* EditablePlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditablePlayerView.swift; sourceTree = ""; }; FF1162842BD00279000C4809 /* PlayerDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerDetailView.swift; sourceTree = ""; }; FF1162862BD004AD000C4809 /* EditingTeamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditingTeamView.swift; sourceTree = ""; }; - FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateUpdateManagerView.swift; sourceTree = ""; }; + FF1162892BD05247000C4809 /* DatePickingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickingView.swift; sourceTree = ""; }; FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundStepScheduleEditorView.swift; sourceTree = ""; }; + FF17CA482CB915A1003C7323 /* MultiCourtPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiCourtPickerView.swift; sourceTree = ""; }; + FF17CA4C2CB9243E003C7323 /* FollowUpMatchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowUpMatchView.swift; sourceTree = ""; }; + FF17CA522CBE4788003C7323 /* BracketCallingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BracketCallingView.swift; sourceTree = ""; }; + FF17CA562CC02FEA003C7323 /* CoachListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoachListView.swift; sourceTree = ""; }; FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournament.swift; sourceTree = ""; }; FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Calendar+Extensions.swift"; sourceTree = ""; }; FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournamentSearchScope.swift; sourceTree = ""; }; @@ -1184,6 +1244,9 @@ FF6087EB2BE26A2F004E1E47 /* BroadcastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BroadcastView.swift; sourceTree = ""; }; FF6525C22C8C61B400B9498E /* LoserBracketFromGroupStageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserBracketFromGroupStageView.swift; sourceTree = ""; }; FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentFilterView.swift; sourceTree = ""; }; + FF6761522CC77D1900CC9BF2 /* DrawLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawLog.swift; sourceTree = ""; }; + FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawLogsView.swift; sourceTree = ""; }; + FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewBracketPositionView.swift; sourceTree = ""; }; FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowButtonView.swift; sourceTree = ""; }; FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentButtonView.swift; sourceTree = ""; }; FF6EC8FD2B94792300EA7F5A /* Screen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = ""; }; @@ -1199,6 +1262,10 @@ FF70916B2B91005400AB08DA /* TournamentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentView.swift; sourceTree = ""; }; FF70916D2B9108C600AB08DA /* InscriptionManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InscriptionManagerView.swift; sourceTree = ""; }; FF70FBCF2C90584900129CC2 /* PadelClub TestFlight.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "PadelClub TestFlight.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + FF77CE512CCCD1AF00CBCBB4 /* MatchFormatPickingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchFormatPickingView.swift; sourceTree = ""; }; + FF77CE552CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickingViewWithFormat.swift; sourceTree = ""; }; + FF77CE592CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupStageDatePickingView.swift; sourceTree = ""; }; + FF7DCD382CC330260041110C /* TeamRestingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamRestingView.swift; sourceTree = ""; }; FF8044AB2C8F676D00A49A52 /* TournamentSubscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentSubscriptionView.swift; sourceTree = ""; }; FF82CFC42B911F5B00B0CAF2 /* OrganizedTournamentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganizedTournamentView.swift; sourceTree = ""; }; FF82CFC82B9132AF00B0CAF2 /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = ""; }; @@ -1215,7 +1282,7 @@ FF8F26492BAE0B4100650388 /* TournamentLevelPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentLevelPickerView.swift; sourceTree = ""; }; FF8F264A2BAE0B4100650388 /* TournamentDatePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentDatePickerView.swift; sourceTree = ""; }; FF8F264E2BAE0B9600650388 /* MatchTypeSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchTypeSelectionView.swift; sourceTree = ""; }; - FF8F26502BAE0BAD00650388 /* MatchFormatPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchFormatPickerView.swift; sourceTree = ""; }; + FF8F26502BAE0BAD00650388 /* MatchFormatSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchFormatSelectionView.swift; sourceTree = ""; }; FF8F26532BAE1E4400650388 /* TableStructureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableStructureView.swift; sourceTree = ""; }; FF90FC1C2C44FB3E009339B2 /* AddTeamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddTeamView.swift; sourceTree = ""; }; FF92660F2C255E4A002361A4 /* PadelClub.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PadelClub.entitlements; sourceTree = ""; }; @@ -1249,12 +1316,17 @@ FF967D0E2BAF63B000A9A3BD /* PlayerBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerBlockView.swift; sourceTree = ""; }; FF9AC3942BE3627B00C2E883 /* GroupStageTeamReplacementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupStageTeamReplacementView.swift; sourceTree = ""; }; FFA1B1282BB71773006CE248 /* PadelClubButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PadelClubButtonView.swift; sourceTree = ""; }; + FFA252A82CDB70520074E63F /* PlayerStatisticView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerStatisticView.swift; sourceTree = ""; }; + FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UmpireStatisticView.swift; sourceTree = ""; }; + FFA252B02CDD2C080074E63F /* OngoingContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OngoingContainerView.swift; sourceTree = ""; }; + FFA252B42CDD2C630074E63F /* OngoingDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OngoingDestination.swift; sourceTree = ""; }; FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileImportManager.swift; sourceTree = ""; }; FFA6D7862BB0B7A2003A31F3 /* CloudConvert.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudConvert.swift; sourceTree = ""; }; FFA6D78A2BB0BEB3003A31F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentBroadcastRowView.swift; sourceTree = ""; }; FFB9C8702BBADDE200A0EF4F /* Selectable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Selectable.swift; sourceTree = ""; }; FFB9C8742BBADDF700A0EF4F /* SeedInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedInterval.swift; sourceTree = ""; }; + FFBE62042CE9DA0900815D33 /* MatchViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchViewStyle.swift; sourceTree = ""; }; FFBF065B2BBD2657009D6715 /* GroupStageTeamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupStageTeamView.swift; sourceTree = ""; }; FFBF065D2BBD8040009D6715 /* MatchListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchListView.swift; sourceTree = ""; }; FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.swift; sourceTree = ""; }; @@ -1283,7 +1355,7 @@ FFCFC0132BBC59FC00B82851 /* MatchDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchDescriptor.swift; sourceTree = ""; }; FFCFC0152BBC5A4C00B82851 /* SetInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetInputView.swift; sourceTree = ""; }; FFCFC0172BBC5A6800B82851 /* SetLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetLabelView.swift; sourceTree = ""; }; - FFCFC0192BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchTypeSmallSelectionView.swift; sourceTree = ""; }; + FFCFC0192BBC5A8500B82851 /* MatchFormatRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchFormatRowView.swift; sourceTree = ""; }; FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetDescriptor.swift; sourceTree = ""; }; FFD655D72C8DE27400E5B35E /* TournamentLookUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentLookUpView.swift; sourceTree = ""; }; FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PadelClubView.swift; sourceTree = ""; }; @@ -1460,6 +1532,7 @@ C488C8132CC7E4240082001F /* Court.json */, C488C8142CC7E4240082001F /* CustomUser.json */, C488C8152CC7E4240082001F /* DateInterval.json */, + C471D1532D0C8FE80068091F /* Drawlog.json */, C488C8162CC7E4240082001F /* Event.json */, C488C8172CC7E4240082001F /* GroupStage.json */, C488C8182CC7E4240082001F /* Match.json */, @@ -1475,6 +1548,7 @@ C488C8022CC7E1E40082001F /* BaseCourt.swift */, C488C8062CC7E4240082001F /* BaseCustomUser.swift */, C488C8072CC7E4240082001F /* BaseDateInterval.swift */, + C471D1572D0C91FE0068091F /* BaseDrawLog.swift */, C488C8082CC7E4240082001F /* BaseEvent.swift */, C488C8092CC7E4240082001F /* BaseGroupStage.swift */, C488C80A2CC7E4240082001F /* BaseMatch.swift */, @@ -1514,6 +1588,7 @@ FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */, FFC91B002BD85C2F00B29808 /* Court.swift */, FFF116E02BD2A9B600A33B06 /* DateInterval.swift */, + FF6761522CC77D1900CC9BF2 /* DrawLog.swift */, FF6EC9012B94799200EA7F5A /* Coredata */, FF6EC9022B9479B900EA7F5A /* Federal */, ); @@ -1625,6 +1700,7 @@ children = ( FF089EBC2BB0287D00F0AEC7 /* PlayerView.swift */, FF1162842BD00279000C4809 /* PlayerDetailView.swift */, + FFA252A82CDB70520074E63F /* PlayerStatisticView.swift */, FF089EB02BB001EA00F0AEC7 /* Components */, ); path = Player; @@ -1692,7 +1768,11 @@ FF1162882BD0523B000C4809 /* Components */ = { isa = PBXGroup; children = ( - FF1162892BD05247000C4809 /* DateUpdateManagerView.swift */, + FF1162892BD05247000C4809 /* DatePickingView.swift */, + FF77CE592CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift */, + FF77CE552CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift */, + FF77CE512CCCD1AF00CBCBB4 /* MatchFormatPickingView.swift */, + FF17CA482CB915A1003C7323 /* MultiCourtPickerView.swift */, ); path = Components; sourceTree = ""; @@ -1823,6 +1903,7 @@ isa = PBXGroup; children = ( FF3F74F52B919E45004CFE0E /* UmpireView.swift */, + FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */, FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */, C488C8812CCBE8FC0082001F /* NetworkStatusView.swift */, ); @@ -1846,6 +1927,7 @@ FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */, FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */, FF5BAF6D2BE0B3C8008B4B7E /* FederalDataViewModel.swift */, + FFBE62042CE9DA0900815D33 /* MatchViewStyle.swift */, ); path = ViewModel; sourceTree = ""; @@ -1862,6 +1944,8 @@ isa = PBXGroup; children = ( FF5D30552BD95B1100F2B93D /* OngoingView.swift */, + FFA252B02CDD2C080074E63F /* OngoingContainerView.swift */, + FFA252B42CDD2C630074E63F /* OngoingDestination.swift */, ); path = Ongoing; sourceTree = ""; @@ -1870,11 +1954,11 @@ isa = PBXGroup; children = ( FF8F264E2BAE0B9600650388 /* MatchTypeSelectionView.swift */, - FF8F26502BAE0BAD00650388 /* MatchFormatPickerView.swift */, + FF8F26502BAE0BAD00650388 /* MatchFormatSelectionView.swift */, FF4AB6BC2B9256E10002987F /* SelectablePlayerListView.swift */, FF4AB6BE2B92577A0002987F /* ImportedPlayerView.swift */, FF5D0D6F2BB3EFA5005CB568 /* LearnMoreSheetView.swift */, - FFCFC0192BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift */, + FFCFC0192BBC5A8500B82851 /* MatchFormatRowView.swift */, FF663FBD2BE019EC0031AE83 /* TournamentFilterView.swift */, FFE2D2E12C231BEE00D0C7BE /* SupportButtonView.swift */, FFE103112C366E5900684FC9 /* ImagePickerView.swift */, @@ -1943,6 +2027,7 @@ FF9268082BCEDC2C0080F940 /* CallView.swift */, FF1162792BCF8109000C4809 /* CallMessageCustomizationView.swift */, FF2EFBEF2BDE295E0049CE3B /* SendToAllView.swift */, + FF17CA522CBE4788003C7323 /* BracketCallingView.swift */, FFEF7F4C2BDE68F80033D0F0 /* Components */, ); path = Calling; @@ -1981,6 +2066,8 @@ FF967D0A2BAF3D4C00A9A3BD /* TeamPickerView.swift */, FF089EB52BB00A3800F0AEC7 /* TeamRowView.swift */, FF1162862BD004AD000C4809 /* EditingTeamView.swift */, + FF17CA562CC02FEA003C7323 /* CoachListView.swift */, + FF7DCD382CC330260041110C /* TeamRestingView.swift */, FF025AD62BD0C0FB00A86CF8 /* Components */, ); path = Team; @@ -2027,6 +2114,8 @@ FFC2DCB12BBE75D40046DB9F /* LoserRoundView.swift */, FFC2DCB32BBE9ECD0046DB9F /* LoserRoundsView.swift */, FF5647122C0B6F380081F995 /* LoserRoundSettingsView.swift */, + FF6761562CC7803600CC9BF2 /* DrawLogsView.swift */, + FF67615A2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift */, ); path = Round; sourceTree = ""; @@ -2035,6 +2124,7 @@ isa = PBXGroup; children = ( FFCFC0012BBC39A600B82851 /* EditScoreView.swift */, + FF17CA4C2CB9243E003C7323 /* FollowUpMatchView.swift */, FFCFC0152BBC5A4C00B82851 /* SetInputView.swift */, FFCFC0172BBC5A6800B82851 /* SetLabelView.swift */, FFCFC00D2BBC3D4600B82851 /* PointSelectionView.swift */, @@ -2322,6 +2412,7 @@ C488C8202CC7E4240082001F /* Event.json in Resources */, C488C8212CC7E4240082001F /* Court.json in Resources */, C488C8222CC7E4240082001F /* Tournament.json in Resources */, + C471D1552D0C8FED0068091F /* Drawlog.json in Resources */, C488C8232CC7E4240082001F /* CustomUser.json in Resources */, C488C8242CC7E4240082001F /* Round.json in Resources */, C488C8252CC7E4240082001F /* MatchScheduler.json in Resources */, @@ -2376,6 +2467,7 @@ C488C83A2CC7E4240082001F /* Event.json in Resources */, C488C83B2CC7E4240082001F /* Court.json in Resources */, C488C83C2CC7E4240082001F /* Tournament.json in Resources */, + C471D1562D0C8FED0068091F /* Drawlog.json in Resources */, C488C83D2CC7E4240082001F /* CustomUser.json in Resources */, C488C83E2CC7E4240082001F /* Round.json in Resources */, C488C83F2CC7E4240082001F /* MatchScheduler.json in Resources */, @@ -2416,6 +2508,7 @@ C488C8612CC7E4240082001F /* Event.json in Resources */, C488C8622CC7E4240082001F /* Court.json in Resources */, C488C8632CC7E4240082001F /* Tournament.json in Resources */, + C471D1542D0C8FED0068091F /* Drawlog.json in Resources */, C488C8642CC7E4240082001F /* CustomUser.json in Resources */, C488C8652CC7E4240082001F /* Round.json in Resources */, C488C8662CC7E4240082001F /* MatchScheduler.json in Resources */, @@ -2449,6 +2542,7 @@ FF7091662B90F0B000AB08DA /* TabDestination.swift in Sources */, FF9267F82BCE78C70080F940 /* CashierView.swift in Sources */, FF8F263F2BAD7D5C00650388 /* Event.swift in Sources */, + FF77CE562CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift in Sources */, FF5D30532BD94E2E00F2B93D /* PlayerHolder.swift in Sources */, FF11628C2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift in Sources */, FF53FBB82BFB302B0051D4C3 /* ClubCourtSetupView.swift in Sources */, @@ -2471,6 +2565,7 @@ FF025AE12BD0EB9000A86CF8 /* TournamentClubSettingsView.swift in Sources */, FFBF065C2BBD2657009D6715 /* GroupStageTeamView.swift in Sources */, FF5DA1932BB9279B00A33061 /* RoundSettingsView.swift in Sources */, + FFBE62052CE9DA0900815D33 /* MatchViewStyle.swift in Sources */, FFE2D2E22C231BEE00D0C7BE /* SupportButtonView.swift in Sources */, FFB1C98B2C10255100B154A7 /* TournamentBroadcastRowView.swift in Sources */, FF025ADF2BD0CE0A00A86CF8 /* TeamWeightView.swift in Sources */, @@ -2485,11 +2580,13 @@ FFE103082C353B7600684FC9 /* EventClubSettingsView.swift in Sources */, C4A47DB32B86387500ADC637 /* AccountView.swift in Sources */, FFCEDA4C2C2C08EA00F8C0F2 /* PlayersWithoutContactView.swift in Sources */, + FF17CA4F2CB9243E003C7323 /* FollowUpMatchView.swift in Sources */, FFCD16B32C3E5E590092707B /* TeamsCallingView.swift in Sources */, FF1CBC1D2BB53DC10036DAAB /* Calendar+Extensions.swift in Sources */, FF967CF22BAECC0B00A9A3BD /* TeamScore.swift in Sources */, FF1162832BCFBE4E000C4809 /* EditablePlayerView.swift in Sources */, FF1162852BD00279000C4809 /* PlayerDetailView.swift in Sources */, + FFA252AE2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */, FF5D0D762BB428B2005CB568 /* ListRowViewModifier.swift in Sources */, FF6EC9002B94794700EA7F5A /* PresentationContext.swift in Sources */, FFDB1C6D2BB2A02000F1E467 /* AppSettings.swift in Sources */, @@ -2504,6 +2601,7 @@ C4C01D982C481C0C0059087C /* CapsuleViewModifier.swift in Sources */, C488C82D2CC7E4240082001F /* BaseDateInterval.swift in Sources */, C488C82E2CC7E4240082001F /* BaseMonthData.swift in Sources */, + C471D1592D0C91FE0068091F /* BaseDrawLog.swift in Sources */, C488C82F2CC7E4240082001F /* BaseTeamRegistration.swift in Sources */, C488C8302CC7E4240082001F /* BaseGroupStage.swift in Sources */, C488C8312CC7E4240082001F /* BaseCustomUser.swift in Sources */, @@ -2522,6 +2620,7 @@ FF5D30562BD95B1100F2B93D /* OngoingView.swift in Sources */, FF1DC5552BAB36DD00FD8220 /* CreateClubView.swift in Sources */, C4607A7D2C04DDE2004CB781 /* APICallsListView.swift in Sources */, + FF7DCD3B2CC330270041110C /* TeamRestingView.swift in Sources */, FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */, FF025AEF2BD1AE9400A86CF8 /* DurationSettingsView.swift in Sources */, FF025AED2BD1513700A86CF8 /* AppScreen.swift in Sources */, @@ -2543,6 +2642,7 @@ C44B79112BBDA63A00906534 /* Locale+Extensions.swift in Sources */, FF1F4B742BFA00FC000B4573 /* HtmlService.swift in Sources */, FF967CEA2BAEC70100A9A3BD /* GroupStage.swift in Sources */, + FF6761542CC77D2100CC9BF2 /* DrawLog.swift in Sources */, FF1162812BCF945C000C4809 /* TournamentCashierView.swift in Sources */, C4A47D902B7BBBEC00ADC637 /* StoreManager.swift in Sources */, FF4AB6BB2B9256D50002987F /* SearchViewModel.swift in Sources */, @@ -2604,7 +2704,9 @@ FF9267FC2BCE84870080F940 /* PlayerPayView.swift in Sources */, FF2B51552C7A4DAF00FFF126 /* PlanningByCourtView.swift in Sources */, FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */, + FFA252B22CDD2C080074E63F /* OngoingContainerView.swift in Sources */, FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */, + FF6761582CC7803600CC9BF2 /* DrawLogsView.swift in Sources */, FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */, FFBF06602BBD9F6D009D6715 /* NavigationViewModel.swift in Sources */, FF6EC9092B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift in Sources */, @@ -2613,6 +2715,7 @@ FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */, FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */, FF70916E2B9108C600AB08DA /* InscriptionManagerView.swift in Sources */, + FF77CE542CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */, FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */, FFDB1C732BB2CFE900F1E467 /* MySortDescriptor.swift in Sources */, FF5D0D8B2BB4D1E3005CB568 /* CalendarView.swift in Sources */, @@ -2620,8 +2723,8 @@ FF8F26472BAE0ACB00650388 /* TournamentFieldsManagerView.swift in Sources */, FF1F4B822BFA0124000B4573 /* PrintSettingsView.swift in Sources */, FF025AE32BD0EBA900A86CF8 /* TournamentMatchFormatsSettingsView.swift in Sources */, - FF11628A2BD05247000C4809 /* DateUpdateManagerView.swift in Sources */, - FFCFC01A2BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift in Sources */, + FF11628A2BD05247000C4809 /* DatePickingView.swift in Sources */, + FFCFC01A2BBC5A8500B82851 /* MatchFormatRowView.swift in Sources */, FF025AE92BD1307F00A86CF8 /* MonthData.swift in Sources */, FFEF7F4E2BDE69130033D0F0 /* MenuWarningView.swift in Sources */, FF1F4B6D2BF9E60B000B4573 /* TournamentBuildView.swift in Sources */, @@ -2629,6 +2732,7 @@ FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */, FFBF41842BF75ED7001B24CB /* EventTournamentsView.swift in Sources */, FF1DC55B2BAB80C400FD8220 /* DisplayContext.swift in Sources */, + FF17CA4A2CB915A1003C7323 /* MultiCourtPickerView.swift in Sources */, FF9268072BCE94D90080F940 /* TournamentCallView.swift in Sources */, FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */, FF967CFC2BAEE52E00A9A3BD /* GroupStagesView.swift in Sources */, @@ -2636,6 +2740,7 @@ C49EF01B2BD6A1E80077B5AA /* URLs.swift in Sources */, FFCFC0142BBC59FC00B82851 /* MatchDescriptor.swift in Sources */, FF8F264C2BAE0B4100650388 /* TournamentFormatSelectionView.swift in Sources */, + FF17CA572CC02FEA003C7323 /* CoachListView.swift in Sources */, FFBF065E2BBD8040009D6715 /* MatchListView.swift in Sources */, C425D4012B6D249D002A7B48 /* PadelClubApp.swift in Sources */, FF8F26432BADFE5B00650388 /* TournamentSettingsView.swift in Sources */, @@ -2649,13 +2754,17 @@ C4A47DAD2B85FCCD00ADC637 /* CustomUser.swift in Sources */, C4C33F762C9B1ED4006316DE /* CodingContainer+Extensions.swift in Sources */, FF967D012BAEF0B400A9A3BD /* MatchSummaryView.swift in Sources */, + FFA252B62CDD2C6C0074E63F /* OngoingDestination.swift in Sources */, FF8F26452BAE0A3400650388 /* TournamentDurationManagerView.swift in Sources */, FF1DC5532BAB354A00FD8220 /* MockData.swift in Sources */, FF967D092BAF3D4000A9A3BD /* TeamDetailView.swift in Sources */, C488C8822CCBE8FC0082001F /* NetworkStatusView.swift in Sources */, + FFA252A92CDB70520074E63F /* PlayerStatisticView.swift in Sources */, FF5DA18F2BB9268800A33061 /* GroupStagesSettingsView.swift in Sources */, FF663FBE2BE019EC0031AE83 /* TournamentFilterView.swift in Sources */, + FF67615D2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */, FF1F4B752BFA00FC000B4573 /* HtmlGenerator.swift in Sources */, + FF17CA532CBE4788003C7323 /* BracketCallingView.swift in Sources */, FF8F26382BAD523300650388 /* PadelRule.swift in Sources */, FF967CF42BAECC0B00A9A3BD /* TeamRegistration.swift in Sources */, FFF8ACDB2B923F48008466FA /* Date+Extensions.swift in Sources */, @@ -2680,8 +2789,9 @@ FFBF41862BF75FDA001B24CB /* EventSettingsView.swift in Sources */, FF5D0D782BB42C5B005CB568 /* InscriptionInfoView.swift in Sources */, FF4AB6BD2B9256E10002987F /* SelectablePlayerListView.swift in Sources */, - FF8F26512BAE0BAD00650388 /* MatchFormatPickerView.swift in Sources */, + FF8F26512BAE0BAD00650388 /* MatchFormatSelectionView.swift in Sources */, FF5BAF722BE19274008B4B7E /* TournamentRankView.swift in Sources */, + FF77CE5B2CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift in Sources */, FF5D0D872BB48AFD005CB568 /* NumberFormatter+Extensions.swift in Sources */, FFCFC0182BBC5A6800B82851 /* SetLabelView.swift in Sources */, C4489BE22C05BF5000043F3D /* DebugSettingsView.swift in Sources */, @@ -2737,6 +2847,7 @@ FF4CBF452C996C0600151637 /* TabDestination.swift in Sources */, FF4CBF462C996C0600151637 /* CashierView.swift in Sources */, FF4CBF472C996C0600151637 /* Event.swift in Sources */, + FF77CE582CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift in Sources */, FF4CBF482C996C0600151637 /* PlayerHolder.swift in Sources */, FF4CBF492C996C0600151637 /* LoserRoundStepScheduleEditorView.swift in Sources */, FF4CBF4A2C996C0600151637 /* ClubCourtSetupView.swift in Sources */, @@ -2758,6 +2869,7 @@ FF4CBF5A2C996C0600151637 /* TournamentClubSettingsView.swift in Sources */, FF4CBF5B2C996C0600151637 /* GroupStageTeamView.swift in Sources */, FF4CBF5C2C996C0600151637 /* RoundSettingsView.swift in Sources */, + FFBE62072CE9DA0900815D33 /* MatchViewStyle.swift in Sources */, FF4CBF5D2C996C0600151637 /* SupportButtonView.swift in Sources */, FF4CBF5E2C996C0600151637 /* TournamentBroadcastRowView.swift in Sources */, FF4CBF5F2C996C0600151637 /* TeamWeightView.swift in Sources */, @@ -2771,12 +2883,14 @@ FF4CBF672C996C0600151637 /* EventClubSettingsView.swift in Sources */, FF4CBF682C996C0600151637 /* AccountView.swift in Sources */, FF4CBF692C996C0600151637 /* PlayersWithoutContactView.swift in Sources */, + FF17CA4D2CB9243E003C7323 /* FollowUpMatchView.swift in Sources */, FF4CBF6A2C996C0600151637 /* TeamsCallingView.swift in Sources */, FF4CBF6B2C996C0600151637 /* Calendar+Extensions.swift in Sources */, FF4CBF6C2C996C0600151637 /* TeamScore.swift in Sources */, FF4CBF6D2C996C0600151637 /* EditablePlayerView.swift in Sources */, C488C7F12CC7D22D0082001F /* Club.json in Sources */, FF4CBF6E2C996C0600151637 /* PlayerDetailView.swift in Sources */, + FFA252AF2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */, FF4CBF6F2C996C0600151637 /* ListRowViewModifier.swift in Sources */, FF4CBF702C996C0600151637 /* PresentationContext.swift in Sources */, FF4CBF712C996C0600151637 /* AppSettings.swift in Sources */, @@ -2810,6 +2924,7 @@ FF4CBF802C996C0600151637 /* OngoingView.swift in Sources */, FF4CBF812C996C0600151637 /* CreateClubView.swift in Sources */, FF4CBF822C996C0600151637 /* APICallsListView.swift in Sources */, + FF7DCD392CC330270041110C /* TeamRestingView.swift in Sources */, FF4CBF832C996C0600151637 /* NetworkFederalService.swift in Sources */, FF4CBF842C996C0600151637 /* DurationSettingsView.swift in Sources */, FF4CBF852C996C0600151637 /* AppScreen.swift in Sources */, @@ -2832,6 +2947,7 @@ FF4CBF952C996C0600151637 /* Locale+Extensions.swift in Sources */, FF4CBF962C996C0600151637 /* HtmlService.swift in Sources */, FF4CBF972C996C0600151637 /* GroupStage.swift in Sources */, + FF6761532CC77D2100CC9BF2 /* DrawLog.swift in Sources */, FF4CBF982C996C0600151637 /* TournamentCashierView.swift in Sources */, FF4CBF992C996C0600151637 /* StoreManager.swift in Sources */, FF4CBF9A2C996C0600151637 /* SearchViewModel.swift in Sources */, @@ -2874,6 +2990,7 @@ FF4CBFBD2C996C0600151637 /* AgendaDestination.swift in Sources */, FF4CBFBE2C996C0600151637 /* PadelClubApp.xcdatamodeld in Sources */, FF4CBFBF2C996C0600151637 /* SetInputView.swift in Sources */, + C471D15A2D0C91FF0068091F /* BaseDrawLog.swift in Sources */, FF4CBFC02C996C0600151637 /* ButtonValidateView.swift in Sources */, FF4CBFC12C996C0600151637 /* ClubRowView.swift in Sources */, FF4CBFC22C996C0600151637 /* ClubDetailView.swift in Sources */, @@ -2893,7 +3010,9 @@ FF4CBFD02C996C0600151637 /* PlayerPayView.swift in Sources */, FF4CBFD12C996C0600151637 /* PlanningByCourtView.swift in Sources */, FF4CBFD22C996C0600151637 /* FileImportManager.swift in Sources */, + FFA252B12CDD2C080074E63F /* OngoingContainerView.swift in Sources */, FF4CBFD32C996C0600151637 /* TournamentButtonView.swift in Sources */, + FF6761592CC7803600CC9BF2 /* DrawLogsView.swift in Sources */, FF4CBFD42C996C0600151637 /* FederalPlayer.swift in Sources */, FF4CBFD52C996C0600151637 /* NavigationViewModel.swift in Sources */, FF4CBFD62C996C0600151637 /* FixedWidthInteger+Extensions.swift in Sources */, @@ -2902,6 +3021,7 @@ FF4CBFD92C996C0600151637 /* ClubSearchView.swift in Sources */, FF4CBFDA2C996C0600151637 /* PlayerPopoverView.swift in Sources */, FF4CBFDB2C996C0600151637 /* InscriptionManagerView.swift in Sources */, + FF77CE522CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */, FF4CBFDC2C996C0600151637 /* ActivityView.swift in Sources */, FF4CBFDD2C996C0600151637 /* MySortDescriptor.swift in Sources */, FF4CBFDE2C996C0600151637 /* CalendarView.swift in Sources */, @@ -2909,8 +3029,8 @@ FF4CBFE02C996C0600151637 /* TournamentFieldsManagerView.swift in Sources */, FF4CBFE12C996C0600151637 /* PrintSettingsView.swift in Sources */, FF4CBFE22C996C0600151637 /* TournamentMatchFormatsSettingsView.swift in Sources */, - FF4CBFE32C996C0600151637 /* DateUpdateManagerView.swift in Sources */, - FF4CBFE42C996C0600151637 /* MatchTypeSmallSelectionView.swift in Sources */, + FF4CBFE32C996C0600151637 /* DatePickingView.swift in Sources */, + FF4CBFE42C996C0600151637 /* MatchFormatRowView.swift in Sources */, FF4CBFE52C996C0600151637 /* MonthData.swift in Sources */, FF4CBFE62C996C0600151637 /* MenuWarningView.swift in Sources */, FF4CBFE72C996C0600151637 /* TournamentBuildView.swift in Sources */, @@ -2918,6 +3038,7 @@ FF4CBFE92C996C0600151637 /* CloudConvert.swift in Sources */, FF4CBFEA2C996C0600151637 /* EventTournamentsView.swift in Sources */, FF4CBFEB2C996C0600151637 /* DisplayContext.swift in Sources */, + FF17CA4B2CB915A1003C7323 /* MultiCourtPickerView.swift in Sources */, FF4CBFEC2C996C0600151637 /* TournamentCallView.swift in Sources */, FF4CBFED2C996C0600151637 /* LoserRoundsView.swift in Sources */, FF4CBFEE2C996C0600151637 /* GroupStagesView.swift in Sources */, @@ -2925,6 +3046,7 @@ FF4CBFF02C996C0600151637 /* URLs.swift in Sources */, FF4CBFF12C996C0600151637 /* MatchDescriptor.swift in Sources */, FF4CBFF22C996C0600151637 /* TournamentFormatSelectionView.swift in Sources */, + FF17CA592CC02FEB003C7323 /* CoachListView.swift in Sources */, FF4CBFF32C996C0600151637 /* MatchListView.swift in Sources */, FF4CBFF42C996C0600151637 /* PadelClubApp.swift in Sources */, FF4CBFF52C996C0600151637 /* TournamentSettingsView.swift in Sources */, @@ -2939,12 +3061,16 @@ FF4CBFFC2C996C0600151637 /* UmpireView.swift in Sources */, FF4CBFFD2C996C0600151637 /* CustomUser.swift in Sources */, FF4CBFFE2C996C0600151637 /* MatchSummaryView.swift in Sources */, + FFA252B52CDD2C6C0074E63F /* OngoingDestination.swift in Sources */, FF4CBFFF2C996C0600151637 /* TournamentDurationManagerView.swift in Sources */, FF4CC0002C996C0600151637 /* MockData.swift in Sources */, FF4CC0012C996C0600151637 /* TeamDetailView.swift in Sources */, + FFA252AA2CDB70520074E63F /* PlayerStatisticView.swift in Sources */, FF4CC0022C996C0600151637 /* GroupStagesSettingsView.swift in Sources */, FF4CC0032C996C0600151637 /* TournamentFilterView.swift in Sources */, + FF67615C2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */, FF4CC0042C996C0600151637 /* HtmlGenerator.swift in Sources */, + FF17CA542CBE4788003C7323 /* BracketCallingView.swift in Sources */, FF4CC0052C996C0600151637 /* PadelRule.swift in Sources */, FF4CC0062C996C0600151637 /* TeamRegistration.swift in Sources */, FF4CC0072C996C0600151637 /* Date+Extensions.swift in Sources */, @@ -2969,8 +3095,9 @@ FF4CC0192C996C0600151637 /* EventSettingsView.swift in Sources */, FF4CC01A2C996C0600151637 /* InscriptionInfoView.swift in Sources */, FF4CC01B2C996C0600151637 /* SelectablePlayerListView.swift in Sources */, - FF4CC01C2C996C0600151637 /* MatchFormatPickerView.swift in Sources */, + FF4CC01C2C996C0600151637 /* MatchFormatSelectionView.swift in Sources */, FF4CC01D2C996C0600151637 /* TournamentRankView.swift in Sources */, + FF77CE5C2CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift in Sources */, FF4CC01E2C996C0600151637 /* NumberFormatter+Extensions.swift in Sources */, FF4CC01F2C996C0600151637 /* SetLabelView.swift in Sources */, FF4CC0202C996C0600151637 /* DebugSettingsView.swift in Sources */, @@ -3004,6 +3131,7 @@ FF70FAC42C90584900129CC2 /* TabDestination.swift in Sources */, FF70FAC52C90584900129CC2 /* CashierView.swift in Sources */, FF70FAC62C90584900129CC2 /* Event.swift in Sources */, + FF77CE572CCCD1EB00CBCBB4 /* DatePickingViewWithFormat.swift in Sources */, FF70FAC72C90584900129CC2 /* PlayerHolder.swift in Sources */, FF70FAC82C90584900129CC2 /* LoserRoundStepScheduleEditorView.swift in Sources */, FF70FAC92C90584900129CC2 /* ClubCourtSetupView.swift in Sources */, @@ -3025,6 +3153,7 @@ FF70FAD92C90584900129CC2 /* TournamentClubSettingsView.swift in Sources */, FF70FADA2C90584900129CC2 /* GroupStageTeamView.swift in Sources */, FF70FADB2C90584900129CC2 /* RoundSettingsView.swift in Sources */, + FFBE62062CE9DA0900815D33 /* MatchViewStyle.swift in Sources */, FF70FADC2C90584900129CC2 /* SupportButtonView.swift in Sources */, FF70FADD2C90584900129CC2 /* TournamentBroadcastRowView.swift in Sources */, FF70FADE2C90584900129CC2 /* TeamWeightView.swift in Sources */, @@ -3038,12 +3167,14 @@ FF70FAE62C90584900129CC2 /* EventClubSettingsView.swift in Sources */, FF70FAE72C90584900129CC2 /* AccountView.swift in Sources */, FF70FAE82C90584900129CC2 /* PlayersWithoutContactView.swift in Sources */, + FF17CA4E2CB9243E003C7323 /* FollowUpMatchView.swift in Sources */, FF70FAE92C90584900129CC2 /* TeamsCallingView.swift in Sources */, FF70FAEA2C90584900129CC2 /* Calendar+Extensions.swift in Sources */, FF70FAEB2C90584900129CC2 /* TeamScore.swift in Sources */, FF70FAEC2C90584900129CC2 /* EditablePlayerView.swift in Sources */, C488C7F22CC7D22D0082001F /* Club.json in Sources */, FF70FAED2C90584900129CC2 /* PlayerDetailView.swift in Sources */, + FFA252AD2CDB734A0074E63F /* UmpireStatisticView.swift in Sources */, FF70FAEE2C90584900129CC2 /* ListRowViewModifier.swift in Sources */, FF70FAEF2C90584900129CC2 /* PresentationContext.swift in Sources */, FF70FAF02C90584900129CC2 /* AppSettings.swift in Sources */, @@ -3077,6 +3208,7 @@ FF70FAFF2C90584900129CC2 /* OngoingView.swift in Sources */, FF70FB002C90584900129CC2 /* CreateClubView.swift in Sources */, FF70FB012C90584900129CC2 /* APICallsListView.swift in Sources */, + FF7DCD3A2CC330270041110C /* TeamRestingView.swift in Sources */, FF70FB022C90584900129CC2 /* NetworkFederalService.swift in Sources */, FF70FB032C90584900129CC2 /* DurationSettingsView.swift in Sources */, FF70FB042C90584900129CC2 /* AppScreen.swift in Sources */, @@ -3099,6 +3231,7 @@ FF70FB142C90584900129CC2 /* Locale+Extensions.swift in Sources */, FF70FB152C90584900129CC2 /* HtmlService.swift in Sources */, FF70FB162C90584900129CC2 /* GroupStage.swift in Sources */, + FF6761552CC77D2100CC9BF2 /* DrawLog.swift in Sources */, FF70FB172C90584900129CC2 /* TournamentCashierView.swift in Sources */, FF70FB182C90584900129CC2 /* StoreManager.swift in Sources */, FF70FB192C90584900129CC2 /* SearchViewModel.swift in Sources */, @@ -3141,6 +3274,7 @@ FF70FB3C2C90584900129CC2 /* AgendaDestination.swift in Sources */, FF70FB3D2C90584900129CC2 /* PadelClubApp.xcdatamodeld in Sources */, FF70FB3E2C90584900129CC2 /* SetInputView.swift in Sources */, + C471D1582D0C91FE0068091F /* BaseDrawLog.swift in Sources */, FF70FB3F2C90584900129CC2 /* ButtonValidateView.swift in Sources */, FF70FB402C90584900129CC2 /* ClubRowView.swift in Sources */, FF70FB412C90584900129CC2 /* ClubDetailView.swift in Sources */, @@ -3160,7 +3294,9 @@ FF70FB4F2C90584900129CC2 /* PlayerPayView.swift in Sources */, FF70FB502C90584900129CC2 /* PlanningByCourtView.swift in Sources */, FF70FB512C90584900129CC2 /* FileImportManager.swift in Sources */, + FFA252B32CDD2C080074E63F /* OngoingContainerView.swift in Sources */, FF70FB522C90584900129CC2 /* TournamentButtonView.swift in Sources */, + FF6761572CC7803600CC9BF2 /* DrawLogsView.swift in Sources */, FF70FB532C90584900129CC2 /* FederalPlayer.swift in Sources */, FF70FB542C90584900129CC2 /* NavigationViewModel.swift in Sources */, FF70FB552C90584900129CC2 /* FixedWidthInteger+Extensions.swift in Sources */, @@ -3169,6 +3305,7 @@ FF70FB582C90584900129CC2 /* ClubSearchView.swift in Sources */, FF70FB592C90584900129CC2 /* PlayerPopoverView.swift in Sources */, FF70FB5A2C90584900129CC2 /* InscriptionManagerView.swift in Sources */, + FF77CE532CCCD1B200CBCBB4 /* MatchFormatPickingView.swift in Sources */, FF70FB5B2C90584900129CC2 /* ActivityView.swift in Sources */, FF70FB5C2C90584900129CC2 /* MySortDescriptor.swift in Sources */, FF70FB5D2C90584900129CC2 /* CalendarView.swift in Sources */, @@ -3176,8 +3313,8 @@ FF70FB5F2C90584900129CC2 /* TournamentFieldsManagerView.swift in Sources */, FF70FB602C90584900129CC2 /* PrintSettingsView.swift in Sources */, FF70FB612C90584900129CC2 /* TournamentMatchFormatsSettingsView.swift in Sources */, - FF70FB622C90584900129CC2 /* DateUpdateManagerView.swift in Sources */, - FF70FB632C90584900129CC2 /* MatchTypeSmallSelectionView.swift in Sources */, + FF70FB622C90584900129CC2 /* DatePickingView.swift in Sources */, + FF70FB632C90584900129CC2 /* MatchFormatRowView.swift in Sources */, FF70FB642C90584900129CC2 /* MonthData.swift in Sources */, FF70FB652C90584900129CC2 /* MenuWarningView.swift in Sources */, FF70FB662C90584900129CC2 /* TournamentBuildView.swift in Sources */, @@ -3185,6 +3322,7 @@ FF70FB682C90584900129CC2 /* CloudConvert.swift in Sources */, FF70FB692C90584900129CC2 /* EventTournamentsView.swift in Sources */, FF70FB6A2C90584900129CC2 /* DisplayContext.swift in Sources */, + FF17CA492CB915A1003C7323 /* MultiCourtPickerView.swift in Sources */, FF70FB6B2C90584900129CC2 /* TournamentCallView.swift in Sources */, FF70FB6C2C90584900129CC2 /* LoserRoundsView.swift in Sources */, FF70FB6D2C90584900129CC2 /* GroupStagesView.swift in Sources */, @@ -3192,6 +3330,7 @@ FF70FB6F2C90584900129CC2 /* URLs.swift in Sources */, FF70FB702C90584900129CC2 /* MatchDescriptor.swift in Sources */, FF70FB712C90584900129CC2 /* TournamentFormatSelectionView.swift in Sources */, + FF17CA582CC02FEB003C7323 /* CoachListView.swift in Sources */, FF70FB722C90584900129CC2 /* MatchListView.swift in Sources */, FF70FB732C90584900129CC2 /* PadelClubApp.swift in Sources */, FF70FB742C90584900129CC2 /* TournamentSettingsView.swift in Sources */, @@ -3206,12 +3345,16 @@ FF70FB7C2C90584900129CC2 /* CustomUser.swift in Sources */, C4C33F772C9B1ED4006316DE /* CodingContainer+Extensions.swift in Sources */, FF70FB7D2C90584900129CC2 /* MatchSummaryView.swift in Sources */, + FFA252B72CDD2C6C0074E63F /* OngoingDestination.swift in Sources */, FF70FB7E2C90584900129CC2 /* TournamentDurationManagerView.swift in Sources */, FF70FB7F2C90584900129CC2 /* MockData.swift in Sources */, FF70FB802C90584900129CC2 /* TeamDetailView.swift in Sources */, + FFA252AB2CDB70520074E63F /* PlayerStatisticView.swift in Sources */, FF70FB812C90584900129CC2 /* GroupStagesSettingsView.swift in Sources */, FF70FB822C90584900129CC2 /* TournamentFilterView.swift in Sources */, + FF67615B2CC8ED6900CC9BF2 /* PreviewBracketPositionView.swift in Sources */, FF70FB832C90584900129CC2 /* HtmlGenerator.swift in Sources */, + FF17CA552CBE4788003C7323 /* BracketCallingView.swift in Sources */, FF70FB842C90584900129CC2 /* PadelRule.swift in Sources */, FF70FB852C90584900129CC2 /* TeamRegistration.swift in Sources */, FF70FB862C90584900129CC2 /* Date+Extensions.swift in Sources */, @@ -3236,8 +3379,9 @@ FF70FB982C90584900129CC2 /* EventSettingsView.swift in Sources */, FF70FB992C90584900129CC2 /* InscriptionInfoView.swift in Sources */, FF70FB9A2C90584900129CC2 /* SelectablePlayerListView.swift in Sources */, - FF70FB9B2C90584900129CC2 /* MatchFormatPickerView.swift in Sources */, + FF70FB9B2C90584900129CC2 /* MatchFormatSelectionView.swift in Sources */, FF70FB9C2C90584900129CC2 /* TournamentRankView.swift in Sources */, + FF77CE5A2CCCD1FF00CBCBB4 /* GroupStageDatePickingView.swift in Sources */, FF70FB9D2C90584900129CC2 /* NumberFormatter+Extensions.swift in Sources */, FF70FB9E2C90584900129CC2 /* SetLabelView.swift in Sources */, FF70FB9F2C90584900129CC2 /* DebugSettingsView.swift in Sources */, @@ -3416,7 +3560,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3440,7 +3584,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.19; + MARKETING_VERSION = 1.0.39; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3461,7 +3605,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3484,7 +3628,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.19; + MARKETING_VERSION = 1.0.39; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3577,7 +3721,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 12; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3588,6 +3732,7 @@ INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports"; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; + INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; @@ -3600,7 +3745,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.15; + MARKETING_VERSION = 1.0.30; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3621,7 +3766,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 12; + CURRENT_PROJECT_VERSION = 2; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3631,6 +3776,7 @@ INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (ProdTest)"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports"; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; + INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; @@ -3643,7 +3789,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.15; + MARKETING_VERSION = 1.0.30; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3665,7 +3811,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; @@ -3675,6 +3821,7 @@ INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (Beta)"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports"; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; + INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; @@ -3687,7 +3834,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.14; + MARKETING_VERSION = 1.0.24; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3707,7 +3854,7 @@ CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEFINES_MODULE = YES; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_TEAM = BQ3Y44M3Q6; @@ -3716,6 +3863,7 @@ INFOPLIST_KEY_CFBundleDisplayName = "Padel Club (Beta)"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports"; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; + INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; @@ -3728,7 +3876,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.14; + MARKETING_VERSION = 1.0.24; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub.beta; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/PadelClub/Data/AppSettings.swift b/PadelClub/Data/AppSettings.swift index 60be3e3..4dbc2db 100644 --- a/PadelClub/Data/AppSettings.swift +++ b/PadelClub/Data/AppSettings.swift @@ -29,6 +29,18 @@ final class AppSettings: MicroStorable { var nationalCup: Bool var dayDuration: Int? var dayPeriod: DayPeriod + + func lastDataSourceDate() -> Date? { + guard let lastDataSource else { return nil } + return URL.importDateFormatter.date(from: lastDataSource) + } + + func localizedLastDataSource() -> String? { + guard let lastDataSource else { return nil } + guard let date = URL.importDateFormatter.date(from: lastDataSource) else { return nil } + + return date.monthYearFormatted + } func resetSearch() { tournamentAges = Set() diff --git a/PadelClub/Data/Club.swift b/PadelClub/Data/Club.swift index 19a727e..0d79ea9 100644 --- a/PadelClub/Data/Club.swift +++ b/PadelClub/Data/Club.swift @@ -12,50 +12,6 @@ import LeStorage @Observable final class Club: BaseClub { -// static func resourceName() -> String { return "clubs" } -// static func tokenExemptedMethods() -> [HTTPMethod] { return [.get] } -// static func filterByStoreIdentifier() -> Bool { return false } -// static var relationshipNames: [String] = [] -// -// var id: String = Store.randomId() -// var lastUpdate: Date -// var creator: String? -// var name: String -// var acronym: String -// var phone: String? -// var code: String? -// //var federalClubData: Data? -// var address: String? -// var city: String? -// var zipCode: String? -// var latitude: Double? -// var longitude: Double? -// var courtCount: Int = 2 -// var broadcastCode: String? -//// var alphabeticalName: Bool = false - - internal init(creator: String? = nil, name: String, acronym: String? = nil, phone: String? = nil, code: String? = nil, address: String? = nil, city: String? = nil, zipCode: String? = nil, latitude: Double? = nil, longitude: Double? = nil, courtCount: Int = 2, broadcastCode: String? = nil) { - super.init() - - self.name = name - self.creator = creator - self.acronym = acronym ?? name.acronym() - self.phone = phone - self.code = code - self.address = address - self.city = city - self.zipCode = zipCode - self.latitude = latitude - self.longitude = longitude - self.courtCount = courtCount - self.broadcastCode = broadcastCode - - } - - required init(from decoder: any Decoder) throws { - try super.init(from: decoder) - } - override func copyFromServerInstance(_ instance: any Storable) -> Bool { guard let copy = instance as? Club else { return false } self.broadcastCode = copy.broadcastCode @@ -87,44 +43,6 @@ final class Club: BaseClub { } DataStore.shared.courts.deleteDependencies(customizedCourts) } - -// enum CodingKeys: String, CodingKey { -// case _id = "id" -// case _lastUpdate = "lastUpdate" -// case _creator = "creator" -// case _name = "name" -// case _acronym = "acronym" -// case _phone = "phone" -// case _code = "code" -// case _address = "address" -// case _city = "city" -// case _zipCode = "zipCode" -// case _latitude = "latitude" -// case _longitude = "longitude" -// case _courtCount = "courtCount" -// case _broadcastCode = "broadcastCode" -//// case _alphabeticalName = "alphabeticalName" -// } -// -// func encode(to encoder: Encoder) throws { -// -// var container = encoder.container(keyedBy: CodingKeys.self) -// -// try container.encode(id, forKey: ._id) -// try container.encode(lastUpdate, forKey: ._lastUpdate) -// try container.encode(creator, forKey: ._creator) -// try container.encode(name, forKey: ._name) -// try container.encode(acronym, forKey: ._acronym) -// try container.encode(phone, forKey: ._phone) -// try container.encode(code, forKey: ._code) -// try container.encode(address, forKey: ._address) -// try container.encode(city, forKey: ._city) -// try container.encode(zipCode, forKey: ._zipCode) -// try container.encode(latitude, forKey: ._latitude) -// try container.encode(longitude, forKey: ._longitude) -// try container.encode(courtCount, forKey: ._courtCount) -// try container.encode(broadcastCode, forKey: ._broadcastCode) -// } } diff --git a/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift b/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift index ba05ab0..d08eaa6 100644 --- a/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift +++ b/PadelClub/Data/Coredata/ImportedPlayer+Extensions.swift @@ -58,8 +58,8 @@ extension ImportedPlayer: PlayerHolder { male } - func pasteData() -> String { - return [firstName?.capitalized, lastName?.capitalized, license?.computedLicense].compactMap({ $0 }).joined(separator: " ") + func pasteData(withRank: Bool = false) -> String { + return [firstName?.capitalized, lastName?.capitalized, license?.computedLicense, withRank ? "(\(rank.ordinalFormatted(feminine: isMalePlayer() == false)))" : nil].compactMap({ $0 }).joined(separator: " ") } func isNotFromCurrentDate() -> Bool { diff --git a/PadelClub/Data/DataStore.swift b/PadelClub/Data/DataStore.swift index 6b52a36..d4364d8 100644 --- a/PadelClub/Data/DataStore.swift +++ b/PadelClub/Data/DataStore.swift @@ -105,6 +105,10 @@ class DataStore: ObservableObject { } + deinit { + NotificationCenter.default.removeObserver(self) + } + func saveUser() { if user.username.count > 0 { self.userStorage.update() @@ -291,15 +295,43 @@ class DataStore: ObservableObject { func runningMatches() -> [Match] { let dateNow : Date = Date() - let lastTournaments = self.tournaments.filter { $0.isDeleted == false && $0.startDate <= dateNow }.sorted(by: \Tournament.startDate, order: .descending).prefix(10) + let lastTournaments = self.tournaments.filter { $0.isDeleted == false && $0.startDate <= dateNow && $0.hasEnded() == false }.sorted(by: \Tournament.startDate, order: .descending).prefix(10) var runningMatches: [Match] = [] for tournament in lastTournaments { let matches = tournament.tournamentStore.matches.filter { match in - match.confirmed && match.startDate != nil && match.endDate == nil } + match.disabled == false && match.isRunning() + } runningMatches.append(contentsOf: matches) } return runningMatches } + + func runningAndNextMatches() -> [Match] { + let dateNow : Date = Date() + let lastTournaments = self.tournaments.filter { $0.isDeleted == false && $0.startDate <= dateNow && $0.hasEnded() == false }.sorted(by: \Tournament.startDate, order: .descending).prefix(10) + + var runningMatches: [Match] = [] + for tournament in lastTournaments { + let matches = tournament.tournamentStore.matches.filter { match in + match.disabled == false && match.startDate != nil && match.endDate == nil } + runningMatches.append(contentsOf: matches) + } + return runningMatches + } + + func endMatches() -> [Match] { + let dateNow : Date = Date() + let lastTournaments = self.tournaments.filter { $0.isDeleted == false && $0.startDate <= dateNow && $0.hasEnded() == false }.sorted(by: \Tournament.startDate, order: .descending).prefix(10) + + var runningMatches: [Match] = [] + for tournament in lastTournaments { + let matches = tournament.tournamentStore.matches.filter { match in + match.disabled == false && match.hasEnded() } + runningMatches.append(contentsOf: matches) + } + return runningMatches.sorted(by: \.endDate!, order: .descending) + } + } diff --git a/PadelClub/Data/DrawLog.swift b/PadelClub/Data/DrawLog.swift new file mode 100644 index 0000000..55268f3 --- /dev/null +++ b/PadelClub/Data/DrawLog.swift @@ -0,0 +1,97 @@ +// +// DrawLog.swift +// PadelClub +// +// Created by razmig on 22/10/2024. +// + +import Foundation +import SwiftUI +import LeStorage + +@Observable +final class DrawLog: BaseDrawLog { + + func tournamentObject() -> Tournament? { + Store.main.findById(self.tournament) + } + + func computedBracketPosition() -> Int { + drawMatchIndex * 2 + drawTeamPosition.rawValue + } + + func updateTeamBracketPosition(_ team: TeamRegistration) { + guard let match = drawMatch() else { return } + let seedPosition: Int = match.lockAndGetSeedPosition(atTeamPosition: drawTeamPosition) + team.bracketPosition = seedPosition + tournamentObject()?.updateTeamScores(in: seedPosition) + } + + func exportedDrawLog() -> String { + [drawType.localizedDrawType(), drawDate.localizedDate(), localizedDrawLogLabel(), localizedDrawBranch()].filter({ $0.isEmpty == false }).joined(separator: " ") + } + + func localizedDrawSeedLabel() -> String { + return "\(drawType.localizedDrawType()) #\(drawSeed + 1)" + } + + func localizedDrawLogLabel() -> String { + return [localizedDrawSeedLabel(), positionLabel()].filter({ $0.isEmpty == false }).joined(separator: " -> ") + } + + func localizedDrawBranch() -> String { + switch drawType { + case .seed: + return drawTeamPosition.localizedBranchLabel() + default: + return "" + } + } + + func drawMatch() -> Match? { + switch drawType { + case .seed: + let roundIndex = RoundRule.roundIndex(fromMatchIndex: drawMatchIndex) + return tournamentStore.rounds.first(where: { $0.parent == nil && $0.index == roundIndex })?._matches().first(where: { $0.index == drawMatchIndex }) + default: + return nil + } + } + + func positionLabel() -> String { + return drawMatch()?.roundAndMatchTitle() ?? "" + } + + func roundLabel() -> String { + return drawMatch()?.roundTitle() ?? "" + } + + func matchLabel() -> String { + return drawMatch()?.matchTitle() ?? "" + } + + var tournamentStore: TournamentStore { + return TournamentLibrary.shared.store(tournamentId: self.tournament) + } + + override func deleteDependencies() { + } + +} + +enum DrawType: Int, Codable { + case seed + case groupStage + case court + + func localizedDrawType() -> String { + switch self { + case .seed: + return "Tête de série" + case .groupStage: + return "Poule" + case .court: + return "Terrain" + } + } +} diff --git a/PadelClub/Data/Federal/FederalTournament.swift b/PadelClub/Data/Federal/FederalTournament.swift index e61f36c..003218e 100644 --- a/PadelClub/Data/Federal/FederalTournament.swift +++ b/PadelClub/Data/Federal/FederalTournament.swift @@ -239,7 +239,7 @@ struct CategorieAge: Codable { return FederalTournamentAge(rawValue: id) } if let libelle { - return FederalTournamentAge.allCases.first(where: { $0.localizedLabel().localizedCaseInsensitiveContains(libelle) }) + return FederalTournamentAge.allCases.first(where: { $0.localizedFederalAgeLabel().localizedCaseInsensitiveContains(libelle) }) } return nil } diff --git a/PadelClub/Data/Gen/BaseClub.swift b/PadelClub/Data/Gen/BaseClub.swift index ad4ec3e..df595de 100644 --- a/PadelClub/Data/Gen/BaseClub.swift +++ b/PadelClub/Data/Gen/BaseClub.swift @@ -24,6 +24,7 @@ class BaseClub: SyncedModelObject, SyncedStorable { var longitude: Double? = nil var courtCount: Int = 2 var broadcastCode: String? = nil + var timezone: String? = TimeZone.current.identifier init( id: String = Store.randomId(), @@ -38,7 +39,8 @@ class BaseClub: SyncedModelObject, SyncedStorable { latitude: Double? = nil, longitude: Double? = nil, courtCount: Int = 2, - broadcastCode: String? = nil + broadcastCode: String? = nil, + timezone: String? = TimeZone.current.identifier ) { super.init() self.id = id @@ -54,6 +56,7 @@ class BaseClub: SyncedModelObject, SyncedStorable { self.longitude = longitude self.courtCount = courtCount self.broadcastCode = broadcastCode + self.timezone = timezone } enum CodingKeys: String, CodingKey { @@ -70,6 +73,7 @@ class BaseClub: SyncedModelObject, SyncedStorable { case _longitude = "longitude" case _courtCount = "courtCount" case _broadcastCode = "broadcastCode" + case _timezone = "timezone" } required init(from decoder: Decoder) throws { @@ -87,6 +91,7 @@ class BaseClub: SyncedModelObject, SyncedStorable { self.longitude = try container.decodeIfPresent(Double.self, forKey: ._longitude) ?? nil self.courtCount = try container.decodeIfPresent(Int.self, forKey: ._courtCount) ?? 2 self.broadcastCode = try container.decodeIfPresent(String.self, forKey: ._broadcastCode) ?? nil + self.timezone = try container.decodeIfPresent(String.self, forKey: ._timezone) ?? TimeZone.current.identifier try super.init(from: decoder) } @@ -105,6 +110,7 @@ class BaseClub: SyncedModelObject, SyncedStorable { try container.encode(self.longitude, forKey: ._longitude) try container.encode(self.courtCount, forKey: ._courtCount) try container.encode(self.broadcastCode, forKey: ._broadcastCode) + try container.encode(self.timezone, forKey: ._timezone) try super.encode(to: encoder) } @@ -128,6 +134,7 @@ class BaseClub: SyncedModelObject, SyncedStorable { self.longitude = club.longitude self.courtCount = club.courtCount self.broadcastCode = club.broadcastCode + self.timezone = club.timezone } static func relationships() -> [Relationship] { diff --git a/PadelClub/Data/Gen/BaseDrawLog.swift b/PadelClub/Data/Gen/BaseDrawLog.swift new file mode 100644 index 0000000..fc0cfba --- /dev/null +++ b/PadelClub/Data/Gen/BaseDrawLog.swift @@ -0,0 +1,96 @@ +// Generated by SwiftModelGenerator +// Do not modify this file manually + +import Foundation +import LeStorage +import SwiftUI + +@Observable +class BaseDrawLog: SyncedModelObject, SyncedStorable { + + static func resourceName() -> String { return "draw-logs" } + static func tokenExemptedMethods() -> [HTTPMethod] { return [] } + + var id: String = Store.randomId() + var tournament: String = "" + var drawDate: Date = Date() + var drawSeed: Int = 0 + var drawMatchIndex: Int = 0 + var drawTeamPosition: TeamPosition = TeamPosition.one + var drawType: DrawType = DrawType.seed + + init( + id: String = Store.randomId(), + tournament: String = "", + drawDate: Date = Date(), + drawSeed: Int = 0, + drawMatchIndex: Int = 0, + drawTeamPosition: TeamPosition = TeamPosition.one, + drawType: DrawType = DrawType.seed + ) { + super.init() + self.id = id + self.tournament = tournament + self.drawDate = drawDate + self.drawSeed = drawSeed + self.drawMatchIndex = drawMatchIndex + self.drawTeamPosition = drawTeamPosition + self.drawType = drawType + } + + enum CodingKeys: String, CodingKey { + case _id = "id" + case _tournament = "tournament" + case _drawDate = "drawDate" + case _drawSeed = "drawSeed" + case _drawMatchIndex = "drawMatchIndex" + case _drawTeamPosition = "drawTeamPosition" + case _drawType = "drawType" + } + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.id = try container.decodeIfPresent(String.self, forKey: ._id) ?? Store.randomId() + self.tournament = try container.decodeIfPresent(String.self, forKey: ._tournament) ?? "" + self.drawDate = try container.decodeIfPresent(Date.self, forKey: ._drawDate) ?? Date() + self.drawSeed = try container.decodeIfPresent(Int.self, forKey: ._drawSeed) ?? 0 + self.drawMatchIndex = try container.decodeIfPresent(Int.self, forKey: ._drawMatchIndex) ?? 0 + self.drawTeamPosition = try container.decodeIfPresent(TeamPosition.self, forKey: ._drawTeamPosition) ?? TeamPosition.one + self.drawType = try container.decodeIfPresent(DrawType.self, forKey: ._drawType) ?? DrawType.seed + try super.init(from: decoder) + } + + override func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.id, forKey: ._id) + try container.encode(self.tournament, forKey: ._tournament) + try container.encode(self.drawDate, forKey: ._drawDate) + try container.encode(self.drawSeed, forKey: ._drawSeed) + try container.encode(self.drawMatchIndex, forKey: ._drawMatchIndex) + try container.encode(self.drawTeamPosition, forKey: ._drawTeamPosition) + try container.encode(self.drawType, forKey: ._drawType) + try super.encode(to: encoder) + } + + func tournamentValue() -> Tournament? { + return Store.main.findById(tournament) + } + + func copy(from other: any Storable) { + guard let drawlog = other as? BaseDrawLog else { return } + self.id = drawlog.id + self.tournament = drawlog.tournament + self.drawDate = drawlog.drawDate + self.drawSeed = drawlog.drawSeed + self.drawMatchIndex = drawlog.drawMatchIndex + self.drawTeamPosition = drawlog.drawTeamPosition + self.drawType = drawlog.drawType + } + + static func relationships() -> [Relationship] { + return [ + Relationship(type: Tournament.self, keyPath: \BaseDrawLog.tournament), + ] + } + +} diff --git a/PadelClub/Data/Gen/BaseMatchScheduler.swift b/PadelClub/Data/Gen/BaseMatchScheduler.swift index 59cb283..7c33848 100644 --- a/PadelClub/Data/Gen/BaseMatchScheduler.swift +++ b/PadelClub/Data/Gen/BaseMatchScheduler.swift @@ -25,6 +25,8 @@ class BaseMatchScheduler: BaseModelObject, Storable { var groupStageChunkCount: Int? = nil var overrideCourtsUnavailability: Bool = false var shouldTryToFillUpCourtsAvailable: Bool = false + var courtsAvailable: Set = Set() + var simultaneousStart: Bool = true init( id: String = Store.randomId(), @@ -40,7 +42,9 @@ class BaseMatchScheduler: BaseModelObject, Storable { shouldEndRoundBeforeStartingNext: Bool = false, groupStageChunkCount: Int? = nil, overrideCourtsUnavailability: Bool = false, - shouldTryToFillUpCourtsAvailable: Bool = false + shouldTryToFillUpCourtsAvailable: Bool = false, + courtsAvailable: Set = Set(), + simultaneousStart: Bool = true ) { super.init() self.id = id @@ -57,6 +61,8 @@ class BaseMatchScheduler: BaseModelObject, Storable { self.groupStageChunkCount = groupStageChunkCount self.overrideCourtsUnavailability = overrideCourtsUnavailability self.shouldTryToFillUpCourtsAvailable = shouldTryToFillUpCourtsAvailable + self.courtsAvailable = courtsAvailable + self.simultaneousStart = simultaneousStart } enum CodingKeys: String, CodingKey { @@ -74,6 +80,8 @@ class BaseMatchScheduler: BaseModelObject, Storable { case _groupStageChunkCount = "groupStageChunkCount" case _overrideCourtsUnavailability = "overrideCourtsUnavailability" case _shouldTryToFillUpCourtsAvailable = "shouldTryToFillUpCourtsAvailable" + case _courtsAvailable = "courtsAvailable" + case _simultaneousStart = "simultaneousStart" } required init(from decoder: Decoder) throws { @@ -92,6 +100,8 @@ class BaseMatchScheduler: BaseModelObject, Storable { self.groupStageChunkCount = try container.decodeIfPresent(Int.self, forKey: ._groupStageChunkCount) ?? nil self.overrideCourtsUnavailability = try container.decodeIfPresent(Bool.self, forKey: ._overrideCourtsUnavailability) ?? false self.shouldTryToFillUpCourtsAvailable = try container.decodeIfPresent(Bool.self, forKey: ._shouldTryToFillUpCourtsAvailable) ?? false + self.courtsAvailable = try container.decodeIfPresent(Set.self, forKey: ._courtsAvailable) ?? Set() + self.simultaneousStart = try container.decodeIfPresent(Bool.self, forKey: ._simultaneousStart) ?? true try super.init(from: decoder) } @@ -111,6 +121,8 @@ class BaseMatchScheduler: BaseModelObject, Storable { try container.encode(self.groupStageChunkCount, forKey: ._groupStageChunkCount) try container.encode(self.overrideCourtsUnavailability, forKey: ._overrideCourtsUnavailability) try container.encode(self.shouldTryToFillUpCourtsAvailable, forKey: ._shouldTryToFillUpCourtsAvailable) + try container.encode(self.courtsAvailable, forKey: ._courtsAvailable) + try container.encode(self.simultaneousStart, forKey: ._simultaneousStart) try super.encode(to: encoder) } @@ -134,6 +146,8 @@ class BaseMatchScheduler: BaseModelObject, Storable { self.groupStageChunkCount = matchscheduler.groupStageChunkCount self.overrideCourtsUnavailability = matchscheduler.overrideCourtsUnavailability self.shouldTryToFillUpCourtsAvailable = matchscheduler.shouldTryToFillUpCourtsAvailable + self.courtsAvailable = matchscheduler.courtsAvailable + self.simultaneousStart = matchscheduler.simultaneousStart } static func relationships() -> [Relationship] { diff --git a/PadelClub/Data/Gen/BaseTournament.swift b/PadelClub/Data/Gen/BaseTournament.swift index 3f29de8..0c50107 100644 --- a/PadelClub/Data/Gen/BaseTournament.swift +++ b/PadelClub/Data/Gen/BaseTournament.swift @@ -52,6 +52,8 @@ class BaseTournament: SyncedModelObject, SyncedStorable { var hidePointsEarned: Bool = false var publishRankings: Bool = false var loserBracketMode: LoserBracketMode = .automatic + var initialSeedRound: Int = 0 + var initialSeedCount: Int = 0 init( id: String = Store.randomId(), @@ -94,7 +96,9 @@ class BaseTournament: SyncedModelObject, SyncedStorable { publishTournament: Bool = false, hidePointsEarned: Bool = false, publishRankings: Bool = false, - loserBracketMode: LoserBracketMode = .automatic + loserBracketMode: LoserBracketMode = .automatic, + initialSeedRound: Int = 0, + initialSeedCount: Int = 0 ) { super.init() self.id = id @@ -138,6 +142,8 @@ class BaseTournament: SyncedModelObject, SyncedStorable { self.hidePointsEarned = hidePointsEarned self.publishRankings = publishRankings self.loserBracketMode = loserBracketMode + self.initialSeedRound = initialSeedRound + self.initialSeedCount = initialSeedCount } enum CodingKeys: String, CodingKey { @@ -182,6 +188,8 @@ class BaseTournament: SyncedModelObject, SyncedStorable { case _hidePointsEarned = "hidePointsEarned" case _publishRankings = "publishRankings" case _loserBracketMode = "loserBracketMode" + case _initialSeedRound = "initialSeedRound" + case _initialSeedCount = "initialSeedCount" } private static func _decodePayment(container: KeyedDecodingContainer) throws -> TournamentPayment? { @@ -288,6 +296,8 @@ class BaseTournament: SyncedModelObject, SyncedStorable { self.hidePointsEarned = try container.decodeIfPresent(Bool.self, forKey: ._hidePointsEarned) ?? false self.publishRankings = try container.decodeIfPresent(Bool.self, forKey: ._publishRankings) ?? false self.loserBracketMode = try container.decodeIfPresent(LoserBracketMode.self, forKey: ._loserBracketMode) ?? .automatic + self.initialSeedRound = try container.decodeIfPresent(Int.self, forKey: ._initialSeedRound) ?? 0 + self.initialSeedCount = try container.decodeIfPresent(Int.self, forKey: ._initialSeedCount) ?? 0 try super.init(from: decoder) } @@ -334,6 +344,8 @@ class BaseTournament: SyncedModelObject, SyncedStorable { try container.encode(self.hidePointsEarned, forKey: ._hidePointsEarned) try container.encode(self.publishRankings, forKey: ._publishRankings) try container.encode(self.loserBracketMode, forKey: ._loserBracketMode) + try container.encode(self.initialSeedRound, forKey: ._initialSeedRound) + try container.encode(self.initialSeedCount, forKey: ._initialSeedCount) try super.encode(to: encoder) } @@ -385,6 +397,8 @@ class BaseTournament: SyncedModelObject, SyncedStorable { self.hidePointsEarned = tournament.hidePointsEarned self.publishRankings = tournament.publishRankings self.loserBracketMode = tournament.loserBracketMode + self.initialSeedRound = tournament.initialSeedRound + self.initialSeedCount = tournament.initialSeedCount } static func relationships() -> [Relationship] { diff --git a/PadelClub/Data/Gen/Club.json b/PadelClub/Data/Gen/Club.json index 201b30b..14a9fc7 100644 --- a/PadelClub/Data/Gen/Club.json +++ b/PadelClub/Data/Gen/Club.json @@ -79,6 +79,12 @@ "type": "String", "optional": true, "defaultValue": "nil" + }, + { + "name": "timezone", + "type": "String", + "optional": true, + "defaultValue": "TimeZone.current.identifier" } ] } diff --git a/PadelClub/Data/Gen/Drawlog.json b/PadelClub/Data/Gen/Drawlog.json new file mode 100644 index 0000000..8f872d9 --- /dev/null +++ b/PadelClub/Data/Gen/Drawlog.json @@ -0,0 +1,47 @@ + +{ + "models": [ + { + "name": "DrawLog", + "synchronizable": true, + "sideStorable": true, + "observable": true, + "relationshipNames": [], + "properties": [ + { + "name": "id", + "type": "String", + "defaultValue": "Store.randomId()" + }, + { + "name": "tournament", + "type": "String", + "foreignKey": "Tournament" + }, + { + "name": "drawDate", + "type": "Date", + "defaultValue": "Date()" + }, + { + "name": "drawSeed", + "type": "Int" + }, + { + "name": "drawMatchIndex", + "type": "Int" + }, + { + "name": "drawTeamPosition", + "type": "TeamPosition", + "defaultValue": "TeamPosition.one" + }, + { + "name": "drawType", + "type": "DrawType", + "defaultValue": "DrawType.seed" + } + ] + } + ] +} diff --git a/PadelClub/Data/Gen/MatchScheduler.json b/PadelClub/Data/Gen/MatchScheduler.json index 34eeb63..90f09d7 100644 --- a/PadelClub/Data/Gen/MatchScheduler.json +++ b/PadelClub/Data/Gen/MatchScheduler.json @@ -67,6 +67,16 @@ "name": "shouldTryToFillUpCourtsAvailable", "type": "Bool", "defaultValue": "false" + }, + { + "name": "courtsAvailable", + "type": "Set", + "defaultValue": "Set()" + }, + { + "name": "simultaneousStart", + "type": "Bool", + "defaultValue": "true" } ] } diff --git a/PadelClub/Data/Gen/Tournament.json b/PadelClub/Data/Gen/Tournament.json index c2ade84..bbec383 100644 --- a/PadelClub/Data/Gen/Tournament.json +++ b/PadelClub/Data/Gen/Tournament.json @@ -209,6 +209,16 @@ "name": "loserBracketMode", "type": "LoserBracketMode", "defaultValue": ".automatic" + }, + { + "name": "initialSeedRound", + "type": "Int", + "defaultValue": "0" + }, + { + "name": "initialSeedCount", + "type": "Int", + "defaultValue": "0" } ] } diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index c67559c..d2b5319 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -13,21 +13,6 @@ import SwiftUI @Observable final class GroupStage: BaseGroupStage, SideStorable { -// static func resourceName() -> String { "group-stages" } -// static func tokenExemptedMethods() -> [HTTPMethod] { return [] } -// static func filterByStoreIdentifier() -> Bool { return true } -// static var relationshipNames: [String] = [] -// -// var id: String = Store.randomId() -// var lastUpdate: Date -// var tournament: String -// var index: Int -// var size: Int -// private(set) var format: MatchFormat? -// var startDate: Date? -// var name: String? -// var step: Int = 0 - var matchFormat: MatchFormat { get { format ?? .defaultFormatForMatchType(.groupStage) @@ -37,26 +22,6 @@ final class GroupStage: BaseGroupStage, SideStorable { } } -// var storeId: String? = nil - - internal init(tournament: String, index: Int, size: Int, matchFormat: MatchFormat? = nil, startDate: Date? = nil, name: String? = nil, step: Int = 0) { - - super.init(tournament: tournament, index: index, size: size, format: matchFormat, startDate: startDate, name: name, step: step) - -// self.lastUpdate = Date() -// self.tournament = tournament -// self.index = index -// self.size = size -// self.format = matchFormat -// self.startDate = startDate -// self.name = name -// self.step = step - } - - required init(from decoder: any Decoder) throws { - try super.init(from: decoder) - } - var tournamentStore: TournamentStore { return TournamentLibrary.shared.store(tournamentId: self.tournament) } @@ -124,17 +89,39 @@ final class GroupStage: BaseGroupStage, SideStorable { format: self.matchFormat, name: self.localizedMatchUpLabel(for: index)) match.store = self.store + print("_createMatch(index)", index) return match } - func buildMatches() { - _removeMatches() + func removeReturnMatches(onlyLast: Bool = false) { - var matches = [Match]() + var returnMatches = _matches().filter({ $0.index >= matchCount }) + if onlyLast { + let matchPhaseCount = matchPhaseCount - 1 + returnMatches = returnMatches.filter({ $0.index >= matchCount * matchPhaseCount }) + } + do { + try self.tournamentStore.matches.delete(contentOfs: returnMatches) + } catch { + Logger.error(error) + } + } + + var matchPhaseCount: Int { + let count = _matches().count + if matchCount > 0 { + return count / matchCount + } else { + return 0 + } + } + + func addReturnMatches() { var teamScores = [TeamScore]() - + var matches = [Match]() + let matchPhaseCount = matchPhaseCount for i in 0..<_numberOfMatchesToBuild() { - let newMatch = self._createMatch(index: i) + let newMatch = self._createMatch(index: i + matchCount * matchPhaseCount) // let newMatch = Match(groupStage: self.id, index: i, matchFormat: self.matchFormat, name: localizedMatchUpLabel(for: i)) teamScores.append(contentsOf: newMatch.createTeamScores()) matches.append(newMatch) @@ -144,29 +131,71 @@ final class GroupStage: BaseGroupStage, SideStorable { self.tournamentStore.teamScores.addOrUpdate(contentOfs: teamScores) } + func buildMatches(keepExistingMatches: Bool = false) { + var teamScores = [TeamScore]() + var matches = [Match]() + clearScoreCache() + + if keepExistingMatches == false { + _removeMatches() + + for i in 0..<_numberOfMatchesToBuild() { + let newMatch = self._createMatch(index: i) + // let newMatch = Match(groupStage: self.id, index: i, matchFormat: self.matchFormat, name: localizedMatchUpLabel(for: i)) + teamScores.append(contentsOf: newMatch.createTeamScores()) + matches.append(newMatch) + } + } else { + for match in _matches() { + match.resetTeamScores(outsideOf: []) + teamScores.append(contentsOf: match.createTeamScores()) + } + } + + do { + try self.tournamentStore.matches.addOrUpdate(contentOfs: matches) + try self.tournamentStore.teamScores.addOrUpdate(contentOfs: teamScores) + } catch { + Logger.error(error) + } + } + func playedMatches() -> [Match] { let ordered = _matches() - if ordered.isEmpty == false && ordered.count == _matchOrder().count { - return _matchOrder().map { - ordered[$0] + let order = _matchOrder() + let matchCount = max(1, matchCount) + let count = ordered.count / matchCount + if ordered.isEmpty == false && ordered.count % order.count == 0 { + let repeatedArray = (0.. Int { + _matchOrder()[safe: match.index] ?? match.index + } + func updateGroupStageState() { clearScoreCache() if hasEnded(), let tournament = tournamentObject() { + let teams = teams(true) for (index, team) in teams.enumerated() { team.qualified = index < tournament.qualifiedPerGroupStage if team.bracketPosition != nil && team.qualified == false { - tournamentObject()?.resetTeamScores(in: team.bracketPosition) - team.bracketPosition = nil + tournamentObject()?.shouldVerifyBracket = true } } + try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams) + if let tournamentObject = tournamentObject() { + try DataStore.shared.tournaments.addOrUpdate(instance: tournamentObject) + } self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams) let groupStagesAreOverAtFirstStep = tournament.groupStagesAreOver(atStep: 0) @@ -183,8 +212,8 @@ final class GroupStage: BaseGroupStage, SideStorable { func scoreLabel(forGroupStagePosition groupStagePosition: Int, score: TeamGroupStageScore? = nil) -> (wins: String, losses: String, setsDifference: String?, gamesDifference: String?)? { if let scoreData = (score ?? _score(forGroupStagePosition: groupStagePosition, nilIfEmpty: true)) { let hideSetDifference = matchFormat.setsToWin == 1 - let setDifference = scoreData.setDifference.formatted(.number.sign(strategy: .always(includingZero: false))) - let gameDifference = scoreData.gameDifference.formatted(.number.sign(strategy: .always(includingZero: false))) + let setDifference = scoreData.setDifference.formatted(.number.sign(strategy: .always(includingZero: true))) + " set" + scoreData.setDifference.pluralSuffix + let gameDifference = scoreData.gameDifference.formatted(.number.sign(strategy: .always(includingZero: true))) + " jeu" + scoreData.gameDifference.localizedPluralSuffix("x") return (wins: scoreData.wins.formatted(), losses: scoreData.loses.formatted(), setsDifference: hideSetDifference ? nil : setDifference, gamesDifference: gameDifference) // return "\(scoreData.wins)/\(scoreData.loses) " + differenceAsString } else { @@ -218,7 +247,7 @@ final class GroupStage: BaseGroupStage, SideStorable { matchIndexes.append(index) } } - return _matches().filter { matchIndexes.contains($0.index) } + return _matches().filter { matchIndexes.contains($0.index%matchCount) } } func initialStartDate(forTeam team: TeamRegistration) -> Date? { @@ -238,7 +267,7 @@ final class GroupStage: BaseGroupStage, SideStorable { return _matches().first(where: { matchIndexes.contains($0.index) }) } - func availableToStart(playedMatches: [Match], in runningMatches: [Match]) -> [Match] { + func availableToStart(playedMatches: [Match], in runningMatches: [Match], checkCanPlay: Bool = true) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { @@ -246,7 +275,7 @@ final class GroupStage: BaseGroupStage, SideStorable { print("func group stage availableToStart", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif - return playedMatches.filter({ $0.canBeStarted(inMatches: runningMatches) && $0.isRunning() == false }) + return playedMatches.filter({ $0.isRunning() == false && $0.canBeStarted(inMatches: runningMatches, checkCanPlay: checkCanPlay) }).sorted(by: \.computedStartDateForSorting) } func runningMatches(playedMatches: [Match]) -> [Match] { @@ -282,40 +311,67 @@ final class GroupStage: BaseGroupStage, SideStorable { return playedMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed() } + func isReturnMatchEnabled() -> Bool { + _matches().count > matchCount + } + private func _matchOrder() -> [Int] { + var order: [Int] + switch size { case 3: - return [1, 2, 0] + order = [1, 2, 0] case 4: - return [2, 3, 1, 4, 5, 0] + order = [2, 3, 1, 4, 5, 0] case 5: -// return [5, 8, 0, 7, 3, 4, 2, 6, 1, 9] - return [3, 5, 8, 2, 6, 1, 9, 4, 7, 0] + order = [3, 5, 8, 2, 6, 1, 9, 4, 7, 0] case 6: - //return [1, 7, 13, 11, 3, 6, 10, 2, 8, 12, 5, 4, 9, 14, 0] - return [4, 7, 9, 3, 6, 11, 2, 8, 10, 1, 13, 5, 12, 14, 0] + order = [4, 7, 9, 3, 6, 11, 2, 8, 10, 1, 13, 5, 12, 14, 0] default: - return [] + order = [] } + + return order } - + + func indexOf(_ matchIndex: Int) -> Int { _matchOrder().firstIndex(of: matchIndex) ?? matchIndex } - private func _matchUp(for matchIndex: Int) -> [Int] { - Array((0.. [Int] { + let combinations = Array((0.. String { + if matchCount > 0 { + let count = _matches().count + if count > matchCount * 2 { + return " - vague \((matchIndex / matchCount) + 1)" + } + + if matchIndex >= matchCount { + return " - retour" + } + } + + return "" } func localizedMatchUpLabel(for matchIndex: Int) -> String { let matchUp = _matchUp(for: matchIndex) if let index = matchUp.first, let index2 = matchUp.last { - return "#\(index + 1) vs #\(index2 + 1)" + return "#\(index + 1) vs #\(index2 + 1)" + returnMatchesSuffix(for: matchIndex) } else { return "--" } } + var matchCount: Int { + (size * (size - 1)) / 2 + } + func team(teamPosition team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? { let _teams = _teams(for: matchIndex) switch team { @@ -328,7 +384,7 @@ final class GroupStage: BaseGroupStage, SideStorable { private func _teams(for matchIndex: Int) -> [TeamRegistration?] { let combinations = Array(0.. Bool { let indexes = [teamPosition, otherTeam].compactMap({ $0.groupStagePosition }).sorted() let combos = Array((0.. 1 { + let scoreA = calculateScore(for: teamPosition, matches: matches, groupStagePosition: teamPosition.groupStagePosition!) + let scoreB = calculateScore(for: otherTeam, matches: matches, groupStagePosition: otherTeam.groupStagePosition!) + + let teamsSorted = [scoreA, scoreB].sorted { (lhs, rhs) in + let predicates: [TeamScoreAreInIncreasingOrder] = [ + { $0.wins < $1.wins }, + { $0.setDifference < $1.setDifference }, + { $0.gameDifference < $1.gameDifference}, + { [self] in $0.team.groupStagePositionAtStep(self.step)! > $1.team.groupStagePositionAtStep(self.step)! } + ] + + for predicate in predicates { + if !predicate(lhs, rhs) && !predicate(rhs, lhs) { + continue + } + + return predicate(lhs, rhs) + } + + return false + }.map({ $0.team }) + + return teamsSorted.first == teamPosition } else { - return false + + if let matchIndex = combos.firstIndex(of: indexes), let match = _matches().first(where: { $0.index == matchIndex }) { + return teamPosition.id == match.losingTeamId + } else { + return false + } + } } @@ -415,16 +501,19 @@ final class GroupStage: BaseGroupStage, SideStorable { guard let team = teamAt(groupStagePosition: groupStagePosition) else { return nil } let matches = matches(forGroupStagePosition: groupStagePosition).filter({ $0.hasEnded() }) if matches.isEmpty && nilIfEmpty { return nil } + let score = calculateScore(for: team, matches: matches, groupStagePosition: groupStagePosition) + scoreCache[groupStagePosition] = score + return score + } + + private func calculateScore(for team: TeamRegistration, matches: [Match], groupStagePosition: Int) -> TeamGroupStageScore { let wins = matches.filter { $0.winningTeamId == team.id }.count let loses = matches.filter { $0.losingTeamId == team.id }.count let differences = matches.compactMap { $0.scoreDifference(groupStagePosition, atStep: step) } let setDifference = differences.map { $0.set }.reduce(0,+) let gameDifference = differences.map { $0.game }.reduce(0,+) - // Calculate the score and store it in the cache - let score = (team, wins, loses, setDifference, gameDifference) - scoreCache[groupStagePosition] = score - return score + return (team, wins, loses, setDifference, gameDifference) } // Clear the cache if necessary, for example when starting a new step or when matches update diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index ec04697..43f177b 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -11,10 +11,9 @@ import LeStorage @Observable final class Match: BaseMatch, SideStorable { -// static func resourceName() -> String { "matches" } -// static func tokenExemptedMethods() -> [HTTPMethod] { return [] } -// static func filterByStoreIdentifier() -> Bool { return true } -// static var relationshipNames: [String] = ["round", "groupStage"] + static func == (lhs: Match, rhs: Match) -> Bool { + lhs.id == rhs.id && lhs.startDate == rhs.startDate + } static func setServerTitle(upperRound: Round, matchIndex: Int) -> String { if upperRound.index == 0 { return upperRound.roundTitle() } @@ -23,54 +22,30 @@ final class Match: BaseMatch, SideStorable { var byeState: Bool = false -// var id: String = Store.randomId() -// var lastUpdate: Date -// var round: String? -// var groupStage: String? -// var startDate: Date? -// var endDate: Date? -// var index: Int -// var format: MatchFormat? -// //var court: String? -// var servingTeamId: String? -// var winningTeamId: String? -// var losingTeamId: String? -// //var broadcasted: Bool -// var name: String? -// //var order: Int -// var disabled: Bool = false -// private(set) var courtIndex: Int? -// var confirmed: Bool = false -// -// var storeId: String? = nil - init(round: String? = nil, groupStage: String? = nil, startDate: Date? = nil, endDate: Date? = nil, index: Int, format: MatchFormat? = nil, servingTeamId: String? = nil, winningTeamId: String? = nil, losingTeamId: String? = nil, name: String? = nil, disabled: Bool = false, courtIndex: Int? = nil, confirmed: Bool = false) { super.init(round: round, groupStage: groupStage, startDate: startDate, endDate: endDate, index: index, format: format, servingTeamId: servingTeamId, winningTeamId: winningTeamId, losingTeamId: losingTeamId, name: name, disabled: disabled, courtIndex: courtIndex, confirmed: confirmed) -// self.lastUpdate = Date() -// self.round = round -// self.groupStage = groupStage -// self.startDate = startDate -// self.endDate = endDate -// self.index = index -// self.format = matchFormat -// //self.court = court -// self.servingTeamId = servingTeamId -// self.winningTeamId = winningTeamId -// self.losingTeamId = losingTeamId -// self.disabled = disabled -// self.name = name -// self.courtIndex = courtIndex -// self.confirmed = confirmed -//// self.broadcasted = broadcasted -//// self.order = order } required init(from decoder: Decoder) throws { try super.init(from: decoder) } + func setMatchName(_ serverName: String?) { + self.name = serverName + } + + func isFromLastRound() -> Bool { + guard let roundObject, roundObject.parent == nil else { return false } + guard let currentTournament = currentTournament() else { return false } + if currentTournament.rounds().count - 1 == roundObject.index { + return true + } else { + return false + } + } + var tournamentStore: TournamentStore { if let id = self.store?.identifier { return TournamentLibrary.shared.store(tournamentId: id) @@ -119,7 +94,7 @@ defer { } func matchWarningMessage() -> String { - [roundTitle(), matchTitle(.short), startDate?.localizedDate(), courtName()].compacted().joined(separator: "\n") + [roundAndMatchTitle(), startDate?.localizedDate(), courtName(), matchFormat.computedLongLabel].compacted().joined(separator: "\n") } func matchTitle(_ displayStyle: DisplayStyle = .wide, inMatches matches: [Match]? = nil) -> String { @@ -142,7 +117,7 @@ defer { case .wide, .title: return "Match \(indexInRound(in: matches) + 1)" case .short: - return "#\(indexInRound(in: matches) + 1)" + return "n˚\(indexInRound(in: matches) + 1)" } } @@ -155,22 +130,8 @@ defer { } @discardableResult - func lockAndGetSeedPosition(atTeamPosition slot: TeamPosition?, opposingSeeding: Bool = false) -> Int { + func lockAndGetSeedPosition(atTeamPosition teamPosition: TeamPosition) -> Int { let matchIndex = index - var teamPosition : TeamPosition { - if let slot { - return slot - } else { - let seedRound = RoundRule.roundIndex(fromMatchIndex: matchIndex) - let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: seedRound) - let isUpper = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) < (numberOfMatches / 2) - var teamPosition = slot ?? (isUpper ? .one : .two) - if opposingSeeding { - teamPosition = slot ?? (isUpper ? .two : .one) - } - return teamPosition - } - } previousMatch(teamPosition)?.disableMatch() return matchIndex * 2 + teamPosition.rawValue } @@ -191,6 +152,12 @@ defer { return self.tournamentStore.teamRegistrations.findById(winningTeamId) } + func loser() -> TeamRegistration? { + guard let losingTeamId else { return nil } + return self.tournamentStore.teamRegistrations.findById(losingTeamId) + } + + func localizedStartDate() -> String { if let startDate { return startDate.formatted(date: .abbreviated, time: .shortened) @@ -211,8 +178,8 @@ defer { } func cleanScheduleAndSave(_ targetStartDate: Date? = nil) { - startDate = targetStartDate - confirmed = targetStartDate == nil ? false : true + startDate = targetStartDate ?? startDate + confirmed = false endDate = nil followingMatch()?.cleanScheduleAndSave(nil) _loserMatch()?.cleanScheduleAndSave(nil) @@ -228,6 +195,7 @@ defer { groupStageObject?.updateGroupStageState() roundObject?.updateTournamentState() currentTournament()?.updateTournamentState() + teams().forEach({ $0.resetRestingTime() }) } func resetScores() { @@ -312,15 +280,21 @@ defer { guard let forwardMatch = _forwardMatch(inRound: roundObject) else { return } guard let next = _otherMatch() else { return } if next.disabled && byeState == false && next.byeState == false { - forwardMatch.byeState = false - forwardMatch._toggleMatchDisableState(state, forward: true) + if forwardMatch.disabled != state || forwardMatch.byeState { + forwardMatch.byeState = false + forwardMatch._toggleMatchDisableState(state, forward: true) + } } else if byeState && next.byeState { print("don't disable forward match") - forwardMatch.byeState = false - forwardMatch._toggleMatchDisableState(false, forward: true) + if forwardMatch.byeState || forwardMatch.disabled { + forwardMatch.byeState = false + forwardMatch._toggleMatchDisableState(false, forward: true) + } } else { - forwardMatch.byeState = true - forwardMatch._toggleMatchDisableState(state, forward: true) + if forwardMatch.byeState == false || forwardMatch.disabled != state { + forwardMatch.byeState = true + forwardMatch._toggleMatchDisableState(state, forward: true) + } } // if next.disabled == false { @@ -359,12 +333,18 @@ defer { func _toggleMatchDisableState(_ state: Bool, forward: Bool = false, single: Bool = false) { //if disabled == state { return } + let currentState = disabled disabled = state - if disabled { - self.tournamentStore.teamScores.delete(contentOfs: teamScores) + + if disabled != currentState { + do { + try self.tournamentStore.teamScores.delete(contentOfs: teamScores) + } catch { + Logger.error(error) + } } - if state == true { + if state == true, state != currentState { let teams = teams() for team in teams { if isSeededBy(team: team) { @@ -374,7 +354,14 @@ defer { } } //byeState = false - self.tournamentStore.matches.addOrUpdate(instance: self) + roundObject?._cachedSeedInterval = nil + name = nil + do { + try self.tournamentStore.matches.addOrUpdate(instance: self) + } catch { + Logger.error(error) + } + if single == false { _toggleLoserMatchDisableState(state) if forward { @@ -408,14 +395,14 @@ defer { } } - func roundTitle() -> String? { + func roundTitle(_ displayStyle: DisplayStyle = .wide) -> String? { if groupStage != nil { return groupStageObject?.groupStageTitle() } else if let roundObject { return roundObject.roundTitle() } else { return nil } } - func roundAndMatchTitle() -> String { - [roundTitle(), matchTitle()].compactMap({ $0 }).joined(separator: " ") + func roundAndMatchTitle(_ displayStyle: DisplayStyle = .wide) -> String { + [roundTitle(displayStyle), matchTitle(displayStyle)].compactMap({ $0 }).joined(separator: " ") } func topPreviousRoundMatchIndex() -> Int { @@ -452,12 +439,26 @@ defer { } } + func loserMatches() -> [Match] { + guard let roundObject else { return [] } + return [roundObject.upperBracketTopMatch(ofMatchIndex: index, previousRound: nil), roundObject.upperBracketBottomMatch(ofMatchIndex: index, previousRound: nil)].compactMap({ $0 }) + } + + func loserMatch(_ teamPosition: TeamPosition) -> Match? { + if teamPosition == .one { + return roundObject?.upperBracketTopMatch(ofMatchIndex: index, previousRound: nil) + } else { + return roundObject?.upperBracketBottomMatch(ofMatchIndex: index, previousRound: nil) + } + + } + var computedOrder: Int { if let groupStageObject { return (groupStageObject.index + 1) * 100 + groupStageObject.indexOf(index) } guard let roundObject else { return index } - return roundObject.isLoserBracket() ? (roundObject.index + 1) * 1000 + indexInRound() : (roundObject.index + 1) * 10000 + indexInRound() + return roundObject.isLoserBracket() ? (roundObject.index + 1) * 10000 + indexInRound() : (roundObject.index + 1) * 1000 + indexInRound() } func previousMatches() -> [Match] { @@ -487,7 +488,7 @@ defer { if endDate == nil { endDate = Date() } - + teams().forEach({ $0.resetRestingTime() }) winningTeamId = teamScoreWinning.teamRegistration losingTeamId = teamScoreWalkout.teamRegistration groupStageObject?.updateGroupStageState() @@ -503,6 +504,8 @@ defer { } if startDate == nil { startDate = endDate?.addingTimeInterval(Double(-getDuration()*60)) + } else if let startDate, let endDate, startDate >= endDate { + self.startDate = endDate.addingTimeInterval(Double(-getDuration()*60)) } let teamOne = team(matchDescriptor.winner) @@ -510,7 +513,9 @@ defer { teamOne?.hasArrived() teamTwo?.hasArrived() - + teamOne?.resetRestingTime() + teamTwo?.resetRestingTime() + winningTeamId = teamOne?.id losingTeamId = teamTwo?.id @@ -518,7 +523,17 @@ defer { groupStageObject?.updateGroupStageState() roundObject?.updateTournamentState() - currentTournament()?.updateTournamentState() + if let tournament = currentTournament(), let endDate, let startDate { + if endDate.isEarlierThan(tournament.startDate) { + tournament.startDate = startDate + } + do { + try DataStore.shared.tournaments.addOrUpdate(instance: tournament) + } catch { + Logger.error(error) + } + tournament.updateTournamentState() + } updateFollowingMatchTeamScore() } @@ -570,7 +585,7 @@ defer { } } - func validateMatch(fromStartDate: Date, toEndDate: Date, fieldSetup: MatchFieldSetup) { + func validateMatch(fromStartDate: Date, toEndDate: Date, fieldSetup: MatchFieldSetup, forced: Bool = false) { if hasEnded() == false { startDate = fromStartDate @@ -580,7 +595,8 @@ defer { setCourt(_courtIndex) } case .random: - if let _courtIndex = availableCourts().randomElement() { + let runningMatches: [Match] = DataStore.shared.runningMatches() + if let _courtIndex = availableCourts(runningMatches: runningMatches).randomElement() { setCourt(_courtIndex) } case .field(let _courtIndex): @@ -591,8 +607,12 @@ defer { startDate = fromStartDate endDate = toEndDate } - - confirmed = true + + if let startDate, startDate.timeIntervalSinceNow <= 300 { + confirmed = true + } else { + confirmed = false + } } func courtName() -> String? { @@ -604,12 +624,20 @@ defer { } } + func courtName(for selectedIndex: Int) -> String { + if let courtName = currentTournament()?.courtName(atIndex: selectedIndex) { + return courtName + } else { + return Court.courtIndexedTitle(atIndex: selectedIndex) + } + } + func courtCount() -> Int { return currentTournament()?.courtCount ?? 1 } - func courtIsAvailable(_ courtIndex: Int) -> Bool { - let courtUsed = currentTournament()?.courtUsed() ?? [] + func courtIsAvailable(_ courtIndex: Int, in runningMatches: [Match]) -> Bool { + let courtUsed = currentTournament()?.courtUsed(runningMatches: runningMatches) ?? [] return courtUsed.contains(courtIndex) == false } @@ -622,9 +650,9 @@ defer { return availableCourts } - func availableCourts() -> [Int] { - let courtUsed = currentTournament()?.courtUsed() ?? [] - return Array(Set(allCourts().map { $0 }).subtracting(Set(courtUsed))) + func availableCourts(runningMatches: [Match]) -> [Int] { + let courtUsed = currentTournament()?.courtUsed(runningMatches: runningMatches) ?? [] + return Set(allCourts().map { $0 }).subtracting(Set(courtUsed)).sorted() } func removeCourt() { @@ -635,12 +663,17 @@ defer { self.courtIndex = courtIndex } - func canBeStarted(inMatches matches: [Match]) -> Bool { + func canBeStarted(inMatches matches: [Match], checkCanPlay: Bool) -> Bool { let teams = teamScores - guard teams.count == 2 else { return false } + guard teams.count == 2 else { + //print("teams.count != 2") + return false + } guard hasEnded() == false else { return false } guard hasStarted() == false else { return false } - return teams.compactMap({ $0.team }).allSatisfy({ $0.canPlay() && isTeamPlaying($0, inMatches: matches) == false }) + return teams.compactMap({ $0.team }).allSatisfy({ + ((checkCanPlay && $0.canPlay()) || checkCanPlay == false) && isTeamPlaying($0, inMatches: matches) == false + }) } func isTeamPlaying(_ team: TeamRegistration, inMatches matches: [Match]) -> Bool { @@ -736,8 +769,24 @@ defer { } else { setDifference = zip.filter { $0 > $1 }.count - zip.filter { $1 > $0 }.count } - let gameDifference = zip.map { ($0, $1) }.map { $0.0 - $0.1 }.reduce(0,+) - return (setDifference * reverseValue, gameDifference * reverseValue) + + // si 3 sets et 3eme set super tie break, different des 2 premiers sets, alors super tie points ne sont pas des jeux et doivent etre compté comme un jeu + + if matchFormat.canSuperTie, endedSetsOne.count == 3 { + let games = zip.map { ($0, $1) } + let gameDifference = games.enumerated().map({ index, pair in + if index < 2 { + return pair.0 - pair.1 + } else { + return pair.0 < pair.1 ? -1 : 1 + } + }) + .reduce(0,+) + return (setDifference * reverseValue, gameDifference * reverseValue) + } else { + let gameDifference = zip.map { ($0, $1) }.map { $0.0 - $0.1 }.reduce(0,+) + return (setDifference * reverseValue, gameDifference * reverseValue) + } } func groupStageProjectedTeam(_ team: TeamPosition) -> TeamRegistration? { @@ -798,7 +847,7 @@ defer { func hasStarted() -> Bool { // meaning at least one match is over if let startDate { - return startDate.timeIntervalSinceNow < 0 + return startDate.timeIntervalSinceNow < 0 && confirmed } if hasEnded() { return true @@ -843,48 +892,111 @@ defer { } } -// enum CodingKeys: String, CodingKey { -// case _id = "id" -// case _storeId = "storeId" -// case _lastUpdate = "lastUpdate" -// case _round = "round" -// case _groupStage = "groupStage" -// case _startDate = "startDate" -// case _endDate = "endDate" -// case _index = "index" -// case _format = "format" -//// case _court = "court" -// case _courtIndex = "courtIndex" -// case _servingTeamId = "servingTeamId" -// case _winningTeamId = "winningTeamId" -// case _losingTeamId = "losingTeamId" -//// case _broadcasted = "broadcasted" -// case _name = "name" -//// case _order = "order" -// case _disabled = "disabled" -// case _confirmed = "confirmed" -// } -// -// func encode(to encoder: Encoder) throws { -// var container = encoder.container(keyedBy: CodingKeys.self) -// -// try container.encode(id, forKey: ._id) -// try container.encode(storeId, forKey: ._storeId) -// try container.encode(lastUpdate, forKey: ._lastUpdate) -// try container.encode(round, forKey: ._round) -// try container.encode(groupStage, forKey: ._groupStage) -// try container.encode(startDate, forKey: ._startDate) -// try container.encode(endDate, forKey: ._endDate) -// try container.encode(format, forKey: ._format) -// try container.encode(servingTeamId, forKey: ._servingTeamId) -// try container.encode(index, forKey: ._index) -// try container.encode(winningTeamId, forKey: ._winningTeamId) -// try container.encode(losingTeamId, forKey: ._losingTeamId) -// try container.encode(name, forKey: ._name) -// try container.encode(disabled, forKey: ._disabled) -// try container.encode(courtIndex, forKey: ._courtIndex) -// try container.encode(confirmed, forKey: ._confirmed) -// } + var restingTimeForSorting: TimeInterval { + (teams().compactMap({ $0.restingTime() }).max() ?? .distantFuture).timeIntervalSinceNow + } + + func isValidSpot() -> Bool { + previousMatches().allSatisfy({ $0.isSeeded() == false }) + } + + func expectedToBeRunning() -> Bool { + guard let startDate else { return false } + return confirmed == false && startDate.timeIntervalSinceNow < 0 + } + + func expectedFormattedStartDate(canBePlayedInSpecifiedCourt: Bool, availableCourts: [Int], estimatedStartDate: CourtIndexAndDate?, updatedField: Int?) -> String { + guard let startDate else { return "" } + guard hasEnded() == false, isRunning() == false else { return "" } + let depthReadiness = depthReadiness() + if depthReadiness == 0 { + if canBePlayedInSpecifiedCourt { + return "possible tout de suite" + } else if let updatedField, availableCourts.contains(updatedField) { + return "possible tout de suite \(courtName(for: updatedField))" + } else if let first = availableCourts.first { + return "possible tout de suite \(courtName(for: first))" + } else if let estimatedStartDate { + return "dans ~" + estimatedStartDate.1.timeElapsedString() + " " + courtName(for: estimatedStartDate.0) + } + return "était prévu à " + startDate.formattedAsHourMinute() + } else if depthReadiness == 1 { + return "possible prochaine rotation" + } else { + return "dans \(depthReadiness) rotation\(depthReadiness.pluralSuffix), ~\((getDuration() * depthReadiness).durationInHourMinutes())" + } + } + + func runningDuration() -> String { + guard let startDate else { return "" } + return " depuis " + startDate.timeElapsedString() + } + + func canBePlayedInSpecifiedCourt(runningMatches: [Match]) -> Bool { + guard let courtIndex else { return false } + if expectedToBeRunning() { + return courtIsAvailable(courtIndex, in: runningMatches) + } else { + return true + } + } + + typealias CourtIndexAndDate = (courtIndex: Int, startDate: Date) + + func nextCourtsAvailable(availableCourts: [Int], runningMatches: [Match]) -> [CourtIndexAndDate] { + guard let tournament = currentTournament() else { return [] } + let startDate = Date().withoutSeconds() + if runningMatches.isEmpty { + return availableCourts.map { + ($0, startDate) + } + } + + let optionalDates : [CourtIndexAndDate?] = runningMatches.map({ match in + guard let endDate = match.estimatedEndDate(tournament.additionalEstimationDuration) else { return nil } + guard let courtIndex = match.courtIndex else { return nil } + if endDate <= startDate { + return (courtIndex, startDate.addingTimeInterval(600)) + } else { + return (courtIndex, endDate) + } + }) + + let dates : [CourtIndexAndDate] = optionalDates.compacted().sorted { a, b in + a.1 < b.1 + } + return dates + } + + func estimatedStartDate(availableCourts: [Int], runningMatches: [Match]) -> CourtIndexAndDate? { + guard isReady() else { return nil } + guard let tournament = currentTournament() else { return nil } + let availableCourts = nextCourtsAvailable(availableCourts: availableCourts, runningMatches: runningMatches) + return availableCourts.first(where: { (courtIndex, startDate) in + let endDate = startDate.addingTimeInterval(TimeInterval(matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) * 60) + if tournament.courtUnavailable(courtIndex: courtIndex, from: startDate, to: endDate) == false { + return true + } + + return false + }) + } + + func depthReadiness() -> Int { + // Base case: If this match is ready, the depth is 0 + if isReady() { + return 0 + } + + // Recursive case: If not ready, check the maximum depth of readiness among previous matches + // If previousMatches() is empty, return a default depth of -1 + let previousDepth = ancestors().map { $0.depthReadiness() }.max() ?? -1 + return previousDepth + 1 + } + + func ancestors() -> [Match] { + previousMatches() + loserMatches() + } func insertOnServer() { self.tournamentStore.matches.writeChangeAndInsertOnServer(instance: self) @@ -899,6 +1011,8 @@ enum MatchDateSetup: Hashable, Identifiable { case inMinutes(Int) case now case customDate + case previousRotation + case nextRotation var id: Int { hashValue } } diff --git a/PadelClub/Data/MatchScheduler.swift b/PadelClub/Data/MatchScheduler.swift index eccb7a4..c0845a4 100644 --- a/PadelClub/Data/MatchScheduler.swift +++ b/PadelClub/Data/MatchScheduler.swift @@ -11,75 +11,44 @@ import SwiftUI @Observable final class MatchScheduler: BaseMatchScheduler, SideStorable { - -// static func resourceName() -> String { return "match-scheduler" } -// static func tokenExemptedMethods() -> [HTTPMethod] { return [] } -// static func filterByStoreIdentifier() -> Bool { return false } -// static var relationshipNames: [String] = [] -// -// private(set) var id: String = Store.randomId() -// var tournament: String -// var timeDifferenceLimit: Int -// var loserBracketRotationDifference: Int -// var upperBracketRotationDifference: Int -// var accountUpperBracketBreakTime: Bool -// var accountLoserBracketBreakTime: Bool -// var randomizeCourts: Bool -// var rotationDifferenceIsImportant: Bool -// var shouldHandleUpperRoundSlice: Bool -// var shouldEndRoundBeforeStartingNext: Bool -// var groupStageChunkCount: Int? -// var overrideCourtsUnavailability: Bool = false -// var shouldTryToFillUpCourtsAvailable: Bool = false - - init(tournament: String, - timeDifferenceLimit: Int = 5, - loserBracketRotationDifference: Int = 0, - upperBracketRotationDifference: Int = 1, - accountUpperBracketBreakTime: Bool = true, - accountLoserBracketBreakTime: Bool = false, - randomizeCourts: Bool = true, - rotationDifferenceIsImportant: Bool = false, - shouldHandleUpperRoundSlice: Bool = true, - shouldEndRoundBeforeStartingNext: Bool = true, - groupStageChunkCount: Int? = nil, overrideCourtsUnavailability: Bool = false, shouldTryToFillUpCourtsAvailable: Bool = false) { - super.init() - self.tournament = tournament - self.timeDifferenceLimit = timeDifferenceLimit - self.loserBracketRotationDifference = loserBracketRotationDifference - self.upperBracketRotationDifference = upperBracketRotationDifference - self.accountUpperBracketBreakTime = accountUpperBracketBreakTime - self.accountLoserBracketBreakTime = accountLoserBracketBreakTime - self.randomizeCourts = randomizeCourts - self.rotationDifferenceIsImportant = rotationDifferenceIsImportant - self.shouldHandleUpperRoundSlice = shouldHandleUpperRoundSlice - self.shouldEndRoundBeforeStartingNext = shouldEndRoundBeforeStartingNext - self.groupStageChunkCount = groupStageChunkCount - self.overrideCourtsUnavailability = overrideCourtsUnavailability - self.shouldTryToFillUpCourtsAvailable = shouldTryToFillUpCourtsAvailable - } - - required init(from decoder: any Decoder) throws { - try super.init(from: decoder) - } - -// enum CodingKeys: String, CodingKey { -// case _id = "id" -// case _tournament = "tournament" -// case _timeDifferenceLimit = "timeDifferenceLimit" -// case _loserBracketRotationDifference = "loserBracketRotationDifference" -// case _upperBracketRotationDifference = "upperBracketRotationDifference" -// case _accountUpperBracketBreakTime = "accountUpperBracketBreakTime" -// case _accountLoserBracketBreakTime = "accountLoserBracketBreakTime" -// case _randomizeCourts = "randomizeCourts" -// case _rotationDifferenceIsImportant = "rotationDifferenceIsImportant" -// case _shouldHandleUpperRoundSlice = "shouldHandleUpperRoundSlice" -// case _shouldEndRoundBeforeStartingNext = "shouldEndRoundBeforeStartingNext" -// case _groupStageChunkCount = "groupStageChunkCount" -// case _overrideCourtsUnavailability = "overrideCourtsUnavailability" -// case _shouldTryToFillUpCourtsAvailable = "shouldTryToFillUpCourtsAvailable" +// +// init(tournament: String, +// timeDifferenceLimit: Int = 5, +// loserBracketRotationDifference: Int = 0, +// upperBracketRotationDifference: Int = 1, +// accountUpperBracketBreakTime: Bool = true, +// accountLoserBracketBreakTime: Bool = false, +// randomizeCourts: Bool = true, +// rotationDifferenceIsImportant: Bool = false, +// shouldHandleUpperRoundSlice: Bool = false, +// shouldEndRoundBeforeStartingNext: Bool = true, +//<<<<<<< HEAD +// groupStageChunkCount: Int? = nil, overrideCourtsUnavailability: Bool = false, shouldTryToFillUpCourtsAvailable: Bool = false) { +// super.init() +//======= +// groupStageChunkCount: Int? = nil, +// overrideCourtsUnavailability: Bool = false, +// shouldTryToFillUpCourtsAvailable: Bool = true, +// courtsAvailable: Set = Set(), +// simultaneousStart: Bool = true) { +//>>>>>>> main +// self.tournament = tournament +// self.timeDifferenceLimit = timeDifferenceLimit +// self.loserBracketRotationDifference = loserBracketRotationDifference +// self.upperBracketRotationDifference = upperBracketRotationDifference +// self.accountUpperBracketBreakTime = accountUpperBracketBreakTime +// self.accountLoserBracketBreakTime = accountLoserBracketBreakTime +// self.randomizeCourts = randomizeCourts +// self.rotationDifferenceIsImportant = rotationDifferenceIsImportant +// self.shouldHandleUpperRoundSlice = shouldHandleUpperRoundSlice +// self.shouldEndRoundBeforeStartingNext = shouldEndRoundBeforeStartingNext +// self.groupStageChunkCount = groupStageChunkCount +// self.overrideCourtsUnavailability = overrideCourtsUnavailability +// self.shouldTryToFillUpCourtsAvailable = shouldTryToFillUpCourtsAvailable +// self.courtsAvailable = courtsAvailable +// self.simultaneousStart = simultaneousStart // } - + var courtsUnavailability: [DateInterval]? { guard let event = tournamentObject()?.eventObject() else { return nil } return event.courtsUnavailability + (overrideCourtsUnavailability ? [] : event.tournamentsCourtsUsed(exluding: tournament)) @@ -105,7 +74,6 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable { if let specificGroupStage { groupStages = [specificGroupStage] } - let numberOfCourtsAvailablePerRotation: Int = tournament.courtCount let matches = groupStages.flatMap { $0._matches() } matches.forEach({ @@ -133,7 +101,7 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable { lastDate = time } let groups = groupStages.filter({ $0.startDate == time }) - let dispatch = groupStageDispatcher(numberOfCourtsAvailablePerRotation: numberOfCourtsAvailablePerRotation, groupStages: groups, startingDate: lastDate) + let dispatch = groupStageDispatcher(groupStages: groups, startingDate: lastDate) dispatch.timedMatches.forEach { matchSchedule in if let match = matches.first(where: { $0.id == matchSchedule.matchID }) { @@ -157,7 +125,7 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable { Logger.error(error) } - let dispatch = groupStageDispatcher(numberOfCourtsAvailablePerRotation: numberOfCourtsAvailablePerRotation, groupStages: groups, startingDate: lastDate) + let dispatch = groupStageDispatcher(groupStages: groups, startingDate: lastDate) dispatch.timedMatches.forEach { matchSchedule in if let match = matches.first(where: { $0.id == matchSchedule.matchID }) { @@ -180,20 +148,24 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable { return lastDate } - func groupStageDispatcher(numberOfCourtsAvailablePerRotation: Int, groupStages: [GroupStage], startingDate: Date) -> GroupStageMatchDispatcher { + func groupStageDispatcher(groupStages: [GroupStage], startingDate: Date) -> GroupStageMatchDispatcher { let _groupStages = groupStages // Get the maximum count of matches in any group let maxMatchesCount = _groupStages.map { $0._matches().count }.max() ?? 0 - - // Flatten matches in a round-robin order by cycling through each group - let flattenedMatches = (0.. 0 { rotationMatches = rotationMatches.sorted(by: { if counts[$0.groupStageObject!.index] ?? 0 == counts[$1.groupStageObject!.index] ?? 0 { - return $0.groupStageObject!.index < $1.groupStageObject!.index + if simultaneousStart { + return $0.groupStageObject!.orderedIndexOfMatch($0) < $1.groupStageObject!.orderedIndexOfMatch($1) + } else { + return $0.groupStageObject!.index < $1.groupStageObject!.index + } } else { return counts[$0.groupStageObject!.index] ?? 0 < counts[$1.groupStageObject!.index] ?? 0 } }) } - (0.. MatchDispatcher { + func roundDispatcher(flattenedMatches: [Match], dispatcherStartDate: Date, initialCourts: [Int]?) -> MatchDispatcher { var slots = [TimeMatch]() var _startDate: Date? var rotationIndex = 0 @@ -442,7 +418,7 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable { var issueFound: Bool = false // Log start of the function - print("Starting roundDispatcher with \(availableMatchs.count) matches and \(numberOfCourtsAvailablePerRotation) courts available") + print("Starting roundDispatcher with \(availableMatchs.count) matches and \(courtsAvailable) courts available") flattenedMatches.filter { $0.startDate != nil }.sorted(by: \.startDate!).forEach { match in if _startDate == nil { @@ -461,20 +437,28 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable { } var freeCourtPerRotation = [Int: [Int]]() - let availableCourt = numberOfCourtsAvailablePerRotation - var courts = initialCourts ?? (0.. 0 - - while !availableMatchs.isEmpty && !issueFound && rotationIndex < 100 { + var suitableDate: Date? + + while !availableMatchs.isEmpty && !issueFound && rotationIndex < 50 { freeCourtPerRotation[rotationIndex] = [] let previousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 1 }) - var rotationStartDate: Date = getNextStartDate(fromPreviousRotationSlots: previousRotationSlots, includeBreakTime: false) ?? dispatcherStartDate + + var rotationStartDate: Date + if previousRotationSlots.isEmpty && rotationIndex > 0 { + let computedSuitableDate = slots.sorted(by: \.computedEndDateForSorting).last?.computedEndDateForSorting + print("Previous rotation was empty, find a suitable rotationStartDate \(suitableDate)") + rotationStartDate = suitableDate ?? computedSuitableDate ?? dispatcherStartDate + } else { + rotationStartDate = getNextStartDate(fromPreviousRotationSlots: previousRotationSlots, includeBreakTime: false) ?? dispatcherStartDate + } if shouldStartAtDispatcherDate { rotationStartDate = dispatcherStartDate shouldStartAtDispatcherDate = false } else { - courts = rotationIndex == 0 ? courts : (0.. 0, let freeCourtPreviousRotation = freeCourtPerRotation[rotationIndex - 1], !freeCourtPreviousRotation.isEmpty { print("Handling break time conflicts or waiting for free courts") let previousPreviousRotationSlots = slots.filter { $0.rotationIndex == rotationIndex - 2 && freeCourtPreviousRotation.contains($0.courtIndex) } - let previousEndDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: accountUpperBracketBreakTime) - let previousEndDateNoBreak = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: false) + var previousEndDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: accountUpperBracketBreakTime) + var previousEndDateNoBreak = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: false) + if let courtsUnavailability, previousEndDate != nil { + previousEndDate = getFirstFreeCourt(startDate: previousEndDate!, duration: 0, courts: courts, courtsUnavailability: courtsUnavailability).earliestFreeDate + } + + if let courtsUnavailability, previousEndDateNoBreak != nil { + previousEndDateNoBreak = getFirstFreeCourt(startDate: previousEndDateNoBreak!, duration: 0, courts: courts, courtsUnavailability: courtsUnavailability).earliestFreeDate + } + let noBreakAlreadyTested = previousRotationSlots.anySatisfy { $0.startDate == previousEndDateNoBreak } if let previousEndDate, let previousEndDateNoBreak { @@ -499,13 +491,23 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable { let timeDifferenceLimitInSeconds = Double(timeDifferenceLimit * 60) var difference = differenceWithBreak - if differenceWithBreak <= 0 { + if differenceWithBreak <= 0, accountUpperBracketBreakTime == false { difference = differenceWithoutBreak } else if differenceWithBreak > timeDifferenceLimitInSeconds && differenceWithoutBreak > timeDifferenceLimitInSeconds { difference = noBreakAlreadyTested ? differenceWithBreak : max(differenceWithBreak, differenceWithoutBreak) } - if difference > timeDifferenceLimitInSeconds && rotationStartDate.addingTimeInterval(-difference) != previousEndDate { + print("Final difference to evaluate: \(difference)") + + if (difference > timeDifferenceLimitInSeconds && rotationStartDate.addingTimeInterval(-difference) != previousEndDate) || difference < 0 { + print(""" + Adjusting rotation start: + - Initial rotationStartDate: \(rotationStartDate) + - Adjusted by difference: \(difference) + - Adjusted rotationStartDate: \(rotationStartDate.addingTimeInterval(-difference)) + - PreviousEndDate: \(previousEndDate) + """) + courts.removeAll(where: { freeCourtPreviousRotation.contains($0) }) freeCourtPerRotation[rotationIndex] = courts courts = freeCourtPreviousRotation @@ -516,16 +518,22 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable { let duration = firstMatch.matchFormat.getEstimatedDuration(additionalEstimationDuration) let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: duration, courtsUnavailability: courtsUnavailability) - if courtsUnavailable.count == numberOfCourtsAvailablePerRotation { + if Array(Set(courtsAvailable).subtracting(Set(courtsUnavailable))).isEmpty { print("Issue: All courts unavailable in this rotation") - issueFound = true + if let courtsUnavailability { + let computedStartDateAndCourts = getFirstFreeCourt(startDate: rotationStartDate, duration: duration, courts: courts, courtsUnavailability: courtsUnavailability) + rotationStartDate = computedStartDateAndCourts.earliestFreeDate + courts = computedStartDateAndCourts.availableCourts + } else { + issueFound = true + } } else { - courts = Array(Set(courts).subtracting(Set(courtsUnavailable))) + courts = Array(Set(courtsAvailable).subtracting(Set(courtsUnavailable))) } } // Dispatch courts and schedule matches - dispatchCourts(availableCourts: numberOfCourtsAvailablePerRotation, courts: courts, availableMatchs: &availableMatchs, slots: &slots, rotationIndex: rotationIndex, rotationStartDate: rotationStartDate, freeCourtPerRotation: &freeCourtPerRotation, courtsUnavailability: courtsUnavailability) + suitableDate = dispatchCourts(courts: courts, availableMatchs: &availableMatchs, slots: &slots, rotationIndex: rotationIndex, rotationStartDate: rotationStartDate, freeCourtPerRotation: &freeCourtPerRotation, courtsUnavailability: courtsUnavailability) rotationIndex += 1 } @@ -544,10 +552,10 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable { print("Finished roundDispatcher with \(organizedSlots.count) scheduled matches") - return MatchDispatcher(timedMatches: slots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex, issueFound: issueFound) + return MatchDispatcher(timedMatches: organizedSlots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex, issueFound: issueFound) } - func dispatchCourts(availableCourts: Int, courts: [Int], availableMatchs: inout [Match], slots: inout [TimeMatch], rotationIndex: Int, rotationStartDate: Date, freeCourtPerRotation: inout [Int: [Int]], courtsUnavailability: [DateInterval]?) { + func dispatchCourts(courts: [Int], availableMatchs: inout [Match], slots: inout [TimeMatch], rotationIndex: Int, rotationStartDate: Date, freeCourtPerRotation: inout [Int: [Int]], courtsUnavailability: [DateInterval]?) -> Date { var matchPerRound = [String: Int]() var minimumTargetedEndDate = rotationStartDate @@ -563,7 +571,7 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable { let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: duration, courtsUnavailability: courtsUnavailability) - if courtsUnavailable.contains(courtPosition) { + if courtsUnavailable.contains(courtIndex) { print("Returning false: Court \(courtIndex) unavailable due to schedule conflicts during \(rotationStartDate).") return false } @@ -587,13 +595,18 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable { let indexInRound = match.indexInRound() - if roundObject.parent == nil && roundObject.index > 0 && indexInRound == 0, let nextMatch = match.next() { - if courtPosition < courts.count - 1 && canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate) { - print("Returning true: Both current \(match.index) and next match \(nextMatch.index) can be played in rotation \(rotationIndex).") - return true - } else { - print("Returning false: Either current match or next match cannot be played in rotation \(rotationIndex).") - return false + + if shouldTryToFillUpCourtsAvailable == false { + if roundObject.parent == nil && roundObject.index > 1 && indexInRound == 0, let nextMatch = match.next() { + + var nextMinimumTargetedEndDate = minimumTargetedEndDate + if courtPosition < courts.count - 1 && canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &nextMinimumTargetedEndDate) { + print("Returning true: Both current \(match.index) and next match \(nextMatch.index) can be played in rotation \(rotationIndex).") + return true + } else { + print("Returning false: Either current match or next match cannot be played in rotation \(rotationIndex).") + return false + } } } @@ -622,15 +635,22 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable { } - if freeCourtPerRotation[rotationIndex]?.count == availableCourts { - print("All courts in rotation \(rotationIndex) are free") + if freeCourtPerRotation[rotationIndex]?.count == courtsAvailable.count { + print("All courts in rotation \(rotationIndex) are free, minimumTargetedEndDate : \(minimumTargetedEndDate)") + } + + if let courtsUnavailability { + let computedStartDateAndCourts = getFirstFreeCourt(startDate: minimumTargetedEndDate, duration: 0, courts: courts, courtsUnavailability: courtsUnavailability) + return computedStartDateAndCourts.earliestFreeDate } + + return minimumTargetedEndDate } @discardableResult func updateBracketSchedule(tournament: Tournament, fromRoundId roundId: String?, fromMatchId matchId: String?, startDate: Date) -> Bool { let upperRounds: [Round] = tournament.rounds() - let allMatches: [Match] = tournament.allMatches() + let allMatches: [Match] = tournament.allMatches().filter({ $0.hasEnded() == false && $0.hasStarted() == false }) var rounds = [Round]() @@ -651,7 +671,7 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable { } let flattenedMatches = rounds.flatMap { round in - round._matches().filter({ $0.disabled == false }).sorted(by: \.index) + round._matches().filter({ $0.disabled == false && $0.hasEnded() == false && $0.hasStarted() == false }).sorted(by: \.index) } flattenedMatches.forEach({ @@ -709,7 +729,7 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable { print("initial available courts at beginning: \(courts ?? [])") - let roundDispatch = self.roundDispatcher(numberOfCourtsAvailablePerRotation: tournament.courtCount, flattenedMatches: flattenedMatches, dispatcherStartDate: startDate, initialCourts: courts) + let roundDispatch = self.roundDispatcher(flattenedMatches: flattenedMatches, dispatcherStartDate: startDate, initialCourts: courts) roundDispatch.timedMatches.forEach { matchSchedule in if let match = flattenedMatches.first(where: { $0.id == matchSchedule.matchID }) { @@ -746,7 +766,50 @@ final class MatchScheduler: BaseMatchScheduler, SideStorable { }) } + func getFirstFreeCourt(startDate: Date, duration: Int, courts: [Int], courtsUnavailability: [DateInterval]) -> (earliestFreeDate: Date, availableCourts: [Int]) { + var earliestEndDate: Date? + var availableCourtsAtEarliest: [Int] = [] + + // Iterate through each court and find the earliest time it becomes free + for courtIndex in courts { + let unavailabilityForCourt = courtsUnavailability.filter { $0.courtIndex == courtIndex } + var isAvailable = true + + for interval in unavailabilityForCourt { + if interval.startDate <= startDate && interval.endDate > startDate { + isAvailable = false + if let currentEarliest = earliestEndDate { + earliestEndDate = min(currentEarliest, interval.endDate) + } else { + earliestEndDate = interval.endDate + } + } + } + + // If the court is available at the start date, add it to the list of available courts + if isAvailable { + availableCourtsAtEarliest.append(courtIndex) + } + } + + // If there are no unavailable courts, return the original start date and all courts + if let earliestEndDate = earliestEndDate { + // Find which courts will be available at the earliest free date + let courtsAvailableAtEarliest = courts.filter { courtIndex in + let unavailabilityForCourt = courtsUnavailability.filter { $0.courtIndex == courtIndex } + return unavailabilityForCourt.allSatisfy { $0.endDate <= earliestEndDate } + } + return (earliestFreeDate: earliestEndDate, availableCourts: courtsAvailableAtEarliest) + } else { + // If no courts were unavailable, all courts are available at the start date + return (earliestFreeDate: startDate.addingTimeInterval(Double(duration) * 60), availableCourts: courts) + } + } + func updateSchedule(tournament: Tournament) -> Bool { + if tournament.courtCount < courtsAvailable.count { + courtsAvailable = Set(tournament.courtsAvailable()) + } var lastDate = tournament.startDate if tournament.groupStageCount > 0 { lastDate = updateGroupStageSchedule(tournament: tournament) @@ -777,6 +840,10 @@ struct TimeMatch { let minutesToAdd = Double(durationLeft + (includeBreakTime ? minimumBreakTime : 0)) return startDate.addingTimeInterval(minutesToAdd * 60.0) } + + var computedEndDateForSorting: Date { + estimatedEndDate(includeBreakTime: false) + } } struct GroupStageMatchDispatcher { @@ -801,4 +868,16 @@ extension Match { func containsTeamId(_ id: String) -> Bool { return teamIds().contains(id) } + + func containsTeamIndex(_ id: String) -> Bool { + matchUp().contains(id) + } + + func matchUp() -> [String] { + guard let groupStageObject else { + return [] + } + + return groupStageObject._matchUp(for: index).map { groupStageObject.id + "_\($0)" } + } } diff --git a/PadelClub/Data/PlayerRegistration.swift b/PadelClub/Data/PlayerRegistration.swift index 003024f..86efd29 100644 --- a/PadelClub/Data/PlayerRegistration.swift +++ b/PadelClub/Data/PlayerRegistration.swift @@ -67,18 +67,18 @@ final class PlayerRegistration: BasePlayerRegistration, SideStorable { internal init(importedPlayer: ImportedPlayer) { super.init() self.teamRegistration = "" - self.firstName = (importedPlayer.firstName ?? "").trimmed.capitalized - self.lastName = (importedPlayer.lastName ?? "").trimmed.uppercased() - self.licenceId = importedPlayer.license ?? nil + self.firstName = (importedPlayer.firstName ?? "").prefixTrimmed(50).capitalized + self.lastName = (importedPlayer.lastName ?? "").prefixTrimmed(50).uppercased() + self.licenceId = importedPlayer.license?.prefixTrimmed(50) ?? nil self.rank = Int(importedPlayer.rank) self.sex = importedPlayer.male ? .male : .female self.tournamentPlayed = importedPlayer.tournamentPlayed self.points = importedPlayer.getPoints() - self.clubName = importedPlayer.clubName - self.ligueName = importedPlayer.ligueName - self.assimilation = importedPlayer.assimilation + self.clubName = importedPlayer.clubName?.prefixTrimmed(200) + self.ligueName = importedPlayer.ligueName?.prefixTrimmed(200) + self.assimilation = importedPlayer.assimilation?.prefixTrimmed(50) self.source = .frenchFederation - self.birthdate = importedPlayer.birthYear + self.birthdate = importedPlayer.birthYear?.prefixTrimmed(50) } internal init?(federalData: [String], sex: Int, sexUnknown: Bool) { @@ -86,11 +86,11 @@ final class PlayerRegistration: BasePlayerRegistration, SideStorable { let _lastName = federalData[0].trimmed.uppercased() let _firstName = federalData[1].trimmed.capitalized if _lastName.isEmpty && _firstName.isEmpty { return nil } - lastName = _lastName - firstName = _firstName - birthdate = federalData[2].formattedAsBirthdate() - licenceId = federalData[3] - clubName = federalData[4] + lastName = _lastName.prefixTrimmed(50) + firstName = _firstName.prefixTrimmed(50) + birthdate = federalData[2].formattedAsBirthdate().prefixTrimmed(50) + licenceId = federalData[3].prefixTrimmed(50) + clubName = federalData[4].prefixTrimmed(200) let stringRank = federalData[5] if stringRank.isEmpty { rank = nil @@ -99,11 +99,11 @@ final class PlayerRegistration: BasePlayerRegistration, SideStorable { } let _email = federalData[6] if _email.isEmpty == false { - self.email = _email + self.email = _email.prefixTrimmed(50) } let _phoneNumber = federalData[7] if _phoneNumber.isEmpty == false { - self.phoneNumber = _phoneNumber + self.phoneNumber = _phoneNumber.prefixTrimmed(50) } source = .beachPadel @@ -168,12 +168,27 @@ final class PlayerRegistration: BasePlayerRegistration, SideStorable { } func contains(_ searchField: String) -> Bool { - firstName.localizedCaseInsensitiveContains(searchField) || lastName.localizedCaseInsensitiveContains(searchField) + let nameComponents = searchField.canonicalVersion.split(separator: " ") + + if nameComponents.count > 1 { + let pairs = nameComponents.pairs() + return pairs.contains(where: { + (firstName.canonicalVersion.localizedCaseInsensitiveContains(String($0)) && + lastName.canonicalVersion.localizedCaseInsensitiveContains(String($1))) || + (firstName.canonicalVersion.localizedCaseInsensitiveContains(String($1)) && + lastName.canonicalVersion.localizedCaseInsensitiveContains(String($0))) + }) + } else { + return nameComponents.contains { component in + firstName.canonicalVersion.localizedCaseInsensitiveContains(component) || + lastName.canonicalVersion.localizedCaseInsensitiveContains(component) + } + } } func isSameAs(_ player: PlayerRegistration) -> Bool { - firstName.trimmedMultiline.localizedCaseInsensitiveCompare(player.firstName.trimmedMultiline) == .orderedSame && - lastName.trimmedMultiline.localizedCaseInsensitiveCompare(player.lastName.trimmedMultiline) == .orderedSame + firstName.trimmedMultiline.canonicalVersion.localizedCaseInsensitiveCompare(player.firstName.trimmedMultiline.canonicalVersion) == .orderedSame && + lastName.trimmedMultiline.canonicalVersion.localizedCaseInsensitiveCompare(player.lastName.trimmedMultiline.canonicalVersion) == .orderedSame } func tournament() -> Tournament? { @@ -186,6 +201,10 @@ final class PlayerRegistration: BasePlayerRegistration, SideStorable { return self.tournamentStore.teamRegistrations.findById(teamRegistration) } + func isHere() -> Bool { + hasArrived + } + func hasPaid() -> Bool { paymentType != nil } @@ -313,7 +332,7 @@ final class PlayerRegistration: BasePlayerRegistration, SideStorable { if let currentLicenceId = licenceId { if currentLicenceId.trimmed.hasSuffix("(\(year-1))") { self.licenceId = currentLicenceId.replacingOccurrences(of: "\(year-1)", with: "\(year)") - } else if let computedLicense = currentLicenceId.strippedLicense { + } else if let computedLicense = currentLicenceId.strippedLicense?.computedLicense { self.licenceId = computedLicense + " (\(year))" } } @@ -392,14 +411,8 @@ final class PlayerRegistration: BasePlayerRegistration, SideStorable { case 0: return 0 case womanMax: return manMax - womanMax case manMax: return 0 - case 1...10: return 400 - case 11...30: return 1000 - case 31...60: return 2000 - case 61...100: return 3000 - case 101...200: return 8000 - case 201...500: return 12000 default: - return 15000 + return TournamentCategory.femaleInMaleAssimilationAddition(playerRank) } } diff --git a/PadelClub/Data/README.md b/PadelClub/Data/README.md index 22a1795..a0f4424 100644 --- a/PadelClub/Data/README.md +++ b/PadelClub/Data/README.md @@ -16,14 +16,14 @@ Dans Swift: - Ajouter le champ dans classe - Ajouter le champ dans le constructeur si possible - Ajouter la codingKey correspondante -- Ajouter le champ dans l'encoding +- Ajouter le champ dans l'encoding/decoding - Ouvrir **ServerDataTests** et ajouter un test sur le champ - Pour que les tests sur les dates fonctionnent, on peut tester date.formatted() par exemple Dans Django: - Ajouter le champ dans la classe - Si c'est une ForeignKey, toujours mettre un related_name sinon la synchro casse -- S'il c'est un champ dans **CustomUser**: +- Si c'est un champ dans **CustomUser**: - Ajouter le champ à la méthode fields_for_update - Ajouter le champ dans UserSerializer > create > create_user dans serializers.py - L'ajouter aussi dans admin.py si nécéssaire diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index 941f29e..ddc0270 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -11,24 +11,9 @@ import SwiftUI @Observable final class Round: BaseRound, SideStorable { - -// static func resourceName() -> String { "rounds" } -// static func tokenExemptedMethods() -> [HTTPMethod] { return [] } -// static func filterByStoreIdentifier() -> Bool { return true } -// static var relationshipNames: [String] = [] -// -// var id: String = Store.randomId() -// var lastUpdate: Date -// var tournament: String -// var index: Int -// var parent: String? -// private(set) var format: MatchFormat? -// var startDate: Date? -// var groupStageLoserBracket: Bool = false -// var loserBracketMode: LoserBracketMode = .automatic -// -// var storeId: String? = nil - + + var _cachedSeedInterval: SeedInterval? + internal init(tournament: String, index: Int, parent: String? = nil, matchFormat: MatchFormat? = nil, startDate: Date? = nil, groupStageLoserBracket: Bool = false, loserBracketMode: LoserBracketMode = .automatic) { super.init(tournament: tournament, index: index, parent: parent, format: matchFormat, startDate: startDate, groupStageLoserBracket: groupStageLoserBracket, loserBracketMode: loserBracketMode) @@ -130,8 +115,7 @@ final class Round: BaseRound, SideStorable { func seed(_ team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? { return self.tournamentStore.teamRegistrations.first(where: { - $0.tournament == tournament - && $0.bracketPosition != nil + $0.bracketPosition != nil && ($0.bracketPosition! / 2) == matchIndex && ($0.bracketPosition! % 2) == team.rawValue }) @@ -156,12 +140,21 @@ final class Round: BaseRound, SideStorable { let initialMatchIndex = RoundRule.matchIndex(fromRoundIndex: index) let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: index) return self.tournamentStore.teamRegistrations.filter { - $0.tournament == tournament - && $0.bracketPosition != nil + $0.bracketPosition != nil && ($0.bracketPosition! / 2) >= initialMatchIndex && ($0.bracketPosition! / 2) < initialMatchIndex + numberOfMatches } } + + func teamsOrSeeds() -> [TeamRegistration] { + let seeds = seeds() + if seeds.isEmpty { + return playedMatches().flatMap({ $0.teams() }) + } else { + return seeds + } + } + func losers() -> [TeamRegistration] { let teamIds: [String] = self._matches().compactMap { $0.losingTeamId } @@ -459,6 +452,8 @@ defer { func correspondingLoserRoundTitle(_ displayStyle: DisplayStyle = .wide) -> String { + if let _cachedSeedInterval { return _cachedSeedInterval.localizedLabel(displayStyle) } + #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { @@ -477,8 +472,16 @@ defer { // && $0.bracketPosition != nil // && ($0.bracketPosition! / 2) < initialMatchIndexFromRoundIndex // }) + + var seedsCount = seedsAfterThisRound.count + if seedsAfterThisRound.isEmpty { + let nextRoundsDisableMatches = nextRoundsDisableMatches() + seedsCount = disabledMatches().count - nextRoundsDisableMatches + } let playedMatches = playedMatches() - let seedInterval = SeedInterval(first: playedMatches.count + seedsAfterThisRound.count + 1, last: playedMatches.count * 2 + seedsAfterThisRound.count) + let seedInterval = SeedInterval(first: playedMatches.count + seedsCount + 1, last: playedMatches.count * 2 + seedsCount) + + _cachedSeedInterval = seedInterval return seedInterval.localizedLabel(displayStyle) } @@ -500,6 +503,8 @@ defer { } func seedInterval(initialMode: Bool = false) -> SeedInterval? { + if initialMode == false, let _cachedSeedInterval { return _cachedSeedInterval } + #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { @@ -511,17 +516,32 @@ defer { if isUpperBracket() { if index == 0 { return SeedInterval(first: 1, last: 2) } let initialMatchIndexFromRoundIndex = RoundRule.matchIndex(fromRoundIndex: index) - let seedsAfterThisRound : [TeamRegistration] = self.tournamentStore.teamRegistrations.filter { - $0.bracketPosition != nil - && ($0.bracketPosition! / 2) < initialMatchIndexFromRoundIndex - } - let playedMatches = playedMatches().count - let minimumMatches = initialMode ? RoundRule.numberOfMatches(forRoundIndex: index) : playedMatches * 2 - //print("playedMatches \(playedMatches)", initialMode, parent, parentRound?.roundTitle(), seedsAfterThisRound.count) - let seedInterval = SeedInterval(first: playedMatches + seedsAfterThisRound.count + 1, last: minimumMatches + seedsAfterThisRound.count) - //print(seedInterval.localizedLabel()) - return seedInterval + if initialMode { + let playedMatches = RoundRule.numberOfMatches(forRoundIndex: index) + let seedInterval = SeedInterval(first: playedMatches + 1, last: playedMatches * 2) + //print(seedInterval.localizedLabel()) + return seedInterval + } else { + let seedsAfterThisRound : [TeamRegistration] = self.tournamentStore.teamRegistrations.filter { + $0.bracketPosition != nil + && ($0.bracketPosition! / 2) < initialMatchIndexFromRoundIndex + } + + var seedsCount = seedsAfterThisRound.count + if seedsAfterThisRound.isEmpty { + let nextRoundsDisableMatches = nextRoundsDisableMatches() + seedsCount = disabledMatches().count - nextRoundsDisableMatches + } + + let playedMatches = playedMatches() + //print("playedMatches \(playedMatches)", initialMode, parent, parentRound?.roundTitle(), seedsAfterThisRound.count) + let seedInterval = SeedInterval(first: playedMatches.count + seedsCount + 1, last: playedMatches.count * 2 + seedsCount) + //print(seedInterval.localizedLabel()) + _cachedSeedInterval = seedInterval + return seedInterval + + } } if let previousRound = previousRound() { @@ -641,6 +661,14 @@ defer { guard let parent = parent else { return nil } return self.tournamentStore.rounds.findById(parent) } + + func nextRoundsDisableMatches() -> Int { + if parent == nil, index > 0 { + return tournamentObject()?.rounds().suffix(index).flatMap { $0.disabledMatches() }.count ?? 0 + } else { + return 0 + } + } func updateMatchFormat(_ updatedMatchFormat: MatchFormat, checkIfPossible: Bool, andLoserBracket: Bool) { if updatedMatchFormat.weight < self.matchFormat.weight { diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index 2325180..3e96978 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -50,7 +50,7 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable { // self.storeId = tournament self.tournament = tournament self.groupStage = groupStage - self.registrationDate = registrationDate + self.registrationDate = registrationDate ?? Date() self.callDate = callDate self.bracketPosition = bracketPosition self.groupStagePosition = groupStagePosition @@ -120,13 +120,39 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable { } func setSeedPosition(inSpot match: Match, slot: TeamPosition?, opposingSeeding: Bool) { - let seedPosition: Int = match.lockAndGetSeedPosition(atTeamPosition: slot, opposingSeeding: opposingSeeding) + var teamPosition : TeamPosition { + if let slot { + return slot + } else { + let matchIndex = match.index + let seedRound = RoundRule.roundIndex(fromMatchIndex: matchIndex) + let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: seedRound) + let isUpper = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) < (numberOfMatches / 2) + var teamPosition = slot ?? (isUpper ? .one : .two) + if opposingSeeding { + teamPosition = slot ?? (isUpper ? .two : .one) + } + return teamPosition + } + } + + let seedPosition: Int = match.lockAndGetSeedPosition(atTeamPosition: teamPosition) tournamentObject()?.resetTeamScores(in: bracketPosition) self.bracketPosition = seedPosition if groupStagePosition != nil && qualified == false { qualified = true } - tournamentObject()?.updateTeamScores(in: bracketPosition) + if let tournament = tournamentObject() { + if let index = index(in: tournament.selectedSortedTeams()) { + let drawLog = DrawLog(tournament: tournament.id, drawSeed: index, drawMatchIndex: match.index, drawTeamPosition: teamPosition, drawType: .seed) + do { + try tournamentStore.drawLogs.addOrUpdate(instance: drawLog) + } catch { + Logger.error(error) + } + } + tournament.updateTeamScores(in: bracketPosition) + } } func expectedSummonDate() -> Date? { @@ -432,7 +458,7 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable { self.tournamentStore.playerRegistrations.filter { $0.teamRegistration == self.id }.sorted { (lhs, rhs) in let predicates: [AreInIncreasingOrder] = [ { $0.sex?.rawValue ?? 0 < $1.sex?.rawValue ?? 0 }, - { $0.rank ?? 0 < $1.rank ?? 0 }, + { $0.rank ?? Int.max < $1.rank ?? Int.max }, { $0.lastName < $1.lastName}, { $0.firstName < $1.firstName } ] @@ -515,59 +541,50 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable { return nil } -// enum CodingKeys: String, CodingKey { -// case _id = "id" -// case _lastUpdate = "lastUpdate" -// case _storeId = "storeId" -// case _tournament = "tournament" -// case _groupStage = "groupStage" -// case _registrationDate = "registrationDate" -// case _callDate = "callDate" -// case _bracketPosition = "bracketPosition" -// case _groupStagePosition = "groupStagePosition" -// case _comment = "comment" -// case _source = "source" -// case _sourceValue = "sourceValue" -// case _logo = "logo" -// case _name = "name" -// case _wildCardBracket = "wildCardBracket" -// case _wildCardGroupStage = "wildCardGroupStage" -// case _weight = "weight" -// case _walkOut = "walkOut" -// case _lockedWeight = "lockedWeight" -// case _confirmationDate = "confirmationDate" -// case _qualified = "qualified" -// case _finalRanking = "finalRanking" -// case _pointsEarned = "pointsEarned" -// } -// -// func encode(to encoder: Encoder) throws { -// var container = encoder.container(keyedBy: CodingKeys.self) -// -// try container.encode(id, forKey: ._id) -// try container.encode(storeId, forKey: ._storeId) -// try container.encode(lastUpdate, forKey: ._lastUpdate) -// try container.encode(tournament, forKey: ._tournament) -// try container.encode(groupStage, forKey: ._groupStage) -// try container.encode(registrationDate, forKey: ._registrationDate) -// try container.encode(callDate, forKey: ._callDate) -// try container.encode(bracketPosition, forKey: ._bracketPosition) -// try container.encode(groupStagePosition, forKey: ._groupStagePosition) -// try container.encode(comment, forKey: ._comment) -// try container.encode(source, forKey: ._source) -// try container.encode(sourceValue, forKey: ._sourceValue) -// try container.encode(logo, forKey: ._logo) -// try container.encode(name, forKey: ._name) -// try container.encode(walkOut, forKey: ._walkOut) -// try container.encode(wildCardBracket, forKey: ._wildCardBracket) -// try container.encode(wildCardGroupStage, forKey: ._wildCardGroupStage) -// try container.encode(weight, forKey: ._weight) -// try container.encode(lockedWeight, forKey: ._lockedWeight) -// try container.encode(confirmationDate, forKey: ._confirmationDate) -// try container.encode(qualified, forKey: ._qualified) -// try container.encode(finalRanking, forKey: ._finalRanking) -// try container.encode(pointsEarned, forKey: ._pointsEarned) -// } + func wildcardLabel() -> String? { + if isWildCard() { + let wildcardLabel: String = ["wildcard", (wildCardBracket ? "tableau" : "poule")].joined(separator: " ") + return wildcardLabel + } else { + return nil + } + } + + var _cachedRestingTime: (Bool, Date?)? + + func restingTime() -> Date? { + if let _cachedRestingTime { return _cachedRestingTime.1 } + let restingTime = matches().filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).last?.endDate + _cachedRestingTime = (true, restingTime) + return restingTime + } + + func resetRestingTime() { + _cachedRestingTime = nil + } + + var restingTimeForSorting: Date { + restingTime()! + } + + func teamNameLabel() -> String { + if let name, name.isEmpty == false { + return name + } else { + return "Toute l'équipe" + } + } + + func isDifferentPosition(_ drawMatchIndex: Int?) -> Bool { + if let bracketPosition, let drawMatchIndex { + return drawMatchIndex != bracketPosition + } else if let bracketPosition { + return true + } else if let drawMatchIndex { + return true + } + return false + } func insertOnServer() { self.tournamentStore.teamRegistrations.writeChangeAndInsertOnServer(instance: self) diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index efac161..2475919 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -1,5 +1,5 @@ // -// Tournament.swift +// swift // PadelClub // // Created by Laurent Morvillier on 02/02/2024. @@ -11,116 +11,22 @@ import SwiftUI @Observable final class Tournament: BaseTournament { - -// static func resourceName() -> String { "tournaments" } -// static func tokenExemptedMethods() -> [HTTPMethod] { return [] } -// static func filterByStoreIdentifier() -> Bool { return false } -// static var relationshipNames: [String] = [] -// -// var id: String = Store.randomId() -// var lastUpdate: Date -// var event: String? -// var name: String? -// var startDate: Date -// var endDate: Date? -// private(set) var creationDate: Date -// var isPrivate: Bool -// private(set) var groupStageFormat: MatchFormat? -// private(set) var roundFormat: MatchFormat? -// private(set) var loserRoundFormat: MatchFormat? -// var groupStageSortMode: GroupStageOrderingMode -// var groupStageCount: Int -// var rankSourceDate: Date? -// var dayDuration: Int -// var teamCount: Int -// var teamSorting: TeamSortingType -// var federalCategory: TournamentCategory -// var federalLevelCategory: TournamentLevel -// var federalAgeCategory: FederalTournamentAge -// var closedRegistrationDate: Date? -// var groupStageAdditionalQualified: Int -// var courtCount: Int = 2 -// var prioritizeClubMembers: Bool -// var qualifiedPerGroupStage: Int -// var teamsPerGroupStage: Int -// var entryFee: Double? -// var payment: TournamentPayment? = nil -// var additionalEstimationDuration: Int = 0 -// var isDeleted: Bool = false -// var isCanceled: Bool = false -// var publishTeams: Bool = false -// //var publishWaitingList: Bool = false -// var publishSummons: Bool = false -// var publishGroupStages: Bool = false -// var publishBrackets: Bool = false -// var shouldVerifyGroupStage: Bool = false -// var shouldVerifyBracket: Bool = false -// var hideTeamsWeight: Bool = false -// var publishTournament: Bool = false -// var hidePointsEarned: Bool = false -// var publishRankings: Bool = false -// var loserBracketMode: LoserBracketMode = .automatic - + @ObservationIgnored var navigationPath: [Screen] = [] -// enum CodingKeys: String, CodingKey { -// case _id = "id" -// case _lastUpdate = "lastUpdate" -// case _event = "event" -// case _creator = "creator" -// case _name = "name" -// case _startDate = "startDate" -// case _endDate = "endDate" -// case _creationDate = "creationDate" -// case _isPrivate = "isPrivate" -// case _groupStageFormat = "groupStageFormat" -// case _roundFormat = "roundFormat" -// case _loserRoundFormat = "loserRoundFormat" -// case _groupStageSortMode = "groupStageSortMode" -// case _groupStageCount = "groupStageCount" -// case _rankSourceDate = "rankSourceDate" -// case _dayDuration = "dayDuration" -// case _teamCount = "teamCount" -// case _teamSorting = "teamSorting" -// case _federalCategory = "federalCategory" -// case _federalLevelCategory = "federalLevelCategory" -// case _federalAgeCategory = "federalAgeCategory" -// case _seedCount = "seedCount" -// case _closedRegistrationDate = "closedRegistrationDate" -// case _groupStageAdditionalQualified = "groupStageAdditionalQualified" -// case _courtCount = "courtCount" -// case _prioritizeClubMembers = "prioritizeClubMembers" -// case _qualifiedPerGroupStage = "qualifiedPerGroupStage" -// case _teamsPerGroupStage = "teamsPerGroupStage" -// case _entryFee = "entryFee" -// case _additionalEstimationDuration = "additionalEstimationDuration" -// case _isDeleted = "isDeleted" -// case _isCanceled = "localId" -// case _payment = "globalId" -// case _publishTeams = "publishTeams" -// //case _publishWaitingList = "publishWaitingList" -// case _publishSummons = "publishSummons" -// case _publishGroupStages = "publishGroupStages" -// case _publishBrackets = "publishBrackets" -// case _shouldVerifyGroupStage = "shouldVerifyGroupStage" -// case _shouldVerifyBracket = "shouldVerifyBracket" -// case _hideTeamsWeight = "hideTeamsWeight" -// case _publishTournament = "publishTournament" -// case _hidePointsEarned = "hidePointsEarned" -// case _publishRankings = "publishRankings" -// case _loserBracketMode = "loserBracketMode" -// } - - internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = false, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false, shouldVerifyBracket: Bool = false, shouldVerifyGroupStage: Bool = false, hideTeamsWeight: Bool = false, publishTournament: Bool = false, hidePointsEarned: Bool = false, publishRankings: Bool = false, loserBracketMode: LoserBracketMode = .automatic) { + internal init(event: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = false, groupStageFormat: MatchFormat? = nil, roundFormat: MatchFormat? = nil, loserRoundFormat: MatchFormat? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, additionalEstimationDuration: Int = 0, isDeleted: Bool = false, publishTeams: Bool = false, publishSummons: Bool = false, publishGroupStages: Bool = false, publishBrackets: Bool = false, shouldVerifyBracket: Bool = false, shouldVerifyGroupStage: Bool = false, hideTeamsWeight: Bool = false, publishTournament: Bool = false, hidePointsEarned: Bool = false, publishRankings: Bool = false, loserBracketMode: LoserBracketMode = .automatic, initialSeedRound: Int = 0, initialSeedCount: Int = 0) { super.init() - self.event = event self.name = name self.startDate = startDate self.endDate = endDate self.creationDate = creationDate +#if DEBUG + self.isPrivate = false +#else self.isPrivate = Guard.main.purchasedTransactions.isEmpty +#endif self.groupStageFormat = groupStageFormat self.roundFormat = roundFormat self.loserRoundFormat = loserRoundFormat @@ -142,198 +48,47 @@ final class Tournament: BaseTournament { self.entryFee = entryFee self.additionalEstimationDuration = additionalEstimationDuration self.isDeleted = isDeleted +#if DEBUG + self.publishTeams = true + self.publishSummons = true + self.publishBrackets = true + self.publishGroupStages = true + self.publishRankings = true + self.publishTournament = true +#else self.publishTeams = publishTeams self.publishSummons = publishSummons self.publishBrackets = publishBrackets self.publishGroupStages = publishGroupStages + self.publishRankings = publishRankings + self.publishTournament = publishTournament +#endif self.shouldVerifyBracket = shouldVerifyBracket self.shouldVerifyGroupStage = shouldVerifyGroupStage self.hideTeamsWeight = hideTeamsWeight - self.publishTournament = publishTournament self.hidePointsEarned = hidePointsEarned - self.publishRankings = publishRankings self.loserBracketMode = loserBracketMode + self.initialSeedRound = initialSeedRound + self.initialSeedCount = initialSeedCount + } required init(from decoder: Decoder) throws { try super.init(from: decoder) } -// required init(from decoder: Decoder) throws { -// let container = try decoder.container(keyedBy: CodingKeys.self) -// id = try container.decode(String.self, forKey: ._id) -// lastUpdate = try container.decode(Date.self, forKey: ._lastUpdate) -// event = try container.decodeIfPresent(String.self, forKey: ._event) -// name = try container.decodeIfPresent(String.self, forKey: ._name) -// startDate = try container.decode(Date.self, forKey: ._startDate) -// endDate = try container.decodeIfPresent(Date.self, forKey: ._endDate) -// creationDate = try container.decode(Date.self, forKey: ._creationDate) -// isPrivate = try container.decode(Bool.self, forKey: ._isPrivate) -// groupStageFormat = try container.decodeIfPresent(MatchFormat.self, forKey: ._groupStageFormat) -// roundFormat = try container.decodeIfPresent(MatchFormat.self, forKey: ._roundFormat) -// loserRoundFormat = try container.decodeIfPresent(MatchFormat.self, forKey: ._loserRoundFormat) -// groupStageSortMode = try container.decode(GroupStageOrderingMode.self, forKey: ._groupStageSortMode) -// groupStageCount = try container.decode(Int.self, forKey: ._groupStageCount) -// rankSourceDate = try container.decodeIfPresent(Date.self, forKey: ._rankSourceDate) -// dayDuration = try container.decode(Int.self, forKey: ._dayDuration) -// teamCount = try container.decode(Int.self, forKey: ._teamCount) -// teamSorting = try container.decode(TeamSortingType.self, forKey: ._teamSorting) -// federalCategory = try container.decode(TournamentCategory.self, forKey: ._federalCategory) -// federalLevelCategory = try container.decode(TournamentLevel.self, forKey: ._federalLevelCategory) -// federalAgeCategory = try container.decode(FederalTournamentAge.self, forKey: ._federalAgeCategory) -// closedRegistrationDate = try container.decodeIfPresent(Date.self, forKey: ._closedRegistrationDate) -// groupStageAdditionalQualified = try container.decode(Int.self, forKey: ._groupStageAdditionalQualified) -// courtCount = try container.decode(Int.self, forKey: ._courtCount) -// prioritizeClubMembers = try container.decode(Bool.self, forKey: ._prioritizeClubMembers) -// qualifiedPerGroupStage = try container.decode(Int.self, forKey: ._qualifiedPerGroupStage) -// teamsPerGroupStage = try container.decode(Int.self, forKey: ._teamsPerGroupStage) -// entryFee = try container.decodeIfPresent(Double.self, forKey: ._entryFee) -// payment = try Tournament._decodePayment(container: container) -// additionalEstimationDuration = try container.decode(Int.self, forKey: ._additionalEstimationDuration) -// isDeleted = try container.decode(Bool.self, forKey: ._isDeleted) -// isCanceled = try Tournament._decodeCanceled(container: container) -// publishTeams = try container.decodeIfPresent(Bool.self, forKey: ._publishTeams) ?? false -// publishSummons = try container.decodeIfPresent(Bool.self, forKey: ._publishSummons) ?? false -// publishGroupStages = try container.decodeIfPresent(Bool.self, forKey: ._publishGroupStages) ?? false -// publishBrackets = try container.decodeIfPresent(Bool.self, forKey: ._publishBrackets) ?? false -// shouldVerifyBracket = try container.decodeIfPresent(Bool.self, forKey: ._shouldVerifyBracket) ?? false -// shouldVerifyGroupStage = try container.decodeIfPresent(Bool.self, forKey: ._shouldVerifyGroupStage) ?? false -// hideTeamsWeight = try container.decodeIfPresent(Bool.self, forKey: ._hideTeamsWeight) ?? false -// publishTournament = try container.decodeIfPresent(Bool.self, forKey: ._publishTournament) ?? false -// hidePointsEarned = try container.decodeIfPresent(Bool.self, forKey: ._hidePointsEarned) ?? false -// publishRankings = try container.decodeIfPresent(Bool.self, forKey: ._publishRankings) ?? false -// loserBracketMode = try container.decodeIfPresent(LoserBracketMode.self, forKey: ._loserBracketMode) ?? .automatic -// } -// -// fileprivate static let _numberFormatter: NumberFormatter = NumberFormatter() -// -// fileprivate static func _decodePayment(container: KeyedDecodingContainer) throws -> TournamentPayment? { -// let data = try container.decodeIfPresent(Data.self, forKey: ._payment) -// -// if let data { -// do { -// let decoded: String = try data.decryptData(pass: CryptoKey.pass.rawValue) -// let sequence = decoded.compactMap { _numberFormatter.number(from: String($0))?.intValue } -// return TournamentPayment(rawValue: sequence[18]) -// } catch { -// Logger.error(error) -// } -// } -// return nil -// } -// -// fileprivate static func _decodeCanceled(container: KeyedDecodingContainer) throws -> Bool { -// let data = try container.decodeIfPresent(Data.self, forKey: ._isCanceled) -// if let data { -// do { -// let decoded: String = try data.decryptData(pass: CryptoKey.pass.rawValue) -// let sequence = decoded.compactMap { _numberFormatter.number(from: String($0))?.intValue } -// return Bool.decodeInt(sequence[18]) -// } catch { -// Logger.error(error) -// } -// } -// return false -// } -// -// func encode(to encoder: Encoder) throws { -// var container = encoder.container(keyedBy: CodingKeys.self) -// -// try container.encode(id, forKey: ._id) -// try container.encode(lastUpdate, forKey: ._lastUpdate) -// try container.encode(event, forKey: ._event) -// try container.encode(name, forKey: ._name) -// -// try container.encode(startDate, forKey: ._startDate) -// try container.encode(endDate, forKey: ._endDate) -// -// try container.encode(creationDate, forKey: ._creationDate) -// try container.encode(isPrivate, forKey: ._isPrivate) -// -// try container.encode(groupStageFormat, forKey: ._groupStageFormat) -// try container.encode(roundFormat, forKey: ._roundFormat) -// try container.encode(loserRoundFormat, forKey: ._loserRoundFormat) -// -// try container.encode(groupStageSortMode, forKey: ._groupStageSortMode) -// try container.encode(groupStageCount, forKey: ._groupStageCount) -// -// try container.encode(rankSourceDate, forKey: ._rankSourceDate) -// -// try container.encode(dayDuration, forKey: ._dayDuration) -// try container.encode(teamCount, forKey: ._teamCount) -// try container.encode(teamSorting, forKey: ._teamSorting) -// try container.encode(federalCategory, forKey: ._federalCategory) -// try container.encode(federalLevelCategory, forKey: ._federalLevelCategory) -// try container.encode(federalAgeCategory, forKey: ._federalAgeCategory) -// -// try container.encode(closedRegistrationDate, forKey: ._closedRegistrationDate) -// -// try container.encode(groupStageAdditionalQualified, forKey: ._groupStageAdditionalQualified) -// try container.encode(courtCount, forKey: ._courtCount) -// try container.encode(prioritizeClubMembers, forKey: ._prioritizeClubMembers) -// try container.encode(qualifiedPerGroupStage, forKey: ._qualifiedPerGroupStage) -// try container.encode(teamsPerGroupStage, forKey: ._teamsPerGroupStage) -// try container.encode(entryFee, forKey: ._entryFee) -// -// try self._encodePayment(container: &container) -// try container.encode(additionalEstimationDuration, forKey: ._additionalEstimationDuration) -// try container.encode(isDeleted, forKey: ._isDeleted) -// try self._encodeIsCanceled(container: &container) -// try container.encode(publishTeams, forKey: ._publishTeams) -// try container.encode(publishSummons, forKey: ._publishSummons) -// try container.encode(publishBrackets, forKey: ._publishBrackets) -// try container.encode(publishGroupStages, forKey: ._publishGroupStages) -// try container.encode(shouldVerifyBracket, forKey: ._shouldVerifyBracket) -// try container.encode(shouldVerifyGroupStage, forKey: ._shouldVerifyGroupStage) -// try container.encode(hideTeamsWeight, forKey: ._hideTeamsWeight) -// try container.encode(publishTournament, forKey: ._publishTournament) -// try container.encode(hidePointsEarned, forKey: ._hidePointsEarned) -// try container.encode(publishRankings, forKey: ._publishRankings) -// try container.encode(loserBracketMode, forKey: ._loserBracketMode) -// } -// -// fileprivate func _encodePayment(container: inout KeyedEncodingContainer) throws { -// -// guard let payment else { -// try container.encodeNil(forKey: ._payment) -// return -// } -// -// let max: Int = TournamentPayment.allCases.count -// var sequence = (1...18).map { _ in Int.random(in: (0..) throws { -// -// let max: Int = 9 -// var sequence = (1...18).map { _ in Int.random(in: (0...max)) } -// sequence.append(self.isCanceled.encodedValue) -// sequence.append(contentsOf: (1...13).map { _ in Int.random(in: (0...max ))} ) -// -// let stringCombo: [String] = sequence.map { $0.formatted() } -// let joined: String = stringCombo.joined(separator: "") -// if let data = joined.data(using: .utf8) { -// let encryped: Data = try data.encrypt(pass: CryptoKey.pass.rawValue) -// try container.encode(encryped, forKey: ._isCanceled) -// } -// } - var tournamentStore: TournamentStore { return TournamentLibrary.shared.store(tournamentId: self.id) } override func deleteDependencies() { let store = self.tournamentStore + let drawLogs = self.tournamentStore.drawLogs + for drawLog in drawLogs { + drawLog.deleteDependencies() + } + store.drawLogs.deleteDependencies(drawLogs) + let teams = self.tournamentStore.teamRegistrations for team in Array(teams) { team.deleteDependencies() @@ -485,16 +240,14 @@ final class Tournament: BaseTournament { return URLs.main.url.appending(path: "tournament/\(id)").appending(path: pageLink.path) } - func courtUsed() -> [Int] { -#if DEBUG //DEBUGING TIME + func courtUsed(runningMatches: [Match]) -> [Int] { +#if _DEBUGING_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func courtUsed()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif - - let runningMatches: [Match] = self.tournamentStore.matches.filter { $0.isRunning() } return Set(runningMatches.compactMap { $0.courtIndex }).sorted() } @@ -543,7 +296,7 @@ defer { return endDate != nil } - func state() -> Tournament.State { + func state() -> State { if self.isCanceled == true { return .canceled } @@ -676,11 +429,15 @@ defer { if availableSeeds().isEmpty == false && roundIndex >= lastSeedRound() { if availableSeedGroup == SeedInterval(first: 1, last: 2) { return availableSeedGroup } - let availableSeeds = seeds(inSeedGroup: availableSeedGroup) let availableSeedSpot = availableSeedSpot(inRoundIndex: roundIndex) let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex) + if availableSeedGroup == SeedInterval(first: 3, last: 4), availableSeedSpot.count == 6 { + print("availableSeedGroup == SeedInterval(first: 3, last: 4)") + return availableSeedGroup + } + if availableSeeds.count == availableSeedSpot.count && availableSeedGroup.count == availableSeeds.count { return availableSeedGroup } else if availableSeeds.count == availableSeedOpponentSpot.count && availableSeedGroup.count == availableSeedOpponentSpot.count { @@ -714,22 +471,32 @@ defer { let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex) let availableSeeds = seeds(inSeedGroup: seedGroup) - if availableSeeds.count <= availableSeedSpot.count { - let spots = availableSeedSpot.shuffled() + if seedGroup == SeedInterval(first: 3, last: 4), availableSeedSpot.count == 6 { + var spots = [Match]() + spots.append(availableSeedSpot[1]) + spots.append(availableSeedSpot[4]) + spots = spots.shuffled() for (index, seed) in availableSeeds.enumerated() { seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: false) } - } else if (availableSeeds.count <= availableSeedOpponentSpot.count && availableSeeds.count <= self.availableSeeds().count) { - - let spots = availableSeedOpponentSpot.shuffled() - for (index, seed) in availableSeeds.enumerated() { - seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: true) - } - } else if let chunks = seedGroup.chunks() { - if let chunk = chunks.first(where: { seedInterval in - seedInterval.first >= self.seededTeams().count - }) { - setSeeds(inRoundIndex: roundIndex, inSeedGroup: chunk) + } else { + if availableSeeds.count <= availableSeedSpot.count { + let spots = availableSeedSpot.shuffled() + for (index, seed) in availableSeeds.enumerated() { + seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: false) + } + } else if (availableSeeds.count <= availableSeedOpponentSpot.count && availableSeeds.count <= self.availableSeeds().count) { + + let spots = availableSeedOpponentSpot.shuffled() + for (index, seed) in availableSeeds.enumerated() { + seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: true) + } + } else if let chunks = seedGroup.chunks() { + if let chunk = chunks.first(where: { seedInterval in + seedInterval.first >= self.seededTeams().count + }) { + setSeeds(inRoundIndex: roundIndex, inSeedGroup: chunk) + } } } } @@ -825,14 +592,14 @@ defer { let defaultSorting : [MySortDescriptor] = _defaultSorting() - let _completeTeams = _teams.sorted(using: defaultSorting, order: .ascending).filter { $0.isWildCard() == false }.prefix(teamCount).sorted(by: \.initialWeight) + let _completeTeams = _teams.sorted(using: defaultSorting, order: .ascending).filter { $0.isWildCard() == false }.prefix(teamCount).sorted(using: [.keyPath(\.initialWeight), .keyPath(\.id)], order: .ascending) let wcGroupStage = _teams.filter { $0.wildCardGroupStage }.sorted(using: _currentSelectionSorting, order: .ascending) let wcBracket = _teams.filter { $0.wildCardBracket }.sorted(using: _currentSelectionSorting, order: .ascending) let groupStageSpots: Int = self.groupStageSpots() - var bracketSeeds: Int = min(teamCount, _completeTeams.count) - groupStageSpots - wcBracket.count + var bracketSeeds: Int = min(teamCount, _teams.count) - groupStageSpots - wcBracket.count var groupStageTeamCount: Int = groupStageSpots - wcGroupStage.count if groupStageTeamCount < 0 { groupStageTeamCount = 0 } if bracketSeeds < 0 { bracketSeeds = 0 } @@ -1024,8 +791,20 @@ defer { func playersWithoutValidLicense(in players: [PlayerRegistration], isImported: Bool) -> [PlayerRegistration] { let licenseYearValidity = self.licenseYearValidity() - return players.filter({ - ($0.isImported() && $0.isValidLicenseNumber(year: licenseYearValidity) == false) || ($0.isImported() == false && ($0.licenceId == nil || $0.formattedLicense().isLicenseNumber == false || $0.licenceId?.isEmpty == true) || ($0.isImported() == false && isImported)) + return players.filter({ player in + if player.isImported() { + // Player is marked as imported: check if the license is valid + return !player.isValidLicenseNumber(year: licenseYearValidity) + } else { + // Player is not imported: validate license and handle `isImported` flag for non-imported players + let noLicenseId = player.licenceId == nil || player.licenceId?.isEmpty == true + let invalidFormattedLicense = player.formattedLicense().isLicenseNumber == false + + // If global `isImported` is true, check license number as well + let invalidLicenseForImportedFlag = isImported && !player.isValidLicenseNumber(year: licenseYearValidity) + + return noLicenseId || invalidFormattedLicense || invalidLicenseForImportedFlag + } }) } @@ -1075,13 +854,15 @@ defer { } } - func registrationIssues() -> Int { + func registrationIssues() async -> Int { let players : [PlayerRegistration] = unsortedPlayers() let selectedTeams : [TeamRegistration] = selectedSortedTeams() let callDateIssue : [TeamRegistration] = selectedTeams.filter { $0.callDate != nil && isStartDateIsDifferentThanCallDate($0) } let duplicates : [PlayerRegistration] = duplicates(in: players) let problematicPlayers : [PlayerRegistration] = players.filter({ $0.sex == nil }) let inadequatePlayers : [PlayerRegistration] = inadequatePlayers(in: players) + let homonyms = homonyms(in: players) + let ageInadequatePlayers = ageInadequatePlayers(in: players) let isImported = players.anySatisfy({ $0.isImported() }) let playersWithoutValidLicense : [PlayerRegistration] = playersWithoutValidLicense(in: players, isImported: isImported) let playersMissing : [TeamRegistration] = selectedTeams.filter({ $0.unsortedPlayers().count < 2 }) @@ -1089,12 +870,13 @@ defer { let waitingListInBracket = waitingList.filter({ $0.bracketPosition != nil }) let waitingListInGroupStage = waitingList.filter({ $0.groupStage != nil }) - return callDateIssue.count + duplicates.count + problematicPlayers.count + inadequatePlayers.count + playersWithoutValidLicense.count + playersMissing.count + waitingListInBracket.count + waitingListInGroupStage.count + return callDateIssue.count + duplicates.count + problematicPlayers.count + inadequatePlayers.count + playersWithoutValidLicense.count + playersMissing.count + waitingListInBracket.count + waitingListInGroupStage.count + ageInadequatePlayers.count + homonyms.count } - func isStartDateIsDifferentThanCallDate(_ team: TeamRegistration) -> Bool { + func isStartDateIsDifferentThanCallDate(_ team: TeamRegistration, expectedSummonDate: Date? = nil) -> Bool { guard let summonDate = team.callDate else { return true } - guard let expectedSummonDate = team.expectedSummonDate() else { return true } + let expectedSummonDate : Date? = team.expectedSummonDate() ?? expectedSummonDate + guard let expectedSummonDate else { return true } return Calendar.current.compare(summonDate, to: expectedSummonDate, toGranularity: .minute) != ComparisonResult.orderedSame } @@ -1103,7 +885,9 @@ defer { // return Store.main.filter(isIncluded: { $0.groupStage != nil && groupStageIds.contains($0.groupStage!) }) } - func availableToStart(_ allMatches: [Match], in runningMatches: [Match]) -> [Match] { + static let defaultSorting : [MySortDescriptor] = [.keyPath(\Match.computedStartDateForSorting), .keyPath(\Match.index)] + + static func availableToStart(_ allMatches: [Match], in runningMatches: [Match], checkCanPlay: Bool = true) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { @@ -1111,10 +895,10 @@ defer { print("func tournament availableToStart", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif - return allMatches.filter({ $0.canBeStarted(inMatches: runningMatches) && $0.isRunning() == false }).sorted(by: \.computedStartDateForSorting) + return allMatches.filter({ $0.isRunning() == false && $0.canBeStarted(inMatches: runningMatches, checkCanPlay: checkCanPlay) }).sorted(using: defaultSorting, order: .ascending) } - func runningMatches(_ allMatches: [Match]) -> [Match] { + static func runningMatches(_ allMatches: [Match]) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { @@ -1122,10 +906,10 @@ defer { print("func tournament runningMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif - return allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(by: \.computedStartDateForSorting) + return allMatches.filter({ $0.isRunning() && $0.isReady() }).sorted(using: defaultSorting, order: .ascending) } - func readyMatches(_ allMatches: [Match]) -> [Match] { + static func readyMatches(_ allMatches: [Match]) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { @@ -1133,10 +917,22 @@ defer { print("func tournament readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif - return allMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }).sorted(by: \.computedStartDateForSorting) + return allMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }).sorted(using: defaultSorting, order: .ascending) + } + + static func matchesLeft(_ allMatches: [Match]) -> [Match] { + #if _DEBUG_TIME //DEBUGING TIME + let start = Date() + defer { + let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) + print("func tournament readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) + } + #endif + return allMatches.filter({ $0.isRunning() == false && $0.hasEnded() == false }).sorted(using: defaultSorting, order: .ascending) } - func finishedMatches(_ allMatches: [Match], limit: Int?) -> [Match] { + + static func finishedMatches(_ allMatches: [Match], limit: Int?) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { @@ -1277,7 +1073,7 @@ defer { if team.qualified == false && alreadyPlaceTeams.contains(team.id) == false { let groupStageWidth = max(((index == qualifiedPerGroupStage) ? groupStageCount - groupStageAdditionalQualified : groupStageCount) * (index - qualifiedPerGroupStage), 0) - let _index = baseRank + groupStageWidth + 1 + let _index = baseRank + groupStageWidth + 1 - (index > qualifiedPerGroupStage ? groupStageAdditionalQualified : 0) if let existingTeams = teams[_index] { teams[_index] = existingTeams + [team.id] } else { @@ -1390,7 +1186,7 @@ defer { return tournamentLevel.localizedLevelLabel(.title) } } - let title: String = [tournamentLevel.localizedLevelLabel(displayStyle), tournamentCategory.localizedLabel(displayStyle), federalTournamentAge.localizedLabel(displayStyle)].filter({ $0.isEmpty == false }).joined(separator: " ") + let title: String = [tournamentLevel.localizedLevelLabel(displayStyle), tournamentCategory.localizedLabel(displayStyle), federalTournamentAge.localizedFederalAgeLabel(displayStyle)].filter({ $0.isEmpty == false }).joined(separator: " ") if displayStyle == .wide, let name { return [title, name].joined(separator: " - ") } else { @@ -1488,10 +1284,10 @@ defer { var entryFeeMessage: String { if let entryFee { - let message: String = "Inscription: \(entryFee.formatted(.currency(code: "EUR"))) par joueur." + let message: String = "Inscription : \(entryFee.formatted(.currency(code: Locale.defaultCurrency()))) par joueur." return [message, self._paymentMethodMessage()].compactMap { $0 }.joined(separator: "\n") } else { - return "Inscription: gratuite." + return "Inscription : gratuite." } } @@ -1551,7 +1347,9 @@ defer { func callStatus() async -> TournamentStatus { let selectedSortedTeams = selectedSortedTeams() let called = selectedSortedTeams.filter { isStartDateIsDifferentThanCallDate($0) == false } - let label = "\(called.count.formatted()) / \(selectedSortedTeams.count.formatted()) convoquées au bon horaire" + let justCalled = selectedSortedTeams.filter { $0.called() } + + let label = "\(justCalled.count.formatted()) / \(selectedSortedTeams.count.formatted()) (\(called.count.formatted()) au bon horaire)" let completion = (Double(called.count) / Double(selectedSortedTeams.count)) let completionLabel = completion.isNaN ? "" : completion.formatted(.percent.precision(.fractionLength(0))) return TournamentStatus(label: label, completion: completionLabel) @@ -1629,16 +1427,61 @@ defer { deleteGroupStages() switch preset { - case .manual: - buildGroupStages() - buildBracket() case .doubleGroupStage: buildGroupStages() addNewGroupStageStep() - qualifiedPerGroupStage = 0 groupStageAdditionalQualified = 0 + default: + buildGroupStages() + buildBracket() + } + } + + func addWildCardIfNeeded(_ count: Int, _ type: MatchType) { + let currentCount = selectedSortedTeams().filter({ + if type == .bracket { + return $0.wildCardBracket + } else { + return $0.wildCardGroupStage + } + }).count + + if currentCount < count { + let _diff = count - currentCount + addWildCard(_diff, type) + } + } + + func addWildCard(_ count: Int, _ type: MatchType) { + let wcs = (0.. 0 { switch groupStageOrderingMode { case .random: - setGroupStage(randomize: true) + setGroupStage(randomize: true, keepExistingMatches: keepExistingMatches) case .snake: - setGroupStage(randomize: false) + setGroupStage(randomize: false, keepExistingMatches: keepExistingMatches) case .swiss: - setGroupStage(randomize: true) + setGroupStage(randomize: true, keepExistingMatches: keepExistingMatches) } } } - func setGroupStage(randomize: Bool) { + func setGroupStage(randomize: Bool, keepExistingMatches: Bool = false) { let groupStages = groupStages() let numberOfBracketsAsInt = groupStages.count // let teamsPerBracket = teamsPerBracket @@ -1751,7 +1602,7 @@ defer { buildGroupStages() } else { setGroupStageTeams(randomize: randomize) - groupStages.forEach { $0.buildMatches() } + groupStages.forEach { $0.buildMatches(keepExistingMatches: keepExistingMatches) } } } @@ -1787,15 +1638,14 @@ defer { func labelIndexOf(team: TeamRegistration) -> String? { if let teamIndex = indexOf(team: team) { - return "#" + (teamIndex + 1).formatted() + return "Tête de série #" + (teamIndex + 1).formatted() } else { return nil } } func addTeam(_ players: Set, registrationDate: Date? = nil, name: String? = nil) -> TeamRegistration { - let date: Date = registrationDate ?? Date() - let team = TeamRegistration(tournament: id, registrationDate: date, name: name) + let team = TeamRegistration(tournament: id, registrationDate: registrationDate, name: name) team.setWeight(from: Array(players), inTournamentCategory: tournamentCategory) players.forEach { player in player.teamRegistration = team.id @@ -1921,7 +1771,7 @@ defer { private func _defaultSorting() -> [MySortDescriptor] { switch teamSorting { case .rank: - [.keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.registrationDate!), .keyPath(\TeamRegistration.id)] + [.keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.id)] case .inscriptionDate: [.keyPath(\TeamRegistration.registrationDate!), .keyPath(\TeamRegistration.initialWeight), .keyPath(\TeamRegistration.id)] } @@ -1933,7 +1783,7 @@ defer { && federalTournamentAge == build.age } - private let _currentSelectionSorting : [MySortDescriptor] = [.keyPath(\.weight), .keyPath(\.registrationDate!), .keyPath(\.id)] + private let _currentSelectionSorting : [MySortDescriptor] = [.keyPath(\.weight), .keyPath(\.id)] private func _matchSchedulers() -> [MatchScheduler] { return self.tournamentStore.matchSchedulers.filter { $0.tournament == self.id } @@ -1944,6 +1794,10 @@ defer { return self._matchSchedulers().first } + func courtsAvailable() -> [Int] { + (0.. MonthData? { guard let rankSourceDate else { return nil } let dateString = URL.importDateFormatter.string(from: rankSourceDate) @@ -2022,7 +1876,8 @@ defer { let newGroup = selected.prefix(seedCount) + selected.filter({ $0.qualified }) let currentGroup = allTeams.filter({ $0.bracketPosition != nil }) let selectedIds = newGroup.map { $0.id } - let groupIds = currentGroup.map { $0.id } + let groupStageTeamsInBracket = selected.filter({ $0.qualified == false && $0.inGroupStage() && $0.inRound() }) + let groupIds = currentGroup.map { $0.id } + groupStageTeamsInBracket.map { $0.id } let shouldBeInIt = Set(selectedIds).subtracting(groupIds) let shouldNotBeInIt = Set(groupIds).subtracting(selectedIds) return (Array(shouldBeInIt), Array(shouldNotBeInIt)) @@ -2056,8 +1911,12 @@ defer { groupStages().chunked(into: 2).forEach { gss in let placeCount = i * 2 + 1 let match = Match(round: groupStageLoserBracket.id, index: placeCount, format: groupStageLoserBracket.matchFormat) - match.name = "\(placeCount)\(placeCount.ordinalFormattedSuffix(feminine: true)) place" - tournamentStore.matches.addOrUpdate(instance: match) + match.setMatchName("\(placeCount)\(placeCount.ordinalFormattedSuffix(feminine: true)) place") + do { + try tournamentStore.matches.addOrUpdate(instance: match) + } catch { + Logger.error(error) + } if let gs1 = gss.first, let gs2 = gss.last, let score1 = gs1.teams(true)[safe: i], let score2 = gs2.teams(true)[safe: i] { print("rang \(i)") @@ -2084,6 +1943,138 @@ defer { rounds().flatMap { $0.loserRoundsAndChildren().flatMap({ $0._matches() }) } } + func seedsCount() -> Int { + selectedSortedTeams().count - groupStageSpots() + } + + func lastDrawnDate() -> Date? { + drawLogs().last?.drawDate + } + + func drawLogs() -> [DrawLog] { + self.tournamentStore.drawLogs.sorted(by: \.drawDate) + } + + func seedSpotsLeft() -> Bool { + let alreadySeededRounds = rounds().filter({ $0.seeds().isEmpty == false }) + if alreadySeededRounds.isEmpty { return true } + let spotsLeft = alreadySeededRounds.flatMap({ $0.playedMatches() }).filter { $0.isEmpty() || $0.isValidSpot() } + + return spotsLeft.isEmpty == false + } + + func isRoundValidForSeeding(roundIndex: Int) -> Bool { + if let lastRoundWithSeeds = rounds().last(where: { $0.seeds().isEmpty == false }) { + return roundIndex >= lastRoundWithSeeds.index + } else { + return true + } + } + + + func updateSeedsBracketPosition() async { + await removeAllSeeds() + let drawLogs = drawLogs().reversed() + let seeds = seeds() + for (index, seed) in seeds.enumerated() { + if let drawLog = drawLogs.first(where: { $0.drawSeed == index }) { + drawLog.updateTeamBracketPosition(seed) + } + } + + do { + try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: seeds) + } catch { + Logger.error(error) + } + } + + func removeAllSeeds() async { + unsortedTeams().forEach({ team in + team.bracketPosition = nil + }) + let ts = allRoundMatches().flatMap { match in + match.teamScores + } + + do { + try tournamentStore.teamScores.delete(contentOfs: ts) + } catch { + Logger.error(error) + } + do { + try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: unsortedTeams()) + } catch { + Logger.error(error) + } + allRounds().forEach({ round in + round.enableRound() + }) + + } + + func addNewRound(_ roundIndex: Int) async { + let round = Round(tournament: id, index: roundIndex, matchFormat: matchFormat) + let matchCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex) + let matchStartIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex) + let nextRound = round.nextRound() + var currentIndex = 0 + let matches = (0.. String { + var logs : [String] = ["Journal des tirages\n\n"] + logs.append(drawLogs().map { $0.exportedDrawLog() }.joined(separator: "\n\n")) + return logs.joined() + } + + + func courtUnavailable(courtIndex: Int, from startDate: Date, to endDate: Date) -> Bool { + guard let source = eventObject()?.courtsUnavailability else { return false } + let courtLockedSchedule = source.filter({ $0.courtIndex == courtIndex }) + return courtLockedSchedule.anySatisfy({ dateInterval in + let range = startDate.. String { if isAnimation() { if displayAgeAndCategory(forBuild: build) == false { - return [build.category.localizedLabel(), build.age.localizedLabel()].filter({ $0.isEmpty == false }).joined(separator: " ") + return [build.category.localizedLabel(), build.age.localizedFederalAgeLabel()].filter({ $0.isEmpty == false }).joined(separator: " ") } else if name != nil { return build.level.localizedLevelLabel(.title) } else { diff --git a/PadelClub/Data/TournamentStore.swift b/PadelClub/Data/TournamentStore.swift index ef27a3a..bdc5bb9 100644 --- a/PadelClub/Data/TournamentStore.swift +++ b/PadelClub/Data/TournamentStore.swift @@ -21,6 +21,7 @@ class TournamentStore: ObservableObject { fileprivate(set) var teamScores: StoredCollection = StoredCollection.placeholder() fileprivate(set) var matchSchedulers: StoredCollection = StoredCollection.placeholder() + fileprivate(set) var drawLogs: StoredCollection = StoredCollection.placeholder() // convenience init(tournament: Tournament) { // let store = StoreCenter.main.store(identifier: tournament.id) @@ -35,12 +36,10 @@ class TournamentStore: ObservableObject { fileprivate func _initialize() { -// super.init(identifier: identifier, parameter: parameter) - var synchronized: Bool = true let indexed: Bool = true - #if _DEBUG_OPTIONS + #if DEBUG if let sync = PListReader.readBool(plist: "local", key: "synchronized") { synchronized = sync } @@ -53,6 +52,7 @@ class TournamentStore: ObservableObject { self.matches = self.store.registerSynchronizedCollection(indexed: indexed) self.teamScores = self.store.registerSynchronizedCollection(indexed: indexed) self.matchSchedulers = self.store.registerCollection(indexed: indexed) + self.drawLogs = self.store.registerCollection(indexed: indexed) self.store.loadCollectionsFromServerIfNoFile() diff --git a/PadelClub/Extensions/Date+Extensions.swift b/PadelClub/Extensions/Date+Extensions.swift index 2758f48..de590b3 100644 --- a/PadelClub/Extensions/Date+Extensions.swift +++ b/PadelClub/Extensions/Date+Extensions.swift @@ -36,6 +36,14 @@ enum TimeOfDay { extension Date { + func withoutSeconds() -> Date { + let calendar = Calendar.current + return calendar.date(bySettingHour: calendar.component(.hour, from: self), + minute: calendar.component(.minute, from: self), + second: 0, + of: self)! + } + func localizedDate() -> String { self.formatted(.dateTime.weekday().day().month()) + " à " + self.formattedAsHourMinute() } @@ -231,4 +239,19 @@ extension Date { func localizedWeekDay() -> String { self.formatted(.dateTime.weekday(.wide)) } + + func timeElapsedString() -> String { + let timeInterval = abs(Date().timeIntervalSince(self)) + let duration = Duration.seconds(timeInterval) + + let formatStyle = Duration.UnitsFormatStyle(allowedUnits: [.hours, .minutes], width: .narrow) + return formatStyle.format(duration) + } + + static var hourMinuteFormatter: DateComponentsFormatter = { + let formatter = DateComponentsFormatter() + formatter.allowedUnits = [.hour, .minute] // Customize units + formatter.unitsStyle = .abbreviated // You can choose .abbreviated or .short + return formatter + }() } diff --git a/PadelClub/Extensions/FixedWidthInteger+Extensions.swift b/PadelClub/Extensions/FixedWidthInteger+Extensions.swift index f9c0d6d..37815d3 100644 --- a/PadelClub/Extensions/FixedWidthInteger+Extensions.swift +++ b/PadelClub/Extensions/FixedWidthInteger+Extensions.swift @@ -19,11 +19,25 @@ public extension FixedWidthInteger { return self.formatted() + self.ordinalFormattedSuffix(feminine: feminine) } + private var isMany: Bool { + self > 1 || self < -1 + } + var pluralSuffix: String { - return self > 1 ? "s" : "" + return isMany ? "s" : "" + } + + func localizedPluralSuffix(_ plural: String = "s") -> String { + return isMany ? plural : "" } func formattedAsRawString() -> String { String(self) } + + func durationInHourMinutes() -> String { + let duration = Duration.seconds(self*60) + let formatStyle = Duration.UnitsFormatStyle(allowedUnits: [.hours, .minutes], width: .narrow) + return formatStyle.format(duration) + } } diff --git a/PadelClub/Extensions/Locale+Extensions.swift b/PadelClub/Extensions/Locale+Extensions.swift index 5e6534b..646be4c 100644 --- a/PadelClub/Extensions/Locale+Extensions.swift +++ b/PadelClub/Extensions/Locale+Extensions.swift @@ -20,4 +20,9 @@ extension Locale { return countries.sorted() } + + static func defaultCurrency() -> String { +// return "EUR" + Locale.current.currency?.identifier ?? "EUR" + } } diff --git a/PadelClub/Extensions/String+Extensions.swift b/PadelClub/Extensions/String+Extensions.swift index 37094b9..4655383 100644 --- a/PadelClub/Extensions/String+Extensions.swift +++ b/PadelClub/Extensions/String+Extensions.swift @@ -166,8 +166,7 @@ extension String { // MARK: - FFT Source Importing extension String { enum RegexStatic { - static let mobileNumber = /^0[6-7]/ - //static let mobileNumber = /^(?:(?:\+|00)33[\s.-]{0,3}(?:\(0\)[\s.-]{0,3})?|0)[1-9](?:(?:[\s.-]?\d{2}){4}|\d{2}(?:[\s.-]?\d{3}){2})$/ + static let mobileNumber = /^(?:\+33|0033|0)[6-7](?:[ .-]?[0-9]{2}){4}$/ } func isMobileNumber() -> Bool { diff --git a/PadelClub/Info.plist b/PadelClub/Info.plist index 40756ec..7b68d4a 100644 --- a/PadelClub/Info.plist +++ b/PadelClub/Info.plist @@ -33,7 +33,5 @@ ITSAppUsesNonExemptEncryption - UIFileSharingEnabled - diff --git a/PadelClub/PadelClubApp.swift b/PadelClub/PadelClubApp.swift index ccf8660..4cdba91 100644 --- a/PadelClub/PadelClubApp.swift +++ b/PadelClub/PadelClubApp.swift @@ -17,7 +17,8 @@ struct PadelClubApp: App { @StateObject var dataStore = DataStore.shared @State private var registrationError: RegistrationError? = nil @State private var importObserverViewModel = ImportObserver() - + @Environment(\.horizontalSizeClass) var horizontalSizeClass + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var presentError: Binding { @@ -62,6 +63,7 @@ struct PadelClubApp: App { var body: some Scene { WindowGroup { MainView() + .environment(\.horizontalSizeClass, .compact) .alert(isPresented: presentError, error: registrationError) { Button("Contactez-nous") { _openMail() diff --git a/PadelClub/Utils/ContactManager.swift b/PadelClub/Utils/ContactManager.swift index 5c9d793..8c620a7 100644 --- a/PadelClub/Utils/ContactManager.swift +++ b/PadelClub/Utils/ContactManager.swift @@ -117,8 +117,16 @@ Il est conseillé de vous présenter 10 minutes avant de jouer.\n\nMerci de me c (DataStore.shared.user.summonsDisplayEntryFee) ? tournament?.entryFeeMessage : nil } + var linkMessage: String? { + if let tournament, tournament.isPrivate == false, let shareLink = tournament.shareURL(.matches)?.absoluteString { + return "Vous pourrez suivre tous les résultats de ce tournoi sur le site :\n\n".appending(shareLink) + } else { + return nil + } + } + var computedMessage: String { - [entryFeeMessage, message].compacted().map { $0.trimmedMultiline }.joined(separator: "\n\n") + [entryFeeMessage, message, linkMessage].compacted().map { $0.trimmedMultiline }.joined(separator: "\n\n") } let intro = reSummon ? "Suite à des forfaits, vous êtes finalement" : "Vous êtes" diff --git a/PadelClub/Utils/DisplayContext.swift b/PadelClub/Utils/DisplayContext.swift index a5aaebe..e9d0338 100644 --- a/PadelClub/Utils/DisplayContext.swift +++ b/PadelClub/Utils/DisplayContext.swift @@ -21,12 +21,9 @@ enum DisplayStyle { case short } -enum MatchViewStyle { - case standardStyle // vue normal - case sectionedStandardStyle // vue normal avec des sections indiquant déjà la manche - case feedStyle // vue programmation - case plainStyle // vue detail - case tournamentResultStyle //vue resultat tournoi +enum SummoningDisplayContext { + case footer + case menu } struct DeviceHelper { diff --git a/PadelClub/Utils/FileImportManager.swift b/PadelClub/Utils/FileImportManager.swift index 16d0c2f..01ffa69 100644 --- a/PadelClub/Utils/FileImportManager.swift +++ b/PadelClub/Utils/FileImportManager.swift @@ -64,9 +64,10 @@ class FileImportManager { importedPlayer.firstName = firstName.trimmed.capitalized } } - playersLeft.removeAll(where: { $0.lastName.isEmpty == false }) } }) + + players = playersLeft } func foundInWomenData(license: String?) -> Bool { diff --git a/PadelClub/Utils/PadelRule.swift b/PadelClub/Utils/PadelRule.swift index 7db94a5..4b21a3d 100644 --- a/PadelClub/Utils/PadelRule.swift +++ b/PadelClub/Utils/PadelRule.swift @@ -48,7 +48,7 @@ struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identifiable { } var identifier: String { - level.localizedLevelLabel()+":"+category.localizedLabel()+":"+age.localizedLabel() + level.localizedLevelLabel()+":"+category.localizedLabel()+":"+age.localizedFederalAgeLabel() } func computedLabel(_ displayStyle: DisplayStyle = .wide) -> String { @@ -65,7 +65,7 @@ struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identifiable { } func localizedAge(_ displayStyle: DisplayStyle = .wide) -> String { - age.localizedLabel(displayStyle) + age.localizedFederalAgeLabel(displayStyle) } } @@ -252,7 +252,7 @@ enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifiable { } } - func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { + func localizedFederalAgeLabel(_ displayStyle: DisplayStyle = .wide) -> String { switch self { case .unlisted: return displayStyle == .title ? "Aucune" : "" @@ -265,7 +265,7 @@ enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifiable { case .a17_18: return "17/18 ans" case .senior: - return "Senior" + return displayStyle == .short ? "" : "Senior" case .a45: return "+45 ans" case .a55: @@ -274,7 +274,7 @@ enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifiable { } var tournamentDescriptionLabel: String { - return localizedLabel() + return localizedFederalAgeLabel() } func isAgeValid(age: Int?) -> Bool { @@ -540,7 +540,7 @@ enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable { case .p25: switch count { case 9...12: - return [17, 13, 11, 9, 7, 5, 4, 3, 2, 1] + return [17, 15, 13, 11, 9, 7, 5, 4, 3, 2, 1] case 13...16: return [18,16,15,14,13,12,11,10,9,7,5,4,3,2, 1] case 17...20: @@ -996,6 +996,15 @@ enum TeamPosition: Int, Identifiable, Hashable, Codable, CaseIterable { return shortName } } + + func localizedBranchLabel() -> String { + switch self { + case .one: + return "Branche du haut" + case .two: + return "Branche du bas" + } + } } enum SetFormat: Int, Hashable, Codable { @@ -1126,19 +1135,27 @@ enum MatchType: String { case loserBracket = "loserBracket" } -enum MatchFormat: Int, Hashable, Codable, CaseIterable { +enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable { + var id: Int { self.rawValue } case twoSets case twoSetsSuperTie case twoSetsOfFourGames case nineGames case superTie case megaTie - + case twoSetsDecisivePoint case twoSetsDecisivePointSuperTie case twoSetsOfFourGamesDecisivePoint case nineGamesDecisivePoint + case twoSetsOfSuperTie + case singleSet + case singleSetDecisivePoint + case singleSetOfFourGames + case singleSetOfFourGamesDecisivePoint + + init?(rawValue: Int?) { guard let value = rawValue else { return nil } self.init(rawValue: value) @@ -1147,7 +1164,7 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { func defaultWalkOutScore(_ asWalkOutTeam: Bool) -> [Int] { Array(repeating: asWalkOutTeam ? 0 : setFormat.scoreToWin, count: setsToWin) } - + var weight: Int { switch self { case .twoSets, .twoSetsDecisivePoint: @@ -1162,6 +1179,12 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { return 4 case .megaTie: return 5 + case .twoSetsOfSuperTie: + return 6 + case .singleSet, .singleSetDecisivePoint: + return 7 + case .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint: + return 8 } } @@ -1187,6 +1210,12 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { return 4 case .megaTie: return 5 + case .twoSetsOfSuperTie: + return 6 + case .singleSet, .singleSetDecisivePoint: + return 7 + case .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint: + return 8 } } @@ -1202,7 +1231,7 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { } static var allCases: [MatchFormat] { - [.twoSets, .twoSetsDecisivePoint, .twoSetsSuperTie, .twoSetsDecisivePointSuperTie, .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .nineGames, .nineGamesDecisivePoint, .superTie, .megaTie] + [.twoSets, .twoSetsDecisivePoint, .twoSetsSuperTie, .twoSetsDecisivePointSuperTie, .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .nineGames, .nineGamesDecisivePoint, .superTie, .megaTie, .twoSetsOfSuperTie, .singleSet, .singleSetDecisivePoint, .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint] } func winner(scoreTeamOne: Int, scoreTeamTwo: Int) -> TeamPosition { @@ -1215,7 +1244,7 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { var canSuperTie: Bool { switch self { - case .twoSetsSuperTie, .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .twoSetsDecisivePointSuperTie: + case .twoSetsSuperTie, .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .twoSetsDecisivePointSuperTie, .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint: return true default: return false @@ -1233,16 +1262,18 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { func formattedEstimatedDuration(_ additionalDuration: Int = 0) -> String { Duration.seconds((estimatedDuration + additionalDuration) * 60).formatted(.units(allowed: [.minutes])) } - + func formattedEstimatedBreakDuration() -> String { var label = Duration.seconds(breakTime.breakTime * 60).formatted(.units(allowed: [.minutes])) if breakTime.matchCount > 1 { - label += " après \(breakTime.matchCount) match" + label += " de pause après \(breakTime.matchCount) match" label += breakTime.matchCount.pluralSuffix + } else { + label += " de pause" } return label } - + var defaultEstimatedDuration: Int { switch self { case .twoSets: @@ -1262,12 +1293,22 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { case .nineGamesDecisivePoint: return 40 case .megaTie: - return 30 + return 20 case .superTie: + return 15 + case .twoSetsOfSuperTie: + return 25 + case .singleSet: + return 30 + case .singleSetDecisivePoint: return 25 + case .singleSetOfFourGames: + return 15 + case .singleSetOfFourGamesDecisivePoint: + return 10 } } - + var estimatedTimeWithBreak: Int { estimatedDuration + breakTime.breakTime @@ -1283,7 +1324,7 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { return (30, 1) case .superTie: return (15, 3) - case .megaTie: + default: return (5, 1) } } @@ -1298,14 +1339,14 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { return matchCount < 7 ? 6 : 2 case .superTie: return 7 - case .megaTie: - return 7 + default: + return 10 } } var hasDecisivePoint: Bool { switch self { - case .nineGamesDecisivePoint, .twoSetsDecisivePoint, .twoSetsOfFourGamesDecisivePoint, .twoSetsDecisivePointSuperTie: + case .nineGamesDecisivePoint, .twoSetsDecisivePoint, .twoSetsOfFourGamesDecisivePoint, .twoSetsDecisivePointSuperTie, .singleSetDecisivePoint, .singleSetOfFourGamesDecisivePoint: return true default: return false @@ -1319,9 +1360,18 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { return setFormat } + func formatTitle(_ displayStyle: DisplayStyle = .wide) -> String { + switch displayStyle { + case .short: + return ["Format ", shortFormat].joined() + default: + return ["Format ", shortFormat, suffix].joined() + } + } + var suffix: String { switch self { - case .twoSetsDecisivePoint, .twoSetsDecisivePointSuperTie, .twoSetsOfFourGamesDecisivePoint, .nineGamesDecisivePoint: + case .twoSetsDecisivePoint, .twoSetsDecisivePointSuperTie, .twoSetsOfFourGamesDecisivePoint, .nineGamesDecisivePoint, .singleSetDecisivePoint: return " [Point Décisif]" default: return "" @@ -1335,8 +1385,20 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { var shortPrefix: String { return "\(format) : " } + + var isFederal: Bool { + switch self { + case .megaTie, .twoSetsOfSuperTie, .singleSet, .singleSetDecisivePoint, .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint: + return false + default: + return true + } + } var format: String { + shortFormat + (isFederal ? "" : " (non officiel)") + } + var shortFormat: String { switch self { case .twoSets: return "A1" @@ -1348,8 +1410,14 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { return "D1" case .superTie: return "E" + case .twoSetsOfSuperTie: + return "G" case .megaTie: return "F" + case .singleSet: + return "H1" + case .singleSetDecisivePoint: + return "H2" case .twoSetsDecisivePoint: return "A2" case .twoSetsDecisivePointSuperTie: @@ -1358,11 +1426,17 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { return "C2" case .nineGamesDecisivePoint: return "D2" + case .singleSetOfFourGames: + return "I1" + case .singleSetOfFourGamesDecisivePoint: + return "I2" } } var longLabel: String { switch self { + case .singleSet, .singleSetDecisivePoint: + return "1 set de 6" case .twoSets, .twoSetsDecisivePoint: return "2 sets de 6" case .twoSetsSuperTie, .twoSetsDecisivePointSuperTie: @@ -1371,10 +1445,14 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { return "2 sets de 4, tiebreak à 4/4, supertie au 3ème" case .nineGames, .nineGamesDecisivePoint: return "9 jeux, tiebreak à 8/8" + case .twoSetsOfSuperTie: + return "2 sets de supertie de 10 points" case .superTie: return "supertie de 10 points" case .megaTie: return "supertie de 15 points" + case .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint: + return "1 set de 4 jeux, tiebreak à 4/4" } } @@ -1392,22 +1470,22 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { var setsToWin: Int { switch self { - case .twoSets, .twoSetsSuperTie, .twoSetsOfFourGames, .twoSetsDecisivePoint, .twoSetsOfFourGamesDecisivePoint, .twoSetsDecisivePointSuperTie: + case .twoSets, .twoSetsSuperTie, .twoSetsOfFourGames, .twoSetsDecisivePoint, .twoSetsOfFourGamesDecisivePoint, .twoSetsDecisivePointSuperTie, .twoSetsOfSuperTie: return 2 - case .nineGames, .nineGamesDecisivePoint, .superTie, .megaTie: + case .nineGames, .nineGamesDecisivePoint, .superTie, .megaTie, .singleSet, .singleSetDecisivePoint, .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint: return 1 } } var setFormat: SetFormat { switch self { - case .twoSets, .twoSetsSuperTie, .twoSetsDecisivePoint, .twoSetsDecisivePointSuperTie: + case .twoSets, .twoSetsSuperTie, .twoSetsDecisivePoint, .twoSetsDecisivePointSuperTie, .singleSet, .singleSetDecisivePoint: return .six - case .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint: + case .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint: return .four case .nineGames, .nineGamesDecisivePoint: return .nine - case .superTie: + case .superTie, .twoSetsOfSuperTie: return .superTieBreak case .megaTie: return .megaTieBreak @@ -1592,7 +1670,8 @@ enum RoundRule { } static func numberOfRounds(forTeams teams: Int) -> Int { - Int(log2(Double(teamsInFirstRound(forTeams: teams)))) + if teams == 0 { return 0 } + return Int(log2(Double(teamsInFirstRound(forTeams: teams)))) } static func matchIndex(fromRoundIndex roundIndex: Int) -> Int { @@ -1680,6 +1759,113 @@ enum PadelTournamentStructurePreset: Int, Identifiable, CaseIterable { case manual case doubleGroupStage + case federalStructure_8 + case federalStructure_12 + case federalStructure_16 + case federalStructure_20 + case federalStructure_24 + case federalStructure_32 + case federalStructure_48 + case federalStructure_64 + + // Maximum qualified pairs based on the structure preset + func tableDimension() -> Int { + switch self { + case .federalStructure_8: + return 8 + case .federalStructure_12: + return 12 + case .federalStructure_16: + return 16 + case .federalStructure_20: + return 20 + case .federalStructure_24: + return 24 + case .federalStructure_32: + return 32 + case .federalStructure_48: + return 48 + case .federalStructure_64: + return 64 + case .manual: + return 24 + case .doubleGroupStage: + return 9 + } + } + + // Wildcards allowed in the Qualifiers + func wildcardBrackets() -> Int { + switch self { + case .federalStructure_8: + return 0 + case .federalStructure_12: + return 1 + case .federalStructure_16, .federalStructure_20, .federalStructure_24, .federalStructure_32: + return 2 + case .federalStructure_48, .federalStructure_64: + return 4 + case .manual, .doubleGroupStage: + return 0 + } + } + // Wildcards allowed in the Qualifiers + func wildcardQualifiers() -> Int { + switch self { + case .federalStructure_8: + return 0 + case .federalStructure_12, .federalStructure_16: + return 1 + case .federalStructure_20, .federalStructure_24: + return 2 + case .federalStructure_32: + return 4 + case .federalStructure_48: + return 6 + case .federalStructure_64: + return 8 + case .manual, .doubleGroupStage: + return 0 + } + } + + // Number of teams admitted to the Qualifiers + func teamsInQualifiers() -> Int { + switch self { + case .federalStructure_8: + return 8 + case .federalStructure_12: + return 12 + case .federalStructure_16: + return 16 + case .federalStructure_20: + return 20 + case .federalStructure_24: + return 24 + case .federalStructure_32: + return 32 + case .federalStructure_48: + return 48 + case .federalStructure_64: + return 64 + case .manual, .doubleGroupStage: + return 0 + } + } + + // Maximum teams that can qualify from the Qualifiers to the Final Table + func maxTeamsFromQualifiers() -> Int { + switch self { + case .federalStructure_8, .federalStructure_12: + return 2 + case .federalStructure_16, .federalStructure_20, .federalStructure_24: + return 4 + case .federalStructure_32, .federalStructure_48, .federalStructure_64: + return 8 + case .manual, .doubleGroupStage: + return 0 + } + } func localizedStructurePresetTitle() -> String { switch self { @@ -1687,6 +1873,22 @@ enum PadelTournamentStructurePreset: Int, Identifiable, CaseIterable { return "Défaut" case .doubleGroupStage: return "2 phases de poules" + case .federalStructure_8: + return "Structure fédérale 8" + case .federalStructure_12: + return "Structure fédérale 12" + case .federalStructure_16: + return "Structure fédérale 16" + case .federalStructure_20: + return "Structure fédérale 20" + case .federalStructure_24: + return "Structure fédérale 24" + case .federalStructure_32: + return "Structure fédérale 32" + case .federalStructure_48: + return "Structure fédérale 48" + case .federalStructure_64: + return "Structure fédérale 64" } } @@ -1695,7 +1897,85 @@ enum PadelTournamentStructurePreset: Int, Identifiable, CaseIterable { case .manual: return "24 équipes, 4 poules de 4, 1 qualifié par poule" case .doubleGroupStage: - return "Poules qui enchaîne sur une autre phase de poule : les premiers de chaque se retrouve ensemble, puis les 2èmes, etc." + return "Poules qui enchaînent sur une autre phase de poules: les premiers de chaque se retrouvent ensemble, puis les deuxièmes, etc." + case .federalStructure_8: + return "Tableau final à 8 paires, dont 2 qualifiées sortant de qualifications à 8 paires maximum. Aucune wildcard." + case .federalStructure_12, .federalStructure_16, .federalStructure_20, .federalStructure_24, .federalStructure_32, .federalStructure_48, .federalStructure_64: + return "Tableau final à \(tableDimension()) paires, dont \(maxTeamsFromQualifiers()) qualifiées sortant de qualifications à \(teamsInQualifiers()) paires maximum. \(wildcardBrackets()) wildcard\(wildcardBrackets().pluralSuffix) en tableau et \(wildcardQualifiers()) wildcard\(wildcardQualifiers().pluralSuffix) en qualifications." + } + } + + func groupStageCount() -> Int { + switch self { + case .manual: + 4 + case .doubleGroupStage: + 3 + case .federalStructure_8: + 2 + case .federalStructure_12: + 2 + case .federalStructure_16: + 4 + case .federalStructure_20: + 4 + case .federalStructure_24: + 4 + case .federalStructure_32: + 8 + case .federalStructure_48: + 8 + case .federalStructure_64: + 8 + } + } + + func teamsPerGroupStage() -> Int { + switch self { + case .manual: + 4 + case .doubleGroupStage: + 3 + case .federalStructure_8: + 4 + case .federalStructure_12: + 6 + case .federalStructure_16: + 4 + case .federalStructure_20: + 5 + case .federalStructure_24: + 6 + case .federalStructure_32: + 4 + case .federalStructure_48: + 6 + case .federalStructure_64: + 8 + } + } + + func qualifiedPerGroupStage() -> Int { + switch self { + case .doubleGroupStage: + 0 + default: + 1 + } + } + + func hasWildcards() -> Bool { + wildcardBrackets() > 0 || wildcardQualifiers() > 0 + } + + func isFederalPreset() -> Bool { + switch self { + case .manual: + return false + case .doubleGroupStage: + return false + case .federalStructure_8, .federalStructure_12, .federalStructure_16, .federalStructure_20, .federalStructure_24, .federalStructure_32, .federalStructure_48, .federalStructure_64: + return true } } } diff --git a/PadelClub/Utils/Patcher.swift b/PadelClub/Utils/Patcher.swift index 0b508be..885348f 100644 --- a/PadelClub/Utils/Patcher.swift +++ b/PadelClub/Utils/Patcher.swift @@ -47,120 +47,9 @@ class Patcher { case .syncUpgrade: self._syncUpgrade() } } -// -// fileprivate static func _patchAlexisLeDu() { -// guard StoreCenter.main.userId == "94f45ed2-8938-4c32-a4b6-e4525073dd33" else { return } -// -// let clubs = DataStore.shared.clubs -// StoreCenter.main.resetApiCalls(collection: clubs) -//// clubs.resetApiCalls() -// -// for club in clubs.filter({ $0.creator == "d5060b89-e979-4c19-bf78-e459a6ed5318"}) { -// club.creator = StoreCenter.main.userId -// clubs.writeChangeAndInsertOnServer(instance: club) -// } -// -// } -// -// fileprivate static func _importDataFromDev() throws { -// -// let devServices = Services(url: "https://xlr.alwaysdata.net/roads/") -// guard devServices.hasToken() else { -// return -// } -// guard StoreCenter.main.synchronizationApiURL == "https://padelclub.app/roads/" else { -// return -// } -// -// guard let userId = StoreCenter.main.userId else { -// return -// } -// -// try StoreCenter.main.migrateToken(devServices) -// -// -// let myClubs: [Club] = DataStore.shared.clubs.filter { $0.creator == userId } -// let clubIds: [String] = myClubs.map { $0.id } -// -// myClubs.forEach { club in -// DataStore.shared.clubs.insertIntoCurrentService(item: club) -// -// let courts = DataStore.shared.courts.filter { clubIds.contains($0.club) } -// for court in courts { -// DataStore.shared.courts.insertIntoCurrentService(item: court) -// } -// } -// -// DataStore.shared.user.clubs = Array(clubIds) -// DataStore.shared.saveUser() -// -// DataStore.shared.events.insertAllIntoCurrentService() -// DataStore.shared.tournaments.insertAllIntoCurrentService() -// DataStore.shared.dateIntervals.insertAllIntoCurrentService() -// -// for tournament in DataStore.shared.tournaments { -// let store = tournament.tournamentStore -// -// Task { // need to wait for the collections to load -// try await Task.sleep(until: .now + .seconds(2)) -// -// store.teamRegistrations.insertAllIntoCurrentService() -// store.rounds.insertAllIntoCurrentService() -// store.groupStages.insertAllIntoCurrentService() -// store.matches.insertAllIntoCurrentService() -// store.playerRegistrations.insertAllIntoCurrentService() -// store.teamScores.insertAllIntoCurrentService() -// -// } -// } -// -// } -// -// fileprivate static func _patchMissingMatches() { -// -// guard let url = StoreCenter.main.synchronizationApiURL else { -// return -// } -// guard url == "https://padelclub.app/roads/" else { -// return -// } -// let services = Services(url: url) -// -// for tournament in DataStore.shared.tournaments { -// -// let store = tournament.tournamentStore -// let identifier = StoreIdentifier(value: tournament.id, parameterName: "tournament") -// -// Task { -// -// do { -// // if nothing is online we upload the data -// let matches: [Match] = try await services.get(identifier: identifier) -// if matches.isEmpty { -// store.matches.insertAllIntoCurrentService() -// } -// -// let playerRegistrations: [PlayerRegistration] = try await services.get(identifier: identifier) -// if playerRegistrations.isEmpty { -// store.playerRegistrations.insertAllIntoCurrentService() -// } -// -// let teamScores: [TeamScore] = try await services.get(identifier: identifier) -// if teamScores.isEmpty { -// store.teamScores.insertAllIntoCurrentService() -// } -// -// } catch { -// Logger.error(error) -// } -// -// } -// } -// -// } fileprivate static func _cleanLogs() { -// StoreCenter.main.resetLoggingCollections() + StoreCenter.main.resetLoggingCollections() } fileprivate static func _syncUpgrade() { diff --git a/PadelClub/Utils/SourceFileManager.swift b/PadelClub/Utils/SourceFileManager.swift index 5313241..436f30b 100644 --- a/PadelClub/Utils/SourceFileManager.swift +++ b/PadelClub/Utils/SourceFileManager.swift @@ -60,9 +60,9 @@ class SourceFileManager { } } - func exportToCSV(players: [FederalPlayer], sourceFileType: SourceFile, date: Date) { + func exportToCSV(_ prefix: String = "", players: [FederalPlayer], sourceFileType: SourceFile, date: Date) { let lastDateString = URL.importDateFormatter.string(from: date) - let dateString = ["CLASSEMENT-PADEL", sourceFileType.rawValue, lastDateString].joined(separator: "-") + "." + "csv" + let dateString = [prefix, "CLASSEMENT-PADEL", sourceFileType.rawValue, lastDateString].filter({ $0.isEmpty == false }).joined(separator: "-") + "." + "csv" let documentsUrl:URL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first as URL?)! let destinationFileUrl = documentsUrl.appendingPathComponent("\(dateString)") diff --git a/PadelClub/Utils/URLs.swift b/PadelClub/Utils/URLs.swift index d7d0222..4ec9daa 100644 --- a/PadelClub/Utils/URLs.swift +++ b/PadelClub/Utils/URLs.swift @@ -50,6 +50,7 @@ enum URLs: String, Identifiable { } enum PageLink: String, Identifiable, CaseIterable { + case info = "Informations" case teams = "Équipes" case summons = "Convocations" case groupStages = "Poules" @@ -68,6 +69,8 @@ enum PageLink: String, Identifiable, CaseIterable { switch self { case .matches: return "" + case .info: + return "info" case .teams: return "teams" case .summons: diff --git a/PadelClub/ViewModel/FederalDataViewModel.swift b/PadelClub/ViewModel/FederalDataViewModel.swift index 71c0cf1..2ca3b2f 100644 --- a/PadelClub/ViewModel/FederalDataViewModel.swift +++ b/PadelClub/ViewModel/FederalDataViewModel.swift @@ -22,12 +22,13 @@ class FederalDataViewModel { var searchAttemptCount: Int = 0 var dayDuration: Int? var dayPeriod: DayPeriod = .all - + var lastError: NetworkManagerError? + func filterStatus() -> String { var labels: [String] = [] labels.append(contentsOf: levels.map { $0.localizedLevelLabel() }.formatList()) labels.append(contentsOf: categories.map { $0.localizedLabel() }.formatList()) - labels.append(contentsOf: ageCategories.map { $0.localizedLabel() }.formatList()) + labels.append(contentsOf: ageCategories.map { $0.localizedFederalAgeLabel() }.formatList()) let clubNames = selectedClubs.compactMap { codeClub in let club: Club? = DataStore.shared.clubs.first(where: { $0.code == codeClub }) return club?.clubTitle(.short) diff --git a/PadelClub/ViewModel/MatchDescriptor.swift b/PadelClub/ViewModel/MatchDescriptor.swift index 8072cb3..dd88446 100644 --- a/PadelClub/ViewModel/MatchDescriptor.swift +++ b/PadelClub/ViewModel/MatchDescriptor.swift @@ -1,11 +1,12 @@ // -// MatchDescriptor.swift +// swift // PadelClub // // Created by Razmig Sarkissian on 02/04/2024. // import Foundation +import SwiftUI class MatchDescriptor: ObservableObject { @Published var matchFormat: MatchFormat @@ -16,6 +17,58 @@ class MatchDescriptor: ObservableObject { var teamLabelTwo: String = "" var startDate: Date = Date() var match: Match? + let colorTeamOne: Color = .teal + let colorTeamTwo: Color = .indigo + + var teamOneSetupIsActive: Bool { + if hasEnded && showSetInputView == false && showTieBreakInputView == false { + return false + } + + guard let setDescriptor = setDescriptors.last else { + return false + } + if setDescriptor.valueTeamOne == nil { + return true + } else if setDescriptor.valueTeamTwo == nil { + return false + } else if setDescriptor.tieBreakValueTeamOne == nil, setDescriptor.shouldTieBreak { + return true + } else if setDescriptor.tieBreakValueTeamTwo == nil, setDescriptor.shouldTieBreak { + return false + } + + return false + } + + var teamTwoSetupIsActive: Bool { + if hasEnded && showSetInputView == false && showTieBreakInputView == false { + return false + } + guard let setDescriptor = setDescriptors.last else { + return false + } + + if setDescriptor.valueTeamOne == nil { + return false + } else if setDescriptor.valueTeamTwo == nil { + return true + } else if setDescriptor.tieBreakValueTeamOne == nil, setDescriptor.shouldTieBreak { + return false + } else if setDescriptor.tieBreakValueTeamTwo == nil, setDescriptor.shouldTieBreak { + return true + } + + return true + } + + var showSetInputView: Bool { + return setDescriptors.anySatisfy({ $0.showSetInputView }) + } + + var showTieBreakInputView: Bool { + return setDescriptors.anySatisfy({ $0.showTieBreakInputView }) + } init(match: Match? = nil) { self.match = match diff --git a/PadelClub/ViewModel/MatchViewStyle.swift b/PadelClub/ViewModel/MatchViewStyle.swift new file mode 100644 index 0000000..5917fc7 --- /dev/null +++ b/PadelClub/ViewModel/MatchViewStyle.swift @@ -0,0 +1,51 @@ +// +// MatchViewStyle.swift +// PadelClub +// +// Created by razmig on 17/11/2024. +// + +import SwiftUI + +enum MatchViewStyle { + case standardStyle // vue normal + case sectionedStandardStyle // vue normal avec des sections indiquant déjà la manche + case feedStyle // vue programmation + case plainStyle // vue detail + //case tournamentResultStyle //vue resultat tournoi + case followUpStyle // vue normal + + func displayRestingTime() -> Bool { + switch self { + case .standardStyle: + return false + case .sectionedStandardStyle: + return false + case .feedStyle: + return false + case .plainStyle: + return false +// case .tournamentResultStyle: +// return false + case .followUpStyle: + return true + } + } +} + +struct MatchViewStyleKey: EnvironmentKey { + static let defaultValue: MatchViewStyle = .standardStyle +} + +extension EnvironmentValues { + var matchViewStyle: MatchViewStyle { + get { self[MatchViewStyleKey.self] } + set { self[MatchViewStyleKey.self] = newValue } + } +} + +extension View { + func matchViewStyle(_ style: MatchViewStyle) -> some View { + environment(\.matchViewStyle, style) + } +} diff --git a/PadelClub/ViewModel/Screen.swift b/PadelClub/ViewModel/Screen.swift index 9ac761e..ab2187e 100644 --- a/PadelClub/ViewModel/Screen.swift +++ b/PadelClub/ViewModel/Screen.swift @@ -21,4 +21,5 @@ enum Screen: String, Codable { case event case print case share + case restingTime } diff --git a/PadelClub/ViewModel/SearchViewModel.swift b/PadelClub/ViewModel/SearchViewModel.swift index 0b066c3..be8a85c 100644 --- a/PadelClub/ViewModel/SearchViewModel.swift +++ b/PadelClub/ViewModel/SearchViewModel.swift @@ -36,8 +36,7 @@ class SearchViewModel: ObservableObject, Identifiable { @Published var filterSelectionEnabled: Bool = false @Published var isPresented: Bool = false @Published var selectedAgeCategory: FederalTournamentAge = .unlisted - - var mostRecentDate: Date? = nil + @Published var mostRecentDate: Date? = nil var selectionIsOver: Bool { if allowSingleSelection && selectedPlayers.count == 1 { @@ -69,9 +68,6 @@ class SearchViewModel: ObservableObject, Identifiable { var message = ["Vérifiez l'ortographe ou lancez une nouvelle recherche."] if tokens.isEmpty { message.append("Il est possible que cette personne n'est joué aucun tournoi depuis les 12 derniers mois, dans ce cas, Padel Club ne pourra pas le trouver.") - if filterOption == .male { - message.append("Depuis août 2024, le classement fédérale disponible est limité aux 40.000 premiers joueurs. Si le joueur n'a pas encore assez de points pour être visible, Padel Club ne pourra pas non plus le trouver.") - } } return message.joined(separator: "\n") } @@ -231,7 +227,7 @@ class SearchViewModel: ObservableObject, Identifiable { ] if let mostRecentDate { - //predicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg)) + predicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg)) } if hideAssimilation { @@ -344,7 +340,7 @@ class SearchViewModel: ObservableObject, Identifiable { } if let mostRecentDate { - //andPredicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg)) + andPredicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg)) } if nameComponents.count > 1 { diff --git a/PadelClub/ViewModel/SetDescriptor.swift b/PadelClub/ViewModel/SetDescriptor.swift index 2811e4e..6ef5bfe 100644 --- a/PadelClub/ViewModel/SetDescriptor.swift +++ b/PadelClub/ViewModel/SetDescriptor.swift @@ -14,6 +14,12 @@ struct SetDescriptor: Identifiable, Equatable { var tieBreakValueTeamOne: Int? var tieBreakValueTeamTwo: Int? var setFormat: SetFormat + var showSetInputView: Bool = true + var showTieBreakInputView: Bool = false + + var isTeamOneSet: Bool { + return valueTeamOne != nil || tieBreakValueTeamOne != nil + } var hasEnded: Bool { if let valueTeamTwo, let valueTeamOne { @@ -30,4 +36,8 @@ struct SetDescriptor: Identifiable, Equatable { return nil } } + + var shouldTieBreak: Bool { + setFormat.shouldTiebreak(scoreTeamOne: valueTeamOne ?? 0, scoreTeamTwo: valueTeamTwo ?? 0) + } } diff --git a/PadelClub/Views/Calling/BracketCallingView.swift b/PadelClub/Views/Calling/BracketCallingView.swift new file mode 100644 index 0000000..810919e --- /dev/null +++ b/PadelClub/Views/Calling/BracketCallingView.swift @@ -0,0 +1,214 @@ +// +// BracketCallingView.swift +// PadelClub +// +// Created by razmig on 15/10/2024. +// + +import SwiftUI +import LeStorage + +struct BracketCallingView: View { + @EnvironmentObject var dataStore: DataStore + @Environment(Tournament.self) var tournament: Tournament + @State private var initialSeedRound: Int = 0 + @State private var initialSeedCount: Int = 0 + let tournamentRounds: [Round] + let teams: [TeamRegistration] + + init(tournament: Tournament) { + let rounds = tournament.rounds() + self.tournamentRounds = rounds + self.teams = tournament.availableSeeds() + if tournament.initialSeedRound == 0, rounds.count > 0 { + let index = rounds.count - 1 + _initialSeedRound = .init(wrappedValue: index) + _initialSeedCount = .init(wrappedValue: RoundRule.numberOfMatches(forRoundIndex: index)) + } else if tournament.initialSeedRound < rounds.count { + _initialSeedRound = .init(wrappedValue: tournament.initialSeedRound) + _initialSeedCount = .init(wrappedValue: tournament.initialSeedCount) + } else if rounds.count > 0 { + let index = rounds.count - 1 + _initialSeedRound = .init(wrappedValue: index) + _initialSeedCount = .init(wrappedValue: RoundRule.numberOfMatches(forRoundIndex: index)) + } + } + + var initialRound: Round { + tournamentRounds.first(where: { $0.index == initialSeedRound })! + } + + func filteredRounds() -> [Round] { + tournamentRounds.filter({ $0.index >= initialSeedRound }).reversed() + } + + func seedCount(forRoundIndex roundIndex: Int) -> Int { + if roundIndex < initialSeedRound { return 0 } + if roundIndex == initialSeedRound { + return initialSeedCount + } + + let seedCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex) + let previousSeedCount = self.seedCount(forRoundIndex: roundIndex - 1) + + let total = seedCount - previousSeedCount + if total < 0 { return 0 } + return total + } + + func seeds(forRoundIndex roundIndex: Int) -> [TeamRegistration] { + let previousSeeds: Int = (initialSeedRound.. some View { + List { + + let uncalledTeams = seeds.filter({ $0.callDate == nil }) + if uncalledTeams.isEmpty == false { + NavigationLink { + TeamsCallingView(teams: uncalledTeams) + .environment(tournament) + } label: { + LabeledContent("Équipe\(uncalledTeams.count.pluralSuffix) non contactée\(uncalledTeams.count.pluralSuffix)", value: uncalledTeams.count.formatted()) + } + } + + let startDate = round.startDate ?? round.playedMatches().first?.startDate + let badCalled = seeds.filter({ tournament.isStartDateIsDifferentThanCallDate($0, expectedSummonDate: startDate) }) + + if badCalled.isEmpty == false { + Section { + ForEach(badCalled) { team in + TeamCallView(team: team) + } + } header: { + HStack { + Text("Mauvais horaire") + Spacer() + Text(badCalled.count.formatted() + " équipe\(badCalled.count.pluralSuffix)") + } + } footer: { + if let startDate { + CallView(teams: badCalled, callDate: startDate, matchFormat: round.matchFormat, roundLabel: round.roundTitle()) + } + } + } + + Section { + ForEach(seeds) { team in + TeamCallView(team: team) + } + } header: { + HStack { + Text(round.roundTitle()) + Spacer() + Text(seeds.count.formatted() + " équipe\(seeds.count.pluralSuffix)") + } + } + } + .overlay { + if seeds.isEmpty { + ContentUnavailableView { + Label("Aucune équipe dans ce tour", systemImage: "clock.badge.questionmark") + } description: { + Text("Padel Club n'a pas réussi à déterminer quelles équipes jouent ce tour.") + } actions: { +// RowButtonView("Horaire intelligent") { +// selectedScheduleDestination = nil +// } + } + } + } + .headerProminence(.increased) + .navigationTitle(round.roundTitle()) + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + } +} + +//#Preview { +// SeedsCallingView() +//} diff --git a/PadelClub/Views/Calling/CallMessageCustomizationView.swift b/PadelClub/Views/Calling/CallMessageCustomizationView.swift index 569f420..0ed9a54 100644 --- a/PadelClub/Views/Calling/CallMessageCustomizationView.swift +++ b/PadelClub/Views/Calling/CallMessageCustomizationView.swift @@ -43,7 +43,15 @@ struct CallMessageCustomizationView: View { } var computedMessage: String { - [entryFeeMessage, customCallMessageBody].compacted().map { $0.trimmedMultiline }.joined(separator: "\n") + var linkMessage: String? { + if tournament.isPrivate == false, let shareLink = tournament.shareURL(.matches)?.absoluteString { + return "Vous pourrez suivre tous les résultats de ce tournoi sur le site :\n\n".appending(shareLink) + } else { + return nil + } + } + + return [entryFeeMessage, customCallMessageBody, linkMessage].compacted().map { $0.trimmedMultiline }.joined(separator: "\n") } var finalMessage: String? { @@ -259,7 +267,7 @@ struct CallMessageCustomizationView: View { } }.italic().foregroundStyle(.gray) } header: { - Text("Rendu généré automatiquement") + Text("Exemple généré automatiquement") } } diff --git a/PadelClub/Views/Calling/CallView.swift b/PadelClub/Views/Calling/CallView.swift index 58f953c..86f12af 100644 --- a/PadelClub/Views/Calling/CallView.swift +++ b/PadelClub/Views/Calling/CallView.swift @@ -14,6 +14,7 @@ struct CallView: View { let count: Int let total: Int let startDate: Date? + var title: String = "convoquées au bon horaire" var body: some View { VStack(spacing: 0) { @@ -32,7 +33,7 @@ struct CallView: View { Text(startDate.formatted(.dateTime.weekday().day(.twoDigits).month().year())) } Spacer() - Text("convoquées au bon horaire") + Text(title) } .font(.caption) .foregroundColor(.secondary) @@ -40,14 +41,6 @@ struct CallView: View { } } - struct TeamView: View { - let team: TeamRegistration - - var body: some View { - TeamRowView(team: team, displayCallDate: true) - } - } - @EnvironmentObject var dataStore: DataStore @EnvironmentObject var networkMonitor: NetworkMonitor @@ -57,6 +50,7 @@ struct CallView: View { let callDate: Date let matchFormat: MatchFormat let roundLabel: String + let displayContext: SummoningDisplayContext @State private var contactType: ContactType? = nil @State private var sentError: ContactManagerError? = nil @@ -67,6 +61,49 @@ struct CallView: View { @State var summonParamByMessage: Bool = false @State var summonParamReSummon: Bool = false + let simpleMode : Bool + + init(teams: [TeamRegistration], callDate: Date, matchFormat: MatchFormat, roundLabel: String) { + self.teams = teams + self.callDate = callDate + self.matchFormat = matchFormat + self.roundLabel = roundLabel + self.simpleMode = false + self.displayContext = .footer + } + + init(teams: [TeamRegistration]) { + self.teams = teams + self.callDate = Date() + self.matchFormat = MatchFormat.nineGames + self.roundLabel = "" + self.simpleMode = true + self.displayContext = .footer + } + + init(team: TeamRegistration, displayContext: SummoningDisplayContext) { + self.teams = [team] + let expectedSummonDate = team.expectedSummonDate() + self.displayContext = displayContext + + if let expectedSummonDate, let initialMatch = team.initialMatch() { + self.callDate = expectedSummonDate + self.matchFormat = initialMatch.matchFormat + self.roundLabel = initialMatch.roundTitle() ?? "tableau" + self.simpleMode = false + } else if let expectedSummonDate, let initialGroupStage = team.groupStageObject() { + self.callDate = expectedSummonDate + self.matchFormat = initialGroupStage.matchFormat + self.roundLabel = "poule" + self.simpleMode = false + } else { + self.callDate = Date() + self.matchFormat = MatchFormat.nineGames + self.roundLabel = "" + self.simpleMode = true + } + } + var tournamentStore: TournamentStore { return self.tournament.tournamentStore } @@ -82,9 +119,15 @@ struct CallView: View { } private func _called(_ calledTeams: [TeamRegistration], _ success: Bool) { + if simpleMode { + return + } if success { calledTeams.forEach { team in team.callDate = callDate + if reSummon { + team.confirmationDate = nil + } } do { try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: calledTeams) @@ -94,33 +137,39 @@ struct CallView: View { } } - func finalMessage(reSummon: Bool) -> String { - ContactType.callingMessage(tournament: tournament, startDate: callDate, roundLabel: roundLabel, matchFormat: matchFormat, reSummon: reSummon) + func finalMessage(reSummon: Bool, forcedEmptyMessage: Bool) -> String { + if simpleMode || forcedEmptyMessage { + let signature = dataStore.user.summonsMessageSignature ?? dataStore.user.defaultSignature() + return "\n\n\n\n" + signature + } + + return ContactType.callingMessage(tournament: tournament, startDate: callDate, roundLabel: roundLabel, matchFormat: matchFormat, reSummon: reSummon) } var reSummon: Bool { + if simpleMode { + return false + } return self.teams.allSatisfy({ $0.called() }) } + var mainWord: String { + if simpleMode { + return "Contacter" + } else { + return "Convoquer" + } + } + var body: some View { - let callWord : String = (reSummon ? "Reconvoquer" : "Convoquer") - HStack { - if self.teams.count == 1 { - if let previousCallDate = teams.first?.callDate, Calendar.current.compare(previousCallDate, to: callDate, toGranularity: .minute) != .orderedSame { - Text("Reconvoquer \(self.callDate.localizedDate()) par") - } else { - Text("\(callWord) cette paire par") - } - } else { - Text("\(callWord) ces \(self.teams.count) paires par") + Group { + switch displayContext { + case .footer: + _footerStyleView() + case .menu: + _menuStyleView() } - - self._summonMenu(byMessage: true) - Text("ou") - self._summonMenu(byMessage: false) } - .font(.subheadline) - .buttonStyle(.borderless) .alert("Un problème est survenu", isPresented: messageSentFailed) { Button("OK") { } @@ -216,18 +265,73 @@ struct CallView: View { } }) } + + private func _footerStyleView() -> some View { + HStack { + let callWord : String = (reSummon ? "Reconvoquer" : mainWord) + if self.teams.count == 1 { + if simpleMode { + Text("\(callWord) cette paire par") + } else { + if let previousCallDate = teams.first?.callDate, Calendar.current.compare(previousCallDate, to: callDate, toGranularity: .minute) != .orderedSame { + Text("Reconvoquer \(self.callDate.localizedDate()) par") + } else { + Text("\(callWord) cette paire par") + } + } + } else { + Text("\(callWord) ces \(self.teams.count) paires par") + } + + self._summonMenu(byMessage: true) + Text("ou") + self._summonMenu(byMessage: false) + } + .font(.subheadline) + .buttonStyle(.borderless) + } + + private func _menuStyleView() -> some View { + Menu { + self._summonMenu(byMessage: true) + self._summonMenu(byMessage: false) + } label: { + let callWord : String = (reSummon ? "Reconvoquer" : mainWord) + if self.teams.count == 1 { + if simpleMode { + Text("\(callWord) cette paire") + } else { + if let previousCallDate = teams.first?.callDate, Calendar.current.compare(previousCallDate, to: callDate, toGranularity: .minute) != .orderedSame { + Text("Reconvoquer \(self.callDate.localizedDate())") + } else { + Text("\(callWord) cette paire") + } + } + } else { + Text("\(callWord) ces \(self.teams.count) paires") + } + } + } + @ViewBuilder private func _summonMenu(byMessage: Bool) -> some View { if self.reSummon { Menu { - Button("Convoquer") { + Button(mainWord) { self._summon(byMessage: byMessage, reSummon: false) } Button("Re-convoquer") { self._summon(byMessage: byMessage, reSummon: true) } + + if simpleMode == false { + Divider() + Button("Contacter") { + self._summon(byMessage: byMessage, reSummon: false, forcedEmptyMessage: true) + } + } } label: { Text(byMessage ? "sms" : "mail") @@ -240,15 +344,15 @@ struct CallView: View { } } - private func _summon(byMessage: Bool, reSummon: Bool) { + private func _summon(byMessage: Bool, reSummon: Bool, forcedEmptyMessage: Bool = false) { self.summonParamByMessage = byMessage self.summonParamReSummon = reSummon self._verifyUser { self._payTournamentAndExecute { if byMessage { - self._contactByMessage(reSummon: reSummon) + self._contactByMessage(reSummon: reSummon, forcedEmptyMessage: forcedEmptyMessage) } else { - self._contactByMail(reSummon: reSummon) + self._contactByMail(reSummon: reSummon, forcedEmptyMessage: forcedEmptyMessage) } } } @@ -271,18 +375,18 @@ struct CallView: View { } } - fileprivate func _contactByMessage(reSummon: Bool) { - self.contactType = .message(date: callDate, + fileprivate func _contactByMessage(reSummon: Bool, forcedEmptyMessage: Bool) { + self.contactType = .message(date: callDate, recipients: teams.flatMap { $0.getPhoneNumbers() }, - body: finalMessage(reSummon: reSummon), + body: finalMessage(reSummon: reSummon, forcedEmptyMessage: forcedEmptyMessage), tournamentBuild: nil) } - fileprivate func _contactByMail(reSummon: Bool) { + fileprivate func _contactByMail(reSummon: Bool, forcedEmptyMessage: Bool) { self.contactType = .mail(date: callDate, recipients: tournament.umpireMail(), bccRecipients: teams.flatMap { $0.getMail() }, - body: finalMessage(reSummon: reSummon), + body: finalMessage(reSummon: reSummon, forcedEmptyMessage: forcedEmptyMessage), subject: tournament.tournamentTitle(), tournamentBuild: nil) } @@ -292,3 +396,20 @@ struct CallView: View { } } + +struct TeamCallView: View { + @Environment(Tournament.self) var tournament: Tournament + let team: TeamRegistration + var action: (() -> Void)? + + var body: some View { + NavigationLink { + CallMenuOptionsView(team: team, action: action) + .environment(tournament) + } label: { + TeamRowView(team: team, displayCallDate: true) + } + .buttonStyle(.plain) + .listRowView(isActive: team.confirmed(), color: .green, hideColorVariation: true) + } +} diff --git a/PadelClub/Views/Calling/Components/MenuWarningView.swift b/PadelClub/Views/Calling/Components/MenuWarningView.swift index ef094a0..d770ab1 100644 --- a/PadelClub/Views/Calling/Components/MenuWarningView.swift +++ b/PadelClub/Views/Calling/Components/MenuWarningView.swift @@ -124,7 +124,7 @@ struct MenuWarningView: View { @ViewBuilder func _teamActionView(_ team: TeamRegistration) -> some View { - Menu(team.name ?? "Toute l'équipe") { + Menu(team.teamNameLabel()) { let players = team.players() _actionView(players: players) } diff --git a/PadelClub/Views/Calling/Components/PlayersWithoutContactView.swift b/PadelClub/Views/Calling/Components/PlayersWithoutContactView.swift index 4e8d78e..cffae6e 100644 --- a/PadelClub/Views/Calling/Components/PlayersWithoutContactView.swift +++ b/PadelClub/Views/Calling/Components/PlayersWithoutContactView.swift @@ -31,7 +31,7 @@ struct PlayersWithoutContactView: View { } } - let withoutPhones = players.filter({ $0.phoneNumber?.isEmpty == true || $0.phoneNumber == nil }) + let withoutPhones = players.filter({ $0.phoneNumber?.isEmpty == true || $0.phoneNumber == nil || $0.phoneNumber?.isMobileNumber() == false }) DisclosureGroup { ForEach(withoutPhones) { player in NavigationLink { @@ -45,7 +45,7 @@ struct PlayersWithoutContactView: View { LabeledContent { Text(withoutPhones.count.formatted()) } label: { - Text("Joueurs sans téléphone") + Text("Joueurs sans téléphone portable") } } } header: { diff --git a/PadelClub/Views/Calling/GroupStageCallingView.swift b/PadelClub/Views/Calling/GroupStageCallingView.swift index 357df0f..cb12cd4 100644 --- a/PadelClub/Views/Calling/GroupStageCallingView.swift +++ b/PadelClub/Views/Calling/GroupStageCallingView.swift @@ -15,11 +15,14 @@ struct GroupStageCallingView: View { let groupStages = tournament.groupStages() List { - NavigationLink { - TeamsCallingView(teams: groupStages.flatMap({ $0.unsortedTeams() }).filter({ $0.callDate == nil })) - .environment(tournament) - } label: { - LabeledContent("Équipes non contactées", value: groupStages.flatMap({ $0.unsortedTeams() }).filter({ $0.callDate == nil }).count.formatted()) + let uncalled = groupStages.flatMap({ $0.unsortedTeams() }).filter({ $0.callDate == nil }) + if uncalled.isEmpty == false { + NavigationLink { + TeamsCallingView(teams: uncalled) + .environment(tournament) + } label: { + LabeledContent("Équipe\(uncalled.count.pluralSuffix) non contactée\(uncalled.count.pluralSuffix)", value: uncalled.count.formatted()) + } } PlayersWithoutContactView(players: groupStages.flatMap({ $0.unsortedTeams() }).flatMap({ $0.unsortedPlayers() }).sorted(by: \.computedRank)) @@ -83,7 +86,7 @@ struct GroupStageCallingView: View { ForEach(teams) { team in if let startDate = groupStage.initialStartDate(forTeam: team) { Section { - CallView.TeamView(team: team) + TeamCallView(team: team) } header: { Text(startDate.localizedDate()) } footer: { diff --git a/PadelClub/Views/Calling/SeedsCallingView.swift b/PadelClub/Views/Calling/SeedsCallingView.swift index abfcec7..c1b899e 100644 --- a/PadelClub/Views/Calling/SeedsCallingView.swift +++ b/PadelClub/Views/Calling/SeedsCallingView.swift @@ -14,18 +14,21 @@ struct SeedsCallingView: View { var body: some View { List { let tournamentRounds = tournament.rounds() + let uncalledSeeds = tournament.seededTeams().filter({ $0.callDate == nil }) - NavigationLink { - TeamsCallingView(teams: tournament.seededTeams().filter({ $0.callDate == nil })) - .environment(tournament) - } label: { - LabeledContent("Équipes non contactées", value: tournament.seededTeams().filter({ $0.callDate == nil }).count.formatted()) + if uncalledSeeds.isEmpty == false { + NavigationLink { + TeamsCallingView(teams: uncalledSeeds) + .environment(tournament) + } label: { + LabeledContent("Équipe\(uncalledSeeds.count.pluralSuffix) non contactée\(uncalledSeeds.count.pluralSuffix)", value: uncalledSeeds.count.formatted()) + } } PlayersWithoutContactView(players: tournament.seededTeams().flatMap({ $0.unsortedPlayers() }).sorted(by: \.computedRank)) ForEach(tournamentRounds) { round in - let seeds = round.seeds() + let seeds = round.teamsOrSeeds() let callSeeds = seeds.filter({ tournament.isStartDateIsDifferentThanCallDate($0) == false }) if seeds.isEmpty == false { Section { @@ -63,8 +66,14 @@ struct SeedsCallingView: View { } } - NavigationLink("Équipes non contactées") { - TeamsCallingView(teams: round.teams().filter({ $0.callDate == nil })) + let uncalledTeams = round.teams().filter({ $0.callDate == nil }) + if uncalledTeams.isEmpty == false { + NavigationLink { + TeamsCallingView(teams: uncalledTeams) + .environment(tournament) + } label: { + LabeledContent("Équipe\(uncalledTeams.count.pluralSuffix) non contactée\(uncalledTeams.count.pluralSuffix)", value: uncalledTeams.count.formatted()) + } } @@ -92,7 +101,7 @@ struct SeedsCallingView: View { let teams = round.seeds(inMatchIndex: match.index) Section { ForEach(teams) { team in - CallView.TeamView(team: team) + TeamCallView(team: team) } } header: { HStack { diff --git a/PadelClub/Views/Calling/TeamsCallingView.swift b/PadelClub/Views/Calling/TeamsCallingView.swift index 986322d..8a5d1e9 100644 --- a/PadelClub/Views/Calling/TeamsCallingView.swift +++ b/PadelClub/Views/Calling/TeamsCallingView.swift @@ -11,78 +11,171 @@ import LeStorage struct TeamsCallingView: View { @Environment(Tournament.self) var tournament: Tournament let teams : [TeamRegistration] + + @State private var hideConfirmed: Bool = false + @State private var hideGoodSummoned: Bool = false + @State private var hideSummoned: Bool = false + @State private var searchText: String = "" + var filteredTeams: [TeamRegistration] { + teams + .filter({ hideConfirmed == false || $0.confirmed() == false }) + .filter({ hideSummoned == false || $0.called() == false }) + .filter({ hideGoodSummoned == false || tournament.isStartDateIsDifferentThanCallDate($0) == true }) + .filter({ searchText.isEmpty || $0.contains(searchText) }) + } + + var anyFilterEnabled: Bool { + hideConfirmed || hideGoodSummoned || hideSummoned + } + var body: some View { List { - Section { - ForEach(teams) { team in - Menu { - _menuOptions(team: team) + PlayersWithoutContactView(players: teams.flatMap({ $0.unsortedPlayers() }).sorted(by: \.computedRank)) + + let called = teams.filter { tournament.isStartDateIsDifferentThanCallDate($0) == false } + let confirmed = teams.filter { $0.confirmed() } + let justCalled = teams.filter { $0.called() } + + let label = "\(justCalled.count.formatted()) / \(teams.count.formatted())" + let subtitle = "dont \(called.count.formatted()) au bon horaire" + let confirmedLabel = "\(confirmed.count.formatted()) / \(teams.count.formatted())" + + if teams.isEmpty == false, searchText.isEmpty { + Section { + LabeledContent { + Text(label).font(.title3) } label: { - HStack { - TeamRowView(team: team, displayCallDate: true) - Spacer() - Menu { - _menuOptions(team: team) - } label: { - LabelOptions().labelStyle(.iconOnly) - } + Text("Paire\(justCalled.count.pluralSuffix) convoquée\(justCalled.count.pluralSuffix)") + Text(subtitle) + } + LabeledContent { + Text(confirmedLabel).font(.title3) + } label: { + Text("Paire\(confirmed.count.pluralSuffix) confirmée\(confirmed.count.pluralSuffix)") + } + } footer: { + Text("Vous pouvez filtrer cette liste en appuyant sur ") + Text(Image(systemName: "line.3.horizontal.decrease.circle")) + } + } + + if filteredTeams.isEmpty == false { + Section { + ForEach(filteredTeams) { team in + TeamCallView(team: team) { + searchText = "" } } - .buttonStyle(.plain) - .listRowView(isActive: team.confirmed(), color: .green, hideColorVariation: true) + } header: { + HStack { + Text("Paire\(filteredTeams.count.pluralSuffix)") + Spacer() + Text(filteredTeams.count.formatted()) + } + } footer: { + CallView(teams: filteredTeams) } + } else { + ContentUnavailableView("Aucune équipe", systemImage: "person.2.slash") } } + .toolbar(content: { + ToolbarItem(placement: .topBarTrailing) { + Menu { + Toggle(isOn: $hideConfirmed) { + Text("Masquer les confirmées") + } + Toggle(isOn: $hideSummoned) { + Text("Masquer les convoquées") + } + Toggle(isOn: $hideGoodSummoned) { + Text("Masquer les convoquées à la bonne heure") + } + } label: { + LabelFilter() + .symbolVariant(anyFilterEnabled ? .fill : .none) + } + } + }) + .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always)) .headerProminence(.increased) - .navigationTitle("Statut des équipes") + .navigationTitle("Statut des convocations") .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) } +} - @ViewBuilder - func _menuOptions(team: TeamRegistration) -> some View { - Button { +struct CallMenuOptionsView: View { + @Environment(\.dismiss) private var dismiss + @Environment(Tournament.self) var tournament: Tournament + let team: TeamRegistration + let action: (() -> Void)? + + var confirmed: Binding { + Binding { + team.confirmed() + } set: { _ in team.toggleSummonConfirmation() do { try self.tournament.tournamentStore.teamRegistrations.addOrUpdate(instance: team) } catch { Logger.error(error) } - } label: { - if team.confirmed() { - Label("Confirmation reçue", systemImage: "checkmark.circle.fill").foregroundStyle(.green) - } else { - Label("Confirmation reçue", systemImage: "circle").foregroundStyle(.logoRed) - } + action?() } - Divider() - - Button(role: .destructive) { - team.callDate = nil - do { - try self.tournament.tournamentStore.teamRegistrations.addOrUpdate(instance: team) - } catch { - Logger.error(error) + } + + var body: some View { + List { + Section { + TeamRowView(team: team, displayCallDate: true) + Toggle(isOn: confirmed) { + Text("Confirmation reçue") + } + if team.expectedSummonDate() != nil { + CallView(team: team, displayContext: .menu) + } + } footer: { + CallView(teams: [team]) } - } label: { - Text("Effacer la date de convocation") - } - - - Divider() - - Button(role: .destructive) { - team.callDate = team.initialMatch()?.startDate ?? tournament.startDate - do { - try self.tournament.tournamentStore.teamRegistrations.addOrUpdate(instance: team) - } catch { - Logger.error(error) + + Section { + NavigationLink { + EditingTeamView(team: team) + .environment(tournament) + } label: { + Text("Détails de l'équipe") + } + } + + Section { + RowButtonView("Effacer la date de convocation", role: .destructive) { + team.callDate = nil + do { + try self.tournament.tournamentStore.teamRegistrations.addOrUpdate(instance: team) + } catch { + Logger.error(error) + } + action?() + dismiss() + } + } + + Section { + RowButtonView("Indiquer comme convoquée", role: .destructive) { + team.callDate = team.initialMatch()?.startDate ?? tournament.startDate + do { + try self.tournament.tournamentStore.teamRegistrations.addOrUpdate(instance: team) + } catch { + Logger.error(error) + } + action?() + dismiss() + } } - } label: { - Text("Indiquer comme convoquée") } - - + .navigationTitle("Options de convocation") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) } } diff --git a/PadelClub/Views/Cashier/CashierDetailView.swift b/PadelClub/Views/Cashier/CashierDetailView.swift index ae51e3b..0f1c8a1 100644 --- a/PadelClub/Views/Cashier/CashierDetailView.swift +++ b/PadelClub/Views/Cashier/CashierDetailView.swift @@ -26,7 +26,7 @@ struct CashierDetailView: View { Section { LabeledContent { if let earnings { - Text(earnings.formatted(.currency(code: "EUR").precision(.fractionLength(0)))) + Text(earnings.formatted(.currency(code: Locale.defaultCurrency()).precision(.fractionLength(0)))) } else { ProgressView() } @@ -95,7 +95,7 @@ struct CashierDetailView: View { Section { LabeledContent { if let earnings { - Text(earnings.formatted(.currency(code: "EUR").precision(.fractionLength(0)))) + Text(earnings.formatted(.currency(code: Locale.defaultCurrency()).precision(.fractionLength(0)))) } else { ProgressView() } @@ -144,7 +144,7 @@ struct CashierDetailView: View { var body: some View { LabeledContent { if let value { - Text(value.formatted(.currency(code: "EUR"))) + Text(value.formatted(.currency(code: Locale.defaultCurrency()))) } else { ProgressView() } @@ -173,7 +173,7 @@ struct CashierDetailView: View { LabeledContent { if let entryFee = tournament.entryFee { let sum = Double(count) * entryFee - Text(sum.formatted(.currency(code: "EUR"))) + Text(sum.formatted(.currency(code: Locale.defaultCurrency()))) } } label: { Text(type.localizedLabel()) diff --git a/PadelClub/Views/Cashier/CashierSettingsView.swift b/PadelClub/Views/Cashier/CashierSettingsView.swift index d236a90..fe93c84 100644 --- a/PadelClub/Views/Cashier/CashierSettingsView.swift +++ b/PadelClub/Views/Cashier/CashierSettingsView.swift @@ -24,69 +24,71 @@ struct CashierSettingsView: View { var body: some View { List { Section { - LabeledContent { - TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $entryFee, format: .currency(code: Locale.current.currency?.identifier ?? "EUR")) - .keyboardType(.decimalPad) - .multilineTextAlignment(.trailing) - .frame(maxWidth: .infinity) - .focused($focusedField, equals: ._entryFee) - } label: { - Text("Inscription") - } + TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $entryFee, format: .currency(code: Locale.defaultCurrency())) + .keyboardType(.decimalPad) + .multilineTextAlignment(.trailing) + .frame(maxWidth: .infinity) + .focused($focusedField, equals: ._entryFee) + } header: { + Text("Prix de l'inscription") } footer: { Text("Si vous souhaitez que Padel Club vous aide à suivre les encaissements, indiquer un prix d'inscription. Sinon Padel Club vous aidera à suivre simplement l'arrivée et la présence des joueurs.") } - Section { - RowButtonView("Tout le monde est arrivé", role: .destructive) { - let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() }) - players.forEach { player in - player.hasArrived = true + let players = tournament.selectedPlayers() + + if players.anySatisfy({ $0.hasArrived == false }) { + Section { + RowButtonView("Tout le monde est arrivé", role: .destructive) { + players.forEach { player in + player.hasArrived = true + } + _save(players: players) } - tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) + } footer: { + Text("Indique tous les joueurs sont là") } - } footer: { - Text("Indique tous les joueurs sont là") } - Section { - RowButtonView("Personne n'est là", role: .destructive) { - let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() }) - players.forEach { player in - player.hasArrived = false + if players.anySatisfy({ $0.hasArrived == true }) { + Section { + RowButtonView("Personne n'est là", role: .destructive) { + players.forEach { player in + player.hasArrived = false + } + _save(players: players) } - tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) + } footer: { + Text("Indique qu'aucun joueur n'est arrivé") } - } footer: { - Text("Indique qu'aucun joueur n'est arrivé") } - Section { - RowButtonView("Tout le monde a réglé", role: .destructive) { - let players = tournament.selectedPlayers() // tournaments.flatMap({ $0.selectedPlayers() }) - players.forEach { player in - if player.hasPaid() == false { - player.paymentType = .gift + if players.anySatisfy({ $0.hasPaid() == false }) { + Section { + RowButtonView("Tout le monde a réglé", role: .destructive) { + players.forEach { player in + if player.hasPaid() == false { + player.paymentType = .gift + } } + _save(players: players) } - tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) + } footer: { + Text("Passe tous les joueurs qui n'ont pas réglé en offert") } - } footer: { - Text("Passe tous les joueurs qui n'ont pas réglé en offert") } - Section { - RowButtonView("Personne n'a réglé", role: .destructive) { - let store = tournament.tournamentStore - - let players = tournament.selectedPlayers() - players.forEach { player in - player.paymentType = nil + if players.anySatisfy({ $0.hasPaid() == true }) { + Section { + RowButtonView("Personne n'a réglé", role: .destructive) { + players.forEach { player in + player.paymentType = nil + } + _save(players: players) } - store.playerRegistrations.addOrUpdate(contentOfs: players) + } footer: { + Text("Remet à zéro le type d'encaissement de tous les joueurs") } - } footer: { - Text("Remet à zéro le type d'encaissement de tous les joueurs") } } .navigationBarBackButtonHidden(focusedField != nil) @@ -103,7 +105,7 @@ struct CashierSettingsView: View { HStack { if tournament.isFree() { ForEach(priceTags, id: \.self) { priceTag in - Button(priceTag.formatted(.currency(code: "EUR"))) { + Button(priceTag.formatted(.currency(code: Locale.defaultCurrency()))) { entryFee = priceTag tournament.entryFee = priceTag focusedField = nil @@ -134,6 +136,10 @@ struct CashierSettingsView: View { } } + private func _save(players: [PlayerRegistration]) { + tournament.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) + } + private func _save() { dataStore.tournaments.addOrUpdate(instance: tournament) } diff --git a/PadelClub/Views/Cashier/CashierView.swift b/PadelClub/Views/Cashier/CashierView.swift index b022259..ef95e65 100644 --- a/PadelClub/Views/Cashier/CashierView.swift +++ b/PadelClub/Views/Cashier/CashierView.swift @@ -56,7 +56,7 @@ extension Array { class CashierViewModel: ObservableObject { let id: UUID = UUID() @Published var sortOption: SortOption = .callDate - @Published var filterOption: FilterOption = .all + @Published var filterOption: FilterOption = .didNotPay @Published var presenceFilterOption: PresenceFilterOption = .all @Published var sortOrder: SortOrder = .ascending @Published var searchText: String = "" @@ -70,10 +70,7 @@ class CashierViewModel: ObservableObject { func _shouldDisplayPlayer(_ player: PlayerRegistration) -> Bool { if searchText.isEmpty == false { - sortOption.shouldDisplayPlayer(player) - && filterOption.shouldDisplayPlayer(player) - && presenceFilterOption.shouldDisplayPlayer(player) - && player.contains(searchText) + player.contains(searchText) } else { sortOption.shouldDisplayPlayer(player) && filterOption.shouldDisplayPlayer(player) @@ -306,8 +303,7 @@ struct CashierView: View { case .alphabeticalLastName, .alphabeticalFirstName, .playerRank, .age: PlayerCashierView(players: filteredPlayers, displayTournamentTitle: tournaments.count > 1, editingOptions: _editingOptions()) case .callDate: - let _teams = teams.filter({ $0.callDate != nil }) - TeamCallDateView(teams: _teams, displayTournamentTitle: tournaments.count > 1, editingOptions: _editingOptions()) + TeamCallDateView(teams: teams, displayTournamentTitle: tournaments.count > 1, editingOptions: _editingOptions()) } } .onAppear { @@ -354,7 +350,7 @@ struct CashierView: View { } } footer: { if let teamCallDate = player.team()?.callDate { - Text("équipe convoqué") + Text(teamCallDate.localizedDate()) + Text("équipe convoquée ") + Text(teamCallDate.localizedDate()) } } } @@ -369,16 +365,16 @@ struct CashierView: View { var body: some View { ForEach(teams) { team in - let players = team.players().filter({ cashierViewModel._shouldDisplayPlayer($0) }) - if players.isEmpty == false { + let players = team.players() + if players.isEmpty == false, cashierViewModel._shouldDisplayTeam(team) { Section { ForEach(players) { player in EditablePlayerView(player: player, editingOptions: editingOptions) } } header: { HStack { - if let name = team.name { - Text(name) + if let teamName = team.name, teamName.isEmpty == false { + Text(teamName) } if displayTournamentTitle, let tournamentTitle = team.tournamentObject()?.tournamentTitle() { Spacer() @@ -404,22 +400,22 @@ struct CashierView: View { var body: some View { let groupedTeams = Dictionary(grouping: teams) { team in - team.callDate + team.callDate ?? .distantPast } let keys = cashierViewModel.sortOrder == .ascending ? groupedTeams.keys.compactMap { $0 }.sorted() : groupedTeams.keys.compactMap { $0 }.sorted().reversed() ForEach(keys, id: \.self) { key in if let _teams = groupedTeams[key] { ForEach(_teams) { team in - let players = team.players().filter({ cashierViewModel._shouldDisplayPlayer($0) }) - if players.isEmpty == false { + let players = team.players() + if players.isEmpty == false, cashierViewModel._shouldDisplayTeam(team) { Section { ForEach(players) { player in EditablePlayerView(player: player, editingOptions: editingOptions) } } header: { - if let name = team.name { - Text(name) + if let teamName = team.name, teamName.isEmpty == false { + Text(teamName) } if displayTournamentTitle, let tournamentTitle = team.tournamentObject()?.tournamentTitle() { diff --git a/PadelClub/Views/Cashier/Event/EventSettingsView.swift b/PadelClub/Views/Cashier/Event/EventSettingsView.swift index cad0f0a..4186a8b 100644 --- a/PadelClub/Views/Cashier/Event/EventSettingsView.swift +++ b/PadelClub/Views/Cashier/Event/EventSettingsView.swift @@ -105,7 +105,7 @@ struct EventSettingsView: View { Button("Valider") { textFieldIsFocus = false if eventName.trimmed.isEmpty == false { - event.name = eventName.trimmed + event.name = eventName.prefixTrimmed(200) } else { event.name = nil } diff --git a/PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift b/PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift index 9333ee9..30ec0f6 100644 --- a/PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift +++ b/PadelClub/Views/Cashier/Event/TournamentConfiguratorView.swift @@ -46,7 +46,7 @@ struct TournamentConfigurationView: View { } Picker(selection: $tournament.federalAgeCategory, label: Text("Limite d'âge")) { ForEach(FederalTournamentAge.allCases) { type in - Text(type.localizedLabel(.title)).tag(type) + Text(type.localizedFederalAgeLabel(.title)).tag(type) } } LabeledContent { diff --git a/PadelClub/Views/Club/ClubDetailView.swift b/PadelClub/Views/Club/ClubDetailView.swift index 313e933..d31871e 100644 --- a/PadelClub/Views/Club/ClubDetailView.swift +++ b/PadelClub/Views/Club/ClubDetailView.swift @@ -19,6 +19,8 @@ struct ClubDetailView: View { @Bindable var club: Club @State private var clubDeleted: Bool = false @State private var confirmDeletion: Bool = false + @State private var timezone: String = TimeZone.current.identifier + var displayContext: DisplayContext var selection: ((Club) -> ())? = nil @@ -29,6 +31,10 @@ struct ClubDetailView: View { _acronymMode = State(wrappedValue: club.shortNameMode()) _city = State(wrappedValue: club.city ?? "") _zipCode = State(wrappedValue: club.zipCode ?? "") + + if let timezone = club.timezone { + self.timezone = timezone + } } var body: some View { @@ -216,9 +222,12 @@ struct ClubDetailView: View { .navigationBarBackButtonHidden(focusedField != nil) .toolbar(content: { if focusedField != nil { - ToolbarItem(placement: .topBarLeading) { - Button("Annuler", role: .cancel) { - focusedField = nil + ToolbarItem(placement: .keyboard) { + HStack { + Button("Fermer", role: .cancel) { + focusedField = nil + } + Spacer() } } } diff --git a/PadelClub/Views/Club/CourtView.swift b/PadelClub/Views/Club/CourtView.swift index c4e89f8..6b37fc8 100644 --- a/PadelClub/Views/Club/CourtView.swift +++ b/PadelClub/Views/Club/CourtView.swift @@ -30,9 +30,11 @@ struct CourtView: View { .multilineTextAlignment(.trailing) .frame(maxWidth: .infinity) .onSubmit { - court.name = name - if name.isEmpty { + let courtName = name.prefixTrimmed(50) + if courtName.isEmpty { court.name = nil + } else { + court.name = courtName } do { try dataStore.courts.addOrUpdate(instance: court) diff --git a/PadelClub/Views/Components/CopyPasteButtonView.swift b/PadelClub/Views/Components/CopyPasteButtonView.swift index aac21d4..0b5c976 100644 --- a/PadelClub/Views/Components/CopyPasteButtonView.swift +++ b/PadelClub/Views/Components/CopyPasteButtonView.swift @@ -11,13 +11,29 @@ struct CopyPasteButtonView: View { let pasteValue: String? @State private var copied: Bool = false + @ViewBuilder var body: some View { - Button { - let pasteboard = UIPasteboard.general - pasteboard.string = pasteValue - copied = true - } label: { - Label(copied ? "copié" : "copier", systemImage: "doc.on.doc").symbolVariant(copied ? .fill : .none) + if let pasteValue { + Button { + let pasteboard = UIPasteboard.general + pasteboard.string = pasteValue + copied = true + } label: { + Label(copied ? "Copié" : "Copier", systemImage: "doc.on.doc").symbolVariant(copied ? .fill : .none) + } + } + } +} + +struct PasteButtonView: View { + @Binding var text: String + + @ViewBuilder + var body: some View { + PasteButton(payloadType: String.self) { strings in + if let pasteboard = strings.first { + text = pasteboard + } } } } diff --git a/PadelClub/Views/Components/FortuneWheelView.swift b/PadelClub/Views/Components/FortuneWheelView.swift index ac659d1..795773a 100644 --- a/PadelClub/Views/Components/FortuneWheelView.swift +++ b/PadelClub/Views/Components/FortuneWheelView.swift @@ -8,20 +8,20 @@ import SwiftUI protocol SpinDrawable { - func segmentLabel(_ displayStyle: DisplayStyle) -> [String] + func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] } extension String: SpinDrawable { - func segmentLabel(_ displayStyle: DisplayStyle) -> [String] { + func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] { [self] } } extension Match: SpinDrawable { - func segmentLabel(_ displayStyle: DisplayStyle) -> [String] { + func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] { let teams = teams() - if teams.count == 1 { - return teams.first!.segmentLabel(displayStyle) + if teams.count == 1, hideNames == false { + return teams.first!.segmentLabel(displayStyle, hideNames: hideNames) } else { return [roundTitle(), matchTitle(displayStyle)].compactMap { $0 } } @@ -29,12 +29,16 @@ extension Match: SpinDrawable { } extension TeamRegistration: SpinDrawable { - func segmentLabel(_ displayStyle: DisplayStyle) -> [String] { + func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] { var strings: [String] = [] let indexLabel = tournamentObject()?.labelIndexOf(team: self) if let indexLabel { strings.append(indexLabel) + if hideNames { + return strings + } } + strings.append(contentsOf: self.players().map { $0.playerLabel(displayStyle) }) return strings } @@ -51,8 +55,8 @@ struct DrawOption: Identifiable, SpinDrawable { let initialIndex: Int let option: SpinDrawable - func segmentLabel(_ displayStyle: DisplayStyle) -> [String] { - option.segmentLabel(displayStyle) + func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] { + option.segmentLabel(displayStyle, hideNames: hideNames) } } @@ -62,6 +66,7 @@ struct SpinDrawView: View { let drawees: [any SpinDrawable] @State var segments: [any SpinDrawable] var autoMode: Bool = false + var hideNames: Bool = false let completion: ([DrawResult]) async -> Void // Completion closure @State private var drawCount: Int = 0 @@ -89,12 +94,12 @@ struct SpinDrawView: View { } } else if drawCount < drawees.count { Section { - _segmentLabelView(segment: drawees[drawCount].segmentLabel(.wide), horizontalAlignment: .center) + _segmentLabelView(segment: drawees[drawCount].segmentLabel(.wide, hideNames: hideNames), horizontalAlignment: .center) } Section { ZStack { - FortuneWheelContainerView(segments: drawOptions, autoMode: autoMode) { index in + FortuneWheelContainerView(segments: drawOptions, autoMode: autoMode, hideNames: hideNames) { index in self.selectedIndex = index self.draws.append(DrawResult(drawee: drawCount, drawIndex: drawOptions[index].initialIndex)) self.drawOptions.remove(at: index) @@ -209,8 +214,8 @@ struct SpinDrawView: View { private func _segmentLabelView(segment: [String], horizontalAlignment: HorizontalAlignment = .leading) -> some View { VStack(alignment: horizontalAlignment, spacing: 0.0) { - ForEach(segment, id: \.self) { string in - Text(string).font(.title3) + ForEach(segment.indices, id: \.self) { lineIndex in + Text(segment[lineIndex]).font(.title3) .frame(maxWidth: .infinity) .lineLimit(1) } @@ -221,13 +226,13 @@ struct SpinDrawView: View { private func _validationLabelView(drawee: Int, result: SpinDrawable) -> some View { VStack(spacing: 0.0) { let draw = drawees[drawee] - _segmentLabelView(segment: draw.segmentLabel(.wide), horizontalAlignment: .center) + _segmentLabelView(segment: draw.segmentLabel(.wide, hideNames: hideNames), horizontalAlignment: .center) if result as? TeamRegistration != nil { Image(systemName: "flag.2.crossed.fill").font(.largeTitle).foregroundColor(.logoRed) } else { Image(systemName: "arrowshape.down.fill").font(.largeTitle).foregroundColor(.logoRed) } - _segmentLabelView(segment: result.segmentLabel(.wide), horizontalAlignment: .center) + _segmentLabelView(segment: result.segmentLabel(.wide, hideNames: hideNames), horizontalAlignment: .center) } } } @@ -236,10 +241,11 @@ struct FortuneWheelContainerView: View { @State private var rotation: Double = 0 let segments: [any SpinDrawable] let autoMode: Bool + let hideNames: Bool let completion: (Int) -> Void // Completion closure var body: some View { - FortuneWheelView(segments: segments) + FortuneWheelView(segments: segments, hideNames: hideNames) .rotationEffect(.degrees(rotation)) .aspectRatio(contentMode: .fill) .padding(.top, 5) @@ -303,6 +309,7 @@ struct FortuneWheelContainerView: View { struct FortuneWheelView: View { let segments: [any SpinDrawable] + let hideNames: Bool let colors: [Color] = [.yellow, .cyan, .green, .blue, .orange, .purple, .mint, .brown] func getColor(forIndex index: Int) -> Color { @@ -330,12 +337,12 @@ struct FortuneWheelView: View { path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false) path.closeSubpath() } - .fill(getColor(forIndex:index)) + .fill(getColor(forIndex: index)) VStack(alignment: .trailing, spacing: 0.0) { - let strings = segments[index].segmentLabel(.short) - ForEach(strings, id: \.self) { string in - Text(string).bold() + let strings = labels(forIndex: index) + ForEach(strings.indices, id: \.self) { lineIndex in + Text(strings[lineIndex]).bold() .font(.subheadline) } } @@ -349,6 +356,19 @@ struct FortuneWheelView: View { } } + private func labels(forIndex index: Int) -> [String] { + if segments.count < 5 { + return segments[index].segmentLabel(.short, hideNames: hideNames) + } else { + let values = segments[index].segmentLabel(.short, hideNames: hideNames) + if values.count < 3 { + return values + } else { + return Array(segments[index].segmentLabel(.short, hideNames: hideNames).prefix(1)) + } + } + } + // Calculate the position for the text in the middle of the arc segment private func arcPosition(index: Int, radius: Double) -> CGPoint { let segmentAngle = 360.0 / Double(segments.count) diff --git a/PadelClub/Views/Components/GenericDestinationPickerView.swift b/PadelClub/Views/Components/GenericDestinationPickerView.swift index 59079c5..86e23c8 100644 --- a/PadelClub/Views/Components/GenericDestinationPickerView.swift +++ b/PadelClub/Views/Components/GenericDestinationPickerView.swift @@ -73,7 +73,7 @@ struct GenericDestinationPickerView: ) .offset(x: 3, y: 3) } else if let count, count > 0 { - Image(systemName: count <= 50 ? "\(String(count)).circle.fill" : "plus.circle.fill") + Image(systemName: count <= 50 ? "\(String(count)).circle.fill" : "ellipsis.circle.fill") .foregroundColor(destination.badgeValueColor() ?? .logoRed) .imageScale(.medium) .background ( @@ -93,7 +93,7 @@ struct GenericDestinationPickerView: ) .offset(x: 3, y: 3) } else if let count = destination.badgeValue(), count > 0 { - Image(systemName: count <= 50 ? "\(String(count)).circle.fill" : "plus.circle.fill") + Image(systemName: count <= 50 ? "\(String(count)).circle.fill" : "ellipsis.circle.fill") .foregroundColor(destination.badgeValueColor() ?? .logoRed) .imageScale(.medium) .background ( diff --git a/PadelClub/Views/Components/MatchListView.swift b/PadelClub/Views/Components/MatchListView.swift index 412ee38..0f98f01 100644 --- a/PadelClub/Views/Components/MatchListView.swift +++ b/PadelClub/Views/Components/MatchListView.swift @@ -10,11 +10,10 @@ import SwiftUI struct MatchListView: View { @EnvironmentObject var dataStore: DataStore - @Environment(Tournament.self) var tournament + @Environment(\.matchViewStyle) private var matchViewStyle let section: String let matches: [Match]? - var matchViewStyle: MatchViewStyle = .standardStyle var hideWhenEmpty: Bool = false @State var isExpanded: Bool = true @@ -30,24 +29,22 @@ struct MatchListView: View { @ViewBuilder var body: some View { if _shouldHide() == false { - Section { - DisclosureGroup(isExpanded: $isExpanded) { - if let matches { - ForEach(matches) { match in - MatchRowView(match: match, matchViewStyle: matchViewStyle) - .listRowInsets(EdgeInsets(top: 0, leading: -2, bottom: 0, trailing: 8)) - } + DisclosureGroup(isExpanded: $isExpanded) { + if let matches { + ForEach(matches) { match in + MatchRowView(match: match) + .listRowInsets(EdgeInsets(top: 0, leading: -2, bottom: 0, trailing: 8)) } - } label: { - LabeledContent { - if matches == nil { - ProgressView() - } else { - Text(matches!.count.formatted() + " match" + matches!.count.pluralSuffix) - } - } label: { - Text(section.firstCapitalized) + } + } label: { + LabeledContent { + if matches == nil { + ProgressView() + } else { + Text(matches!.count.formatted() + " match" + matches!.count.pluralSuffix) } + } label: { + Text(section.firstCapitalized) } } } diff --git a/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift b/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift index 64abb0f..521b328 100644 --- a/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift +++ b/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift @@ -42,7 +42,7 @@ struct GroupStageSettingsView: View { .submitLabel(.done) .frame(maxWidth: .infinity) .onSubmit { - groupStageName = groupStageName.trimmed + groupStageName = groupStageName.prefixTrimmed(200) if groupStageName.isEmpty == false { groupStage.name = groupStageName _save() @@ -143,18 +143,60 @@ struct GroupStageSettingsView: View { Section { RowButtonView("Recommencer tous les matchs", role: .destructive) { + let isReturnMatchesEnabled = groupStage.isReturnMatchEnabled() groupStage.buildMatches() + if isReturnMatchesEnabled { + groupStage.addReturnMatches() + } } } footer: { Text("Tous les matchs seront recronstruits, les données des matchs seront perdus.") } + Section { + if groupStage.matchPhaseCount > 2 { + RowButtonView("Effacer la dernière vague", role: .destructive) { + groupStage.removeReturnMatches(onlyLast: true) + } + } else if groupStage.isReturnMatchEnabled() { + RowButtonView("Effacer les matchs retours", role: .destructive) { + groupStage.removeReturnMatches() + } + } + } + + Section { + if groupStage.isReturnMatchEnabled() == false { + RowButtonView("Rajouter les matchs retours", role: .destructive) { + groupStage.addReturnMatches() + } + } else { + RowButtonView("Rajouter une vague de matchs", role: .destructive) { + groupStage.addReturnMatches() + } + } + } + + Section { + RowButtonView("Rafraichir", role: .destructive) { + let playedMatches = groupStage.playedMatches() + playedMatches.forEach { match in + match.updateTeamScores() + } + } + } footer: { + Text("Mets à jour les équipes de la poule si jamais une erreur est persistante.") + } } .onChange(of: size) { if size != groupStage.size { presentConfirmationButton = true } } + .onChange(of: groupStage.matchFormat) { + _save() + groupStage.updateAllMatchesFormat() + } .navigationBarBackButtonHidden(focusedField != nil) .toolbar(content: { if focusedField != nil { @@ -164,6 +206,10 @@ struct GroupStageSettingsView: View { } } } + + ToolbarItem(placement: .topBarTrailing) { + MatchTypeSelectionView(selectedFormat: $groupStage.matchFormat, additionalEstimationDuration: tournament.additionalEstimationDuration, displayStyle: .short) + } }) .navigationTitle("Paramètres") .toolbarBackground(.visible, for: .navigationBar) diff --git a/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift b/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift index b8794db..18a3786 100644 --- a/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift +++ b/PadelClub/Views/GroupStage/Components/GroupStageTeamView.swift @@ -48,12 +48,20 @@ struct GroupStageTeamView: View { var body: some View { List { Section { - if let name = team.name { + if let name = team.name, name.isEmpty == false { Text(name).foregroundStyle(.secondary) } ForEach(team.players()) { player in EditablePlayerView(player: player, editingOptions: _editingOptions()) } + } footer: { + NavigationLink { + EditingTeamView(team: team) + .environment(tournament) + } label: { + Text("détails de l'équipe") + .underline() + } } if groupStage.tournamentObject()?.hasEnded() == false { @@ -66,24 +74,28 @@ struct GroupStageTeamView: View { } } } - - Section { - if team.qualified == false { - RowButtonView("Qualifier l'équipe", role: .destructive) { - team.qualified = true - //team.bracketPosition = nil - _save() - } - } else { - RowButtonView("Annuler la qualification", role: .destructive) { - team.qualified = false - groupStage.tournamentObject()?.resetTeamScores(in: team.bracketPosition) - team.bracketPosition = nil - _save() - } + } + + + Section { + if team.qualified == false { + RowButtonView("Qualifier l'équipe", role: .destructive) { + team.qualified = true + //team.bracketPosition = nil + _save() + } + } else { + RowButtonView("Annuler la qualification", role: .destructive) { + team.qualified = false + groupStage.tournamentObject()?.resetTeamScores(in: team.bracketPosition) + team.bracketPosition = nil + _save() } } + } + + if groupStage.tournamentObject()?.hasEnded() == false { if team.qualified == false { Section { RowButtonView("Retirer de la poule", role: .destructive) { diff --git a/PadelClub/Views/GroupStage/GroupStageQualificationManagerView.swift b/PadelClub/Views/GroupStage/GroupStageQualificationManagerView.swift new file mode 100644 index 0000000..671a96a --- /dev/null +++ b/PadelClub/Views/GroupStage/GroupStageQualificationManagerView.swift @@ -0,0 +1,72 @@ +// +// GroupStageQualificationManager.swift +// PadelClub +// +// Created by razmig on 05/11/2024. +// + + +class GroupStageQualificationManager { + private let tournament: Tournament + private let tournamentStore: TournamentStore + + init(tournament: Tournament, tournamentStore: TournamentStore) { + self.tournament = tournament + self.tournamentStore = tournamentStore + } + + func qualificationSection() -> some View { + guard tournament.groupStageAdditionalQualified > 0 else { return EmptyView() } + + let name = "\(tournament.qualifiedPerGroupStage + 1).ordinalFormatted()" + let missingQualifiedFromGroupStages = tournament.missingQualifiedFromGroupStages() + + return Section { + NavigationLink { + SpinDrawView( + drawees: missingQualifiedFromGroupStages.isEmpty + ? tournament.groupStageAdditionalQualifiedPreDraw() + : ["Qualification d'un \(name) de poule"], + segments: missingQualifiedFromGroupStages.isEmpty + ? tournament.groupStageAdditionalLeft() + : missingQualifiedFromGroupStages + ) { results in + if !missingQualifiedFromGroupStages.isEmpty { + self.handleDrawResults(results, missingQualifiedFromGroupStages) + } + } + } label: { + Label { + Text("Qualifier un \(name) de poule par tirage au sort") + } icon: { + Image(systemName: "exclamationmark.circle.fill") + .foregroundStyle(.logoBackground) + } + } + .disabled(tournament.moreQualifiedToDraw() == 0) + } footer: { + footerText(missingQualifiedFromGroupStages.isEmpty) + } + } + + private func handleDrawResults(_ results: [DrawResult], _ missingQualifiedFromGroupStages: [Team]) { + results.forEach { drawResult in + var team = missingQualifiedFromGroupStages[drawResult.drawIndex] + team.qualified = true + do { + try tournamentStore.teamRegistrations.addOrUpdate(instance: team) + } catch { + Logger.error(error) + } + } + } + + private func footerText(_ noMoreTeams: Bool) -> Text { + if tournament.moreQualifiedToDraw() == 0 { + return Text("Aucune équipe supplémentaire à qualifier. Vous pouvez en rajouter en modifiant le paramètre dans structure.") + } else if noMoreTeams { + return Text("Aucune équipe supplémentaire à tirer au sort. Attendez la fin des poules.") + } + return Text("") + } +} diff --git a/PadelClub/Views/GroupStage/GroupStageView.swift b/PadelClub/Views/GroupStage/GroupStageView.swift index 8a2e134..9fc489f 100644 --- a/PadelClub/Views/GroupStage/GroupStageView.swift +++ b/PadelClub/Views/GroupStage/GroupStageView.swift @@ -28,6 +28,8 @@ struct GroupStageView: View { var body: some View { List { + let playedMatches = groupStage.playedMatches() + Section { GroupStageScoreView(groupStage: groupStage, sortByScore: sortingMode == .score) } header: { @@ -49,15 +51,31 @@ struct GroupStageView: View { } } .headerProminence(.increased) + .onChange(of: playedMatches) { + if groupStage.hasEnded() { + sortingMode = .score + } + } - let playedMatches = groupStage.playedMatches() let runningMatches = groupStage.runningMatches(playedMatches: playedMatches) - MatchListView(section: "en cours", matches: groupStage.runningMatches(playedMatches: playedMatches), hideWhenEmpty: true) + Section { + MatchListView(section: "en cours", matches: groupStage.runningMatches(playedMatches: playedMatches), hideWhenEmpty: true) + } + let availableToStart = groupStage.availableToStart(playedMatches: playedMatches, in: runningMatches) - MatchListView(section: "prêt à démarrer", matches: availableToStart, hideWhenEmpty: true) - .listRowView(isActive: availableToStart.isEmpty == false, color: .green, hideColorVariation: true) - MatchListView(section: "à lancer", matches: groupStage.readyMatches(playedMatches: playedMatches), hideWhenEmpty: true) - MatchListView(section: "terminés", matches: groupStage.finishedMatches(playedMatches: playedMatches), hideWhenEmpty: playedMatches.isEmpty || playedMatches.flatMap({ $0.teamScores }).isEmpty, isExpanded: false) + Section { + MatchListView(section: "prêt à démarrer", matches: availableToStart, hideWhenEmpty: true) + .listRowView(isActive: availableToStart.isEmpty == false, color: .green, hideColorVariation: true) + } + Section { + + MatchListView(section: "à lancer", matches: groupStage.readyMatches(playedMatches: playedMatches), hideWhenEmpty: true) + } + + Section { + + MatchListView(section: "terminés", matches: groupStage.finishedMatches(playedMatches: playedMatches), hideWhenEmpty: playedMatches.isEmpty || playedMatches.flatMap({ $0.teamScores }).isEmpty, isExpanded: false) + } if playedMatches.isEmpty { RowButtonView("Créer les matchs de poules") { @@ -136,17 +154,16 @@ struct GroupStageView: View { .font(.footnote) HStack { VStack(alignment: .leading) { - if let teamName = team.name { - Text(teamName).font(.title3) - } else { - ForEach(team.players()) { player in - Text(player.playerLabel()).lineLimit(1) - .overlay { - if player.hasArrived && team.isHere() == false { - Color.green.opacity(0.6) - } + if let teamName = team.name, teamName.isEmpty == false { + Text(teamName).foregroundStyle(.secondary).font(.footnote) + } + ForEach(team.players()) { player in + Text(player.playerLabel()).lineLimit(1) + .overlay { + if player.hasArrived && team.isHere() == false { + Color.green.opacity(0.6) } - } + } } } Spacer() @@ -160,13 +177,11 @@ struct GroupStageView: View { if let setsDifference = score.setsDifference { HStack(spacing: 4.0) { Text(setsDifference) - Text("sets") }.font(.footnote) } if let gamesDifference = score.gamesDifference { HStack(spacing: 4.0) { Text(gamesDifference) - Text("jeux") }.font(.footnote) } } diff --git a/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift b/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift index ab40d64..6961bae 100644 --- a/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift +++ b/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift @@ -12,7 +12,7 @@ struct GroupStagesSettingsView: View { @EnvironmentObject var dataStore: DataStore @Environment(\.dismiss) private var dismiss @Environment(Tournament.self) var tournament: Tournament - @State private var generationDone: Bool = false + @State private var generationDoneMessage: String? let step: Int var tournamentStore: TournamentStore { @@ -167,6 +167,40 @@ struct GroupStagesSettingsView: View { } footer: { Text("Redistribue les équipes par la méthode du serpentin") } + + let groupStages = tournament.groupStages() + + Section { + if groupStages.anySatisfy({ $0.isReturnMatchEnabled() }) { + RowButtonView("Effacer les matchs retours", role: .destructive) { + groupStages.filter({ $0.isReturnMatchEnabled() }).forEach { groupStage in + groupStage.removeReturnMatches() + } + generationDoneMessage = "Matchs retours effacés" + } + } + + } + + Section { + if groupStages.anySatisfy({ $0.isReturnMatchEnabled() == false }) { + RowButtonView("Rajouter les matchs retours", role: .destructive) { + groupStages.filter({ $0.isReturnMatchEnabled() == false }).forEach { groupStage in + groupStage.addReturnMatches() + } + + generationDoneMessage = "Matchs retours créés" + } + } else if groupStages.allSatisfy({ $0.isReturnMatchEnabled() }) { + RowButtonView("Rajouter une vague de matchs", role: .destructive) { + groupStages.forEach { groupStage in + groupStage.addReturnMatches() + } + + generationDoneMessage = "Nouveaux matchs créés" + } + } + } Section { RowButtonView("Nommer les poules alphabétiquement", role: .destructive) { @@ -220,25 +254,29 @@ struct GroupStagesSettingsView: View { } .overlay(alignment: .bottom) { - if generationDone { - Label("Poules mises à jour", systemImage: "checkmark.circle.fill") + if let generationDoneMessage { + Label(generationDoneMessage, systemImage: "checkmark.circle.fill") .toastFormatted() .deferredRendering(for: .seconds(2)) } } .toolbar { ToolbarItem(placement: .topBarTrailing) { - ShareLink(item: tournament.groupStages().compactMap { $0.pasteData() }.joined(separator: "\n\n")) + ShareLink(item: groupStagesPaste(), preview: .init("Données des poules")) } } } + func groupStagesPaste() -> TournamentGroupStageShareContent { + TournamentGroupStageShareContent(tournament: tournament) + } + var menuBuildAllGroupStages: some View { RowButtonView("Refaire les poules", role: .destructive) { tournament.deleteGroupStages() tournament.buildGroupStages() - generationDone = true + generationDoneMessage = "Poules mises à jour" tournament.shouldVerifyGroupStage = false _save() } @@ -248,8 +286,8 @@ struct GroupStagesSettingsView: View { func menuGenerateGroupStage(_ mode: GroupStageOrderingMode) -> some View { RowButtonView("Poule \(mode.localizedLabel().lowercased())", role: .destructive, systemImage: mode.systemImage) { tournament.groupStageOrderingMode = mode - tournament.refreshGroupStages() - generationDone = true + tournament.refreshGroupStages(keepExistingMatches: true) + generationDoneMessage = "Poules mises à jour" tournament.shouldVerifyGroupStage = false _save() } diff --git a/PadelClub/Views/GroupStage/GroupStagesView.swift b/PadelClub/Views/GroupStage/GroupStagesView.swift index 6f3355d..13cd9e2 100644 --- a/PadelClub/Views/GroupStage/GroupStagesView.swift +++ b/PadelClub/Views/GroupStage/GroupStagesView.swift @@ -111,7 +111,7 @@ struct GroupStagesView: View { GenericDestinationPickerView(selectedDestination: $selectedDestination, destinations: allDestinations(), nilDestinationIsValid: true) switch selectedDestination { case .all: - let finishedMatches = tournament.finishedMatches(allMatches, limit: nil) + let finishedMatches = Tournament.finishedMatches(allMatches, limit: nil) List { if tournament.groupStageAdditionalQualified > 0 { @@ -131,12 +131,7 @@ struct GroupStagesView: View { } } } label: { - Label { - Text("Qualifier un \(name) de poule par tirage au sort") - } icon: { - Image(systemName: "exclamationmark.circle.fill") - .foregroundStyle(.logoBackground) - } + Text("Qualifier un \(name) de poule par tirage au sort") } .disabled(tournament.moreQualifiedToDraw() == 0 || missingQualifiedFromGroupStages.isEmpty) } footer: { @@ -148,12 +143,28 @@ struct GroupStagesView: View { } } - let runningMatches = tournament.runningMatches(allMatches) - MatchListView(section: "en cours", matches: runningMatches, matchViewStyle: .standardStyle, isExpanded: false) - MatchListView(section: "prêt à démarrer", matches: tournament.availableToStart(allMatches, in: runningMatches), matchViewStyle: .standardStyle, isExpanded: false) - MatchListView(section: "à lancer", matches: tournament.readyMatches(allMatches), matchViewStyle: .standardStyle, isExpanded: false) - MatchListView(section: "terminés", matches: finishedMatches, matchViewStyle: .standardStyle, isExpanded: false) + let runningMatches = Tournament.runningMatches(allMatches) + Section { + + MatchListView(section: "en cours", matches: runningMatches, isExpanded: false) + } + Section { + + MatchListView(section: "prêt à démarrer", matches: Tournament.availableToStart(allMatches, in: runningMatches), isExpanded: false) + + } + + Section { + + MatchListView(section: "à lancer", matches: Tournament.readyMatches(allMatches), isExpanded: false) + } + + Section { + + MatchListView(section: "terminés", matches: finishedMatches, isExpanded: false) + } } + .matchViewStyle(.standardStyle) .navigationTitle("Toutes les poules") case .groupStage(let groupStage): GroupStageView(groupStage: groupStage).id(groupStage.id) diff --git a/PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift b/PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift index 469971e..5c42a24 100644 --- a/PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift +++ b/PadelClub/Views/GroupStage/LoserBracketFromGroupStageView.swift @@ -45,7 +45,8 @@ struct LoserBracketFromGroupStageView: View { ForEach(displayableMatches) { match in Section { - MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle) + MatchRowView(match: match) + .matchViewStyle(.sectionedStandardStyle) .environment(\.isEditingTournamentSeed, $isEditingLoserBracketGroupStage) } header: { let tournamentTeamCount = tournament.teamCount @@ -106,8 +107,9 @@ struct LoserBracketFromGroupStageView: View { private func _addNewMatch() { let currentGroupStageLoserBracketsInitialPlace = tournament.groupStageLoserBracketsInitialPlace() let placeCount = displayableMatches.isEmpty ? currentGroupStageLoserBracketsInitialPlace : max(currentGroupStageLoserBracketsInitialPlace, displayableMatches.map({ $0.index }).max()! + 2) + let match = Match(round: loserBracket.id, index: placeCount, format: loserBracket.matchFormat) - match.name = "\(placeCount)\(placeCount.ordinalFormattedSuffix()) place" + match.setMatchName("\(placeCount)\(placeCount.ordinalFormattedSuffix()) place") tournamentStore.matches.addOrUpdate(instance: match) } @@ -194,7 +196,7 @@ struct GroupStageLoserBracketMatchFooterView: View { match.index = newIndexValidated - match.name = "\(newIndexValidated)\(newIndexValidated.ordinalFormattedSuffix()) place" + match.setMatchName("\(newIndexValidated)\(newIndexValidated.ordinalFormattedSuffix()) place") match.tournamentStore.teamScores.addOrUpdate(contentOfs: teamScores) diff --git a/PadelClub/Views/GroupStage/Shared/GroupStageTeamReplacementView.swift b/PadelClub/Views/GroupStage/Shared/GroupStageTeamReplacementView.swift index cf613a4..dbc5673 100644 --- a/PadelClub/Views/GroupStage/Shared/GroupStageTeamReplacementView.swift +++ b/PadelClub/Views/GroupStage/Shared/GroupStageTeamReplacementView.swift @@ -54,7 +54,7 @@ struct GroupStageTeamReplacementView: View { Section { Picker(selection: $selectedPlayer) { HStack { - Text(team.name ?? "Toute l'équipe") + Text(team.teamNameLabel()) Spacer() Text(team.weight.formatted()).bold() } diff --git a/PadelClub/Views/Match/Components/MatchDateView.swift b/PadelClub/Views/Match/Components/MatchDateView.swift index 8034631..51ccc19 100644 --- a/PadelClub/Views/Match/Components/MatchDateView.swift +++ b/PadelClub/Views/Match/Components/MatchDateView.swift @@ -11,19 +11,26 @@ import LeStorage struct MatchDateView: View { @EnvironmentObject var dataStore: DataStore - + @State private var showScoreEditView: Bool = false + @State private var confirmScoreEdition: Bool = false var match: Match var showPrefix: Bool = false private var isReady: Bool private var hasWalkoutTeam: Bool private var hasEnded: Bool + private let updatedField: Int? - init(match: Match, showPrefix: Bool) { + init(match: Match, showPrefix: Bool, updatedField: Int? = nil) { self.match = match self.showPrefix = showPrefix self.isReady = match.isReady() self.hasWalkoutTeam = match.hasWalkoutTeam() self.hasEnded = match.hasEnded() + self.updatedField = updatedField + } + + var currentDate: Date { + Date().withoutSeconds() } var body: some View { @@ -32,44 +39,64 @@ struct MatchDateView: View { } else { Menu { let estimatedDuration = match.getDuration() - if match.startDate == nil && isReady { - Button("Démarrer") { - match.startDate = Date() - match.confirmed = true - _save() - } - Button("Démarrer dans 5 minutes") { - match.startDate = Calendar.current.date(byAdding: .minute, value: 5, to: Date()) - match.confirmed = true - _save() - } - Button("Démarrer dans 15 minutes") { - match.startDate = Calendar.current.date(byAdding: .minute, value: 15, to: Date()) - match.confirmed = true - _save() - } - Button("Démarrer dans \(estimatedDuration.formatted()) minutes") { - match.startDate = Calendar.current.date(byAdding: .minute, value: estimatedDuration, to: Date()) - match.confirmed = true - _save() - } - } else { - if isReady { + if isReady { + Section { Button("Démarrer maintenant") { - match.startDate = Date() + if let updatedField { + match.setCourt(updatedField) + } + match.startDate = currentDate match.endDate = nil match.confirmed = true _save() } - } else { - Button("Décaler de \(estimatedDuration) minutes") { - match.cleanScheduleAndSave(match.startDate?.addingTimeInterval(Double(estimatedDuration) * 60.0)) + Button("Démarrer dans 5 minutes") { + if let updatedField { + match.setCourt(updatedField) + } + match.startDate = Calendar.current.date(byAdding: .minute, value: 5, to: currentDate) + match.endDate = nil + match.confirmed = true + _save() + } + Button("Démarrer dans 15 minutes") { + if let updatedField { + match.setCourt(updatedField) + } + match.startDate = Calendar.current.date(byAdding: .minute, value: 15, to: currentDate) + match.endDate = nil + match.confirmed = true + _save() + } + Button("Démarrer dans \(estimatedDuration.formatted()) minutes") { + if let updatedField { + match.setCourt(updatedField) + } + match.startDate = Calendar.current.date(byAdding: .minute, value: estimatedDuration, to: currentDate) + match.endDate = nil + match.confirmed = true + _save() + } + } header: { + if let updatedField { + Text(match.courtName(for: updatedField)) } } - Button("Retirer l'horaire") { - match.cleanScheduleAndSave() + } else { + Button("Décaler de \(estimatedDuration) minutes") { + if let updatedField { + match.setCourt(updatedField) + } + match.cleanScheduleAndSave(match.startDate?.addingTimeInterval(Double(estimatedDuration) * 60.0)) } } + Button("Indiquer un score") { + showScoreEditView = true + } + Divider() + Button("Retirer l'horaire") { + match.cleanScheduleAndSave() + } } label: { label } @@ -145,6 +172,10 @@ struct MatchDateView: View { } } } + .sheet(isPresented: $showScoreEditView) { + EditScoreView(match: match, confirmScoreEdition: $confirmScoreEdition) + .tint(.master) + } } private func _save() { diff --git a/PadelClub/Views/Match/Components/MatchTeamDetailView.swift b/PadelClub/Views/Match/Components/MatchTeamDetailView.swift index cf952d0..3ee208b 100644 --- a/PadelClub/Views/Match/Components/MatchTeamDetailView.swift +++ b/PadelClub/Views/Match/Components/MatchTeamDetailView.swift @@ -25,17 +25,32 @@ struct MatchTeamDetailView: View { .headerProminence(.increased) .tint(.master) } - .presentationDetents([.fraction(0.66)]) } @ViewBuilder private func _teamDetailView(_ team: TeamRegistration, inTournament tournament: Tournament?) -> some View { Section { + if let teamName = team.name, teamName.isEmpty == false { + Text(teamName).foregroundStyle(.secondary).font(.footnote) + } ForEach(team.players()) { player in EditablePlayerView(player: player, editingOptions: _editingOptions()) } + if let coachList = team.comment, coachList.isEmpty == false { + Text("Coachs : " + coachList).foregroundStyle(.secondary).font(.footnote) + } } header: { - TeamHeaderView(team: team, teamIndex: tournament?.indexOf(team: team)) + TeamHeaderView(team: team, teamIndex: tournament?.indexOf(team: team), tournament: tournament) + } footer: { + if let tournament { + NavigationLink { + EditingTeamView(team: team) + .environment(tournament) + } label: { + Text("détails de l'équipe") + .underline() + } + } } } diff --git a/PadelClub/Views/Match/Components/PlayerBlockView.swift b/PadelClub/Views/Match/Components/PlayerBlockView.swift index 5f5b278..c98a724 100644 --- a/PadelClub/Views/Match/Components/PlayerBlockView.swift +++ b/PadelClub/Views/Match/Components/PlayerBlockView.swift @@ -8,21 +8,26 @@ import SwiftUI struct PlayerBlockView: View { + @Environment(\.matchViewStyle) private var matchViewStyle @State var match: Match let teamPosition: TeamPosition let team: TeamRegistration? - let color: Color - let width: CGFloat let teamScore: TeamScore? let isWalkOut: Bool - init(match: Match, teamPosition: TeamPosition, color: Color, width: CGFloat) { + var displayRestingTime: Bool { + matchViewStyle.displayRestingTime() + } + + var width: CGFloat { + matchViewStyle == .plainStyle ? 1 : 2 + } + + init(match: Match, teamPosition: TeamPosition) { self.match = match self.teamPosition = teamPosition let theTeam = match.team(teamPosition) self.team = theTeam - self.color = color - self.width = width let theTeamScore = match.teamScore(ofTeam: theTeam) self.teamScore = theTeamScore self.isWalkOut = theTeamScore?.isWalkOut() == true @@ -44,39 +49,67 @@ struct PlayerBlockView: View { teamScore?.score?.components(separatedBy: ",") ?? [] } - private func _defaultLabel() -> String { - teamPosition.localizedLabel() + private func _defaultLabel() -> [String] { + var defaultLabels = [String]() + if let previous = match.previousMatch(teamPosition) { + defaultLabels.append("Gagnant \(previous.roundAndMatchTitle(.short))") + if previous.isReady() == true { + if let courtName = previous.courtName(), previous.isRunning() { + defaultLabels.append(courtName + "\(previous.runningDuration())") + } + } + } else if let loser = match.loserMatch(teamPosition) { + defaultLabels.append("Perdant \(loser.roundAndMatchTitle(.short))") + if loser.isReady() == true { + if let courtName = loser.courtName(), loser.isRunning() { + defaultLabels.append(courtName + "\(loser.runningDuration())") + } + } + } else { + defaultLabels.append(teamPosition.localizedLabel()) + } + return defaultLabels } var body: some View { HStack { VStack(alignment: .leading) { - if let names { + if let team { if let teamScore, teamScore.luckyLoser != nil, match.isLoserBracket == false { Text("Repêchée").italic().font(.caption) } - if let name = team?.name { - Text(name).font(.title3) - } else { - ForEach(names, id: \.self) { name in - Text(name).lineLimit(1) - } + if let teamName = team.name { + Text(teamName).foregroundStyle(.secondary).font(.footnote) + } + ForEach(team.players()) { player in + Text(player.playerLabel()).lineLimit(1) + .italic(player.isHere() == false) + .foregroundStyle(player.isHere() == false ? .secondary : .primary) } } else { ZStack(alignment: .leading) { VStack { - if let name = team?.name { - Text(name).font(.title3) - } else { - Text("longLabelPlayerOne").lineLimit(1) - Text("longLabelPlayerTwo").lineLimit(1) + if let teamName = team?.name { + Text(teamName).foregroundStyle(.secondary).font(.footnote) } + Text("longLabelPlayerOne").lineLimit(1) + Text("longLabelPlayerTwo").lineLimit(1) } .opacity(0) - Text(_defaultLabel()).foregroundStyle(.secondary).lineLimit(1) + VStack(alignment: .leading) { + ForEach(_defaultLabel(), id: \.self) { name in + Text(name) + .foregroundStyle(.secondary) + .lineLimit(1) + } + } } } + + if displayRestingTime, let team { + TeamRowView.TeamRestingView(team: team) + } } .bold(hasWon) Spacer() @@ -92,7 +125,7 @@ struct PlayerBlockView: View { if width == 1 { Divider() } else { - Divider().frame(width: width).overlay(color) + Divider().frame(width: width).overlay(Color(white: 0.9)) } Text(string) .font(.title3) diff --git a/PadelClub/Views/Match/MatchDetailView.swift b/PadelClub/Views/Match/MatchDetailView.swift index 300a5fa..45f0444 100644 --- a/PadelClub/Views/Match/MatchDetailView.swift +++ b/PadelClub/Views/Match/MatchDetailView.swift @@ -14,8 +14,8 @@ struct MatchDetailView: View { @EnvironmentObject var networkMonitor: NetworkMonitor @Environment(\.dismiss) var dismiss - let matchViewStyle: MatchViewStyle - + @Environment(\.matchViewStyle) private var matchViewStyle + @State private var showLiveScore: Bool = false @State private var editScore: Bool = false @State private var scoreType: ScoreType? @@ -33,6 +33,10 @@ struct MatchDetailView: View { @State var showSubscriptionView: Bool = false @State var showUserCreationView: Bool = false + @State private var presentFollowUpMatch: Bool = false + @State private var dismissWhenPresentFollowUpMatchIsDismissed: Bool = false + @State private var presentRanking: Bool = false + @State private var confirmScoreEdition: Bool = false var tournamentStore: TournamentStore { return match.tournamentStore @@ -50,9 +54,8 @@ struct MatchDetailView: View { var match: Match - init(match: Match, matchViewStyle: MatchViewStyle = .standardStyle) { + init(match: Match, updatedField: Int? = nil) { self.match = match - self.matchViewStyle = matchViewStyle if match.hasStarted() == false && (match.startDate == nil || match.courtIndex == nil) { _isEditing = State(wrappedValue: true) @@ -69,7 +72,7 @@ struct MatchDetailView: View { _endDate = State(wrappedValue: endDate) } - if let courtIndex = match.courtIndex { + if let courtIndex = updatedField ?? match.courtIndex { _fieldSetup = State(wrappedValue: .field(courtIndex)) } } @@ -85,7 +88,8 @@ struct MatchDetailView: View { } Section { - MatchSummaryView(match: match, matchViewStyle: .plainStyle) + MatchSummaryView(match: match) + .matchViewStyle(.plainStyle) } footer: { if match.isEmpty() == false { HStack { @@ -153,13 +157,47 @@ struct MatchDetailView: View { } } }) - .sheet(item: $scoreType, onDismiss: { - if match.hasEnded() { + .sheet(isPresented: $presentFollowUpMatch, onDismiss: { + if dismissWhenPresentFollowUpMatchIsDismissed { dismiss() } + }) { + NavigationStack { + + FollowUpMatchView(match: match, dismissWhenPresentFollowUpMatchIsDismissed: $dismissWhenPresentFollowUpMatchIsDismissed) + } + .tint(.master) + } + .sheet(isPresented: $presentRanking, content: { + if let currentTournament = match.currentTournament() { + NavigationStack { + TournamentRankView() + .environment(currentTournament) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button("Retour", role: .cancel) { + presentRanking = false + dismiss() + } + } + } + } + .tint(.master) + } + }) + .sheet(item: $scoreType, onDismiss: { + if match.hasEnded(), confirmScoreEdition { + confirmScoreEdition = false + if match.index == 0, match.isGroupStage() == false, match.roundObject?.parent == nil { + presentRanking = true + } else if match.isGroupStage(), match.currentTournament()?.hasEnded() == true { + presentRanking = true + } else { + presentFollowUpMatch = true + } + } }) { scoreType in - let matchDescriptor = MatchDescriptor(match: match) - EditScoreView(matchDescriptor: matchDescriptor) + EditScoreView(match: match, confirmScoreEdition: $confirmScoreEdition) .tint(.master) // switch scoreType { @@ -305,6 +343,7 @@ struct MatchDetailView: View { match.resetScores() match.resetMatch() match.confirmed = false + match.updateFollowingMatchTeamScore() save() } label: { Text("Supprimer les scores") @@ -319,6 +358,26 @@ struct MatchDetailView: View { Text("Remise-à-zéro") } + if match.teamScores.isEmpty == false { + Divider() + Menu { + ForEach(match.teamScores) { teamScore in + Button(role: .destructive) { + do { + try tournamentStore.teamScores.delete(instance: teamScore) + } catch { + Logger.error(error) + } + match.confirmed = false + _saveMatch() + } label: { + Text(teamScore.team?.teamLabel() ?? "Aucun nom") + } + } + } label: { + Text("Supprimer une équipe") + } + } } label: { LabelOptions() } @@ -402,13 +461,13 @@ struct MatchDetailView: View { Text("Partage sur les réseaux sociaux") } -// if let followUpMatch = match.followUpMatch { -// Section { -// MatchRowView(match: followUpMatch) -// } header: { -// Text("à suivre terrain \(match.fieldIndex)") -// } -// } + if match.currentTournament()?.hasEnded() == false { + Section { + RowButtonView("Match à suivre") { + presentFollowUpMatch = true + } + } + } } var editionView: some View { @@ -429,20 +488,27 @@ struct MatchDetailView: View { Text("Dans 5 minutes").tag(MatchDateSetup.inMinutes(5)) Text("Dans 15 minutes").tag(MatchDateSetup.inMinutes(15)) } - Text("Précédente rotation").tag(MatchDateSetup.inMinutes(-rotationDuration)) - Text("Prochaine rotation").tag(MatchDateSetup.inMinutes(rotationDuration)) + Text("Précédente rotation").tag(MatchDateSetup.previousRotation) + Text("Prochaine rotation").tag(MatchDateSetup.nextRotation) Text("À").tag(MatchDateSetup.customDate) } label: { Text("Horaire") } .onChange(of: startDateSetup) { + let date = Date().withoutSeconds() switch startDateSetup { case .customDate: break case .now: - startDate = Date() + startDate = date + case .nextRotation: + let baseDate = match.startDate ?? date + startDate = baseDate.addingTimeInterval(Double(rotationDuration) * 60) + case .previousRotation: + let baseDate = match.startDate ?? date + startDate = baseDate.addingTimeInterval(Double(-rotationDuration) * 60) case .inMinutes(let minutes): - startDate = Date().addingTimeInterval(Double(minutes) * 60) + startDate = date.addingTimeInterval(Double(minutes) * 60) } } } @@ -484,7 +550,7 @@ struct MatchDetailView: View { } RowButtonView("Valider") { - match.validateMatch(fromStartDate: startDateSetup == .now ? Date() : startDate, toEndDate: endDate, fieldSetup: fieldSetup) + match.validateMatch(fromStartDate: startDateSetup == .now ? Date().withoutSeconds() : startDate, toEndDate: endDate, fieldSetup: fieldSetup) save() diff --git a/PadelClub/Views/Match/MatchRowView.swift b/PadelClub/Views/Match/MatchRowView.swift index 954da7f..643078e 100644 --- a/PadelClub/Views/Match/MatchRowView.swift +++ b/PadelClub/Views/Match/MatchRowView.swift @@ -6,13 +6,15 @@ // import SwiftUI +import LeStorage struct MatchRowView: View { @EnvironmentObject var dataStore: DataStore + @Environment(\.matchViewStyle) private var matchViewStyle @State var match: Match - let matchViewStyle: MatchViewStyle var title: String? = nil + var updatedField: Int? = nil @Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed @@ -58,10 +60,28 @@ struct MatchRowView: View { // }) NavigationLink { - MatchDetailView(match: match, matchViewStyle: matchViewStyle) + MatchDetailView(match: match, updatedField: updatedField) } label: { - MatchSummaryView(match: match, matchViewStyle: matchViewStyle, title: title) + MatchSummaryView(match: match, title: title, updatedField: updatedField) .contextMenu { + Section { + ForEach(match.teams().flatMap({ $0.players() })) { player in + Button { + player.hasArrived.toggle() + do { + try player.tournamentStore.playerRegistrations.addOrUpdate(instance: player) + } catch { + Logger.error(error) + } + } label: { + Label(player.playerLabel(), systemImage: player.hasArrived ? "checkmark" : "xmark") + } + } + } header: { + Text("Présence") + } + + Divider() NavigationLink { EditSharingView(match: match) } label: { diff --git a/PadelClub/Views/Match/MatchSetupView.swift b/PadelClub/Views/Match/MatchSetupView.swift index 094dc80..dc932bd 100644 --- a/PadelClub/Views/Match/MatchSetupView.swift +++ b/PadelClub/Views/Match/MatchSetupView.swift @@ -9,7 +9,7 @@ import SwiftUI import LeStorage struct MatchSetupView: View { - static let confirmationMessage = "Au moins une tête de série a été placée dans la branche de ce match dans les tours précédents. En plaçant une équipe sur ici, les équipes déjà placées dans la même branche seront retirées du tableau et devront être replacées." + static let confirmationMessage = "Au moins une tête de série a été placée dans la branche de ce match dans les tours précédents. En plaçant une équipe ici, les équipes déjà placées dans la même branche seront retirées du tableau et devront être replacées." @EnvironmentObject var dataStore: DataStore @@ -166,7 +166,7 @@ struct MatchSetupView: View { Text("Libérer") .underline() } - } else { + } else if match.isFromLastRound() == false { ConfirmButtonView(shouldConfirm: shouldConfirm, message: MatchSetupView.confirmationMessage) { _ = match.lockAndGetSeedPosition(atTeamPosition: teamPosition) do { @@ -190,16 +190,22 @@ struct MatchSetupView: View { func _removeTeam(team: TeamRegistration, teamPosition: TeamPosition) -> some View { Button(role: .cancel) { - //todo if match.isSeededBy(team: team, inTeamPosition: teamPosition) { + if let score = match.teamScore(ofTeam: team) { + do { + try tournamentStore.teamScores.delete(instance: score) + } catch { + Logger.error(error) + } + } + team.bracketPosition = nil do { try tournamentStore.teamRegistrations.addOrUpdate(instance: team) } catch { Logger.error(error) } - //match.updateTeamScores() - match.previousMatches().forEach { previousMatch in + if let previousMatch = match.previousMatch(teamPosition) { if previousMatch.disabled { previousMatch.enableMatch() do { diff --git a/PadelClub/Views/Match/MatchSummaryView.swift b/PadelClub/Views/Match/MatchSummaryView.swift index 5b890f0..51942d3 100644 --- a/PadelClub/Views/Match/MatchSummaryView.swift +++ b/PadelClub/Views/Match/MatchSummaryView.swift @@ -10,27 +10,28 @@ import SwiftUI struct MatchSummaryView: View { @EnvironmentObject var dataStore: DataStore @State var match: Match - let matchViewStyle: MatchViewStyle + @Environment(\.matchViewStyle) private var matchViewStyle let matchTitle: String let roundTitle: String? let courtName: String? - let spacing: CGFloat - let padding: CGFloat - let color: Color - let width: CGFloat + let updatedField: Int? + let estimatedStartDate: Match.CourtIndexAndDate? + let availableCourts: [Int] + let canBePlayedInSpecifiedCourt: Bool - init(match: Match, matchViewStyle: MatchViewStyle, title: String? = nil) { + init(match: Match, title: String? = nil, updatedField: Int? = nil) { self.match = match - self.matchViewStyle = matchViewStyle - self.padding = matchViewStyle == .plainStyle ? 0 : 8 - self.spacing = matchViewStyle == .plainStyle ? 8 : 0 - self.width = matchViewStyle == .plainStyle ? 1 : 2 - self.color = Color(white: 0.9) + self.updatedField = updatedField + + let runningMatches = DataStore.shared.runningMatches() + + let currentAvailableCourts = match.availableCourts(runningMatches: runningMatches) + self.availableCourts = currentAvailableCourts if let groupStage = match.groupStageObject { self.roundTitle = groupStage.groupStageTitle(.title) } else if let round = match.roundObject { - self.roundTitle = round.roundTitle(matchViewStyle == .feedStyle ? .wide : .short) + self.roundTitle = round.roundTitle(.short) } else { self.roundTitle = nil } @@ -42,8 +43,22 @@ struct MatchSummaryView: View { } else { self.courtName = nil } + self.estimatedStartDate = match.estimatedStartDate(availableCourts: currentAvailableCourts, runningMatches: runningMatches) + self.canBePlayedInSpecifiedCourt = match.canBePlayedInSpecifiedCourt(runningMatches: runningMatches) + } + + var spacing: CGFloat { + matchViewStyle == .plainStyle ? 8 : 0 } + var padding: CGFloat { + matchViewStyle == .plainStyle ? 0 : 8 + } + + var width: CGFloat { + matchViewStyle == .plainStyle ? 1 : 2 + } + var body: some View { VStack(alignment: .leading) { if matchViewStyle != .plainStyle { @@ -57,46 +72,93 @@ struct MatchSummaryView: View { } } Spacer() - if let courtName { - Spacer() - Text(courtName) - .foregroundStyle(.gray) - .font(.caption) + VStack(alignment: .trailing, spacing: 0) { + if let courtName { + Text(courtName) + .strikethrough(courtIsNotValid()) + } } + .foregroundStyle(.secondary) + .font(.footnote) } .lineLimit(1) } HStack(spacing: 0) { VStack(alignment: .leading, spacing: spacing) { - PlayerBlockView(match: match, teamPosition: .one, color: color, width: width) + PlayerBlockView(match: match, teamPosition: .one) .padding(padding) if width == 1 { Divider() } else { - Divider().frame(height: width).overlay(color) + Divider().frame(height: width).overlay(Color(white: 0.9)) } - PlayerBlockView(match: match, teamPosition: .two, color: color, width: width) + PlayerBlockView(match: match, teamPosition: .two) .padding(padding) } } .overlay { if matchViewStyle != .plainStyle { RoundedRectangle(cornerRadius: 8) - .stroke(color, lineWidth: 2) + .stroke(Color(white: 0.9), lineWidth: 2) } } if matchViewStyle != .plainStyle { HStack { + if matchViewStyle == .followUpStyle { + if match.expectedToBeRunning() { + Text(match.expectedFormattedStartDate(canBePlayedInSpecifiedCourt: canBePlayedInSpecifiedCourt, availableCourts: availableCourts, estimatedStartDate: estimatedStartDate, updatedField: updatedField)) + .font(.footnote) + .foregroundStyle(.secondary) + } + } Spacer() - MatchDateView(match: match, showPrefix: matchViewStyle == .tournamentResultStyle) + MatchDateView(match: match, showPrefix: false, updatedField: possibleCourtIndex) } } } .padding(.vertical, padding) .monospacedDigit() } + + var possibleCourtIndex: Int? { + if canBePlayedInSpecifiedCourt { + return nil + } else if let updatedField, availableCourts.contains(updatedField) { + return updatedField + } else if let first = availableCourts.first { + return first + } else if let estimatedStartDate { + return estimatedStartDate.0 + } + return updatedField + } + + func courtIsNotValid() -> Bool { + if match.courtIndex == updatedField { + return false + } + + if match.isReady() == false { + return false + } + + if canBePlayedInSpecifiedCourt { + return false + } + + if let estimatedStartDate, estimatedStartDate.0 == updatedField { + return false + } + + if let estimatedStartDate, estimatedStartDate.0 == match.courtIndex { + return false + } + + + return true + } } //#Preview { diff --git a/PadelClub/Views/Navigation/Agenda/ActivityView.swift b/PadelClub/Views/Navigation/Agenda/ActivityView.swift index 58eb1fb..48bdf11 100644 --- a/PadelClub/Views/Navigation/Agenda/ActivityView.swift +++ b/PadelClub/Views/Navigation/Agenda/ActivityView.swift @@ -134,8 +134,12 @@ struct ActivityView: View { ContentUnavailableView { Label("Une erreur est survenue", systemImage: "exclamationmark.circle.fill") } description: { - Text(error.localizedDescription) + Text("Tenup est peut-être en maintenance. " + error.localizedDescription) } actions: { + Link(destination: URLs.tenup.url) { + Text("Voir si tenup est en maintenance") + } + RowButtonView("D'accord.") { self.error = nil } @@ -510,15 +514,32 @@ struct ActivityView: View { .padding() } } else { - ContentUnavailableView { - Label("Aucun tournoi", systemImage: "shield.slash") - } description: { - Text("Aucun tournoi ne correspond aux critères sélectionnés.") - } actions: { - FooterButtonView("modifier vos critères de recherche") { - displaySearchView = true + if federalDataViewModel.lastError == nil { + ContentUnavailableView { + Label("Aucun tournoi", systemImage: "shield.slash") + } description: { + Text("Aucun tournoi ne correspond aux critères sélectionnés.") + } actions: { + FooterButtonView("modifier vos critères de recherche") { + displaySearchView = true + } + .padding() + } + } else { + ContentUnavailableView { + Label("Une erreur est survenue", systemImage: "exclamationmark.circle.fill") + } description: { + Text("Tenup est peut-être en maintenance, veuillez ré-essayer plus tard.") + } actions: { + Link(destination: URLs.tenup.url) { + Text("Voir si tenup est en maintenance") + } + + FooterButtonView("modifier vos critères de recherche") { + displaySearchView = true + } + .padding() } - .padding() } } } diff --git a/PadelClub/Views/Navigation/Agenda/CalendarView.swift b/PadelClub/Views/Navigation/Agenda/CalendarView.swift index b700129..1dab0aa 100644 --- a/PadelClub/Views/Navigation/Agenda/CalendarView.swift +++ b/PadelClub/Views/Navigation/Agenda/CalendarView.swift @@ -124,7 +124,7 @@ struct CalendarView: View { ) .overlay(alignment: .bottomTrailing) { if let count = counts[day.dayInt] { - Image(systemName: count <= 50 ? "\(count).circle.fill" : "plus.circle.fill") + Image(systemName: count <= 50 ? "\(count).circle.fill" : "ellipsis.circle.fill") .foregroundColor(.secondary) .imageScale(.medium) .background ( diff --git a/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift b/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift index 396fb0d..783359c 100644 --- a/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift +++ b/PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift @@ -181,6 +181,7 @@ struct TournamentLookUpView: View { federalDataViewModel.levels = Set(levels) federalDataViewModel.categories = Set(categories) federalDataViewModel.ageCategories = Set(ages) + federalDataViewModel.lastError = nil Task { await getNewPage() @@ -223,7 +224,12 @@ struct TournamentLookUpView: View { await getNewBuildForm() } else { let commands = try await NetworkFederalService.shared.getAllFederalTournaments(sortingOption: dataStore.appSettings.sortingOption, page: page, startDate: dataStore.appSettings.startDate, endDate: dataStore.appSettings.endDate, city: dataStore.appSettings.city, distance: dataStore.appSettings.distance, categories: categories, levels: levels, lat: locationManager.location?.coordinate.latitude.formatted(.number.locale(Locale(identifier: "us"))), lng: locationManager.location?.coordinate.longitude.formatted(.number.locale(Locale(identifier: "us"))), ages: ages, types: types, nationalCup: dataStore.appSettings.nationalCup) + if commands.anySatisfy({ $0.command == "alert" }) { + federalDataViewModel.lastError = .maintenance + } + let resultCommand = commands.first(where: { $0.results != nil }) + if let newTournaments = resultCommand?.results?.items { newTournaments.forEach { ft in // let isValid = ft.tournaments.anySatisfy({ build in @@ -363,7 +369,7 @@ struct TournamentLookUpView: View { NavigationLink { List([FederalTournamentAge.senior, FederalTournamentAge.a45, FederalTournamentAge.a55, FederalTournamentAge.a17_18, FederalTournamentAge.a15_16, FederalTournamentAge.a13_14, FederalTournamentAge.a11_12], selection: $appSettings.tournamentAges) { type in - Text(type.localizedLabel()) + Text(type.localizedFederalAgeLabel()) } .navigationTitle("Limites d'âge") .environment(\.editMode, Binding.constant(EditMode.active)) @@ -375,7 +381,7 @@ struct TournamentLookUpView: View { Text("Tous les âges") .foregroundStyle(.secondary) } else { - Text(ages.map({ $0.localizedLabel()}).joined(separator: ", ")) + Text(ages.map({ $0.localizedFederalAgeLabel()}).joined(separator: ", ")) .foregroundStyle(.secondary) } } diff --git a/PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift b/PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift index c69ffb9..51fe95e 100644 --- a/PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift +++ b/PadelClub/Views/Navigation/Agenda/TournamentSubscriptionView.swift @@ -286,7 +286,7 @@ struct TournamentSubscriptionView: View { } var teamsString: String { - selectedPlayers.map { $0.pasteData() }.joined(separator: "\n") + selectedPlayers.map { $0.pasteData(withRank: true) }.joined(separator: "\n") } var messageBody: String { diff --git a/PadelClub/Views/Navigation/MainView.swift b/PadelClub/Views/Navigation/MainView.swift index bf098ab..8ad7427 100644 --- a/PadelClub/Views/Navigation/MainView.swift +++ b/PadelClub/Views/Navigation/MainView.swift @@ -79,7 +79,7 @@ struct MainView: View { TournamentOrganizerView() .tabItem(for: .tournamentOrganizer) .toolbarBackground(.visible, for: .tabBar) - OngoingView() + OngoingContainerView() .tabItem(for: .ongoing) .badge(self.dataStore.runningMatches().count) .toolbarBackground(.visible, for: .tabBar) @@ -263,7 +263,7 @@ struct MainView: View { await _startImporting(importingDate: mostRecentDateImported) } else if current.dataModelIdentifier != PersistenceController.getModelVersion() && current.fileModelIdentifier != fileURL?.fileModelIdentifier() { await _startImporting(importingDate: mostRecentDateImported) - } else if current.incompleteMode == false || updated == 0 { + } else if updated == 0 { await _calculateMonthData(dataSource: current.monthKey) } } diff --git a/PadelClub/Views/Navigation/Ongoing/OngoingContainerView.swift b/PadelClub/Views/Navigation/Ongoing/OngoingContainerView.swift new file mode 100644 index 0000000..15e4bb9 --- /dev/null +++ b/PadelClub/Views/Navigation/Ongoing/OngoingContainerView.swift @@ -0,0 +1,95 @@ +// +// OngoingContainerView.swift +// PadelClub +// +// Created by razmig on 07/11/2024. +// + +import SwiftUI +import LeStorage + +@Observable +class OngoingViewModel { + static let shared = OngoingViewModel() + + var destination: OngoingDestination? = .running + var hideUnconfirmedMatches: Bool = false + var hideNotReadyMatches: Bool = false + + func areFiltersEnabled() -> Bool { + hideUnconfirmedMatches || hideNotReadyMatches + } + + let defaultSorting : [MySortDescriptor] = [.keyPath(\Match.startDate!), .keyPath(\Match.index), .keyPath(\Match.courtIndexForSorting)] + + var runningAndNextMatches: [Match] { + DataStore.shared.runningAndNextMatches().sorted(using: defaultSorting, order: .ascending) + } + + var filteredRunningAndNextMatches: [Match] { + return runningAndNextMatches.filter({ + (hideUnconfirmedMatches == false || hideUnconfirmedMatches == true && $0.confirmed) + && (hideNotReadyMatches == false || hideNotReadyMatches == true && $0.isReady() ) + }) + } +} + +struct OngoingContainerView: View { + @Environment(NavigationViewModel.self) private var navigation: NavigationViewModel + @State private var showMatchPicker: Bool = false + + var body: some View { + @Bindable var navigation = navigation + @Bindable var ongoingViewModel = OngoingViewModel.shared + NavigationStack(path: $navigation.ongoingPath) { + VStack(spacing: 0) { + GenericDestinationPickerView(selectedDestination: $ongoingViewModel.destination, destinations: OngoingDestination.allCases, nilDestinationIsValid: false) + + switch ongoingViewModel.destination! { + case .running, .followUp, .over: + OngoingView() + case .court, .free: + OngoingCourtView() + } + } + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .navigationTitle("Programmation") + .toolbar { + if ongoingViewModel.destination == .followUp { + ToolbarItem(placement: .topBarLeading) { + Menu { + Toggle(isOn: $ongoingViewModel.hideUnconfirmedMatches) { + Text("masquer non confirmés") + } + Toggle(isOn: $ongoingViewModel.hideNotReadyMatches) { + Text("masquer incomplets") + } + } label: { + Image(systemName: "line.3.horizontal.decrease.circle") + .resizable() + .scaledToFit() + .frame(minHeight: 32) + } + .symbolVariant(ongoingViewModel.areFiltersEnabled() ? .fill : .none) + } + } + ToolbarItem(placement: .topBarTrailing) { + Button { + showMatchPicker = true + } label: { + Image(systemName: "rectangle.stack.badge.plus") + .resizable() + .scaledToFit() + .frame(minHeight: 32) + } + } + } + } + .environment(ongoingViewModel) + .sheet(isPresented: $showMatchPicker, content: { + FollowUpMatchView(selectedCourt: nil, allMatches: ongoingViewModel.runningAndNextMatches, autoDismiss: false) + .tint(.master) + }) + } +} diff --git a/PadelClub/Views/Navigation/Ongoing/OngoingDestination.swift b/PadelClub/Views/Navigation/Ongoing/OngoingDestination.swift new file mode 100644 index 0000000..ebf38f4 --- /dev/null +++ b/PadelClub/Views/Navigation/Ongoing/OngoingDestination.swift @@ -0,0 +1,129 @@ +// +// OngoingDestination.swift +// PadelClub +// +// Created by razmig on 07/11/2024. +// +import SwiftUI + +enum OngoingDestination: Int, CaseIterable, Identifiable, Selectable, Equatable { + var id: Int { self.rawValue } + + static func == (lhs: OngoingDestination, rhs: OngoingDestination) -> Bool { + return lhs.id == rhs.id + } + + case running + case followUp + case court + case free + case over + + var runningAndNextMatches: [Match] { + switch self { + case .running, .court, .free: + return OngoingViewModel.shared.runningAndNextMatches + case .followUp: + return OngoingViewModel.shared.filteredRunningAndNextMatches + case .over: + return DataStore.shared.endMatches() + } + } + + var sortedMatches: [Match] { + return runningAndNextMatches.filter({ self.shouldDisplay($0) }) + } + + var filteredMatches: [Match] { + sortedMatches.filter({ OngoingDestination.running.shouldDisplay($0) }) + } + + var sortedCourtIndex: [Int?] { + let courtUsed = sortedMatches.grouped(by: { $0.courtIndex }).keys + let sortedNumbers = courtUsed.sorted { (a, b) -> Bool in + switch (a, b) { + case (nil, _): return false + case (_, nil): return true + case let (a?, b?): return a < b + } + } + return sortedNumbers + } + + func contentUnavailable() -> some View { + switch self { + case .running: + ContentUnavailableView("Aucun match en cours", systemImage: "figure.tennis", description: Text("Tous vos matchs en cours seront visibles ici, quelque soit le tournoi.")) + case .followUp: + ContentUnavailableView("Aucun match à suivre", systemImage: "figure.tennis", description: Text("Tous vos matchs planifiés et confirmés, seront visibles ici, quelque soit le tournoi.")) + case .court: + ContentUnavailableView("Aucun match en cours", systemImage: "sportscourt", description: Text("Tous vos terrains correspondant aux matchs en cours seront visibles ici, quelque soit le tournoi.")) + case .free: + ContentUnavailableView("Aucun terrain libre", systemImage: "sportscourt", description: Text("Les terrains libres seront visibles ici, quelque soit le tournoi.")) + case .over: + ContentUnavailableView("Aucun match terminé", systemImage: "clock.badge.xmark", description: Text("Les matchs terminés seront visibles ici, quelque soit le tournoi.")) + } + } + + func localizedFilterModeLabel() -> String { + switch self { + case .running: + return "En cours" + case .followUp: + return "À suivre" + case .court: + return "Terrains" + case .free: + return "Libres" + case .over: + return "Finis" + } + } + + func shouldDisplay(_ match: Match) -> Bool { + switch self { + case .running: + return match.isRunning() + case .court, .free: + return true + case .followUp: + return match.isRunning() == false + case .over: + return match.hasEnded() + } + } + + func selectionLabel(index: Int) -> String { + localizedFilterModeLabel() + } + + func systemImage() -> String? { + switch self { + default: + return nil + } + } + + func badgeValue() -> Int? { + switch self { + case .running, .followUp, .over: + sortedMatches.count + case .court: + sortedCourtIndex.filter({ index in + filteredMatches.filter({ $0.courtIndex == index }).isEmpty == false + }).count + case .free: + sortedCourtIndex.filter({ index in + filteredMatches.filter({ $0.courtIndex == index }).isEmpty + }).count + } + } + + func badgeValueColor() -> Color? { + nil + } + + func badgeImage() -> Badge? { + nil + } +} diff --git a/PadelClub/Views/Navigation/Ongoing/OngoingView.swift b/PadelClub/Views/Navigation/Ongoing/OngoingView.swift index 3c41cbb..309885a 100644 --- a/PadelClub/Views/Navigation/Ongoing/OngoingView.swift +++ b/PadelClub/Views/Navigation/Ongoing/OngoingView.swift @@ -8,82 +8,110 @@ import SwiftUI import LeStorage +extension Int: @retroactive Identifiable { + public var id: Int { + return self + } +} + + + struct OngoingView: View { @Environment(NavigationViewModel.self) private var navigation: NavigationViewModel @EnvironmentObject var dataStore: DataStore + @Environment(OngoingViewModel.self) private var ongoingViewModel: OngoingViewModel - @State private var sortByField: Bool = false - - let fieldSorting : [MySortDescriptor] = [.keyPath(\Match.courtIndexForSorting), .keyPath(\Match.startDate!)] - let defaultSorting : [MySortDescriptor] = [.keyPath(\Match.startDate!), .keyPath(\Match.courtIndexForSorting)] - - var matches: [Match] { - let sorting = self.sortByField ? fieldSorting : defaultSorting - return self.dataStore.runningMatches().sorted(using: sorting, order: .ascending) + var filterMode: OngoingDestination { + ongoingViewModel.destination! } - + var body: some View { - @Bindable var navigation = navigation - NavigationStack(path: $navigation.ongoingPath) { - List { - ForEach(matches) { match in - - if let tournament = match.currentTournament() { - - Section { - MatchRowView(match: match, matchViewStyle: .standardStyle) - } header: { - HStack { - Text(tournament.tournamentTitle(.short)) - Spacer() - if let club = tournament.club() { - Text("@" + club.clubTitle(.short)) - } + let filteredMatches = filterMode.sortedMatches + List { + ForEach(filteredMatches) { match in + let tournament = match.currentTournament() + Section { + MatchRowView(match: match) + .matchViewStyle(.followUpStyle) + } header: { + if let tournament { + HStack { + Text(tournament.tournamentTitle(.short)) + Spacer() + if let club = tournament.club() { + Text("@" + club.clubTitle(.short)) } - } footer: { - HStack { - Text(tournament.eventLabel()) + } + } + } footer: { + HStack { + if let tournament { + Text(tournament.eventLabel()) + } #if DEBUG - Spacer() - FooterButtonView("copier l'id") { - let pasteboard = UIPasteboard.general - pasteboard.string = match.id - } -#endif - } + Spacer() + FooterButtonView("copier l'id") { + let pasteboard = UIPasteboard.general + pasteboard.string = match.id } - +#endif } - } } - .headerProminence(.increased) - .overlay { - if matches.isEmpty { - ContentUnavailableView("Aucun match en cours", systemImage: "figure.tennis", description: Text("Tous vos matchs en cours seront visibles ici, quelque soit le tournoi.")) - } + } + .headerProminence(.increased) + .overlay { + if filteredMatches.isEmpty { + filterMode.contentUnavailable() } - .navigationTitle("En cours") - .toolbarBackground(.visible, for: .bottomBar) - .toolbar(matches.isEmpty ? .hidden : .visible, for: .navigationBar) - .toolbar { - ToolbarItem(placement: .status) { - Picker(selection: $sortByField) { - Text("tri par date").tag(true) - Text("tri par terrain").tag(false) - } label: { - + } + } +} + +struct OngoingCourtView: View { + + @Environment(NavigationViewModel.self) private var navigation: NavigationViewModel + @EnvironmentObject var dataStore: DataStore + @Environment(OngoingViewModel.self) private var ongoingViewModel: OngoingViewModel + + var filterMode: OngoingDestination { + ongoingViewModel.destination! + } + + @State private var selectedCourtForFollowUp: Int? + + var body: some View { + let sortedMatches = filterMode.sortedMatches + let filteredMatches = sortedMatches.filter({ OngoingDestination.running.shouldDisplay($0) }) + + List { + ForEach(filterMode.sortedCourtIndex, id: \.self) { index in + let courtFilteredMatches = filteredMatches.filter({ $0.courtIndex == index }) + let title : String = (index == nil ? "Aucun terrain défini" : "Terrain #\(index! + 1)") + if (filterMode == .free && courtFilteredMatches.isEmpty) || (filterMode == .court && courtFilteredMatches.isEmpty == false) { + Section { + MatchListView(section: "En cours", matches: courtFilteredMatches, hideWhenEmpty: true, isExpanded: false) + MatchListView(section: "À venir", matches: sortedMatches.filter({ $0.courtIndex == index && $0.hasStarted() == false }), isExpanded: false) + } header: { + Text(title) + } footer: { + FooterButtonView("Ajouter un match à suivre") { + selectedCourtForFollowUp = index + } } - .pickerStyle(.segmented) - .fixedSize() - .offset(y: -3) } } } + .sheet(item: $selectedCourtForFollowUp, content: { selectedCourtForFollowUp in + FollowUpMatchView(selectedCourt: selectedCourtForFollowUp, allMatches: filterMode.runningAndNextMatches) + .tint(.master) + }) + .headerProminence(.increased) + .overlay { + if (filteredMatches.isEmpty && filterMode != .free) || (filterMode == .free && filterMode.sortedCourtIndex.allSatisfy({ index in filteredMatches.filter({ $0.courtIndex == index }).isEmpty == false })) { + filterMode.contentUnavailable() + } + } } } - -//#Preview { -// OngoingView() -//} diff --git a/PadelClub/Views/Navigation/Toolbox/GlobalSettingsView.swift b/PadelClub/Views/Navigation/Toolbox/GlobalSettingsView.swift index d39811d..ed0e831 100644 --- a/PadelClub/Views/Navigation/Toolbox/GlobalSettingsView.swift +++ b/PadelClub/Views/Navigation/Toolbox/GlobalSettingsView.swift @@ -11,49 +11,115 @@ import LeStorage struct GlobalSettingsView: View { @EnvironmentObject var dataStore : DataStore + var groupStageMatchFormat: Binding { + Binding { + dataStore.user.groupStageMatchFormatPreference ?? .nineGames + } set: { value in + dataStore.user.groupStageMatchFormatPreference = value + } + } + + var groupStageMatchFormatPreference: Binding { + Binding { + dataStore.user.groupStageMatchFormatPreference == nil + } set: { value in + if value { + dataStore.user.groupStageMatchFormatPreference = nil + } else { + dataStore.user.groupStageMatchFormatPreference = .nineGames + } + } + + } + + var bracketMatchFormat: Binding { + Binding { + dataStore.user.bracketMatchFormatPreference ?? .nineGames + } set: { value in + dataStore.user.bracketMatchFormatPreference = value + } + } + + var bracketMatchFormatPreference: Binding { + Binding { + dataStore.user.bracketMatchFormatPreference == nil + } set: { value in + if value { + dataStore.user.bracketMatchFormatPreference = nil + } else { + dataStore.user.bracketMatchFormatPreference = .nineGames + } + } + + } + + var loserBracketMatchFormat: Binding { + Binding { + dataStore.user.loserBracketMatchFormatPreference ?? .nineGames + } set: { value in + dataStore.user.loserBracketMatchFormatPreference = value + } + } + + var loserBracketMatchFormatPreference: Binding { + Binding { + dataStore.user.loserBracketMatchFormatPreference == nil + } set: { value in + if value { + dataStore.user.loserBracketMatchFormatPreference = nil + } else { + dataStore.user.loserBracketMatchFormatPreference = .nineGames + } + } + + } + var body: some View { @Bindable var user = dataStore.user List { Section { - Picker(selection: $user.groupStageMatchFormatPreference) { - Text("Automatique").tag(nil as MatchFormat?) - ForEach(MatchFormat.allCases, id: \.self) { format in - Text(format.format).tag(format as MatchFormat?) - } - } label: { - HStack { - Text("Poule") - Spacer() - } + Toggle(isOn: groupStageMatchFormatPreference) { + Text("Automatique") } - Picker(selection: $user.bracketMatchFormatPreference) { - Text("Automatique").tag(nil as MatchFormat?) - ForEach(MatchFormat.allCases, id: \.self) { format in - Text(format.format).tag(format as MatchFormat?) - } - } label: { - HStack { - Text("Tableau") - Spacer() - } + + if groupStageMatchFormatPreference.wrappedValue == false { + MatchTypeSelectionView(selectedFormat: groupStageMatchFormat) + } + } header: { + Text("Poule") + } footer: { + Text("À minima, les règles fédérales seront toujours prises en compte par défaut.") + } + + Section { + Toggle(isOn: bracketMatchFormatPreference) { + Text("Automatique") + } + + if bracketMatchFormatPreference.wrappedValue == false { + MatchTypeSelectionView(selectedFormat: bracketMatchFormat) + } + } header: { + Text("Tableau") + } footer: { + Text("À minima, les règles fédérales seront toujours prises en compte par défaut.") + } + + Section { + Toggle(isOn: loserBracketMatchFormatPreference) { + Text("Automatique") } - Picker(selection: $user.loserBracketMatchFormatPreference) { - Text("Automatique").tag(nil as MatchFormat?) - ForEach(MatchFormat.allCases, id: \.self) { format in - Text(format.format).tag(format as MatchFormat?) - } - } label: { - HStack { - Text("Match de classement") - Spacer() - } + + if loserBracketMatchFormatPreference.wrappedValue == false { + MatchTypeSelectionView(selectedFormat: loserBracketMatchFormat) } } header: { - Text("Vos formats préférés") + Text("Match de classement") } footer: { Text("À minima, les règles fédérales seront toujours prises en compte par défaut.") } } + .headerProminence(.increased) .onChange(of: [ user.bracketMatchFormatPreference, user.groupStageMatchFormatPreference, diff --git a/PadelClub/Views/Navigation/Toolbox/MatchFormatStorageView.swift b/PadelClub/Views/Navigation/Toolbox/MatchFormatStorageView.swift index 0e03920..7d90518 100644 --- a/PadelClub/Views/Navigation/Toolbox/MatchFormatStorageView.swift +++ b/PadelClub/Views/Navigation/Toolbox/MatchFormatStorageView.swift @@ -24,8 +24,7 @@ struct MatchFormatStorageView: View { LabeledContent { StepperView(title: "minutes", count: $estimatedDuration, step: 5) } label: { - Text("Durée \(matchFormat.format)") - Text(matchFormat.computedShortLabelWithoutPrefix) + MatchFormatRowView(matchFormat: matchFormat, hideDuration: true) } } footer: { if estimatedDuration != matchFormat.defaultEstimatedDuration { diff --git a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift index 8c9dd36..487f80c 100644 --- a/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift +++ b/PadelClub/Views/Navigation/Toolbox/ToolboxView.swift @@ -129,7 +129,7 @@ struct ToolboxView: View { Section { NavigationLink { - SelectablePlayerListView(isPresented: false) + SelectablePlayerListView(isPresented: false, lastDataSource: true) } label: { Label("Rechercher un joueur", systemImage: "person.fill.viewfinder") } @@ -196,10 +196,17 @@ struct ToolboxView: View { Text("Contrat d'utilisation") } } + + Section { + RowButtonView("Effacer les logs", role: .destructive) { + StoreCenter.main.resetLoggingCollections() + didResetApiCalls = true + } + } } .overlay(alignment: .bottom) { if didResetApiCalls { - Label("failed api calls deleted", systemImage: "checkmark") + Label("logs effacés", systemImage: "checkmark") .toastFormatted() .deferredRendering(for: .seconds(3)) .onAppear { @@ -221,10 +228,9 @@ struct ToolboxView: View { ShareLink(item: URLs.appStore.url) { Label("Lien AppStore", systemImage: "link") } - if let zip = _getZip() { - ShareLink(item: zip) { - Label("Mes données", systemImage: "server.rack") - } + + ShareLink(item: ZipLog(), preview: .init("Mon archive")) { + Label("Mes données", systemImage: "server.rack") } } label: { Label("Partagez", systemImage: "square.and.arrow.up").labelStyle(.iconOnly) @@ -233,7 +239,14 @@ struct ToolboxView: View { } } } - +} + +//#Preview { +// ToolboxView() +//} + + +struct ZipLog: Transferable { private func _getZip() -> URL? { do { let filePath = try Club.storageDirectoryPath() @@ -243,8 +256,19 @@ struct ToolboxView: View { return nil } } -} -//#Preview { -// ToolboxView() -//} + func shareFile() -> URL? { + print("Generating URL...") + return _getZip() + } + + static var transferRepresentation: some TransferRepresentation { + FileRepresentation(exportedContentType: .zip) { transferable in + return SentTransferredFile(transferable.shareFile()!) + }.exportingCondition { $0.shareFile() != nil } + + ProxyRepresentation { transferable in + return transferable.shareFile()! + }.exportingCondition { $0.shareFile() != nil } + } +} diff --git a/PadelClub/Views/Navigation/Umpire/PadelClubView.swift b/PadelClub/Views/Navigation/Umpire/PadelClubView.swift index 95311c8..56e46d8 100644 --- a/PadelClub/Views/Navigation/Umpire/PadelClubView.swift +++ b/PadelClub/Views/Navigation/Umpire/PadelClubView.swift @@ -7,6 +7,7 @@ import SwiftUI import LeStorage +import Foundation struct PadelClubView: View { @State private var uuid: UUID = UUID() @@ -74,9 +75,14 @@ struct PadelClubView: View { print("before anonymousPlayers.count", anonymousPlayers.count) FileImportManager.shared.updatePlayers(isMale: fileURL.manData, players: &anonymousPlayers) - print("after anonymousPlayers.count", anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty } + print("after local anonymousPlayers.count", anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty }.count) + + await fetchPlayersDataSequentially(for: &anonymousPlayers) + + print("after beach anonymousPlayers.count", anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty } .count) SourceFileManager.shared.exportToCSV(players: okPlayers + anonymousPlayers, sourceFileType: fileURL.manData ? .messieurs : .dames, date: fileURL.dateFromPath) + SourceFileManager.shared.exportToCSV("anonymes", players: anonymousPlayers.filter { $0.firstName.isEmpty && $0.lastName.isEmpty }, sourceFileType: fileURL.manData ? .messieurs : .dames, date: fileURL.dateFromPath) } catch { Logger.error(error) } @@ -241,3 +247,71 @@ struct PadelClubView: View { //#Preview { // PadelClubView() //} + +// Function to fetch data for a single license ID +func fetchPlayerData(for licenseID: String) async throws -> [Player]? { + guard let url = URL(string: "https://beach-padel.app.fft.fr/beachja/rechercheJoueur/licencies?idHomologation=82477107&numeroLicence=\(licenseID)") else { + throw URLError(.badURL) + } + + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("application/json, text/javascript, */*; q=0.01", forHTTPHeaderField: "Accept") + request.setValue("same-origin", forHTTPHeaderField: "Sec-Fetch-Site") + request.setValue("fr-FR,fr;q=0.9", forHTTPHeaderField: "Accept-Language") + request.setValue("gzip, deflate, br", forHTTPHeaderField: "Accept-Encoding") + request.setValue("cors", forHTTPHeaderField: "Sec-Fetch-Mode") + request.setValue("beach-padel.app.fft.fr", forHTTPHeaderField: "Host") + request.setValue("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.15", forHTTPHeaderField: "User-Agent") + request.setValue("keep-alive", forHTTPHeaderField: "Connection") + request.setValue("https://beach-padel.app.fft.fr/beachja/competitionFiche/inscrireEquipe?identifiantHomologation=82477107", forHTTPHeaderField: "Referer") + request.setValue("XMLHttpRequest", forHTTPHeaderField: "X-Requested-With") + + // Add cookies if needed (example cookie header value shown, replace with valid cookies) + request.setValue("JSESSIONID=F4ED2A1BCF3CD2694FE0B111B8027999; AWSALB=JoZEC/+cnAzmCdbbm3Vuc4CtMGx8BvbveFx+RBRuj8dQCQD52C9iDDbL/OVm98uMb7vc8Jv6/bVPkaByXWmOZmSGwAsN2s8/jt6W5L8QGz7omzNbYF01kvqffRvo; AWSALBCORS=JoZEC/+cnAzmCdbbm3Vuc4CtMGx8BvbveFx+RBRuj8dQCQD52C9iDDbL/OVm98uMb7vc8Jv6/bVPkaByXWmOZmSGwAsN2s8/jt6W5L8QGz7omzNbYF01kvqffRvo; datadome=KlbIdnrCgaY1zLVIZ5CfLJm~KXv9_YnXGhaQdqMEn6Ja9R6imBH~vhzmyuiLxGi1D0z90v5x2EiGDvQ7zsw~fajWLbOupFEajulc86PSJ7RIHpOiduCQ~cNoITQYJOXa; tc_cj_v2=m_iZZZ%22**%22%27%20ZZZKQLNQOPLOSLJOZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQLQJRKOQKSMOZZZ%5D777m_iZZZ%22**%22%27%20ZZZKQLQJRKOQMSLNZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQLQJRKOQNSJMZZZ%5D777m_iZZZ%22**%22%27%20ZZZKQLQJRKOSJMLJZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQLRPQMQQNRQRZZZ%5D777m_iZZZ%22**%22%27%20ZZZKQLRPQNKSLOMSZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQLSNSOPMSOPJZZZ%5D777m_iZZZ%22**%22%27%20ZZZKQMJQSRLJSOOJZZZ%5D777%5Ecl_%5Dny%5B%5D%5D_mmZZZZZZKQMJRJPJMSSKRZZZ%5D; tc_cj_v2_cmp=; tc_cj_v2_med=; tCdebugLib=1; incap_ses_2222_2712217=ui9wOOAjNziUTlU3gCHWHtv/KWcAAAAAhSzbpyITRp7YwRT3vJB2vg==; incap_ses_2224_2712217=NepDAr2kUDShMiCJaDzdHqbjKWcAAAAA0kLlk3lgvGnwWSTMceZoEw==; xtan=-; xtant=1; incap_ses_1350_2712217=g+XhSJRwOS8JlWTYCSq8EtOBJGcAAAAAffg2IobkPUW2BtvgJGHbMw==; TCSESSION=124101910177775608913; nlbi_2712217=jnhtOC5KDiLvfpy/b9lUTgAAAAA7zduh8JyZOVrEfGsEdFlq; TCID=12481811494814553052; xtvrn=$548419$; TCPID=12471746148351334672; visid_incap_2712217=PSfJngzoSuiowsuXXhvOu5K+7mUAAAAAQUIPAAAAAAAleL9ldvN/FC1VykkU9ret; SessionStatId=10.91.140.42.1662124965429001", forHTTPHeaderField: "Cookie") + + let (data, _) = try await URLSession.shared.data(for: request) + let decoder = JSONDecoder() + + // Debug: Print raw JSON data for inspection + if let jsonString = String(data: data, encoding: .utf8) { + print("Raw JSON response: \(jsonString)") + } + + // Decode the response + let response = try decoder.decode(Response.self, from: data) + let players = response.object.listeJoueurs + + // Cast the JSON object to [String: Any] dictionary + return players +} + +// Function to fetch data for multiple license IDs using TaskGroup +func fetchPlayersDataSequentially(for licenseIDs: inout [FederalPlayer]) async { + for licenseID in licenseIDs.filter({ $0.firstName.isEmpty && $0.lastName.isEmpty }) { + do { + if let playerData = try await fetchPlayerData(for: licenseID.license)?.first { + licenseID.lastName = playerData.nom + licenseID.firstName = playerData.prenom + } + } catch { + print(error) + } + } +} + + +struct Player: Codable { + let licence: Int + let nom: String + let prenom: String + let sexe: String +} + +struct Response: Codable { + let object: PlayerList +} + +struct PlayerList: Codable { + let listeJoueurs: [Player] +} diff --git a/PadelClub/Views/Navigation/Umpire/UmpireStatisticView.swift b/PadelClub/Views/Navigation/Umpire/UmpireStatisticView.swift new file mode 100644 index 0000000..a82674b --- /dev/null +++ b/PadelClub/Views/Navigation/Umpire/UmpireStatisticView.swift @@ -0,0 +1,84 @@ +// +// UmpireStatisticView.swift +// PadelClub +// +// Created by razmig on 06/11/2024. +// + +import SwiftUI +import LeStorage + +struct UmpireStatisticView: View { + @EnvironmentObject var dataStore: DataStore + + let walkoutTeams: [TeamRegistration] + let players: [PlayerRegistration] + let countedPlayers: [String: Int] + let countedWalkoutPlayers: [String: Int] + + init() { + let teams = DataStore.shared.tournaments.filter { $0.isDeleted == false }.flatMap({ $0.unsortedTeams() }) + let wos = teams.filter({ $0.walkOut }) + self.walkoutTeams = wos + + var uniquePlayersDict = [String: PlayerRegistration]() + var playerCountDict = [String: Int]() + var playerWalkOutCountDict = [String: Int]() + + for team in teams { + for player in team.unsortedPlayers() { + if let licenceId = player.licenceId?.strippedLicense { + if team.walkOut { + uniquePlayersDict[licenceId] = player + playerWalkOutCountDict[licenceId, default: 0] += 1 + } + playerCountDict[licenceId, default: 0] += 1 + } + } + } + + self.players = Array(uniquePlayersDict.values).sorted(by: { a, b in + playerWalkOutCountDict[a.licenceId!.strippedLicense!]! > playerWalkOutCountDict[b.licenceId!.strippedLicense!]! + }) + self.countedPlayers = playerCountDict + self.countedWalkoutPlayers = playerWalkOutCountDict + } + + var body: some View { + List { + Section { + LabeledContent { + Text(dataStore.tournaments.count.formatted()) + } label: { + Text("Tournois") + } + + LabeledContent { + Text(walkoutTeams.count.formatted()) + } label: { + Text("Équipes forfaites") + } + } + + if players.isEmpty == false { + Section { + ForEach(players) { player in + LabeledContent { + if let licenceId = player.licenceId?.strippedLicense, let count = countedPlayers[licenceId], let walkoutCount = countedWalkoutPlayers[licenceId] { + Text(walkoutCount.formatted() + " / " + count.formatted()) + .font(.title3) + } + } label: { + Text(player.playerLabel()) + } + } + } header: { + Text("Nombre de forfaits / participations") + } + } + } + .navigationTitle("Statistiques") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + } +} diff --git a/PadelClub/Views/Navigation/Umpire/UmpireView.swift b/PadelClub/Views/Navigation/Umpire/UmpireView.swift index d57292c..d72ba88 100644 --- a/PadelClub/Views/Navigation/Umpire/UmpireView.swift +++ b/PadelClub/Views/Navigation/Umpire/UmpireView.swift @@ -121,6 +121,14 @@ struct UmpireView: View { Text("Il s'agit des clubs qui sont utilisés pour récupérer les tournois tenup.") } + Section { + NavigationLink { + UmpireStatisticView() + } label: { + Text("Statistiques de participations") + } + } + Section { @Bindable var user = dataStore.user diff --git a/PadelClub/Views/Planning/Components/DatePickingView.swift b/PadelClub/Views/Planning/Components/DatePickingView.swift new file mode 100644 index 0000000..9c865e3 --- /dev/null +++ b/PadelClub/Views/Planning/Components/DatePickingView.swift @@ -0,0 +1,93 @@ +// +// DatePickingView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 17/04/2024. +// + +import SwiftUI + +struct DatePickingView: View { + let title: String + @Binding var startDate: Date + @Binding var currentDate: Date? + var duration: Int? + var validateAction: (() async -> ()) + + @State private var confirmFollowingScheduleUpdate: Bool = false + @State private var updatingInProgress: Bool = false + + var body: some View { + Section { + DatePicker(selection: $startDate) { + Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline) + } + if confirmFollowingScheduleUpdate { + RowButtonView("Modifier la suite du programme") { + updatingInProgress = true + await validateAction() + updatingInProgress = false + confirmFollowingScheduleUpdate = false + } + } + } header: { + Text(title) + } footer: { + if confirmFollowingScheduleUpdate && updatingInProgress == false { + FooterButtonView("non, ne pas modifier la suite") { + currentDate = startDate + confirmFollowingScheduleUpdate = false + } + } else { + HStack { + Menu { + Button("de 30 minutes") { + startDate = startDate.addingTimeInterval(1800) + } + + Button("d'une heure") { + startDate = startDate.addingTimeInterval(3600) + } + + Button("à 9h") { + startDate = startDate.atNine() + } + + Button("à demain 9h") { + startDate = startDate.tomorrowAtNine + } + + if let duration { + Button("à la prochaine rotation") { + startDate = startDate.addingTimeInterval(Double(duration) * 60) + } + Button("à la précédente rotation") { + startDate = startDate.addingTimeInterval(Double(duration) * -60) + } + } + } label: { + Text("décaler") + .underline() + } + .buttonStyle(.borderless) + Spacer() + + if currentDate != nil { + FooterButtonView("retirer l'horaire bloqué") { + currentDate = nil + } + } else { + FooterButtonView("bloquer l'horaire") { + currentDate = startDate + } + } + } + .buttonStyle(.borderless) + } + } + .onChange(of: startDate) { + confirmFollowingScheduleUpdate = true + } + .headerProminence(.increased) + } +} diff --git a/PadelClub/Views/Planning/Components/DatePickingViewWithFormat.swift b/PadelClub/Views/Planning/Components/DatePickingViewWithFormat.swift new file mode 100644 index 0000000..6eca161 --- /dev/null +++ b/PadelClub/Views/Planning/Components/DatePickingViewWithFormat.swift @@ -0,0 +1,108 @@ +// +// DatePickingViewWithFormat.swift +// PadelClub +// +// Created by razmig on 26/10/2024. +// + +import SwiftUI + +struct DatePickingViewWithFormat: View { + @Environment(Tournament.self) var tournament + @Binding var matchFormat: MatchFormat + let title: String + @Binding var startDate: Date + @Binding var currentDate: Date? + var duration: Int? + var validateAction: ((Bool) async -> ()) + + @State private var confirmScheduleUpdate: Bool = false + @State private var updatingInProgress : Bool = false + + var body: some View { + Section { + MatchTypeSelectionView(selectedFormat: $matchFormat, additionalEstimationDuration: tournament.additionalEstimationDuration) + DatePicker(selection: $startDate) { + Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline) + } + if confirmScheduleUpdate { + RowButtonView("Sauver et modifier la suite") { + updatingInProgress = true + await validateAction(true) + updatingInProgress = false + confirmScheduleUpdate = false + } + } + } header: { + Text(title) + } footer: { + if confirmScheduleUpdate && updatingInProgress == false { + HStack { + FooterButtonView("sauver sans modifier la suite") { + Task { + await validateAction(false) + confirmScheduleUpdate = false + } + } + Text("ou") + FooterButtonView("annuler") { + confirmScheduleUpdate = false + } + } + } else { + HStack { + Menu { + Button("de 30 minutes") { + startDate = startDate.addingTimeInterval(1800) + } + + Button("d'une heure") { + startDate = startDate.addingTimeInterval(3600) + } + + Button("à 9h") { + startDate = startDate.atNine() + } + + Button("à demain 9h") { + startDate = startDate.tomorrowAtNine + } + + if let duration { + Button("à la prochaine rotation") { + startDate = startDate.addingTimeInterval(Double(duration) * 60) + } + Button("à la précédente rotation") { + startDate = startDate.addingTimeInterval(Double(duration) * -60) + } + } + } label: { + Text("décaler") + .underline() + } + .buttonStyle(.borderless) + Spacer() + + if currentDate != nil { + FooterButtonView("retirer l'horaire bloqué") { + currentDate = nil + } + } else { + FooterButtonView("bloquer l'horaire") { + currentDate = startDate + } + } + } + .buttonStyle(.borderless) + } + + } + .headerProminence(.increased) + .onChange(of: matchFormat) { + confirmScheduleUpdate = true + } + .onChange(of: startDate) { + confirmScheduleUpdate = true + } + } +} diff --git a/PadelClub/Views/Planning/Components/DateUpdateManagerView.swift b/PadelClub/Views/Planning/Components/DateUpdateManagerView.swift index 3cdb16d..9c865e3 100644 --- a/PadelClub/Views/Planning/Components/DateUpdateManagerView.swift +++ b/PadelClub/Views/Planning/Components/DateUpdateManagerView.swift @@ -1,5 +1,5 @@ // -// DateUpdateManagerView.swift +// DatePickingView.swift // PadelClub // // Created by Razmig Sarkissian on 17/04/2024. @@ -91,225 +91,3 @@ struct DatePickingView: View { .headerProminence(.increased) } } - -struct MatchFormatPickingView: View { - var title: String? = nil - @Binding var matchFormat: MatchFormat - var validateAction: (() async -> ()) - - @State private var confirmScheduleUpdate: Bool = false - @State private var updatingInProgress : Bool = false - - var body: some View { - Section { - MatchFormatPickerView(headerLabel: "Format", matchFormat: $matchFormat) - if confirmScheduleUpdate { - RowButtonView("Recalculer les horaires") { - updatingInProgress = true - await validateAction() - updatingInProgress = false - confirmScheduleUpdate = false - } - } - } header: { - if let title { - Text(title) - } - } footer: { - if confirmScheduleUpdate && updatingInProgress == false { - FooterButtonView("non, ne pas modifier les horaires") { - confirmScheduleUpdate = false - } - } - } - .headerProminence(.increased) - .onChange(of: matchFormat) { - confirmScheduleUpdate = true - } - } -} - - -struct DatePickingViewWithFormat: View { - @Binding var matchFormat: MatchFormat - let title: String - @Binding var startDate: Date - @Binding var currentDate: Date? - var duration: Int? - var validateAction: ((Bool) async -> ()) - - @State private var confirmScheduleUpdate: Bool = false - @State private var updatingInProgress : Bool = false - - var body: some View { - Section { - MatchFormatPickerView(headerLabel: "Format", matchFormat: $matchFormat) - DatePicker(selection: $startDate) { - Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline) - } - if confirmScheduleUpdate { - RowButtonView("Sauver et modifier la suite") { - updatingInProgress = true - await validateAction(true) - updatingInProgress = false - confirmScheduleUpdate = false - } - } - } header: { - Text(title) - } footer: { - if confirmScheduleUpdate && updatingInProgress == false { - HStack { - FooterButtonView("sauver sans modifier la suite") { - Task { - await validateAction(false) - confirmScheduleUpdate = false - } - } - Text("ou") - FooterButtonView("annuler") { - confirmScheduleUpdate = false - } - } - } else { - HStack { - Menu { - Button("de 30 minutes") { - startDate = startDate.addingTimeInterval(1800) - } - - Button("d'une heure") { - startDate = startDate.addingTimeInterval(3600) - } - - Button("à 9h") { - startDate = startDate.atNine() - } - - Button("à demain 9h") { - startDate = startDate.tomorrowAtNine - } - - if let duration { - Button("à la prochaine rotation") { - startDate = startDate.addingTimeInterval(Double(duration) * 60) - } - Button("à la précédente rotation") { - startDate = startDate.addingTimeInterval(Double(duration) * -60) - } - } - } label: { - Text("décaler") - .underline() - } - .buttonStyle(.borderless) - Spacer() - - if currentDate != nil { - FooterButtonView("retirer l'horaire bloqué") { - currentDate = nil - } - } else { - FooterButtonView("bloquer l'horaire") { - currentDate = startDate - } - } - } - .buttonStyle(.borderless) - } - - } - .headerProminence(.increased) - .onChange(of: matchFormat) { - confirmScheduleUpdate = true - } - .onChange(of: startDate) { - confirmScheduleUpdate = true - } - } -} - -struct GroupStageDatePickingView: View { - let title: String - @Binding var startDate: Date - @Binding var currentDate: Date? - var duration: Int? - var validateAction: (() async -> ()) - - @State private var confirmFollowingScheduleUpdate: Bool = false - @State private var updatingInProgress: Bool = false - - var body: some View { - Section { - DatePicker(selection: $startDate) { - Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline) - } - if confirmFollowingScheduleUpdate { - RowButtonView("Confirmer et modifier les matchs") { - updatingInProgress = true - await validateAction() - updatingInProgress = false - confirmFollowingScheduleUpdate = false - } - } - } header: { - Text(title) - } footer: { - if confirmFollowingScheduleUpdate && updatingInProgress == false { - FooterButtonView("Modifier juste l'horaire de la poule") { - currentDate = startDate - confirmFollowingScheduleUpdate = false - } - } else { - HStack { - Menu { - Button("de 30 minutes") { - startDate = startDate.addingTimeInterval(1800) - } - - Button("d'une heure") { - startDate = startDate.addingTimeInterval(3600) - } - - Button("à 9h") { - startDate = startDate.atNine() - } - - Button("à demain 9h") { - startDate = startDate.tomorrowAtNine - } - - if let duration { - Button("à la prochaine rotation") { - startDate = startDate.addingTimeInterval(Double(duration) * 60) - } - Button("à la précédente rotation") { - startDate = startDate.addingTimeInterval(Double(duration) * -60) - } - } - } label: { - Text("décaler") - .underline() - } - .buttonStyle(.borderless) - Spacer() - - if currentDate != nil { - FooterButtonView("retirer l'horaire bloqué") { - currentDate = nil - } - } else { - FooterButtonView("bloquer l'horaire") { - currentDate = startDate - } - } - } - .buttonStyle(.borderless) - } - } - .onChange(of: startDate) { - confirmFollowingScheduleUpdate = true - } - .headerProminence(.increased) - } -} diff --git a/PadelClub/Views/Planning/Components/GroupStageDatePickingView.swift b/PadelClub/Views/Planning/Components/GroupStageDatePickingView.swift new file mode 100644 index 0000000..29c380f --- /dev/null +++ b/PadelClub/Views/Planning/Components/GroupStageDatePickingView.swift @@ -0,0 +1,93 @@ +// +// GroupStageDatePickingView.swift +// PadelClub +// +// Created by razmig on 26/10/2024. +// + +import SwiftUI + +struct GroupStageDatePickingView: View { + let title: String + @Binding var startDate: Date + @Binding var currentDate: Date? + var duration: Int? + var validateAction: (() async -> ()) + + @State private var confirmFollowingScheduleUpdate: Bool = false + @State private var updatingInProgress: Bool = false + + var body: some View { + Section { + DatePicker(selection: $startDate) { + Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline) + } + if confirmFollowingScheduleUpdate { + RowButtonView("Confirmer et modifier les matchs") { + updatingInProgress = true + await validateAction() + updatingInProgress = false + confirmFollowingScheduleUpdate = false + } + } + } header: { + Text(title) + } footer: { + if confirmFollowingScheduleUpdate && updatingInProgress == false { + FooterButtonView("Modifier juste l'horaire de la poule") { + currentDate = startDate + confirmFollowingScheduleUpdate = false + } + } else { + HStack { + Menu { + Button("de 30 minutes") { + startDate = startDate.addingTimeInterval(1800) + } + + Button("d'une heure") { + startDate = startDate.addingTimeInterval(3600) + } + + Button("à 9h") { + startDate = startDate.atNine() + } + + Button("à demain 9h") { + startDate = startDate.tomorrowAtNine + } + + if let duration { + Button("à la prochaine rotation") { + startDate = startDate.addingTimeInterval(Double(duration) * 60) + } + Button("à la précédente rotation") { + startDate = startDate.addingTimeInterval(Double(duration) * -60) + } + } + } label: { + Text("décaler") + .underline() + } + .buttonStyle(.borderless) + Spacer() + + if currentDate != nil { + FooterButtonView("retirer l'horaire bloqué") { + currentDate = nil + } + } else { + FooterButtonView("bloquer l'horaire") { + currentDate = startDate + } + } + } + .buttonStyle(.borderless) + } + } + .onChange(of: startDate) { + confirmFollowingScheduleUpdate = true + } + .headerProminence(.increased) + } +} diff --git a/PadelClub/Views/Planning/Components/MatchFormatPickingView.swift b/PadelClub/Views/Planning/Components/MatchFormatPickingView.swift new file mode 100644 index 0000000..8b36c13 --- /dev/null +++ b/PadelClub/Views/Planning/Components/MatchFormatPickingView.swift @@ -0,0 +1,47 @@ +// +// MatchFormatPickingView.swift +// PadelClub +// +// Created by razmig on 26/10/2024. +// + +import SwiftUI + +struct MatchFormatPickingView: View { + @Environment(Tournament.self) var tournament + var title: String? = nil + @Binding var matchFormat: MatchFormat + var validateAction: (() async -> ()) + + @State private var confirmScheduleUpdate: Bool = false + @State private var updatingInProgress : Bool = false + + var body: some View { + Section { + MatchTypeSelectionView(selectedFormat: $matchFormat, additionalEstimationDuration: tournament.additionalEstimationDuration) + if confirmScheduleUpdate { + RowButtonView("Recalculer les horaires") { + updatingInProgress = true + await validateAction() + updatingInProgress = false + confirmScheduleUpdate = false + } + } + } header: { + if let title { + Text(title) + } + } footer: { + if confirmScheduleUpdate && updatingInProgress == false { + FooterButtonView("non, ne pas modifier les horaires") { + confirmScheduleUpdate = false + } + } + } + .headerProminence(.increased) + .onChange(of: matchFormat) { + confirmScheduleUpdate = true + } + } +} + diff --git a/PadelClub/Views/Planning/Components/MultiCourtPickerView.swift b/PadelClub/Views/Planning/Components/MultiCourtPickerView.swift new file mode 100644 index 0000000..55c2b26 --- /dev/null +++ b/PadelClub/Views/Planning/Components/MultiCourtPickerView.swift @@ -0,0 +1,38 @@ +// +// MultiCourtPickerView.swift +// PadelClub +// +// Created by razmig on 11/10/2024. +// + +import SwiftUI + +struct MultiCourtPickerView: View { + @Bindable var matchScheduler: MatchScheduler + @Environment(Tournament.self) var tournament: Tournament + + var body: some View { + List { + ForEach(tournament.courtsAvailable(), id: \.self) { courtIndex in + LabeledContent { + Button { + if matchScheduler.courtsAvailable.contains(courtIndex) { + matchScheduler.courtsAvailable.remove(courtIndex) + } else { + matchScheduler.courtsAvailable.insert(courtIndex) + } + } label: { + if matchScheduler.courtsAvailable.contains(courtIndex) { + Image(systemName: "checkmark.circle.fill") + } + } + } label: { + Text(tournament.courtName(atIndex: courtIndex)) + } + } + } + .navigationTitle("Terrains disponibles") + .toolbarBackground(.visible, for: .navigationBar) + .environment(\.editMode, Binding.constant(EditMode.active)) + } +} diff --git a/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift b/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift index 9ade115..8f05d8a 100644 --- a/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift +++ b/PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift @@ -15,9 +15,6 @@ struct CourtAvailabilitySettingsView: View { let event: Event @State private var showingPopover: Bool = false - @State private var courtIndex: Int = 0 - @State private var startDate: Date = Date() - @State private var endDate: Date = Date() @State private var editingSlot: DateInterval? var courtsUnavailability: [Int: [DateInterval]] { @@ -45,10 +42,6 @@ struct CourtAvailabilitySettingsView: View { } Button("éditer") { editingSlot = dateInterval - courtIndex = dateInterval.courtIndex - startDate = dateInterval.startDate - endDate = dateInterval.endDate - showingPopover = true } Button("effacer", role: .destructive) { do { @@ -110,8 +103,6 @@ struct CourtAvailabilitySettingsView: View { Text("Vous pouvez précisez l'indisponibilité d'une ou plusieurs terrains, que ce soit pour une journée entière ou un créneau précis.") } actions: { RowButtonView("Ajouter une indisponibilité", systemImage: "plus.circle.fill") { - startDate = tournament.startDate - endDate = tournament.startDate.addingTimeInterval(5400) showingPopover = true } } @@ -120,8 +111,6 @@ struct CourtAvailabilitySettingsView: View { .toolbar { ToolbarItem(placement: .topBarTrailing) { BarButtonView("Ajouter une indisponibilité", icon: "plus.circle.fill") { - startDate = tournament.startDate - endDate = tournament.startDate.addingTimeInterval(5400) showingPopover = true } } @@ -130,34 +119,99 @@ struct CourtAvailabilitySettingsView: View { .toolbarBackground(.visible, for: .navigationBar) .navigationTitle("Créneau indisponible") .sheet(isPresented: $showingPopover) { - NavigationStack { - Form { - Section { - CourtPicker(title: "Terrain", selection: $courtIndex, maxCourt: tournament.courtCount) - } - - Section { - DatePicker("Début", selection: $startDate) - .onChange(of: startDate) { - if endDate < startDate { - endDate = startDate.addingTimeInterval(90*60) - } - } - DatePicker("Fin", selection: $endDate) - .onChange(of: endDate) { - if startDate > endDate { - startDate = endDate.addingTimeInterval(-90*60) - } + CourtAvailabilityEditorView(event: event) + } + .sheet(item: $editingSlot) { editingSlot in + CourtAvailabilityEditorView(editingSlot: editingSlot, event: event) + } + } +} + +struct CourtPicker: View { + @Environment(Tournament.self) var tournament: Tournament + + let title: String + @Binding var selection: Int + let maxCourt: Int + + var body: some View { + Picker(title, selection: $selection) { + ForEach(0.. endDate { + startDate = endDate.addingTimeInterval(-90*60) + } + + } + } footer: { + FooterButtonView("jour entier") { + startDate = startDate.startOfDay + endDate = startDate.tomorrowAtNine.startOfDay } } - .toolbar { + + + Section { + DateAdjusterView(date: $startDate) + } header: { + Text("Modifier rapidement l'horaire de début") + } + Section { + DateAdjusterView(date: $endDate) + } header: { + Text("Modifier rapidement l'horaire de fin") + } + } + .toolbar { + ToolbarItem(placement: .topBarTrailing) { ButtonValidateView { if editingSlot == nil { let dateInterval = DateInterval(event: event.id, courtIndex: courtIndex, startDate: startDate, endDate: endDate) @@ -176,40 +230,50 @@ struct CourtAvailabilitySettingsView: View { Logger.error(error) } } - showingPopover = false + + dismiss() + } + } + + ToolbarItem(placement: .topBarLeading) { + Button("Annuler", role: .cancel) { + dismiss() } } - .navigationBarTitleDisplayMode(.inline) - .toolbarBackground(.visible, for: .navigationBar) - .navigationTitle("Nouveau créneau") - .tint(.master) - } - .onAppear { - UIDatePicker.appearance().minuteInterval = 5 - } - .onDisappear { - UIDatePicker.appearance().minuteInterval = 1 } + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .navigationTitle(_navigationTitle()) + .tint(.master) } } + + private func _navigationTitle() -> String { + editingSlot == nil ? "Nouveau créneau" : "Édition du créneau" + } } -struct CourtPicker: View { - @Environment(Tournament.self) var tournament: Tournament - - let title: String - @Binding var selection: Int - let maxCourt: Int - +struct DateAdjusterView: View { + @Binding var date: Date + var body: some View { - Picker(title, selection: $selection) { - ForEach(0.. some View { + Button(action: { + date = Calendar.current.date(byAdding: component, value: timeOffset, to: date) ?? date + }) { + Text(label) + .frame(maxWidth: .infinity) // Make buttons take equal space + } + .buttonStyle(.borderedProminent) + .tint(.master) } } - -//#Preview { -// CourtAvailabilitySettingsView(event: Event.mock()) -//} diff --git a/PadelClub/Views/Planning/GroupStageScheduleEditorView.swift b/PadelClub/Views/Planning/GroupStageScheduleEditorView.swift index 50b7cf4..ec29423 100644 --- a/PadelClub/Views/Planning/GroupStageScheduleEditorView.swift +++ b/PadelClub/Views/Planning/GroupStageScheduleEditorView.swift @@ -15,7 +15,8 @@ struct GroupStageScheduleEditorView: View { @Bindable var groupStage: GroupStage var tournament: Tournament @State private var startDate: Date - + @State private var currentDate: Date? + var tournamentStore: TournamentStore { return self.tournament.tournamentStore } @@ -24,14 +25,19 @@ struct GroupStageScheduleEditorView: View { self.groupStage = groupStage self.tournament = tournament self._startDate = State(wrappedValue: groupStage.startDate ?? tournament.startDate) + self._currentDate = State(wrappedValue: groupStage.startDate) } var body: some View { - GroupStageDatePickingView(title: groupStage.groupStageTitle(.title), startDate: $startDate, currentDate: $groupStage.startDate, duration: groupStage.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { + GroupStageDatePickingView(title: groupStage.groupStageTitle(.title), startDate: $startDate, currentDate: $currentDate, duration: groupStage.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { groupStage.startDate = startDate tournament.matchScheduler()?.updateGroupStageSchedule(tournament: tournament, specificGroupStage: groupStage) _save() } + .onChange(of: currentDate) { + groupStage.startDate = currentDate + _save() + } } private func _save() { diff --git a/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift b/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift index f280dd2..e14ad4b 100644 --- a/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift +++ b/PadelClub/Views/Planning/LoserRoundScheduleEditorView.swift @@ -17,7 +17,8 @@ struct LoserRoundScheduleEditorView: View { var loserRounds: [Round] @State private var startDate: Date @State private var matchFormat: MatchFormat - + @State private var currentDate: Date? + var tournamentStore: TournamentStore { return self.tournament.tournamentStore } @@ -27,8 +28,11 @@ struct LoserRoundScheduleEditorView: View { self.tournament = tournament let _loserRounds = upperRound.loserRounds() self.loserRounds = _loserRounds - self._startDate = State(wrappedValue: _loserRounds.first(where: { $0.startDate != nil })?.startDate ?? _loserRounds.first(where: { $0.isDisabled() == false })?.enabledMatches().first?.startDate ?? tournament.startDate) + let startDate = _loserRounds.first(where: { $0.startDate != nil })?.startDate ?? _loserRounds.first(where: { $0.isDisabled() == false })?.enabledMatches().first?.startDate + + self._startDate = State(wrappedValue: startDate ?? tournament.startDate) self._matchFormat = State(wrappedValue: _loserRounds.first?.matchFormat ?? upperRound.matchFormat) + self._currentDate = State(wrappedValue: startDate) } var body: some View { @@ -37,9 +41,17 @@ struct LoserRoundScheduleEditorView: View { await _updateSchedule() } - DatePickingView(title: "Horaire minimum", startDate: $startDate, currentDate: .constant(nil), duration: matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { + DatePickingView(title: "Horaire minimum", startDate: $startDate, currentDate: $currentDate, duration: matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { await _updateSchedule() } + .onChange(of: currentDate) { + let enabledLoserRounds = upperRound.loserRounds().filter({ $0.isDisabled() == false }) + for loserRound in enabledLoserRounds { + loserRound.startDate = currentDate + } + + _save() + } let enabledLoserRounds = upperRound.loserRounds().filter({ $0.isDisabled() == false }) ForEach(enabledLoserRounds.indices, id: \.self) { index in diff --git a/PadelClub/Views/Planning/MatchScheduleEditorView.swift b/PadelClub/Views/Planning/MatchScheduleEditorView.swift index 95a1bce..592a4ac 100644 --- a/PadelClub/Views/Planning/MatchScheduleEditorView.swift +++ b/PadelClub/Views/Planning/MatchScheduleEditorView.swift @@ -6,16 +6,23 @@ // import SwiftUI +import LeStorage struct MatchScheduleEditorView: View { @Bindable var match: Match var tournament: Tournament @State private var startDate: Date + @State private var currentDate: Date? + + var tournamentStore: TournamentStore { + return self.tournament.tournamentStore + } init(match: Match, tournament: Tournament) { self.match = match self.tournament = tournament self._startDate = State(wrappedValue: match.startDate ?? tournament.startDate) + self._currentDate = State(wrappedValue: match.startDate) } var title: String { @@ -31,15 +38,27 @@ struct MatchScheduleEditorView: View { await _updateSchedule() } - DatePickingView(title: title, startDate: $startDate, currentDate: .constant(nil), duration: match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { + DatePickingView(title: title, startDate: $startDate, currentDate: $currentDate, duration: match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { await _updateSchedule() } + .onChange(of: currentDate) { + match.startDate = currentDate + _save() + } } private func _updateSchedule() async { let scheduler: MatchScheduler? = tournament.matchScheduler() scheduler?.updateBracketSchedule(tournament: tournament, fromRoundId: match.round, fromMatchId: match.id, startDate: startDate) } + + private func _save() { + do { + try tournamentStore.matches.addOrUpdate(instance: match) + } catch { + Logger.error(error) + } + } } //#Preview { diff --git a/PadelClub/Views/Planning/PlanningByCourtView.swift b/PadelClub/Views/Planning/PlanningByCourtView.swift index 9b0744a..10b4f92 100644 --- a/PadelClub/Views/Planning/PlanningByCourtView.swift +++ b/PadelClub/Views/Planning/PlanningByCourtView.swift @@ -16,9 +16,10 @@ struct PlanningByCourtView: View { @State private var viewByCourt: Bool = false @State private var selectedDay: Date @State private var selectedCourt: Int = 0 + @State private var uuid: UUID = UUID() var timeSlots: [Date:[Match]] { - Dictionary(grouping: matches) { $0.startDate ?? .distantFuture } + Dictionary(grouping: matches.filter({ $0.startDate != nil })) { $0.startDate! } } var days: [Date] { @@ -38,7 +39,7 @@ struct PlanningByCourtView: View { } init(matches: [Match], selectedScheduleDestination: Binding, startDate: Date) { - self.matches = matches + self.matches = matches.filter({ $0.endDate == nil }) _selectedScheduleDestination = selectedScheduleDestination _selectedDay = State(wrappedValue: startDate) } @@ -48,8 +49,14 @@ struct PlanningByCourtView: View { let noStartDate = matches.allSatisfy({ $0.startDate == nil }) List { - _byCourtView(noStartDate: noStartDate) - .id(selectedCourt) + _byCourtView(selectedCourt: selectedCourt, selectedDay: selectedDay, noStartDate: noStartDate) + .id(uuid) + } + .onChange(of: selectedCourt) { + uuid = UUID() + } + .onChange(of: selectedDay) { + uuid = UUID() } .overlay { if noStartDate { @@ -98,14 +105,15 @@ struct PlanningByCourtView: View { } @ViewBuilder - func _byCourtView(noStartDate: Bool) -> some View { + func _byCourtView(selectedCourt: Int, selectedDay: Date, noStartDate: Bool) -> some View { if let _matches = courtSlots[selectedCourt]?.filter({ $0.startDate?.dayInt == selectedDay.dayInt }) { let _sortedMatches = _matches.sorted(by: \.computedStartDateForSorting) if _sortedMatches.isEmpty == false { ForEach(_sortedMatches.indices, id: \.self) { index in let match = _sortedMatches[index] Section { - MatchRowView(match: match, matchViewStyle: .feedStyle) + MatchRowView(match: match) + .matchViewStyle(.feedStyle) } header: { if let startDate = match.startDate { if index > 0 { @@ -125,38 +133,19 @@ struct PlanningByCourtView: View { } } else if noStartDate == false { ContentUnavailableView { - Label("Aucun match plannifié", systemImage: "clock.badge.questionmark") + Label("Aucun match planifié", systemImage: "clock.badge.questionmark") } description: { - Text("Aucun match n'a été plannifié sur ce terrain et au jour sélectionné") + Text("Aucun match n'a été planifié sur ce terrain et au jour sélectionné") } actions: { } } } else if noStartDate == false { ContentUnavailableView { - Label("Aucun match plannifié", systemImage: "clock.badge.questionmark") + Label("Aucun match planifié", systemImage: "clock.badge.questionmark") } description: { - Text("Aucun match n'a été plannifié sur ce terrain et au jour sélectionné") + Text("Aucun match n'a été planifié sur ce terrain et au jour sélectionné") } actions: { } } - } - - private func _matchesCount(inDayInt dayInt: Int) -> Int { - timeSlots.filter { $0.key.dayInt == dayInt }.flatMap({ $0.value }).count - } - - private func _timeSlotView(key: Date, matches: [Match]) -> some View { - LabeledContent { - Text(self._formattedMatchCount(matches.count)) - } label: { - Text(key.formatted(date: .omitted, time: .shortened)).font(.title).fontWeight(.semibold) - Text(Set(matches.compactMap { $0.roundTitle() }).joined(separator: ", ")) - } - } - - fileprivate func _formattedMatchCount(_ count: Int) -> String { - return "\(count.formatted()) match\(count.pluralSuffix)" - } - - + } } diff --git a/PadelClub/Views/Planning/PlanningSettingsView.swift b/PadelClub/Views/Planning/PlanningSettingsView.swift index ed03bc6..8a0d080 100644 --- a/PadelClub/Views/Planning/PlanningSettingsView.swift +++ b/PadelClub/Views/Planning/PlanningSettingsView.swift @@ -39,7 +39,7 @@ struct PlanningSettingsView: View { _groupStageChunkCount = State(wrappedValue: tournament.getGroupStageChunkValue()) } } else { - self.matchScheduler = MatchScheduler(tournament: tournament.id) + self.matchScheduler = MatchScheduler(tournament: tournament.id, courtsAvailable: Set(tournament.courtsAvailable())) self._groupStageChunkCount = State(wrappedValue: tournament.getGroupStageChunkValue()) } } @@ -68,7 +68,26 @@ struct PlanningSettingsView: View { CourtAvailabilitySettingsView(event: event) .environment(tournament) } label: { - Text("Indisponibilités des terrains") + LabeledContent { + Text(event.courtsUnavailability.count.formatted()) + } label: { + Text("Créneaux d'indisponibilités") + } + } + } + + NavigationLink { + MultiCourtPickerView(matchScheduler: matchScheduler) + .environment(tournament) + } label: { + LabeledContent { + Text(matchScheduler.courtsAvailable.count.formatted() + "/" + tournament.courtCount.formatted()) + } label: { + Text("Sélection des terrains") + if matchScheduler.courtsAvailable.count > tournament.courtCount { + Text("Attention !") + .tint(.red) + } } } } footer: { @@ -88,7 +107,7 @@ struct PlanningSettingsView: View { } } label: { if isCreatedByUser { - Text("Vous avez indiqué plus de terrains dans ce tournoi que dans le club.") + Text("Vous avez indiqué plus de terrains dans ce tournoi que dans le club. ") + Text("Mettre à jour le club ?").underline().foregroundStyle(.master) } else { Label("Vous avez indiqué plus de terrains dans ce tournoi que dans le club.", systemImage: "exclamationmark.triangle.fill").foregroundStyle(.logoRed) @@ -105,83 +124,26 @@ struct PlanningSettingsView: View { .foregroundStyle(.logoRed) } + let event = tournament.eventObject() Section { NavigationLink { _optionsView() } label: { - Text("Voir les options avancées") + Text("Voir plus d'options intelligentes") } - } - - let allMatches = tournament.allMatches() - let allGroupStages = tournament.allGroupStages() - let allRounds = tournament.allRounds() - let matchesWithDate = allMatches.filter({ $0.startDate != nil }) - let groupStagesWithDate = allGroupStages.filter({ $0.startDate != nil }) - let roundsWithDate = allRounds.filter({ $0.startDate != nil }) - if matchesWithDate.isEmpty == false || groupStagesWithDate.isEmpty == false || roundsWithDate.isEmpty == false { - Section { - RowButtonView("Supprimer les horaires des matches", role: .destructive) { - do { - deletingDateMatchesDone = false - allMatches.forEach({ - $0.startDate = nil - $0.confirmed = false - }) - try self.tournamentStore.matches.addOrUpdate(contentOfs: allMatches) - deletingDateMatchesDone = true - } catch { - Logger.error(error) - } + + if let event, event.tournaments.count > 1 { + Toggle(isOn: $matchScheduler.overrideCourtsUnavailability) { + Text("Ne pas tenir compte des autres tournois") } - } footer: { - Text("Garde les horaires définis pour les poules et les manches.") } - - Section { - RowButtonView("Supprimer tous les horaires", role: .destructive) { - do { - deletingDone = false - allMatches.forEach({ - $0.startDate = nil - $0.confirmed = false - }) - try self.tournamentStore.matches.addOrUpdate(contentOfs: allMatches) - - allGroupStages.forEach({ $0.startDate = nil }) - try self.tournamentStore.groupStages.addOrUpdate(contentOfs: allGroupStages) - - allRounds.forEach({ $0.startDate = nil }) - try self.tournamentStore.rounds.addOrUpdate(contentOfs: allRounds) - deletingDone = true - } catch { - Logger.error(error) - } - } + } footer: { + if let event, event.tournaments.count > 1 { + Text("Cette option fait en sorte qu'un terrain pris par un match d'un autre tournoi de cet événement soit toujours considéré comme libre.") } } - Section { - if groupStagesWithDate.isEmpty == false { - Text("Des dates de démarrages ont été indiqué pour les poules et seront prises en compte.") - } - if roundsWithDate.isEmpty == false { - Text("Des dates de démarrages ont été indiqué pour les manches et seront prises en compte.") - } - RowButtonView("Horaire intelligent", role: .destructive) { - await MainActor.run { - issueFound = false - schedulingDone = false - } - self.issueFound = await _setupSchedule() - await MainActor.run { - _save() - schedulingDone = true - } - } - } footer: { - Text("Padel Club programmera tous les matchs de votre tournoi en fonction de différents paramètres, ") + Text("tout en tenant compte des horaires que vous avez fixé.").underline() - } + _smartView() } .headerProminence(.increased) .onAppear { @@ -220,6 +182,7 @@ struct PlanningSettingsView: View { _save() } .onChange(of: tournament.courtCount) { + matchScheduler.courtsAvailable = Set(tournament.courtsAvailable()) _save() } .onChange(of: tournament.dayDuration) { @@ -227,6 +190,165 @@ struct PlanningSettingsView: View { } } + private func _localizedFooterMessage(groupStagesWithDateIsEmpty: Bool, roundsWithDateIsEmpty: Bool) -> String { + let base = "Supprime les horaires des matchs restants non démarrés." + let extend = " Garde les horaires définis pour les " + if groupStagesWithDateIsEmpty && roundsWithDateIsEmpty { + return base + } else if groupStagesWithDateIsEmpty, roundsWithDateIsEmpty == false { + return base + extend + "manches du tableau." + } else if roundsWithDateIsEmpty, groupStagesWithDateIsEmpty == false { + return base + extend + "poules." + } else { + return base + extend + "poules et les manches du tableau." + } + } + + @ViewBuilder + private func _smartView() -> some View { + let allMatches = tournament.allMatches().filter({ $0.hasEnded() == false && $0.hasStarted() == false }) + let allGroupStages = tournament.allGroupStages() + let allRounds = tournament.allRounds() + let matchesWithDate = allMatches.filter({ $0.startDate != nil }) + + let groupMatchesByDay = _groupMatchesByDay(matches: matchesWithDate) + + let countedSet = _matchCountPerDay(matchesByDay: groupMatchesByDay, tournament: tournament) + + _formatPerDayView(matchCountPerDay: countedSet) + + let groupStagesWithDate = allGroupStages.filter({ $0.startDate != nil }) + let roundsWithDate = allRounds.filter({ $0.startDate != nil }) + if matchesWithDate.isEmpty == false { + Section { + RowButtonView("Supprimer les horaires des matches", role: .destructive) { + do { + deletingDateMatchesDone = false + allMatches.forEach({ + $0.startDate = nil + $0.confirmed = false + }) + try self.tournamentStore.matches.addOrUpdate(contentOfs: allMatches) + deletingDateMatchesDone = true + } catch { + Logger.error(error) + } + } + } footer: { + Text(_localizedFooterMessage(groupStagesWithDateIsEmpty: groupStagesWithDate.isEmpty, roundsWithDateIsEmpty: roundsWithDate.isEmpty)) + } + } + + if groupStagesWithDate.isEmpty == false { + Section { + RowButtonView("Supprimer les horaires des poules", role: .destructive) { + do { + deletingDone = false + allGroupStages.forEach({ $0.startDate = nil }) + try self.tournamentStore.groupStages.addOrUpdate(contentOfs: allGroupStages) + + deletingDone = true + } catch { + Logger.error(error) + } + } + } + } + + if roundsWithDate.isEmpty == false { + Section { + RowButtonView("Supprimer les horaires du tableau", role: .destructive) { + do { + deletingDone = false + allRounds.forEach({ $0.startDate = nil }) + try self.tournamentStore.rounds.addOrUpdate(contentOfs: allRounds) + deletingDone = true + } catch { + Logger.error(error) + } + } + } footer: { + Text("Supprime les horaires définis pour les manches du tableau.") + } + } + + if matchesWithDate.isEmpty == false && groupStagesWithDate.isEmpty == false && roundsWithDate.isEmpty == false { + Section { + RowButtonView("Supprimer tous les horaires", role: .destructive) { + do { + deletingDone = false + allMatches.forEach({ + $0.startDate = nil + $0.confirmed = false + }) + try self.tournamentStore.matches.addOrUpdate(contentOfs: allMatches) + + allGroupStages.forEach({ $0.startDate = nil }) + try self.tournamentStore.groupStages.addOrUpdate(contentOfs: allGroupStages) + + allRounds.forEach({ $0.startDate = nil }) + try self.tournamentStore.rounds.addOrUpdate(contentOfs: allRounds) + deletingDone = true + } catch { + Logger.error(error) + } + } + } footer: { + Text("Supprime les horaires des matchs restants non démarrés, les horaires définis pour les poules et les manches du tableau.") + } + } + + #if DEBUG + Section { + RowButtonView("Debug delete all dates", role: .destructive) { + do { + deletingDone = false + tournament.allMatches().forEach({ + $0.startDate = nil + $0.endDate = nil + $0.confirmed = false + }) + try self.tournamentStore.matches.addOrUpdate(contentOfs: tournament.allMatches()) + + allGroupStages.forEach({ $0.startDate = nil }) + try self.tournamentStore.groupStages.addOrUpdate(contentOfs: allGroupStages) + + allRounds.forEach({ $0.startDate = nil }) + try self.tournamentStore.rounds.addOrUpdate(contentOfs: allRounds) + deletingDone = true + } catch { + Logger.error(error) + } + } + } footer: { + Text("Supprime les horaires des matchs, les horaires définis pour les poules et les manches du tableau.") + } + #endif + + + Section { + if groupStagesWithDate.isEmpty == false { + Text("Des dates de démarrages ont été indiqué pour les poules et seront prises en compte.") + } + if roundsWithDate.isEmpty == false { + Text("Des dates de démarrages ont été indiqué pour les manches et seront prises en compte.") + } + RowButtonView("Horaire intelligent", role: .destructive) { + await MainActor.run { + issueFound = false + schedulingDone = false + } + self.issueFound = await _setupSchedule() + await MainActor.run { + _save() + schedulingDone = true + } + } + } footer: { + Text("Padel Club programmera tous les matchs de votre tournoi en fonction de différents paramètres, ") + Text("tout en tenant compte des horaires que vous avez fixé.").underline() + } + } + @ViewBuilder private func _optionsView() -> some View { List { @@ -263,6 +385,14 @@ struct PlanningSettingsView: View { } footer: { Text("Vous pouvez indiquer le nombre de poule démarrant en même temps.") } + + Section { + Toggle(isOn: $matchScheduler.simultaneousStart) { + Text("Démarrage simultané") + } + } footer: { + Text("En simultané, un match de chaque poule d'un groupe de poule sera joué avant de passer à la suite de la programmation. Si l'option est désactivée, un maximum de matchs simultanés d'une poule sera programmé avant de passer à la poule suivante.") + } } Section { @@ -286,15 +416,7 @@ struct PlanningSettingsView: View { } footer: { Text("Cette option permet de programmer une manche sur plusieurs rotation de manière équilibrée dans le cas où il y a plus de matchs à jouer dans cette manche que de terrains.") } - - Section { - Toggle(isOn: $matchScheduler.overrideCourtsUnavailability) { - Text("Ne pas tenir compte des autres tournois") - } - } footer: { - Text("Cette option fait en sorte qu'un terrain pris par un match d'un autre tournoi est toujours considéré comme libre.") - } - + Section { Toggle(isOn: $matchScheduler.shouldEndRoundBeforeStartingNext) { Text("Finir une manche, classement inclus avant de continuer") @@ -369,6 +491,140 @@ struct PlanningSettingsView: View { Logger.error(error) } } + + private func _groupMatchesByDay(matches: [Match]) -> [Date: [Match]] { + var matchesByDay = [Date: [Match]]() + let calendar = Calendar.current + + for match in matches { + // Extract day/month/year and create a date with only these components + let components = calendar.dateComponents([.year, .month, .day], from: match.computedStartDateForSorting) + let strippedDate = calendar.date(from: components)! + + // Group matches by the strippedDate (only day/month/year) + if matchesByDay[strippedDate] == nil { + matchesByDay[strippedDate] = [] + } + + let shouldIncludeMatch: Bool + switch match.matchType { + case .groupStage: + shouldIncludeMatch = !matchesByDay[strippedDate]!.filter { $0.groupStage != nil }.compactMap { $0.groupStage }.contains(match.groupStage!) + case .bracket: + shouldIncludeMatch = !matchesByDay[strippedDate]!.filter { $0.round != nil }.compactMap { $0.round }.contains(match.round!) + case .loserBracket: + shouldIncludeMatch = true + } + + if shouldIncludeMatch { + matchesByDay[strippedDate]!.append(match) + } + } + + return matchesByDay + } + + private func _matchCountPerDay(matchesByDay: [Date: [Match]], tournament: Tournament) -> [Date: NSCountedSet] { + let days = matchesByDay.keys + var matchCountPerDay = [Date: NSCountedSet]() + + for day in days { + if let matches = matchesByDay[day] { + var groupStageCount = 0 + let countedSet = NSCountedSet() + + for match in matches { + switch match.matchType { + case .groupStage: + if let groupStage = match.groupStageObject { + if groupStageCount < groupStage.size - 1 { + groupStageCount = groupStage.size - 1 + } + } + case .bracket: + countedSet.add(match.matchFormat) + case .loserBracket: + break + } + } + + if groupStageCount > 0 { + for _ in 0.. some View { + ForEach(Array(matchCountPerDay.keys).sorted(), id: \.self) { date in + if let countedSet = matchCountPerDay[date] { + Section { + let totalMatches = countedSet.totalCount() + ForEach(Array(countedSet).compactMap { $0 as? MatchFormat }, id: \.self) { matchFormat in + + let count = countedSet.count(for: matchFormat) + let totalForThisFormat = matchFormat.maximumMatchPerDay(for: totalMatches) + let error = count > totalForThisFormat + // Presenting LabeledContent for each match format and its count + LabeledContent { + Image(systemName: error ? "exclamationmark.triangle" : "checkmark") + .font(.title3) + .foregroundStyle(error ? .red : .green) + } label: { + let label : String = "\(count) match\(count.pluralSuffix) en \(matchFormat.format)" + let optionA : String = "aucun match possible à ce format" + let optionB : String = "pas plus de " + totalForThisFormat.formatted() + " match\(totalForThisFormat.pluralSuffix) à ce format" + let subtitle : String = (totalForThisFormat == 0) ? optionA : optionB + Text(label) + Text(subtitle) + } + } + } header: { + Text(date.formatted(.dateTime.weekday(.abbreviated).day(.twoDigits).month(.abbreviated))) + } footer: { + let totalMatches = countedSet.totalCount() + Text("Une équipe jouera potentiellement jusqu'à \(totalMatches) match\(totalMatches.pluralSuffix) ce jour.") + } + } + } + } + + // Helper function to format date to string (you can customize the format) + private func _formattedDate(_ date: Date) -> String { + let formatter = DateFormatter() + formatter.dateStyle = .medium // Set the date style (e.g., Oct 12, 2024) + return formatter.string(from: date) + } + + +} + +// Extension to compute the total count in an NSCountedSet +extension NSCountedSet { + func totalCount() -> Int { + var total = 0 + for element in self { + total += self.count(for: element) + } + return total + } } //#Preview { diff --git a/PadelClub/Views/Planning/PlanningView.swift b/PadelClub/Views/Planning/PlanningView.swift index f2b0348..3a6e07f 100644 --- a/PadelClub/Views/Planning/PlanningView.swift +++ b/PadelClub/Views/Planning/PlanningView.swift @@ -10,24 +10,34 @@ import SwiftUI struct PlanningView: View { @EnvironmentObject var dataStore: DataStore @Environment(Tournament.self) var tournament: Tournament - - let matches: [Match] + @State private var selectedDay: Date? @Binding var selectedScheduleDestination: ScheduleDestination? + @State private var filterOption: PlanningFilterOption = .byDefault + @State private var showFinishedMatches: Bool = false + + let allMatches: [Match] + + init(matches: [Match], selectedScheduleDestination: Binding) { + self.allMatches = matches + _selectedScheduleDestination = selectedScheduleDestination + } + var matches: [Match] { + allMatches.filter({ showFinishedMatches || $0.endDate == nil }) + } + var timeSlots: [Date:[Match]] { Dictionary(grouping: matches) { $0.startDate ?? .distantFuture } } - var days: [Date] { + func days(timeSlots: [Date:[Match]]) -> [Date] { Set(timeSlots.keys.map { $0.startOfDay }).sorted() } - var keys: [Date] { + func keys(timeSlots: [Date:[Match]]) -> [Date] { timeSlots.keys.sorted() } - @State private var filterOption: PlanningFilterOption = .byDefault - enum PlanningFilterOption: Int, CaseIterable, Identifiable { var id: Int { self.rawValue } @@ -39,121 +49,196 @@ struct PlanningView: View { case .byCourt: return "Par terrain" case .byDefault: - return "Par défaut" + return "Par ordre des matchs" } } } - init(matches: [Match], selectedScheduleDestination: Binding) { - self.matches = matches - _selectedScheduleDestination = selectedScheduleDestination + private func _computedTitle(days: [Date]) -> String { + if let selectedDay { + return selectedDay.formatted(.dateTime.day().weekday().month()) + } else { + if days.count > 1 { + return "Tous les jours" + } else { + return "Horaires" + } + } } var body: some View { - List { - _bySlotView() - } - .toolbar(content: { - ToolbarItem(placement: .topBarTrailing) { - Menu { - Picker(selection: $filterOption) { - ForEach(PlanningFilterOption.allCases) { - Text($0.localizedPlanningLabel()).tag($0) + let timeSlots = self.timeSlots + let keys = self.keys(timeSlots: timeSlots) + let days = self.days(timeSlots: timeSlots) + let matches = matches + BySlotView(days: days, keys: keys, timeSlots: timeSlots, matches: matches, selectedDay: selectedDay, filterOption: filterOption, showFinishedMatches: showFinishedMatches) + .navigationTitle(Text(_computedTitle(days: days))) + .toolbar(content: { + if days.count > 1 { + ToolbarTitleMenu { + Picker(selection: $selectedDay) { + Text("Tous les jours").tag(nil as Date?) + ForEach(days, id: \.self) { day in + if day.monthYearFormatted == Date.distantFuture.monthYearFormatted { + Text("Sans horaire").tag(day as Date?) + } else { + Text(day.formatted(.dateTime.day().weekday().month())).tag(day as Date?) + } + } + } label: { + Text("Jour") + } + .pickerStyle(.automatic) + } + } + + ToolbarItemGroup(placement: .topBarTrailing) { + Menu { + Picker(selection: $showFinishedMatches) { + Text("Afficher tous les matchs").tag(true) + Text("Masquer les matchs terminés").tag(false) + } label: { + Text("Option de filtrage") } + .labelsHidden() + .pickerStyle(.inline) } label: { - Text("Option de filtrage") + Label("Filtrer", systemImage: "clock.badge.checkmark") + .symbolVariant(showFinishedMatches ? .fill : .none) } - .labelsHidden() - .pickerStyle(.inline) - } label: { - Label("Filtrer", systemImage: "line.3.horizontal.decrease.circle") - .symbolVariant(filterOption == .byCourt ? .fill : .none) + Menu { + Picker(selection: $filterOption) { + ForEach(PlanningFilterOption.allCases) { + Text($0.localizedPlanningLabel()).tag($0) + } + } label: { + Text("Option de triage") + } + .labelsHidden() + .pickerStyle(.inline) + } label: { + Label("Trier", systemImage: "line.3.horizontal.decrease.circle") + .symbolVariant(filterOption == .byCourt ? .fill : .none) + } + } - } - }) - .overlay { - if matches.allSatisfy({ $0.startDate == nil }) { - ContentUnavailableView { - Label("Aucun horaire défini", systemImage: "clock.badge.questionmark") - } description: { - Text("Vous n'avez pas encore défini d'horaire pour les différentes phases du tournoi") - } actions: { - RowButtonView("Horaire intelligent") { - selectedScheduleDestination = nil + }) + .overlay { + if matches.allSatisfy({ $0.startDate == nil }) { + ContentUnavailableView { + Label("Aucun horaire défini", systemImage: "clock.badge.questionmark") + } description: { + Text("Vous n'avez pas encore défini d'horaire pour les différentes phases du tournoi") + } actions: { + RowButtonView("Horaire intelligent") { + selectedScheduleDestination = nil + } } } } - } } - @ViewBuilder - func _bySlotView() -> some View { - if matches.allSatisfy({ $0.startDate == nil }) == false { - ForEach(days, id: \.self) { day in - Section { - ForEach(keys.filter({ $0.dayInt == day.dayInt }), id: \.self) { key in - if let _matches = timeSlots[key] { - DisclosureGroup { - ForEach(_matches.sorted(by: filterOption == .byDefault ? \.computedOrder : \.courtIndexForSorting)) { match in - NavigationLink { - MatchDetailView(match: match, matchViewStyle: .sectionedStandardStyle) - } label: { - LabeledContent { - if let courtName = match.courtName() { - Text(courtName) - } - } label: { - if let groupStage = match.groupStageObject { - Text(groupStage.groupStageTitle(.title)) - } else if let round = match.roundObject { - Text(round.roundTitle()) + struct BySlotView: View { + @Environment(Tournament.self) var tournament: Tournament + let days: [Date] + let keys: [Date] + let timeSlots: [Date:[Match]] + let matches: [Match] + let selectedDay: Date? + let filterOption: PlanningFilterOption + let showFinishedMatches: Bool + + var body: some View { + List { + if matches.allSatisfy({ $0.startDate == nil }) == false { + ForEach(days.filter({ selectedDay == nil || selectedDay == $0 }), id: \.self) { day in + Section { + ForEach(keys.filter({ $0.dayInt == day.dayInt }), id: \.self) { key in + if let _matches = timeSlots[key]?.sorted(by: filterOption == .byDefault ? \.computedOrder : \.courtIndexForSorting) { + DisclosureGroup { + ForEach(_matches) { match in + NavigationLink { + MatchDetailView(match: match) + .matchViewStyle(.sectionedStandardStyle) + + } label: { + LabeledContent { + if let courtName = match.courtName() { + Text(courtName) + } + } label: { + if let groupStage = match.groupStageObject { + Text(groupStage.groupStageTitle(.title)) + } else if let round = match.roundObject { + Text(round.roundTitle()) + } + Text(match.matchTitle()) + } } - Text(match.matchTitle()) } + } label: { + _timeSlotView(key: key, matches: _matches) } } - } label: { - _timeSlotView(key: key, matches: _matches) + } + } header: { + HStack { + if day.monthYearFormatted == Date.distantFuture.monthYearFormatted { + Text("Sans horaire") + } else { + Text(day.formatted(.dateTime.day().weekday().month())) + } + Spacer() + let count = _matchesCount(inDayInt: day.dayInt, timeSlots: timeSlots) + if showFinishedMatches { + Text(self._formattedMatchCount(count)) + } else { + Text(self._formattedMatchCount(count) + " restant\(count.pluralSuffix)") + } + } + } footer: { + if day.monthYearFormatted == Date.distantFuture.monthYearFormatted { + Text("Il s'agit des matchs qui n'ont pas réussi à être placé par Padel Club. Peut-être à cause de créneaux indisponibles, d'autres tournois ou des réglages.") } } - } - } header: { - HStack { - Text(day.formatted(.dateTime.day().weekday().month())) - Spacer() - let count = _matchesCount(inDayInt: day.dayInt) - Text(self._formattedMatchCount(count)) + .headerProminence(.increased) } } - .headerProminence(.increased) } } - } - - private func _matchesCount(inDayInt dayInt: Int) -> Int { - timeSlots.filter { $0.key.dayInt == dayInt }.flatMap({ $0.value }).count - } - private func _timeSlotView(key: Date, matches: [Match]) -> some View { - LabeledContent { - Text(self._formattedMatchCount(matches.count)) - } label: { - Text(key.formatted(date: .omitted, time: .shortened)).font(.title).fontWeight(.semibold) - let names = matches.sorted(by: \.computedOrder) - .compactMap({ $0.roundTitle() }) - .reduce(into: [String]()) { uniqueNames, name in - if !uniqueNames.contains(name) { - uniqueNames.append(name) - } + private func _matchesCount(inDayInt dayInt: Int, timeSlots: [Date:[Match]]) -> Int { + timeSlots.filter { $0.key.dayInt == dayInt }.flatMap({ $0.value }).count + } + + private func _timeSlotView(key: Date, matches: [Match]) -> some View { + LabeledContent { + Text(self._formattedMatchCount(matches.count)) + } label: { + if key.monthYearFormatted == Date.distantFuture.monthYearFormatted { + Text("Aucun horaire") + } else { + Text(key.formatted(date: .omitted, time: .shortened)).font(.title).fontWeight(.semibold) + } + if matches.count <= tournament.courtCount { + let names = matches.sorted(by: \.computedOrder) + .compactMap({ $0.roundTitle() }) + .reduce(into: [String]()) { uniqueNames, name in + if !uniqueNames.contains(name) { + uniqueNames.append(name) + } + } + Text(names.joined(separator: ", ")).lineLimit(1).truncationMode(.tail) + } else { + Text(matches.count.formatted().appending(" matchs")) } - Text(names.joined(separator: ", ")) + } + } + + fileprivate func _formattedMatchCount(_ count: Int) -> String { + return "\(count.formatted()) match\(count.pluralSuffix)" } } - - fileprivate func _formattedMatchCount(_ count: Int) -> String { - return "\(count.formatted()) match\(count.pluralSuffix)" - } - } //#Preview { diff --git a/PadelClub/Views/Planning/RoundScheduleEditorView.swift b/PadelClub/Views/Planning/RoundScheduleEditorView.swift index b2ad186..679b306 100644 --- a/PadelClub/Views/Planning/RoundScheduleEditorView.swift +++ b/PadelClub/Views/Planning/RoundScheduleEditorView.swift @@ -14,7 +14,8 @@ struct RoundScheduleEditorView: View { var round: Round var tournament: Tournament @State private var startDate: Date - + @State private var currentDate: Date? + var tournamentStore: TournamentStore { return self.tournament.tournamentStore } @@ -23,6 +24,7 @@ struct RoundScheduleEditorView: View { self.round = round self.tournament = tournament self._startDate = State(wrappedValue: round.startDate ?? round.playedMatches().first?.startDate ?? tournament.startDate) + self._currentDate = State(wrappedValue: round.startDate) } var body: some View { @@ -32,9 +34,12 @@ struct RoundScheduleEditorView: View { await _updateSchedule() } - DatePickingView(title: "Horaire minimum", startDate: $startDate, currentDate: $round.startDate, duration: round.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { + DatePickingView(title: "Horaire minimum", startDate: $startDate, currentDate: $currentDate, duration: round.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) { await _updateSchedule() } + .onChange(of: currentDate) { + round.startDate = currentDate + } ForEach(round.playedMatches()) { match in MatchScheduleEditorView(match: match, tournament: tournament) diff --git a/PadelClub/Views/Player/Components/EditablePlayerView.swift b/PadelClub/Views/Player/Components/EditablePlayerView.swift index b2955fb..6569682 100644 --- a/PadelClub/Views/Player/Components/EditablePlayerView.swift +++ b/PadelClub/Views/Player/Components/EditablePlayerView.swift @@ -78,6 +78,13 @@ struct EditablePlayerView: View { Logger.error(error) } } + .onChange(of: player.licenceId) { + do { + try self.tournamentStore.playerRegistrations.addOrUpdate(instance: player) + } catch { + Logger.error(error) + } + } .onChange(of: player.hasArrived) { do { try self.tournamentStore.playerRegistrations.addOrUpdate(instance: player) @@ -137,7 +144,7 @@ struct EditablePlayerView: View { Button { player.validateLicenceId(licenseYearValidity) } label: { - Text("Valider la licence \(licenseYearValidity)") + Text("Valider la licence \(String(licenseYearValidity))") } } } diff --git a/PadelClub/Views/Player/PlayerDetailView.swift b/PadelClub/Views/Player/PlayerDetailView.swift index 982f11e..69c153a 100644 --- a/PadelClub/Views/Player/PlayerDetailView.swift +++ b/PadelClub/Views/Player/PlayerDetailView.swift @@ -34,33 +34,33 @@ struct PlayerDetailView: View { Form { Section { Toggle("Joueur sur place", isOn: $player.hasArrived) - + LabeledContent { TextField("Nom", text: $player.lastName) .keyboardType(.alphabet) .multilineTextAlignment(.trailing) .frame(maxWidth: .infinity) .onSubmit(of: .text) { - player.lastName = player.lastName.trimmedMultiline + player.lastName = player.lastName.prefixTrimmed(50) _save() } } label: { Text("Nom") } - + LabeledContent { TextField("Prénom", text: $player.firstName) .keyboardType(.alphabet) .multilineTextAlignment(.trailing) .frame(maxWidth: .infinity) .onSubmit(of: .text) { - player.firstName = player.firstName.trimmedMultiline + player.firstName = player.firstName.prefixTrimmed(50) _save() } } label: { Text("Prénom") } - + PlayerSexPickerView(player: player) if let birthdate = player.birthdate { @@ -109,61 +109,112 @@ struct PlayerDetailView: View { Text("Calculé en fonction du sexe") } } - + Section { LabeledContent { TextField("Licence", text: $licenceId) + .focused($focusedField, equals: ._licenceId) .keyboardType(.alphabet) + .textContentType(nil) .multilineTextAlignment(.trailing) .autocorrectionDisabled() .frame(maxWidth: .infinity) .onSubmit(of: .text) { - player.licenceId = licenceId + player.licenceId = licenceId.prefixTrimmed(50) _save() } } label: { - Text("Licence") + Menu { + CopyPasteButtonView(pasteValue: player.licenceId) + PasteButtonView(text: $licenceId) + .onChange(of: licenceId) { + player.licenceId = licenceId.prefixTrimmed(50) + _save() + } + } label: { + Text("Licence") + } } - } footer: { - CopyPasteButtonView(pasteValue: player.licenceId?.strippedLicense) - } - - Section { + LabeledContent { TextField("Téléphone", text: $phoneNumber) + .focused($focusedField, equals: ._phoneNumber) .keyboardType(.namePhonePad) + .textContentType(nil) .multilineTextAlignment(.trailing) .autocorrectionDisabled() .frame(maxWidth: .infinity) .onSubmit(of: .text) { - player.phoneNumber = phoneNumber + player.phoneNumber = phoneNumber.prefixTrimmed(50) _save() } } label: { - Text("Téléphone") + Menu { + CopyPasteButtonView(pasteValue: player.phoneNumber) + PasteButtonView(text: $phoneNumber) + .onChange(of: phoneNumber) { + player.phoneNumber = phoneNumber.prefixTrimmed(50) + _save() + } + } label: { + Text("Téléphone") + } } - } footer: { - CopyPasteButtonView(pasteValue: player.phoneNumber) - } - - Section { + LabeledContent { TextField("Email", text: $email) + .focused($focusedField, equals: ._email) .keyboardType(.emailAddress) + .textContentType(nil) .multilineTextAlignment(.trailing) .autocorrectionDisabled() .frame(maxWidth: .infinity) .onSubmit(of: .text) { - player.email = email + player.email = email.prefixTrimmed(50) _save() } } label: { - Text("Email") + Menu { + CopyPasteButtonView(pasteValue: player.email) + PasteButtonView(text: $email) + .onChange(of: email) { + player.email = email.prefixTrimmed(50) + _save() + } + } label: { + Text("Email") + } + } + } + + Section { + if let number = player.phoneNumber?.replacingOccurrences(of: " ", with: "") { + if let url = URL(string: "tel:\(number)") { + Link(destination: url) { + Label("Appeler", systemImage: "phone") + } + } + if let url = URL(string: "sms:\(number)") { + Link(destination: url) { + Label("Message", systemImage: "message") + } + } + } + + if let mail = player.email, let mailURL = URL(string: "mail:\(mail)") { + Link(destination: mailURL) { + Label("Mail", systemImage: "mail") + } + } + } + + Section { + NavigationLink { + PlayerStatisticView(player: player) + } label: { + Text("Statistiques de participations") } - } footer: { - CopyPasteButtonView(pasteValue: player.email) } - } .toolbar { ToolbarItem(placement: .topBarTrailing) { diff --git a/PadelClub/Views/Player/PlayerStatisticView.swift b/PadelClub/Views/Player/PlayerStatisticView.swift new file mode 100644 index 0000000..5a2faf3 --- /dev/null +++ b/PadelClub/Views/Player/PlayerStatisticView.swift @@ -0,0 +1,43 @@ +// +// PlayerStatisticView.swift +// PadelClub +// +// Created by razmig on 06/11/2024. +// + +import SwiftUI +import LeStorage + +struct PlayerStatisticView: View { + let player: PlayerRegistration + + let teams: [TeamRegistration] + + init(player: PlayerRegistration) { + self.player = player + self.teams = DataStore.shared.tournaments.filter { $0.isDeleted == false }.flatMap({ $0.unsortedTeams() }).filter({ $0.includes(player: player) + }) + } + + + var body: some View { + List { + Section { + LabeledContent { + Text(teams.count.formatted()) + } label: { + Text("Participations") + } + LabeledContent { + Text(teams.filter({ $0.walkOut }).count.formatted()) + } label: { + Text("Forfaits") + } + } + } + .headerProminence(.increased) + .navigationTitle("Statistiques") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + } +} diff --git a/PadelClub/Views/Round/DrawLogsView.swift b/PadelClub/Views/Round/DrawLogsView.swift new file mode 100644 index 0000000..6459955 --- /dev/null +++ b/PadelClub/Views/Round/DrawLogsView.swift @@ -0,0 +1,69 @@ +// +// DrawLogsView.swift +// PadelClub +// +// Created by razmig on 22/10/2024. +// + +import SwiftUI +import LeStorage + +struct DrawLogsView: View { + @Environment(Tournament.self) var tournament + + var drawLogs: [DrawLog] { + tournament.drawLogs().reversed() + } + + var body: some View { + List { + ForEach(drawLogs) { drawLog in + HStack { + VStack(alignment: .leading) { + Text(drawLog.localizedDrawSeedLabel()) + Text(drawLog.drawDate.localizedDate()) + .font(.footnote) + .foregroundStyle(.secondary) + } + Spacer() + VStack(alignment: .trailing) { + Text(drawLog.positionLabel()).lineLimit(1).truncationMode(.middle) + Text(drawLog.localizedDrawBranch()) + .font(.footnote) + .foregroundStyle(.secondary) + } + } + } + } + .overlay(content: { + if drawLogs.isEmpty { + ContentUnavailableView("Aucun tirage", systemImage: "dice", description: Text("Aucun tirage au sort n'a été effectué.")) + } + }) + .toolbar(content: { + ToolbarItem(placement: .topBarTrailing) { + Menu { + ShareLink(item: tournament.exportedDrawLogs()) { + Label("Partager les tirages", systemImage: "square.and.arrow.up") + .labelStyle(.titleAndIcon) + } + + Divider() + + Button("Tout effacer", role: .destructive) { + do { + try tournament.tournamentStore.drawLogs.deleteAll() + } catch { + Logger.error(error) + } + } + } label: { + LabelOptions() + } + } + }) + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .navigationTitle("Journal des tirages") + } +} diff --git a/PadelClub/Views/Round/LoserRoundSettingsView.swift b/PadelClub/Views/Round/LoserRoundSettingsView.swift index 3b4706a..f39455f 100644 --- a/PadelClub/Views/Round/LoserRoundSettingsView.swift +++ b/PadelClub/Views/Round/LoserRoundSettingsView.swift @@ -62,7 +62,7 @@ struct LoserRoundSettingsView: View { RowButtonView("Synchroniser les noms des matchs") { let allRoundMatches = upperBracketRound.loserRounds.flatMap({ $0.allMatches }) - allRoundMatches.forEach({ $0.name = $0.roundTitle() }) + allRoundMatches.forEach({ $0.setMatchName($0.roundTitle()) }) do { try self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: allRoundMatches) } catch { @@ -80,15 +80,7 @@ struct LoserRoundSettingsView: View { Section { RowButtonView("Créer les matchs de classements", role: .destructive) { - upperBracketRound.round.buildLoserBracket() - upperBracketRound.round.disabledMatches().forEach { match in - match.disableMatch() - } - do { - try self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: upperBracketRound.round.allLoserRoundMatches()) - } catch { - Logger.error(error) - } + await _addLoserBracketMatches() } } .disabled(upperBracketRound.round.loserRounds().isEmpty == false) @@ -147,6 +139,17 @@ struct LoserRoundSettingsView: View { Text(" Modifier quand même ?").foregroundStyle(.red) } + private func _addLoserBracketMatches() async { + upperBracketRound.round.buildLoserBracket() + upperBracketRound.round.disabledMatches().forEach { match in + match._toggleLoserMatchDisableState(true) + } + do { + try self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: upperBracketRound.round.allLoserRoundMatches()) + } catch { + Logger.error(error) + } + } } //#Preview { diff --git a/PadelClub/Views/Round/LoserRoundView.swift b/PadelClub/Views/Round/LoserRoundView.swift index eb7b2fa..fba3563 100644 --- a/PadelClub/Views/Round/LoserRoundView.swift +++ b/PadelClub/Views/Round/LoserRoundView.swift @@ -40,7 +40,8 @@ struct LoserRoundView: View { if matches.isEmpty == false { Section { ForEach(matches) { match in - MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle) + MatchRowView(match: match) + .matchViewStyle(.sectionedStandardStyle) .overlay { if match.disabled && isEditingTournamentSeed.wrappedValue == true { Image(systemName: "xmark") @@ -70,13 +71,13 @@ struct LoserRoundView: View { if let seedInterval = previousRound.seedInterval(initialMode: isEditingTournamentSeed.wrappedValue == true) { Text(seedInterval.localizedLabel()) } else { - Text("no previous round") + Text("seedInterval is missing (previous round)") } } else if let parentRound = loserRound.parentRound { if let seedInterval = parentRound.seedInterval(initialMode: isEditingTournamentSeed.wrappedValue == true) { Text(seedInterval.localizedLabel()) } else { - Text("no parent round") + Text("seedInterval is missing (parent round)") } } } @@ -103,13 +104,7 @@ struct LoserRoundView: View { isEditingTournamentSeed.wrappedValue.toggle() if isEditingTournamentSeed.wrappedValue == false { - let allRoundMatches = loserBracket.allMatches - allRoundMatches.forEach({ $0.name = $0.roundTitle() }) - do { - try self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: allRoundMatches) - } catch { - Logger.error(error) - } + _refreshNames() } } } @@ -131,4 +126,17 @@ struct LoserRoundView: View { } } } + + private func _refreshNames() { + DispatchQueue.global(qos: .background).async { + + let allRoundMatches = loserBracket.allMatches.filter({ $0.name == nil }) + allRoundMatches.forEach({ $0.setMatchName($0.roundTitle()) }) + do { + try self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: allRoundMatches) + } catch { + Logger.error(error) + } + } + } } diff --git a/PadelClub/Views/Round/PreviewBracketPositionView.swift b/PadelClub/Views/Round/PreviewBracketPositionView.swift new file mode 100644 index 0000000..f906247 --- /dev/null +++ b/PadelClub/Views/Round/PreviewBracketPositionView.swift @@ -0,0 +1,107 @@ +// +// PreviewBracketPositionView.swift +// PadelClub +// +// Created by razmig on 23/10/2024. +// + +import SwiftUI + +struct PreviewBracketPositionView: View { + let seeds: [TeamRegistration] + let drawLogs: [DrawLog] + + @State private var filterOption: PreviewBracketPositionFilterOption = .difference + + enum PreviewBracketPositionFilterOption: Int, Identifiable, CaseIterable { + var id: Int { self.rawValue } + case all + case difference + case summon + + func isDisplayable(_ team: TeamRegistration, drawLog: DrawLog?) -> Bool { + switch self { + case .all: + true + case .difference: + team.isDifferentPosition(drawLog?.computedBracketPosition()) == true + case .summon: + team.callDate != drawLog?.drawMatch()?.startDate + } + } + } + + var body: some View { + List { + Section { + ForEach(seeds.indices, id: \.self) { seedIndex in + let seed = seeds[seedIndex] + let drawLog = drawLogs.first(where: { $0.drawSeed == seedIndex }) + if filterOption.isDisplayable(seed, drawLog: drawLog) { + HStack { + VStack(alignment: .leading) { + Text("Tête de série #\(seedIndex + 1)").font(.caption) + TeamRowView.TeamView(team: seed) + TeamRowView.TeamCallDateView(team: seed) + } + Spacer() + if let drawLog { + VStack(alignment: .trailing) { + Text(drawLog.roundLabel()).lineLimit(1).truncationMode(.middle).font(.caption) + Text(drawLog.matchLabel()).lineLimit(1).truncationMode(.middle) + Text(drawLog.localizedDrawBranch()) + if let expectedDate = drawLog.drawMatch()?.startDate { + Text(expectedDate.localizedDate()) + .font(.caption) + } else { + Text("Aucun horaire") + .font(.caption) + } + } + } + } + .listRowView(isActive: true, color: seed.isDifferentPosition(drawLog?.computedBracketPosition()) ? .logoRed : .green, hideColorVariation: true) + } + } + } header: { + Picker(selection: $filterOption) { + Text("Tous").tag(PreviewBracketPositionFilterOption.all) + Text("Changements").tag(PreviewBracketPositionFilterOption.difference) + Text("Convoc.").tag(PreviewBracketPositionFilterOption.summon) + } label: { + Text("Filter") + } + .labelsHidden() + .pickerStyle(.segmented) + .textCase(nil) + } + } + .overlay(content: { + if seeds.isEmpty { + ContentUnavailableView("Aucune équipe", systemImage: "person.2.slash", description: Text("Aucun tête de série dans le tournoi.")) + } else if filterOption == .difference, noSeedHasDifferentPlace() { + ContentUnavailableView("Aucun changement", systemImage: "dice", description: Text("Aucun changement dans le tableau.")) + } else if filterOption == .summon, noSeedHasDifferentSummon() { + ContentUnavailableView("Aucun changement", systemImage: "dice", description: Text("Aucun changement dans le tableau.")) + } + }) + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .navigationTitle("Aperçu du tableau tiré") + + } + + func noSeedHasDifferentPlace() -> Bool { + seeds.enumerated().allSatisfy({ seedIndex, seed in + let drawLog = drawLogs.first(where: { $0.drawSeed == seedIndex }) + return seed.isDifferentPosition(drawLog?.computedBracketPosition()) == false + }) + } + + func noSeedHasDifferentSummon() -> Bool { + seeds.enumerated().allSatisfy({ seedIndex, seed in + let drawLog = drawLogs.first(where: { $0.drawSeed == seedIndex }) + return seed.callDate == drawLog?.drawMatch()?.startDate + }) + } +} diff --git a/PadelClub/Views/Round/RoundSettingsView.swift b/PadelClub/Views/Round/RoundSettingsView.swift index f0c23ab..9bac2be 100644 --- a/PadelClub/Views/Round/RoundSettingsView.swift +++ b/PadelClub/Views/Round/RoundSettingsView.swift @@ -65,7 +65,33 @@ struct RoundSettingsView: View { Text("Suite à un changement dans votre liste d'inscrits, veuillez vérifier l'intégrité de votre tableau et valider que tout est ok.") } } - + + let previewAvailable = tournament.rounds().flatMap({ $0.seeds() }).count < tournament.seedsCount() && tournament.lastDrawnDate() != nil && tournament.seedSpotsLeft() + Section { + NavigationLink { + DrawLogsView() + .environment(tournament) + } label: { + Text("Gestionnaire des tirages au sort") + } + + if previewAvailable { + + NavigationLink { + PreviewBracketPositionView(seeds: tournament.seeds(), drawLogs: tournament.drawLogs().filter({ $0.drawType == .seed })) + } label: { + Text("Aperçu du repositionnement") + } + RowButtonView("Replacer toutes les têtes de série", role: .destructive) { + await tournament.updateSeedsBracketPosition() + } + } + } footer: { + if previewAvailable { + Text("Vous avez une ou plusieurs places libres dans votre tableau. Pour respecter le tirage au sort effectué, vous pouvez décaler les têtes de séries.") + } + } + // Section { // RowButtonView("Enabled", role: .destructive) { // let allMatches = tournament._allMatchesIncludingDisabled() @@ -85,91 +111,65 @@ struct RoundSettingsView: View { Section { let roundIndex = tournament.rounds().count RowButtonView("Ajouter " + RoundRule.roundName(fromRoundIndex: roundIndex), role: .destructive) { - let round = Round(tournament: tournament.id, index: roundIndex, matchFormat: tournament.matchFormat) - let matchCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex) - let matchStartIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex) - let nextRound = round.nextRound() - var currentIndex = 0 - let matches = (0.. TournamentRoundShareContent { + TournamentRoundShareContent(tournament: tournament) + } + + private func _removeAllSeeds() async { - tournament.unsortedTeams().forEach({ team in - team.bracketPosition = nil - }) - let ts = tournament.allRoundMatches().flatMap { match in - match.teamScores - } - - tournamentStore.teamScores.delete(contentOfs: ts) - tournamentStore.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams()) - tournament.allRounds().forEach({ round in - round.enableRound() - }) + await tournament.removeAllSeeds() self.isEditingTournamentSeed.wrappedValue = true } + + private func _addNewRound(_ roundIndex: Int) async { + await tournament.addNewRound(roundIndex) + } + + private func _removeRound(_ lastRound: Round) async { + do { + let teams = lastRound.seeds() + teams.forEach { team in + team.resetBracketPosition() + } + try tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams) + try tournamentStore.rounds.delete(instance: lastRound) + } catch { + Logger.error(error) + } + } } //#Preview { diff --git a/PadelClub/Views/Round/RoundView.swift b/PadelClub/Views/Round/RoundView.swift index 540b94a..23bf3d5 100644 --- a/PadelClub/Views/Round/RoundView.swift +++ b/PadelClub/Views/Round/RoundView.swift @@ -15,9 +15,10 @@ struct RoundView: View { @Environment(Tournament.self) var tournament: Tournament @EnvironmentObject var dataStore: DataStore @Environment(NavigationViewModel.self) private var navigation: NavigationViewModel - + @State private var selectedSeedGroup: SeedInterval? @State private var showPrintScreen: Bool = false + @State private var hideNames: Bool = true var upperRound: UpperRound @@ -37,14 +38,14 @@ struct RoundView: View { let displayableMatches: [Match] = self.upperRound.round.playedMatches() return displayableMatches.filter { match in match.teamScores.count == 1 - } + }.filter({ $0.isValidSpot() }) } private var seedSpaceLeft: [Match] { let displayableMatches: [Match] = self.upperRound.round.playedMatches() return displayableMatches.filter { match in match.teamScores.count == 0 - } + }.filter({ $0.isValidSpot() }) } private var availableSeedGroup: SeedInterval? { @@ -59,7 +60,7 @@ struct RoundView: View { } } )} - + var body: some View { List { let displayableMatches = upperRound.round.playedMatches().sorted(by: \.index) @@ -74,13 +75,14 @@ struct RoundView: View { let bracketTip = BracketEditTip(nextRoundName: upperRound.round.nextRound()?.roundTitle()) TipView(bracketTip).tipStyle(tint: .green, asSection: true) - if upperRound.round.hasStarted() == false { + let leftToPlay = (RoundRule.numberOfMatches(forRoundIndex: upperRound.round.index) - disabledMatchesCount) + + if upperRound.round.hasStarted() == false, leftToPlay >= 0 { Section { - let leftToPlay = (RoundRule.numberOfMatches(forRoundIndex: upperRound.round.index) - disabledMatchesCount) LabeledContent { Text(leftToPlay.formatted()) } label: { - Text("Match\(leftToPlay.pluralSuffix) à jouer \(upperRound.title)") + Text("Match\(leftToPlay.pluralSuffix) à jouer en \(upperRound.title)") } } footer: { Text("\(disabledMatchesCount) match\(disabledMatchesCount.pluralSuffix) désactivé\(disabledMatchesCount.pluralSuffix) automatiquement") @@ -95,7 +97,7 @@ struct RoundView: View { showPrintScreen = true } .tipStyle(tint: .master, asSection: true) - + if upperRound.round.index > 0 { let correspondingLoserRoundTitle = upperRound.round.correspondingLoserRoundTitle() Section { @@ -107,7 +109,11 @@ struct RoundView: View { LabeledContent { let status = upperRound.status() if status.0 == status.1 { - Image(systemName: "checkmark").foregroundStyle(.green) + if status.0 == 0 { + Text("aucun match") + } else { + Image(systemName: "checkmark").foregroundStyle(.green) + } } else { Text("\(status.0) terminé\(status.0.pluralSuffix) sur \(status.1)") } @@ -120,15 +126,16 @@ struct RoundView: View { } } } else { + let isRoundValidForSeeding = tournament.isRoundValidForSeeding(roundIndex: upperRound.round.index) let availableSeeds = tournament.availableSeeds() let availableQualifiedTeams = tournament.availableQualifiedTeams() - + if availableSeeds.isEmpty == false, let availableSeedGroup { Section { RowButtonView("Placer \(availableSeedGroup.localizedInterval())" + ((availableSeedGroup.isFixed() == false) ? " au hasard" : "")) { Task { tournament.setSeeds(inRoundIndex: upperRound.round.index, inSeedGroup: availableSeedGroup) - _save() + _save(seeds: availableSeeds) } } } footer: { @@ -139,6 +146,12 @@ struct RoundView: View { if (availableSeedGroup.isFixed() == false) { Section { + Toggle(isOn: $hideNames) { + Text("Masquer les noms") + if hideNames { + Text("Réalise un tirage des positions.") + } + } RowButtonView("Tirage au sort \(availableSeedGroup.localizedInterval()) visuel") { self.selectedSeedGroup = availableSeedGroup } @@ -147,92 +160,53 @@ struct RoundView: View { } } } - + if availableQualifiedTeams.isEmpty == false { let qualifiedOnSeedSpot = (spaceLeft.isEmpty || tournament.seeds().isEmpty) ? true : false let availableSeedSpot : [any SpinDrawable] = qualifiedOnSeedSpot ? (seedSpaceLeft + spaceLeft).flatMap({ $0.matchSpots() }).filter({ $0.match.team( $0.teamPosition) == nil }) : spaceLeft - if availableSeedSpot.isEmpty == false { - Section { - DisclosureGroup { - ForEach(availableQualifiedTeams) { team in - NavigationLink { - - SpinDrawView(drawees: [team], segments: availableSeedSpot) { results in - Task { - results.forEach { drawResult in - if let matchSpot : MatchSpot = availableSeedSpot[drawResult.drawIndex] as? MatchSpot { - team.setSeedPosition(inSpot: matchSpot.match, slot: matchSpot.teamPosition, opposingSeeding: false) - } else if let matchSpot : Match = availableSeedSpot[drawResult.drawIndex] as? Match { - team.setSeedPosition(inSpot: matchSpot, slot: nil, opposingSeeding: true) - } + Section { + DisclosureGroup { + ForEach(availableQualifiedTeams) { team in + NavigationLink { + + SpinDrawView(drawees: [team], segments: availableSeedSpot) { results in + Task { + results.forEach { drawResult in + if let matchSpot : MatchSpot = availableSeedSpot[drawResult.drawIndex] as? MatchSpot { + team.setSeedPosition(inSpot: matchSpot.match, slot: matchSpot.teamPosition, opposingSeeding: false) + } else if let matchSpot : Match = availableSeedSpot[drawResult.drawIndex] as? Match { + team.setSeedPosition(inSpot: matchSpot, slot: nil, opposingSeeding: true) } - _save() } + + _save(seeds: [team]) } - } label: { - TeamRowView(team: team, displayCallDate: false) } + } label: { + TeamRowView(team: team, displayCallDate: false) } - } label: { - Text("Qualifié\(availableQualifiedTeams.count.pluralSuffix) à placer").badge(availableQualifiedTeams.count) + .disabled(availableSeedSpot.isEmpty || isRoundValidForSeeding == false) } - } header: { - Text("Tirage au sort visuel d'un qualifié").font(.subheadline) + } label: { + Text("Qualifié\(availableQualifiedTeams.count.pluralSuffix) à placer").badge(availableQualifiedTeams.count) + } + } header: { + Text("Tirage au sort visuel d'un qualifié").font(.subheadline) + } footer: { + if availableSeedSpot.isEmpty || isRoundValidForSeeding == false { + Text("Aucune place disponible !") + .foregroundStyle(.red) } } } if availableSeeds.isEmpty == false { - if seedSpaceLeft.isEmpty == false { - Section { - DisclosureGroup { - ForEach(availableSeeds) { team in - NavigationLink { - SpinDrawView(drawees: [team], segments: seedSpaceLeft) { results in - Task { - results.forEach { drawResult in - team.setSeedPosition(inSpot: seedSpaceLeft[drawResult.drawIndex], slot: nil, opposingSeeding: false) - } - _save() - } - } - } label: { - TeamRowView(team: team, displayCallDate: false) - } - } - } label: { - Text("Tête\(availableSeeds.count.pluralSuffix) de série à placer").badge(availableSeeds.count) - } - } header: { - Text("Tirage au sort visuel d'une tête de série").font(.subheadline) - } - } else if spaceLeft.isEmpty == false { - Section { - DisclosureGroup { - ForEach(availableSeeds) { team in - NavigationLink { - SpinDrawView(drawees: [team], segments: spaceLeft) { results in - Task { - results.forEach { drawResult in - team.setSeedPosition(inSpot: spaceLeft[drawResult.drawIndex], slot: nil, opposingSeeding: true) - } - _save() - } - } - } label: { - TeamRowView(team: team, displayCallDate: false) - } - } - } label: { - Text("Tête\(availableSeeds.count.pluralSuffix) de série à placer").badge(availableSeeds.count) - } - } header: { - Text("Tirage au sort visuel d'une tête de série").font(.subheadline) - } - } + let spots = (seedSpaceLeft.isEmpty == false) ? seedSpaceLeft : spaceLeft + let opposingSeeding = (seedSpaceLeft.isEmpty == false) ? false : true + _drawSection(availableSeeds: availableSeeds, spots: spots, opposingSeeding: opposingSeeding, isRoundValidForSeeding: isRoundValidForSeeding) } } - + if isEditingTournamentSeed.wrappedValue == true { let slideToDelete = SlideToDeleteSeedTip() TipView(slideToDelete).tipStyle(tint: .logoRed, asSection: true) @@ -241,7 +215,8 @@ struct RoundView: View { ForEach(displayableMatches) { match in let matchTitle = match.matchTitle(.short, inMatches: displayableMatches) Section { - MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle, title: matchTitle) + MatchRowView(match: match, title: matchTitle) + .matchViewStyle(.sectionedStandardStyle) } header: { HStack { Text(upperRound.round.roundTitle(.wide)) @@ -256,11 +231,11 @@ struct RoundView: View { } } - #if DEBUG +#if DEBUG Spacer() Text(match.index.formatted() + " " + match.teamScores.count.formatted()) - #endif +#endif } } footer: { if isEditingTournamentSeed.wrappedValue == true && match.followingMatch()?.disabled == true { @@ -300,17 +275,15 @@ struct RoundView: View { } .fullScreenCover(isPresented: showVisualDrawView) { if let availableSeedGroup = selectedSeedGroup { - let seeds = tournament.seeds(inSeedGroup: availableSeedGroup) - let opposingSeeding = seedSpaceLeft.isEmpty ? true : false - let availableSeedSpot = opposingSeeding ? spaceLeft : seedSpaceLeft + let seeds = _seeds(availableSeedGroup: availableSeedGroup) + let availableSeedSpot = _availableSeedSpot(availableSeedGroup: availableSeedGroup) NavigationStack { - SpinDrawView(drawees: seeds, segments: availableSeedSpot, autoMode: true) { draws in + SpinDrawView(drawees: seeds, segments: availableSeedSpot, autoMode: true, hideNames: hideNames) { draws in Task { draws.forEach { drawResult in seeds[drawResult.drawee].setSeedPosition(inSpot: availableSeedSpot[drawResult.drawIndex], slot: nil, opposingSeeding: opposingSeeding) } - - _save() + _save(seeds: seeds) } } } @@ -331,51 +304,112 @@ struct RoundView: View { } } - private func _save() { + private func _seeds(availableSeedGroup: SeedInterval) -> [TeamRegistration] { + tournament.seeds(inSeedGroup: availableSeedGroup) + } + + var opposingSeeding: Bool { + seedSpaceLeft.isEmpty ? true : false + } + + private func _availableSeedSpot(availableSeedGroup: SeedInterval) -> [Match] { + let spots = opposingSeeding ? spaceLeft : seedSpaceLeft + if availableSeedGroup == SeedInterval(first: 3, last: 4), spots.count == 6 { + var array = [Match]() + array.append(spots[1]) + array.append(spots[4]) + return array + } else { + return spots + } + } + + + private func _save(seeds: [TeamRegistration]) { do { - try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: tournament.unsortedTeams()) + try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: seeds) } catch { Logger.error(error) } - //todo should be done server side - let rounds = tournament.rounds() - rounds.forEach { round in - let matches = round.playedMatches() - matches.forEach { match in - match.name = Match.setServerTitle(upperRound: round, matchIndex: match.indexInRound(in: matches)) - } + if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty { + self.isEditingTournamentSeed.wrappedValue = false } - let loserMatches = self.upperRound.loserMatches() - loserMatches.forEach { match in - match.name = match.roundTitle() - } - - let allRoundMatches = tournament.allRoundMatches() - - do { - try tournament.tournamentStore.matches.addOrUpdate(contentOfs: allRoundMatches) - } catch { - Logger.error(error) - } - + _refreshNames() + } + + private func _save() { if tournament.availableSeeds().isEmpty && tournament.availableQualifiedTeams().isEmpty { self.isEditingTournamentSeed.wrappedValue = false } + _refreshNames() + } + + private func _refreshNames() { + DispatchQueue.global(qos: .background).async { + //todo should be done server side + let rounds = tournament.rounds() + var matchesToUpdate: [Match] = [Match]() + rounds.forEach { round in + let matches = round.playedMatches().filter({ $0.name == nil }) + matches.forEach { match in + match.setMatchName(Match.setServerTitle(upperRound: round, matchIndex: match.indexInRound(in: matches))) + } + matchesToUpdate.append(contentsOf: matches) + } + + let loserMatches = self.upperRound.loserMatches().filter({ $0.name == nil }) + loserMatches.forEach { match in + match.setMatchName(match.roundTitle()) + } + + do { + try tournament.tournamentStore.matches.addOrUpdate(contentOfs: matchesToUpdate + loserMatches) + } catch { + Logger.error(error) + } + } + } + + private func _drawSection(availableSeeds: [TeamRegistration], spots: [Match], opposingSeeding: Bool, isRoundValidForSeeding: Bool) -> some View { + Section { + DisclosureGroup { + ForEach(availableSeeds) { team in + NavigationLink { + SpinDrawView(drawees: [team], segments: spots) { results in + Task { + results.forEach { drawResult in + team.setSeedPosition(inSpot: spots[drawResult.drawIndex], slot: nil, opposingSeeding: opposingSeeding) + } + + _save(seeds: [team]) + } + } + } label: { + TeamRowView(team: team, displayCallDate: false) + } + .disabled(spots.isEmpty || isRoundValidForSeeding == false) + } + } label: { + Text("Tête\(availableSeeds.count.pluralSuffix) de série à placer").badge(availableSeeds.count) + } + } header: { + Text("Tirage au sort visuel d'une tête de série").font(.subheadline) + } footer: { + if spots.isEmpty || isRoundValidForSeeding == false { + Text("Aucune place disponible ! Ajouter une manche via les réglages du tableau.") + .foregroundStyle(.red) + } + } } } -//#Preview { -// RoundView(round: Round.mock()) -// .environment(Tournament.mock()) -//} - struct MatchSpot: SpinDrawable { let match: Match let teamPosition: TeamPosition - func segmentLabel(_ displayStyle: DisplayStyle) -> [String] { + func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] { [match.roundTitle(), matchTitle(displayStyle: displayStyle)].compactMap { $0 } } diff --git a/PadelClub/Views/Score/EditScoreView.swift b/PadelClub/Views/Score/EditScoreView.swift index c1de469..52e3225 100644 --- a/PadelClub/Views/Score/EditScoreView.swift +++ b/PadelClub/Views/Score/EditScoreView.swift @@ -12,8 +12,33 @@ struct EditScoreView: View { @EnvironmentObject var dataStore: DataStore - @ObservedObject var matchDescriptor: MatchDescriptor + @StateObject var matchDescriptor: MatchDescriptor + @State private var presentMatchFormatSelection: Bool = false + @Binding var confirmScoreEdition: Bool @Environment(\.dismiss) private var dismiss + @State private var firstTeamIsFirstScoreToEnter: Bool = true + + init(match: Match, confirmScoreEdition: Binding) { + let matchDescriptor = MatchDescriptor(match: match) + _matchDescriptor = .init(wrappedValue: matchDescriptor) + _confirmScoreEdition = confirmScoreEdition + } + + var defaultTeamIsActive: Bool { + if firstTeamIsFirstScoreToEnter { + matchDescriptor.teamOneSetupIsActive + } else { + matchDescriptor.teamTwoSetupIsActive + } + } + + var otherTeamIsActive: Bool { + if firstTeamIsFirstScoreToEnter { + matchDescriptor.teamTwoSetupIsActive + } else { + matchDescriptor.teamOneSetupIsActive + } + } func walkout(_ team: TeamPosition) { self.matchDescriptor.match?.setWalkOut(team) @@ -21,13 +46,80 @@ struct EditScoreView: View { dismiss() } + func pointRange(winner: Bool) -> Int? { + guard let match = matchDescriptor.match else { return nil } + guard let tournament = match.currentTournament() else { + return nil + } + + let teamsCount = tournament.teamCount + guard let round = match.roundObject else { return nil } + + guard let seedInterval = round.seedInterval(), match.index == 0 else { + return nil + } + + return winner ? tournament.tournamentLevel.points(for: seedInterval.first - 1, count: teamsCount) : tournament.tournamentLevel.points(for: seedInterval.last - 1, count: teamsCount) + } + + func getColor(setDescriptor: SetDescriptor) -> Color { + switch (firstTeamIsFirstScoreToEnter, setDescriptor.isTeamOneSet == false) { + case (true, true), (false, false): + return matchDescriptor.colorTeamOne + default: + return matchDescriptor.colorTeamTwo + } + } + + func teamScorePositionLabel(teamPosition: TeamPosition) -> String { + switch (firstTeamIsFirstScoreToEnter, teamPosition == .one) { + case (true, true), (false, false): + return "score de gauche" + default: + return "score de droite" + } + } + var body: some View { Form { Section { - Text(matchDescriptor.teamLabelOne) + HStack { + VStack(alignment: .leading) { + Text(teamScorePositionLabel(teamPosition: .one)).font(.caption) + Text(matchDescriptor.teamLabelOne) + if matchDescriptor.hasEnded, let pointRange = pointRange(winner: matchDescriptor.winner == .one) { + Text(pointRange.formatted(.number.sign(strategy: .always())) + " pts") + .bold() + } + } + Spacer() + } + .listRowView(isActive: defaultTeamIsActive, color: matchDescriptor.colorTeamOne, hideColorVariation: false) + .contentShape(Rectangle()) + .frame(maxWidth: .infinity) + .onTapGesture { + firstTeamIsFirstScoreToEnter.toggle() + } HStack { Spacer() - Text(matchDescriptor.teamLabelTwo).multilineTextAlignment(.trailing) + VStack(alignment: .trailing) { + Text(teamScorePositionLabel(teamPosition: .two)).font(.caption) + Text(matchDescriptor.teamLabelTwo).multilineTextAlignment(.trailing) + if matchDescriptor.hasEnded, let pointRange = pointRange(winner: matchDescriptor.winner == .two) { + Text(pointRange.formatted(.number.sign(strategy: .always())) + " pts") + .bold() + } + } + } + .listRowView(isActive: otherTeamIsActive, color: matchDescriptor.colorTeamTwo, hideColorVariation: false, alignment: .trailing) + .contentShape(Rectangle()) + .frame(maxWidth: .infinity) + .onTapGesture { + firstTeamIsFirstScoreToEnter.toggle() + } + } header: { + if let roundTitle = matchDescriptor.match?.roundAndMatchTitle() { + Text(roundTitle) } } footer: { HStack { @@ -48,15 +140,13 @@ struct EditScoreView: View { } Spacer() - MatchTypeSmallSelectionView(selectedFormat: $matchDescriptor.matchFormat, format: "Format") - .onChange(of: matchDescriptor.matchFormat) { - matchDescriptor.setDescriptors.removeAll() - matchDescriptor.addNewSet() - } + FooterButtonView("Format : \(matchDescriptor.matchFormat.shortFormat)") { + presentMatchFormatSelection = true + } } } ForEach($matchDescriptor.setDescriptors) { $setDescriptor in - SetInputView(setDescriptor: $setDescriptor) + SetInputView(setDescriptor: $setDescriptor, firstTeamIsFirstScoreToEnter: firstTeamIsFirstScoreToEnter) .onChange(of: setDescriptor.hasEnded) { if setDescriptor.hasEnded { if matchDescriptor.hasEnded == false { @@ -67,6 +157,7 @@ struct EditScoreView: View { matchDescriptor.setDescriptors = Array(matchDescriptor.setDescriptors[0...index]) } } + .tint(getColor(setDescriptor: setDescriptor)) } if matchDescriptor.hasEnded { @@ -101,9 +192,19 @@ struct EditScoreView: View { } } } + .sheet(isPresented: $presentMatchFormatSelection) { + MatchFormatSelectionView(matchFormat: $matchDescriptor.matchFormat, additionalEstimationDuration: matchDescriptor.match?.currentTournament()?.additionalEstimationDuration) + .tint(.master) + } + .onChange(of: matchDescriptor.matchFormat) { + presentMatchFormatSelection = false + matchDescriptor.setDescriptors.removeAll() + matchDescriptor.addNewSet() + } } func save() { + self.confirmScoreEdition = true if let match = matchDescriptor.match { do { try match.tournamentStore.matches.addOrUpdate(instance: match) diff --git a/PadelClub/Views/Score/FollowUpMatchView.swift b/PadelClub/Views/Score/FollowUpMatchView.swift new file mode 100644 index 0000000..78cda7f --- /dev/null +++ b/PadelClub/Views/Score/FollowUpMatchView.swift @@ -0,0 +1,299 @@ +// +// FollowUpMatchView.swift +// PadelClub +// +// Created by razmig on 11/10/2024. +// + +import SwiftUI + +struct FollowUpMatchView: View { + @EnvironmentObject var dataStore: DataStore + @Environment(\.dismiss) private var dismiss + let match: Match? + let readyMatches: [Match] + let matchesLeft: [Match] + let isFree: Bool + var autoDismiss: Bool = false + + @State private var sortingMode: SortingMode? = .index + @State private var selectedCourt: Int? + @State private var checkCanPlay: Bool = false + @State private var seeAll: Bool = true + @Binding var dismissWhenPresentFollowUpMatchIsDismissed: Bool + + var matches: [Match] { + seeAll ? matchesLeft : readyMatches + } + + enum SortingMode: Int, Identifiable, CaseIterable, Selectable, Equatable { + func selectionLabel(index: Int) -> String { + localizedSortingModeLabel() + } + + func badgeValue() -> Int? { + nil + } + + func badgeImage() -> Badge? { + nil + } + + func badgeValueColor() -> Color? { + nil + } + + static func == (lhs: SortingMode, rhs: SortingMode) -> Bool { + return lhs.id == rhs.id + } + + var id: Int { self.rawValue } + case winner + case loser + case index + case restingTime + case court + + func canHaveSeeAllOption() -> Bool { + switch self { + case .index, .restingTime: + return true + case .winner, .loser, .court: + return false + } + } + + func localizedSortingModeLabel() -> String { + switch self { + case .index: + return "Ordre prévu" + case .court: + return "Terrain" + case .restingTime: + return "Temps de repos" + case .winner: + return "Gagnant" + case .loser: + return "Perdant" + } + } + } + + init(match: Match, dismissWhenPresentFollowUpMatchIsDismissed: Binding) { + _dismissWhenPresentFollowUpMatchIsDismissed = dismissWhenPresentFollowUpMatchIsDismissed + self.match = match + _selectedCourt = .init(wrappedValue: match.courtIndex) + let currentTournament = match.currentTournament() + let allMatches = currentTournament?.allMatches() ?? [] + self.matchesLeft = Tournament.matchesLeft(allMatches) + let runningMatches = Tournament.runningMatches(allMatches) + let readyMatches = Tournament.readyMatches(allMatches) + self.readyMatches = Tournament.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false) + self.isFree = currentTournament?.isFree() ?? true + } + + init(selectedCourt: Int?, allMatches: [Match], autoDismiss: Bool = true) { + _dismissWhenPresentFollowUpMatchIsDismissed = .constant(false) + _selectedCourt = .init(wrappedValue: selectedCourt) + self.match = nil + self.autoDismiss = autoDismiss + self.matchesLeft = Tournament.matchesLeft(allMatches) + let runningMatches = Tournament.runningMatches(allMatches) + let readyMatches = Tournament.readyMatches(allMatches) + self.readyMatches = Tournament.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false) + self.isFree = false + } + + + var winningTeam: TeamRegistration? { + match?.winner() + } + + var losingTeam: TeamRegistration? { + match?.loser() + } + + var sortingModeCases: [SortingMode] { + var sortingModes = [SortingMode]() + if winningTeam != nil { + sortingModes.append(.winner) + } + if losingTeam != nil { + sortingModes.append(.loser) + } + sortingModes.append(.index) + sortingModes.append(.restingTime) +// sortingModes.append(.court) + return sortingModes + } + + func contentUnavailableDescriptionLabel() -> String { + switch sortingMode! { + case .winner: + if let winningTeam { + return "Aucun match à suivre pour \(winningTeam.teamLabel())" + } else { + return "La paire gagnante n'a pas été décidé" + } + case .loser: + if let losingTeam { + return "Aucun match à suivre pour \(losingTeam.teamLabel())" + } else { + return "La paire perdante n'a pas été décidé" + } + case .index: + return "Ce tournoi n'a aucun match prêt à démarrer" + case .restingTime: + return "Ce tournoi n'a aucun match prêt à démarrer" + case .court: + return "Ce tournoi n'a aucun match prêt à démarrer" + } + } + + var sortedMatches: [Match] { + switch sortingMode! { + case .index: + return matches + case .restingTime: + return matches.sorted(by: \.restingTimeForSorting) + case .court: + return matchesLeft.filter({ $0.courtIndex == selectedCourt }) + case .winner: + if let winningTeam, let followUpMatch = matchesLeft.first(where: { $0.containsTeamId(winningTeam.id) }) { + return [followUpMatch] + } else { + return [] + } + case .loser: + if let losingTeam, let followUpMatch = matchesLeft.first(where: { $0.containsTeamId(losingTeam.id) }) { + return [followUpMatch] + } else { + return [] + } + } + } + + var body: some View { + NavigationStack { + VStack(spacing: 0) { + GenericDestinationPickerView(selectedDestination: $sortingMode, destinations: sortingModeCases, nilDestinationIsValid: false) + + List { + // + // Toggle(isOn: $checkCanPlay) { + // if isFree { + // Text("Vérifier le paiement ou la présence") + // } else { + // Text("Vérifier la présence") + // } + // } + // } footer: { + // if isFree { + // Text("Masque les matchs où un ou plusieurs joueurs qui ne sont pas encore arrivé") + // } else { + // Text("Masque les matchs où un ou plusieurs joueurs n'ont pas encore réglé ou qui ne sont pas encore arrivé") + // } + + if sortedMatches.isEmpty == false { + ForEach(sortedMatches) { match in + let tournament = match.currentTournament() + Section { + MatchRowView(match: match, updatedField: selectedCourt) + .matchViewStyle(.followUpStyle) + } header: { + if let tournament { + HStack { + Text(tournament.tournamentTitle(.short)) + Spacer() + if let club = tournament.club() { + Text("@" + club.clubTitle(.short)) + } + } + } + } footer: { + HStack { + if let tournament { + Text(tournament.eventLabel()) + } + #if DEBUG + Spacer() + FooterButtonView("copier l'id") { + let pasteboard = UIPasteboard.general + pasteboard.string = match.id + } + #endif + } + } + } + } else { + ContentUnavailableView("Aucun match à venir", systemImage: "xmark.circle", description: Text(contentUnavailableDescriptionLabel())) + } + } + .navigationTitle("À suivre sur") + .toolbarBackground(.visible, for: .navigationBar) + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .bottomBar) + .toolbarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button("Retour", role: .cancel) { + if readyMatches.isEmpty && matchesLeft.isEmpty { + dismissWhenPresentFollowUpMatchIsDismissed = true + } + dismiss() + } + } + + ToolbarItem(placement: .status) { + Button { + seeAll.toggle() + } label: { + Text(seeAll ? "Masquer les matchs incomplets" : "Voir tous les matchs") + .underline() + } + .disabled(sortingMode?.canHaveSeeAllOption() == false) + } + + // ToolbarItem(placement: .principal) { + // Picker(selection: $sortingMode) { + // ForEach(sortingModeCases) { sortingMode in + // Text(sortingMode.localizedSortingModeLabel()).tag(sortingMode) + // } + // } label: { + // Text("Méthode de tri") + // } + // .labelsHidden() + // .pickerStyle(.segmented) + // } + + ToolbarItem(placement: .topBarTrailing) { + Picker(selection: $selectedCourt) { + Image(systemName: "sportscourt").tag(nil as Int?) + if let tournament = match?.currentTournament() { + ForEach(0.. { + Binding { + defaultTeamValue + } set: { value in + if firstTeamIsFirstScoreToEnter { + setDescriptor.valueTeamOne = value + } else { + setDescriptor.valueTeamTwo = value + } + } + } + + var otherTeamValue: Int? { + if firstTeamIsFirstScoreToEnter { + return setDescriptor.valueTeamTwo + } else { + return setDescriptor.valueTeamOne + } + } + + var otherTeamValueBinding: Binding { + Binding { + otherTeamValue + } set: { value in + if firstTeamIsFirstScoreToEnter { + setDescriptor.valueTeamTwo = value + } else { + setDescriptor.valueTeamOne = value + } + } + + } + + var defaultTeamTieBreakValue: Int? { + if firstTeamIsFirstScoreToEnter { + return setDescriptor.tieBreakValueTeamOne + } else { + return setDescriptor.tieBreakValueTeamTwo + } + } + + var defaultTeamTieBreakValueBinding: Binding { + Binding { + defaultTeamTieBreakValue + } set: { value in + if firstTeamIsFirstScoreToEnter { + setDescriptor.tieBreakValueTeamOne = value + } else { + setDescriptor.tieBreakValueTeamTwo = value + } + } + } + + var otherTeamTieBreakValue: Int? { + if firstTeamIsFirstScoreToEnter { + return setDescriptor.tieBreakValueTeamTwo + } else { + return setDescriptor.tieBreakValueTeamOne + } + } + + var otherTeamTieBreakValueBinding: Binding { + Binding { + otherTeamTieBreakValue + } set: { value in + if firstTeamIsFirstScoreToEnter { + setDescriptor.tieBreakValueTeamTwo = value + } else { + setDescriptor.tieBreakValueTeamOne = value + } + } + + } + private var currentValue: Binding { Binding { - if setDescriptor.valueTeamOne != nil { - return setDescriptor.valueTeamTwo + if defaultTeamValue != nil { + return otherTeamValue } else { - return setDescriptor.valueTeamOne + return defaultTeamValue } } set: { newValue, _ in - if setDescriptor.valueTeamOne != nil { - setDescriptor.valueTeamTwo = newValue + if defaultTeamValue != nil { + otherTeamValueBinding.wrappedValue = newValue } else { - setDescriptor.valueTeamOne = newValue + defaultTeamValueBinding.wrappedValue = newValue } } } private var currentTiebreakValue: Binding { Binding { - if setDescriptor.tieBreakValueTeamOne != nil { - return setDescriptor.tieBreakValueTeamTwo + if defaultTeamTieBreakValue != nil { + return otherTeamTieBreakValue } else { - return setDescriptor.tieBreakValueTeamOne + return defaultTeamTieBreakValue } } set: { newValue, _ in - if let tieBreakValueTeamOne = setDescriptor.tieBreakValueTeamOne, let tieBreakValueTeamTwo = setDescriptor.tieBreakValueTeamTwo { + if let tieBreakValueTeamOne = defaultTeamTieBreakValue, let tieBreakValueTeamTwo = otherTeamTieBreakValue { if tieBreakValueTeamOne < tieBreakValueTeamTwo && tieBreakValueTeamTwo > 6 { - setDescriptor.tieBreakValueTeamOne = newValue + defaultTeamTieBreakValueBinding.wrappedValue = newValue } else if tieBreakValueTeamOne > tieBreakValueTeamTwo && tieBreakValueTeamOne > 6 { - setDescriptor.tieBreakValueTeamTwo = newValue + otherTeamTieBreakValueBinding.wrappedValue = newValue } } - else if setDescriptor.tieBreakValueTeamOne != nil { - setDescriptor.tieBreakValueTeamTwo = newValue + else if defaultTeamTieBreakValue != nil { + otherTeamTieBreakValueBinding.wrappedValue = newValue } else { - setDescriptor.tieBreakValueTeamOne = newValue + defaultTeamTieBreakValueBinding.wrappedValue = newValue } } } private var disableValues: [Int] { - if let valueTeamOne = setDescriptor.valueTeamOne { + if let valueTeamOne = defaultTeamValue { return setFormat.disableValuesForTeamTwo(with: valueTeamOne) } return [] } private var disableTieBreakValues: [Int] { - if let tieBreakValueTeamOne = setDescriptor.tieBreakValueTeamOne { + if let tieBreakValueTeamOne = defaultTeamTieBreakValue { if tieBreakValueTeamOne == 7 { return [7,6] } @@ -90,7 +173,7 @@ struct SetInputView: View { } func possibleValues() -> [Int] { - if let valueTeamOne = setDescriptor.valueTeamOne { + if let valueTeamOne = defaultTeamValue { if valueTeamOne == 7 && setFormat == .six { return [6,5] } @@ -102,7 +185,7 @@ struct SetInputView: View { } func tieBreakPossibleValues() -> [Int] { - if let tieBreakValueTeamOne = setDescriptor.tieBreakValueTeamOne, let tieBreakValueTeamTwo = setDescriptor.tieBreakValueTeamTwo { + if let tieBreakValueTeamOne = defaultTeamTieBreakValue, let tieBreakValueTeamTwo = otherTeamTieBreakValue { if tieBreakValueTeamOne == 6 && tieBreakValueTeamTwo == 8 { return [] } @@ -114,7 +197,7 @@ struct SetInputView: View { } return Array(((max(tieBreakValueTeamOne, tieBreakValueTeamTwo)+2).. 10 && setFormat == .superTieBreak { - setDescriptor.valueTeamTwo = newValue - 2 + otherTeamValueBinding.wrappedValue = newValue - 2 } else if newValue > 15 && setFormat == .megaTieBreak { - setDescriptor.valueTeamTwo = newValue - 2 + otherTeamValueBinding.wrappedValue = newValue - 2 } } } - .onChange(of: setDescriptor.valueTeamTwo) { - if setDescriptor.valueTeamOne != nil && setDescriptor.valueTeamTwo != nil { + .onChange(of: otherTeamValue) { + if defaultTeamValue != nil && otherTeamValue != nil { showSetInputView = false } } - .onChange(of: setDescriptor.tieBreakValueTeamOne) { - if let newValue = setDescriptor.tieBreakValueTeamOne, setDescriptor.tieBreakValueTeamTwo == nil { + .onChange(of: defaultTeamTieBreakValue) { + if let newValue = defaultTeamTieBreakValue, otherTeamTieBreakValue == nil { if newValue > 7 { - setDescriptor.tieBreakValueTeamTwo = newValue - 2 + otherTeamTieBreakValueBinding.wrappedValue = newValue - 2 } if newValue == 6 { - setDescriptor.tieBreakValueTeamTwo = newValue + 2 + otherTeamTieBreakValueBinding.wrappedValue = newValue + 2 } if newValue <= 5 { - setDescriptor.tieBreakValueTeamTwo = 7 + otherTeamTieBreakValueBinding.wrappedValue = 7 } } - else if let newValue = setDescriptor.tieBreakValueTeamOne, let tieBreakValueTeamTwo = setDescriptor.tieBreakValueTeamTwo { + else if let newValue = defaultTeamTieBreakValue, let tieBreakValueTeamTwo = otherTeamTieBreakValue { if newValue > 6 && tieBreakValueTeamTwo < newValue { - setDescriptor.tieBreakValueTeamTwo = newValue - 2 + otherTeamTieBreakValueBinding.wrappedValue = newValue - 2 } if newValue > 6 && tieBreakValueTeamTwo > newValue { - setDescriptor.tieBreakValueTeamTwo = newValue + 2 + otherTeamTieBreakValueBinding.wrappedValue = newValue + 2 } if newValue == 6 { - setDescriptor.tieBreakValueTeamTwo = newValue + 2 + otherTeamTieBreakValueBinding.wrappedValue = newValue + 2 } if newValue <= 5 { - setDescriptor.tieBreakValueTeamTwo = 7 + otherTeamTieBreakValueBinding.wrappedValue = 7 showTieBreakInputView = false } } } - .onChange(of: setDescriptor.tieBreakValueTeamTwo) { - if let tieBreakValueTeamOne = setDescriptor.tieBreakValueTeamOne, tieBreakValueTeamOne <= 5 { + .onChange(of: otherTeamTieBreakValue) { + if let tieBreakValueTeamOne = defaultTeamTieBreakValue, tieBreakValueTeamOne <= 5 { showTieBreakInputView = false } else { - if let tieBreakValueTeamTwo = setDescriptor.tieBreakValueTeamTwo { - if tieBreakValueTeamTwo > 6 && tieBreakValueTeamTwo > setDescriptor.tieBreakValueTeamOne ?? 0 { - setDescriptor.tieBreakValueTeamOne = tieBreakValueTeamTwo - 2 + if let tieBreakValueTeamTwo = otherTeamTieBreakValue { + if tieBreakValueTeamTwo > 6 && tieBreakValueTeamTwo > defaultTeamTieBreakValue ?? 0 { + defaultTeamTieBreakValueBinding.wrappedValue = tieBreakValueTeamTwo - 2 } - if tieBreakValueTeamTwo > 4 && tieBreakValueTeamTwo < setDescriptor.tieBreakValueTeamOne ?? 0 { - setDescriptor.tieBreakValueTeamOne = tieBreakValueTeamTwo + 2 + if tieBreakValueTeamTwo > 4 && tieBreakValueTeamTwo < defaultTeamTieBreakValue ?? 0 { + defaultTeamTieBreakValueBinding.wrappedValue = tieBreakValueTeamTwo + 2 } } - if let tieBreakValueTeamTwo = setDescriptor.tieBreakValueTeamTwo, let tieBreakValueTeamOne = setDescriptor.tieBreakValueTeamOne { + if let tieBreakValueTeamTwo = otherTeamTieBreakValue, let tieBreakValueTeamOne = defaultTeamTieBreakValue { if tieBreakValueTeamTwo < 6 && tieBreakValueTeamOne == 7 { showTieBreakInputView = false } diff --git a/PadelClub/Views/Shared/MatchFormatPickerView.swift b/PadelClub/Views/Shared/MatchFormatPickerView.swift deleted file mode 100644 index 8004386..0000000 --- a/PadelClub/Views/Shared/MatchFormatPickerView.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// MatchFormatPickerView.swift -// PadelClub -// -// Created by Razmig Sarkissian on 22/03/2024. -// - -import SwiftUI - -struct MatchFormatPickerView: View { - @Environment(Tournament.self) var tournament: Tournament - let headerLabel: String - @Binding var matchFormat: MatchFormat - @State private var isExpanded: Bool = false - - var body: some View { - DisclosureGroup(isExpanded: $isExpanded) { - Picker(selection: $matchFormat) { - ForEach(MatchFormat.allCases, id: \.rawValue) { format in - Text(format.computedShortLabel).tag(format) - } - } label: { - } - .pickerStyle(.inline) - .onChange(of: matchFormat) { - isExpanded = false - } - } label: { - descriptionView - } - } - - var descriptionView: some View { - VStack(alignment: .leading) { - HStack { - Text(headerLabel).font(.footnote) - Spacer() - Text("Durée").font(.footnote) - } - HStack { - Text(matchFormat.format).font(.title).fontWeight(.semibold) - Spacer() - VStack(alignment: .trailing) { - Text("~" + matchFormat.formattedEstimatedDuration(tournament.additionalEstimationDuration)) - Text(matchFormat.formattedEstimatedBreakDuration() + " de pause").foregroundStyle(.secondary).font(.subheadline) - } - } - } - } -} - - -//#Preview { -// List { -// MatchFormatPickerView(headerLabel: "Test", matchFormat: .constant(MatchFormat.superTie)) -// } -//} diff --git a/PadelClub/Views/Shared/MatchFormatRowView.swift b/PadelClub/Views/Shared/MatchFormatRowView.swift new file mode 100644 index 0000000..b253e63 --- /dev/null +++ b/PadelClub/Views/Shared/MatchFormatRowView.swift @@ -0,0 +1,47 @@ +// +// MatchFormatRowView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 02/04/2024. +// + +import SwiftUI + +struct MatchFormatRowView: View { + let matchFormat: MatchFormat + var headerLabel: String? = nil + var hideExplanation: Bool = false + var hideDuration: Bool = false + var additionalEstimationDuration: Int? + var displayStyle: DisplayStyle = .wide + + var body: some View { + VStack(alignment: .leading) { + if let headerLabel { + HStack { + Text(headerLabel).font(.footnote) + if hideDuration == false { + Spacer() + Text("Durée").font(.footnote) + } + } + } + HStack { + Text(matchFormat.formatTitle(displayStyle)) + if hideDuration == false && displayStyle != .short { + Spacer() + Text("~" + matchFormat.formattedEstimatedDuration(additionalEstimationDuration ?? 0)) + } + } + .font(.headline) + + if hideExplanation == false { + Text(matchFormat.longLabel) + Text(matchFormat.formattedEstimatedBreakDuration()).font(.footnote) + if matchFormat.isFederal == false { + Text("Non officiel").foregroundStyle(.logoRed).font(.footnote) + } + } + } + } +} diff --git a/PadelClub/Views/Shared/MatchFormatSelectionView.swift b/PadelClub/Views/Shared/MatchFormatSelectionView.swift new file mode 100644 index 0000000..3249090 --- /dev/null +++ b/PadelClub/Views/Shared/MatchFormatSelectionView.swift @@ -0,0 +1,33 @@ +// +// MatchFormatSelectionView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 22/03/2024. +// + +import SwiftUI + +struct MatchFormatSelectionView: View { + @Environment(\.dismiss) private var dismiss + @Binding var matchFormat: MatchFormat + var additionalEstimationDuration: Int? + + var body: some View { + List { + Section { + Picker(selection: $matchFormat) { + ForEach(MatchFormat.allCases) { format in + MatchFormatRowView(matchFormat: format, additionalEstimationDuration: additionalEstimationDuration).tag(format) + } + } label: { + + } + .labelsHidden() + .pickerStyle(.inline) + } + } + .onChange(of: matchFormat) { + dismiss() + } + } +} diff --git a/PadelClub/Views/Shared/MatchTypeSelectionView.swift b/PadelClub/Views/Shared/MatchTypeSelectionView.swift index 2d3ba3c..2801067 100644 --- a/PadelClub/Views/Shared/MatchTypeSelectionView.swift +++ b/PadelClub/Views/Shared/MatchTypeSelectionView.swift @@ -9,9 +9,17 @@ import SwiftUI struct MatchTypeSelectionView: View { @Binding var selectedFormat: MatchFormat - let format: String + var format: String? + var additionalEstimationDuration: Int? + var displayStyle: DisplayStyle = .wide var body: some View { - MatchFormatPickerView(headerLabel: format, matchFormat: $selectedFormat) + NavigationLink { + MatchFormatSelectionView(matchFormat: $selectedFormat) + .navigationTitle("Choix du format") + .toolbarBackground(.visible, for: .navigationBar) + } label: { + MatchFormatRowView(matchFormat: selectedFormat, headerLabel: format, hideExplanation: true, additionalEstimationDuration: additionalEstimationDuration, displayStyle: displayStyle) + } } } diff --git a/PadelClub/Views/Shared/MatchTypeSmallSelectionView.swift b/PadelClub/Views/Shared/MatchTypeSmallSelectionView.swift deleted file mode 100644 index cc30267..0000000 --- a/PadelClub/Views/Shared/MatchTypeSmallSelectionView.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// MatchTypeSmallSelectionView.swift -// PadelClub -// -// Created by Razmig Sarkissian on 02/04/2024. -// - -import SwiftUI - -struct MatchTypeSmallSelectionView: View { - @Binding var selectedFormat: MatchFormat - let format: String - - var body: some View { - Picker(selection: $selectedFormat) { - ForEach(MatchFormat.allCases, id: \.rawValue) { matchFormat in - Text(format + " " + matchFormat.format) - .tag(matchFormat) - } - } label: { - } - } -} diff --git a/PadelClub/Views/Shared/SelectablePlayerListView.swift b/PadelClub/Views/Shared/SelectablePlayerListView.swift index 9b61575..352251d 100644 --- a/PadelClub/Views/Shared/SelectablePlayerListView.swift +++ b/PadelClub/Views/Shared/SelectablePlayerListView.swift @@ -24,23 +24,19 @@ struct SelectablePlayerListView: View { @StateObject private var searchViewModel: SearchViewModel @Environment(\.dismiss) var dismiss - var lastDataSource: String? { - dataStore.appSettings.lastDataSource - } - @State private var searchText: String = "" - var mostRecentDate: Date? { - guard let lastDataSource else { return nil } - return URL.importDateFormatter.date(from: lastDataSource) - } - - init(allowSelection: Int = 0, isPresented: Bool = true, searchField: String? = 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, tokens: [SearchToken] = [], hidePlayers: [String]? = nil, playerSelectionAction: PlayerSelectionAction? = nil, contentUnavailableAction: ContentUnavailableAction? = nil) { + + init(allowSelection: Int = 0, isPresented: Bool = true, searchField: String? = 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, tokens: [SearchToken] = [], hidePlayers: [String]? = nil, lastDataSource: Bool = false, playerSelectionAction: PlayerSelectionAction? = nil, contentUnavailableAction: ContentUnavailableAction? = nil) { self.allowSelection = allowSelection self.playerSelectionAction = playerSelectionAction self.contentUnavailableAction = contentUnavailableAction self.searchText = searchField ?? "" let searchViewModel = SearchViewModel() searchViewModel.tokens = tokens + if lastDataSource { + searchViewModel.mostRecentDate = DataStore.shared.appSettings.lastDataSourceDate() + } + searchViewModel.searchText = searchField ?? "" searchViewModel.debouncableText = searchField ?? "" searchViewModel.showFemaleInMaleAssimilation = showFemaleInMaleAssimilation @@ -59,6 +55,18 @@ struct SelectablePlayerListView: View { _searchViewModel = StateObject(wrappedValue: searchViewModel) } + var enableSourceCheck: Binding { + Binding { + searchViewModel.mostRecentDate != nil + } set: { value in + if value == false { + searchViewModel.mostRecentDate = nil + } else { + searchViewModel.mostRecentDate = dataStore.appSettings.lastDataSourceDate() + } + } + } + var body: some View { VStack(spacing: 0) { if importObserver.isImportingFile() == false { @@ -73,6 +81,13 @@ struct SelectablePlayerListView: View { } .pickerStyle(.segmented) Menu { + if let lastDataSource = dataStore.appSettings.localizedLastDataSource() { + Section { + Toggle(isOn: enableSourceCheck) { + Text("Limité à \(lastDataSource)") + } + } + } Section { ForEach(SourceFileManager.getSortOption()) { option in Toggle(isOn: .init(get: { @@ -94,7 +109,7 @@ struct SelectablePlayerListView: View { Section { Picker(selection: $searchViewModel.selectedAgeCategory) { ForEach(FederalTournamentAge.allCases) { ageCategory in - Text(ageCategory.localizedLabel(.title)).tag(ageCategory) + Text(ageCategory.localizedFederalAgeLabel(.title)).tag(ageCategory) } } label: { Text("Catégorie d'âge") @@ -127,7 +142,7 @@ struct SelectablePlayerListView: View { VStack(alignment: .trailing) { Label(searchViewModel.sortOption.localizedLabel(), systemImage: searchViewModel.ascending ? "chevron.up" : "chevron.down") if searchViewModel.selectedAgeCategory != .unlisted { - Text(searchViewModel.selectedAgeCategory.localizedLabel()).font(.caption) + Text(searchViewModel.selectedAgeCategory.localizedFederalAgeLabel()).font(.caption) } } } @@ -191,7 +206,6 @@ struct SelectablePlayerListView: View { } } .onAppear { - searchViewModel.mostRecentDate = mostRecentDate if searchViewModel.tokens.isEmpty && searchText.isEmpty { searchViewModel.debouncableText.removeAll() searchViewModel.searchText.removeAll() diff --git a/PadelClub/Views/Shared/TournamentFilterView.swift b/PadelClub/Views/Shared/TournamentFilterView.swift index 6af4d7c..3686aff 100644 --- a/PadelClub/Views/Shared/TournamentFilterView.swift +++ b/PadelClub/Views/Shared/TournamentFilterView.swift @@ -107,7 +107,7 @@ struct TournamentFilterView: View { } } } label: { - Text(category.localizedLabel(.title)) + Text(category.localizedFederalAgeLabel(.title)) } } } header: { diff --git a/PadelClub/Views/Team/CoachListView.swift b/PadelClub/Views/Team/CoachListView.swift new file mode 100644 index 0000000..7a980d6 --- /dev/null +++ b/PadelClub/Views/Team/CoachListView.swift @@ -0,0 +1,59 @@ +// +// CoachListView.swift +// PadelClub +// +// Created by razmig on 16/10/2024. +// + +import SwiftUI +import LeStorage + +struct CoachListView: View { + @Environment(Tournament.self) var tournament + @State private var coachNames: String + var team: TeamRegistration + + init(team: TeamRegistration) { + self.team = team + _coachNames = .init(wrappedValue: team.comment ?? "") + } + + var body: some View { + Section { + HStack { + TextField("Coach", text: $coachNames) + .autocorrectionDisabled() + .keyboardType(.alphabet) + .frame(maxWidth: .infinity) + .submitLabel(.done) + .onSubmit(of: .text) { + let trimmed = coachNames.prefixTrimmed(200) + if trimmed.isEmpty { + team.comment = nil + } else { + team.comment = trimmed + } + _save() + } + if coachNames.isEmpty == false { + FooterButtonView("effacer", role: .destructive) { + coachNames = "" + team.comment = nil + _save() + } + } + } + } header: { + Text("Coachs") + } + } + + private func _save() { + do { + try self.tournament.tournamentStore.teamRegistrations.addOrUpdate(instance: team) + } catch { + Logger.error(error) + } + } + +} diff --git a/PadelClub/Views/Team/Components/TeamHeaderView.swift b/PadelClub/Views/Team/Components/TeamHeaderView.swift index 4109b18..69b2013 100644 --- a/PadelClub/Views/Team/Components/TeamHeaderView.swift +++ b/PadelClub/Views/Team/Components/TeamHeaderView.swift @@ -29,10 +29,10 @@ struct TeamHeaderView: View { Text(team.weight.formatted()) } } - if let name = team.name { + if let name = team.name, name.isEmpty == false { VStack(alignment: .leading, spacing: 0) { Text("Nom de l'équipe").font(.caption) - Text(name) + Text(name).lineLimit(1).truncationMode(.tail) } } } diff --git a/PadelClub/Views/Team/EditingTeamView.swift b/PadelClub/Views/Team/EditingTeamView.swift index c095d97..f8cc5d5 100644 --- a/PadelClub/Views/Team/EditingTeamView.swift +++ b/PadelClub/Views/Team/EditingTeamView.swift @@ -88,8 +88,10 @@ struct EditingTeamView: View { Text("Cette équipe n'a pas été convoquée") } - Toggle(isOn: hasArrived) { - Text("Équipe sur place") + if team.unsortedPlayers().isEmpty == false { + Toggle(isOn: hasArrived) { + Text("Équipe sur place") + } } Toggle(isOn: .init(get: { @@ -142,26 +144,39 @@ struct EditingTeamView: View { } Section { - TextField("Nom de l'équipe", text: $name) - .autocorrectionDisabled() - .focused($focusedField, equals: ._name) - .keyboardType(.alphabet) - .frame(maxWidth: .infinity) - .submitLabel(.done) - .onSubmit(of: .text) { - let trimmed = name.trimmedMultiline - if trimmed.isEmpty { + HStack { + TextField("Nom de l'équipe", text: $name) + .autocorrectionDisabled() + .focused($focusedField, equals: ._name) + .keyboardType(.alphabet) + .frame(maxWidth: .infinity) + .submitLabel(.done) + .onSubmit(of: .text) { + let trimmed = name.prefixTrimmed(200) + if trimmed.isEmpty { + team.name = nil + } else { + team.name = trimmed + } + + _save() + } + if name.isEmpty == false { + FooterButtonView("effacer", role: .destructive) { + name = "" team.name = nil - } else { - team.name = trimmed + _save() } - - _save() } + } } header: { Text("Nom de l'équipe") } + if tournament.tournamentLevel.coachingIsAuthorized { + CoachListView(team: team) + } + Section { RowButtonView("Retirer des poules", role: .destructive) { team.resetGroupeStagePosition() diff --git a/PadelClub/Views/Team/TeamRestingView.swift b/PadelClub/Views/Team/TeamRestingView.swift new file mode 100644 index 0000000..712cb05 --- /dev/null +++ b/PadelClub/Views/Team/TeamRestingView.swift @@ -0,0 +1,98 @@ +// +// TeamRestingView.swift +// PadelClub +// +// Created by razmig on 19/10/2024. +// + +import SwiftUI + +struct TeamRestingView: View { + @Environment(Tournament.self) var tournament: Tournament + @State private var displayMode: DisplayMode = .teams + @State private var selectedCourt: Int? + @State private var readyMatches: [Match] = [] + @State private var matchesLeft: [Match] = [] + @State private var teams: [TeamRegistration] = [] + + enum DisplayMode: Int, Identifiable, CaseIterable { + var id: Int { self.rawValue } + case teams + case restingTime + + func localizedSortingModeLabel() -> String { + switch self { + case .teams: + return "Équipes" + case .restingTime: + return "Matchs" + } + } + } + + func contentUnavailableDescriptionLabel() -> String { + switch displayMode { + case .restingTime: + return "Ce tournoi n'a aucun match prêt à démarrer" + case .teams: + return "Ce tournoi n'a aucune équipe ayant déjà terminé un match." + } + } + + var sortedMatches: [Match] { + return readyMatches.sorted(by: \.restingTimeForSorting) + } + + var sortedTeams: [TeamRegistration] { + return teams + } + + var body: some View { + List { + Section { + switch displayMode { + case .teams: + if sortedTeams.isEmpty == false { + ForEach(sortedTeams) { team in + TeamRowView(team: team, displayRestingTime: true) + } + } else { + ContentUnavailableView("Aucune équipe en repos", systemImage: "xmark.circle", description: Text(contentUnavailableDescriptionLabel())) + } + case .restingTime: + if sortedMatches.isEmpty == false { + ForEach(sortedMatches) { match in + MatchRowView(match: match, updatedField: selectedCourt) + .matchViewStyle(.followUpStyle) + } + } else { + ContentUnavailableView("Aucun match à venir", systemImage: "xmark.circle", description: Text(contentUnavailableDescriptionLabel())) + } + } + } header: { + Picker(selection: $displayMode) { + ForEach(DisplayMode.allCases) { sortingMode in + Text(sortingMode.localizedSortingModeLabel()).tag(sortingMode) + } + } label: { + Text("Affichage") + } + .labelsHidden() + .pickerStyle(.segmented) + } + .textCase(nil) + } + .navigationTitle("Temps de repos") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .onAppear { + let allMatches = tournament.allMatches() + let matchesLeft = Tournament.matchesLeft(allMatches) + let runningMatches = Tournament.runningMatches(allMatches) + let readyMatches = Tournament.readyMatches(allMatches) + self.readyMatches = Tournament.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false) + self.matchesLeft = matchesLeft + self.teams = tournament.selectedSortedTeams().filter({ $0.restingTime() != nil }).sorted(by: \.restingTimeForSorting) + } + } +} diff --git a/PadelClub/Views/Team/TeamRowView.swift b/PadelClub/Views/Team/TeamRowView.swift index 6cbf087..f32365b 100644 --- a/PadelClub/Views/Team/TeamRowView.swift +++ b/PadelClub/Views/Team/TeamRowView.swift @@ -12,62 +12,114 @@ struct TeamRowView: View { var team: TeamRegistration var teamPosition: TeamPosition? = nil var displayCallDate: Bool = false + var displayRestingTime: Bool = false var body: some View { LabeledContent { TeamWeightView(team: team, teamPosition: teamPosition) } label: { VStack(alignment: .leading) { - HStack { - if let groupStage = team.groupStageObject() { - HStack { - Text(groupStage.groupStageTitle(.title)) - if let finalPosition = groupStage.finalPosition(ofTeam: team) { - Text((finalPosition + 1).ordinalFormatted()) - } - } - } else if let round = team.initialRound() { - Text(round.roundTitle(.wide)) - } - - if team.isWildCard() { - Text("wildcard").italic().foregroundStyle(.red).font(.caption) - } + TeamHeadlineView(team: team) + TeamView(team: team) + } + if displayCallDate { + TeamCallDateView(team: team) + } + if displayRestingTime { + TeamRestingView(team: team) + } + } + } + + struct TeamRestingView: View { + let team: TeamRegistration + + @ViewBuilder + var body: some View { + if let restingTime = team.restingTime()?.timeIntervalSinceNow, let value = Date.hourMinuteFormatter.string(from: restingTime * -1) { + if restingTime > -300 { + Text("vient de finir") + .font(.footnote) + .foregroundStyle(.secondary) + } else { + Text("en repos depuis " + value) + .font(.footnote) + .foregroundStyle(.secondary) } + } + } + } + + struct TeamView: View { + let team: TeamRegistration - if let name = team.name { - Text(name).font(.title3) - if team.players().isEmpty { - Text("Aucun joueur") - } + var body: some View { + if let name = team.name, name.isEmpty == false { + Text(name).foregroundStyle(.secondary).font(.footnote) + if team.players().isEmpty { + Text("Aucun joueur") + } else { + CompactTeamView(team: team) + } + } else { + if team.players().isEmpty == false { + CompactTeamView(team: team) } else { - if team.players().isEmpty == false { - ForEach(team.players()) { player in - Text(player.playerLabel()).lineLimit(1).truncationMode(.tail) + Text("Place réservée") + Text("Place réservée") + } + } + } + } + + struct TeamHeadlineView: View { + let team: TeamRegistration + + var body: some View { + HStack { + if let groupStage = team.groupStageObject() { + HStack { + Text(groupStage.groupStageTitle(.title)) + if let finalPosition = groupStage.finalPosition(ofTeam: team) { + Text((finalPosition + 1).ordinalFormatted()) } - } else { - Text("Place réservée") - Text("Place réservée") } + } else if let round = team.initialRound() { + Text(round.roundTitle(.wide)) } - } - if displayCallDate { - if let callDate = team.callDate { - Text("Déjà convoquée \(callDate.localizedDate())") - .foregroundStyle(.logoRed) - .italic() - .font(.caption) - } else { - Text("Pas encore convoquée") - .foregroundStyle(.logoRed) - .italic() - .font(.caption) + + if let wildcardLabel = team.wildcardLabel() { + Text(wildcardLabel).italic().foregroundStyle(.red).font(.caption) } } } } -} -//#Preview { -// TeamRowView(team: TeamRegistration.mock()) -//} + struct TeamCallDateView: View { + let team: TeamRegistration + + var body: some View { + if let callDate = team.callDate { + Text("Déjà convoquée \(callDate.localizedDate())") + .foregroundStyle(.logoRed) + .italic() + .font(.caption) + } else { + Text("Pas encore convoquée") + .foregroundStyle(.logoRed) + .italic() + .font(.caption) + } + } + } + + struct CompactTeamView: View { + let team: TeamRegistration + + var body: some View { + ForEach(team.players()) { player in + Text(player.playerLabel()).lineLimit(1).truncationMode(.tail) + } + } + } +} diff --git a/PadelClub/Views/Tournament/FileImportView.swift b/PadelClub/Views/Tournament/FileImportView.swift index 7ea2bcc..a25219b 100644 --- a/PadelClub/Views/Tournament/FileImportView.swift +++ b/PadelClub/Views/Tournament/FileImportView.swift @@ -84,7 +84,22 @@ struct FileImportView: View { @State private var multiImport: Bool = false @State private var presentFormatHelperView: Bool = false @State private var validatedTournamentIds: Set = Set() - @State private var chunkByParameter: Bool = true + @State private var chunkMode: ChunkMode = .byParameter + + enum ChunkMode: Int, Identifiable, CaseIterable { + var id: Int { self.rawValue } + case byParameter + case byCoupleOfLines + + func localizedChunkModeLabel() -> String { + switch self { + case .byParameter: + return "Nom d'équipe" + case .byCoupleOfLines: + return "Groupe de 2 lignes" + } + } + } init(defaultFileProvider: FileImportManager.FileProvider = .frenchFederation) { _fileProvider = .init(wrappedValue: defaultFileProvider) @@ -94,6 +109,10 @@ struct FileImportView: View { return self.tournament.tournamentStore } + var chunkByParameter: Bool { + return chunkMode == .byParameter + } + private func filteredTeams(tournament: Tournament) -> [FileImportManager.TeamHolder] { if tournament.isAnimation() { return teams.sorted(by: \.weight) @@ -141,14 +160,7 @@ struct FileImportView: View { } if fileProvider == .custom || fileProvider == .customAutoSearch { - Toggle(isOn: $chunkByParameter) { - Text("Détection des équipes") - if chunkByParameter { - Text("via le nom de l'équipe") - } else { - Text("couple de deux lignes") - } - } + _chunkModePickerView() } RowButtonView("Démarrer l'importation") { @@ -558,7 +570,7 @@ struct FileImportView: View { Section { HStack { VStack(alignment: .leading) { - if let teamName = team.name { + if let teamName = team.name, teamName.isEmpty == false { Text(teamName).foregroundStyle(.secondary) } ForEach(team.players.sorted(by: \.computedRank)) { @@ -590,6 +602,17 @@ struct FileImportView: View { Logger.error(error) } } + + private func _chunkModePickerView() -> some View { + Picker(selection: $chunkMode) { + ForEach(ChunkMode.allCases) { mode in + Text(mode.localizedChunkModeLabel()).tag(mode) + } + } label: { + Text("Détection des équipes") + } + } + } //#Preview { diff --git a/PadelClub/Views/Tournament/Screen/AddTeamView.swift b/PadelClub/Views/Tournament/Screen/AddTeamView.swift index 466ddff..bf8b9ec 100644 --- a/PadelClub/Views/Tournament/Screen/AddTeamView.swift +++ b/PadelClub/Views/Tournament/Screen/AddTeamView.swift @@ -364,16 +364,20 @@ struct AddTeamView: View { self.tournamentStore.teamRegistrations.addOrUpdate(instance: team) self.tournamentStore.playerRegistrations.addOrUpdate(contentOfs: players) - pasteString = nil - editableTextField = "" - - if team.players().count > 1 { - createdPlayers.removeAll() - createdPlayerIds.removeAll() - dismiss() - } else { - editedTeam = team - } + pasteString = nil + editableTextField = "" + createdPlayers.removeAll() + createdPlayerIds.removeAll() + + if team.players().count > 1 { + dismiss() + } else { + editedTeam = team + team.unsortedPlayers().forEach { player in + createdPlayers.insert(player) + createdPlayerIds.insert(player.id) + } + } } private func _updateTeam(checkDuplicates: Bool) { @@ -648,7 +652,7 @@ struct TeamSelectionSection: View { var body: some View { Section { - PlayerList(createdPlayerIds: createdPlayerIds, + PlayerListView(createdPlayerIds: createdPlayerIds, createdPlayers: createdPlayers, unsortedPlayers: unsortedPlayers, fetchPlayers: fetchPlayers, @@ -668,7 +672,7 @@ struct TeamSelectionSection: View { } } -struct PlayerList: View { +struct PlayerListView: View { let createdPlayerIds: Set let createdPlayers: Set let unsortedPlayers: [PlayerRegistration] diff --git a/PadelClub/Views/Tournament/Screen/BroadcastView.swift b/PadelClub/Views/Tournament/Screen/BroadcastView.swift index d687e29..4e12408 100644 --- a/PadelClub/Views/Tournament/Screen/BroadcastView.swift +++ b/PadelClub/Views/Tournament/Screen/BroadcastView.swift @@ -102,18 +102,14 @@ struct BroadcastView: View { } Section { - Toggle(isOn: $tournament.isPrivate) { - Text("Tournoi privé") - } + Toggle("Visible sur Padel Club", isOn: Binding( + get: { !tournament.isPrivate }, + set: { tournament.isPrivate = !$0 } + )) Toggle(isOn: $tournament.hideTeamsWeight) { Text("Masquer les poids des équipes") } - - } footer: { - let verb : String = tournament.isPrivate ? "est" : "sera" - let footerString = " Le tournoi \(verb) masqué sur le site [Padel Club](\(URLs.main.rawValue))" - Text(.init(footerString)) } if tournament.isPrivate == false { diff --git a/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift b/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift index c670fbd..3fb55c6 100644 --- a/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift @@ -27,44 +27,60 @@ struct InscriptionInfoView: View { var body: some View { List { Section { - let footerString = "via [beach-padel.app.fft.fr](\(URLs.beachPadel.rawValue))" - - LabeledContent { - Text(entriesFromBeachPadel.count.formatted()) - } label: { - Text("Paires importées") - Text(.init(footerString)) - } - .listRowView(color: .indigo) - LabeledContent { Text(selectedTeams.filter { $0.called() }.count.formatted()) } label: { Text("Paires convoquées") - Text("Vous avez envoyé une convocation par sms ou email") } .listRowView(color: .cyan) -// LabeledContent { -// Text(selectedTeams.filter { $0.confirmed() }.count.formatted()) -// } label: { -// Text("Paires ayant confirmées") -// Text("Vous avez noté la confirmation de l'équipe") -// } -// .listRowView(color: .green) + LabeledContent { + Text(selectedTeams.filter { $0.confirmed() }.count.formatted()) + } label: { + Text("Paires ayant confirmées") + } + .listRowView(color: .green) } + + if tournament.isAnimation() == false, entriesFromBeachPadel.count > 0 { + let notFromBeach = selectedTeams.filter({ $0.isImported() == false }) + Section { + let subtitle = "via [beach-padel.app.fft.fr](\(URLs.beachPadel.rawValue))" + + LabeledContent { + Text(entriesFromBeachPadel.count.formatted()) + } label: { + Text("Paires importées") + Text(.init(subtitle)) + } + .listRowView(color: .indigo) + + DisclosureGroup { + ForEach(notFromBeach) { team in + TeamDetailView(team: team) + } + } label: { + LabeledContent { + Text(notFromBeach.count.formatted()) + } label: { + Text("Paires non importées") + } + } + .listRowView(color: .brown) + } footer: { + if notFromBeach.isEmpty == false { + let footerString = "Au moins un joueur ne provient pas du fichier. Il est possible que vous ayez à mettre à jour les équipes sur [beach-padel.app.fft.fr](\(URLs.beachPadel.rawValue))" + + Text(.init(footerString)) + } + } + } + Section { DisclosureGroup { ForEach(callDateIssue) { team in - CallView.TeamView(team: team) - if let groupStage = team.groupStageObject(), let callDate = groupStage.startDate { - CallView(teams: [team], callDate: callDate, matchFormat: groupStage.matchFormat, roundLabel: "poule") - } else if let initialRound = team.initialRound(), - let initialMatch = team.initialMatch(), - let callDate = initialMatch.startDate { - CallView(teams: [team], callDate: callDate, matchFormat: initialMatch.matchFormat, roundLabel: initialRound.roundTitle()) - } + TeamCallView(team: team) } } label: { LabeledContent { @@ -126,9 +142,7 @@ struct InscriptionInfoView: View { } } .listRowView(color: .logoRed) - } - - Section { + DisclosureGroup { ForEach(homonyms) { player in ImportedPlayerView(player: player) @@ -141,8 +155,20 @@ struct InscriptionInfoView: View { } } .listRowView(color: .logoRed) - } + DisclosureGroup { + ForEach(playersMissing) { + TeamDetailView(team: $0) + } + } label: { + LabeledContent { + Text(playersMissing.count.formatted()) + } label: { + Text("Paires incomplètes") + } + } + .listRowView(color: .pink) + } Section { DisclosureGroup { @@ -200,6 +226,11 @@ struct InscriptionInfoView: View { ForEach(playersWithoutValidLicense) { EditablePlayerView(player: $0, editingOptions: [.licenceId]) .environmentObject(tournament.tournamentStore) + .onChange(of: $0.licenceId) { + players = tournament.unsortedPlayers() + let isImported = players.anySatisfy({ $0.isImported() }) + playersWithoutValidLicense = tournament.playersWithoutValidLicense(in: players, isImported: isImported) + } } } label: { LabeledContent { @@ -212,21 +243,6 @@ struct InscriptionInfoView: View { } footer: { Text("importé du fichier beach-padel sans licence valide ou créé sans licence") } - - Section { - DisclosureGroup { - ForEach(playersMissing) { - TeamDetailView(team: $0) - } - } label: { - LabeledContent { - Text(playersMissing.count.formatted()) - } label: { - Text("Paires incomplètes") - } - } - .listRowView(color: .pink) - } } .task { await _getIssues() diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentFormatSelectionView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentFormatSelectionView.swift index 74ef46c..5f6f301 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentFormatSelectionView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentFormatSelectionView.swift @@ -15,9 +15,9 @@ struct TournamentFormatSelectionView: View { @Bindable var tournament = tournament Section { - MatchTypeSelectionView(selectedFormat: $tournament.groupStageMatchFormat, format: "Poule") - MatchTypeSelectionView(selectedFormat: $tournament.matchFormat, format: "Tableau") - MatchTypeSelectionView(selectedFormat: $tournament.loserBracketMatchFormat, format: "Match de classement") + MatchTypeSelectionView(selectedFormat: $tournament.groupStageMatchFormat, format: "Poule", additionalEstimationDuration: tournament.additionalEstimationDuration) + MatchTypeSelectionView(selectedFormat: $tournament.matchFormat, format: "Tableau", additionalEstimationDuration: tournament.additionalEstimationDuration) + MatchTypeSelectionView(selectedFormat: $tournament.loserBracketMatchFormat, format: "Match de classement", additionalEstimationDuration: tournament.additionalEstimationDuration) } footer: { Text("À minima, les règles fédérales seront toujours prises en compte par défaut.") } diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift index f7096d1..7b2f6fe 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentGeneralSettingsView.swift @@ -45,7 +45,7 @@ struct TournamentGeneralSettingsView: View { TournamentDatePickerView() TournamentDurationManagerView() LabeledContent { - TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $entryFee, format: .currency(code: Locale.current.currency?.identifier ?? "EUR")) + TextField(tournament.isFree() ? "Gratuite" : "Inscription", value: $entryFee, format: .currency(code: Locale.defaultCurrency())) .keyboardType(.decimalPad) .multilineTextAlignment(.trailing) .frame(maxWidth: .infinity) @@ -125,7 +125,7 @@ struct TournamentGeneralSettingsView: View { if focusedField == ._entryFee { if tournament.isFree() { ForEach(priceTags, id: \.self) { priceTag in - Button(priceTag.formatted(.currency(code: "EUR"))) { + Button(priceTag.formatted(.currency(code: Locale.defaultCurrency()))) { entryFee = priceTag tournament.entryFee = priceTag focusedField = nil @@ -145,7 +145,8 @@ struct TournamentGeneralSettingsView: View { Spacer() Button("Valider") { if focusedField == ._name { - if tournamentName.trimmed.isEmpty { + let tournamentName = tournamentName.prefixTrimmed(200) + if tournamentName.isEmpty { tournament.name = nil } else { tournament.name = tournamentName diff --git a/PadelClub/Views/Tournament/Screen/Components/TournamentLevelPickerView.swift b/PadelClub/Views/Tournament/Screen/Components/TournamentLevelPickerView.swift index bc81b41..524a65a 100644 --- a/PadelClub/Views/Tournament/Screen/Components/TournamentLevelPickerView.swift +++ b/PadelClub/Views/Tournament/Screen/Components/TournamentLevelPickerView.swift @@ -42,7 +42,7 @@ struct TournamentLevelPickerView: View { Picker(selection: $tournament.federalTournamentAge, label: Text("Limite d'âge")) { ForEach(FederalTournamentAge.allCases) { type in - Text(type.localizedLabel(.title)).tag(type) + Text(type.localizedFederalAgeLabel(.title)).tag(type) } } Picker(selection: $tournament.groupStageOrderingMode, label: Text("Répartition en poule")) { diff --git a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift index 063f382..be075c8 100644 --- a/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift +++ b/PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift @@ -26,7 +26,7 @@ struct InscriptionManagerView: View { @Environment(\.dismiss) var dismiss - var tournament: Tournament + @Bindable var tournament: Tournament var cancelShouldDismiss: Bool = false @State private var searchField: String = "" @@ -48,6 +48,7 @@ struct InscriptionManagerView: View { @State private var presentAddTeamView: Bool = false @State private var compactMode: Bool = true @State private var pasteString: String? + @State private var registrationIssues: Int? = nil var tournamentStore: TournamentStore { return self.tournament.tournamentStore @@ -77,6 +78,7 @@ struct InscriptionManagerView: View { case groupStage case wildcardGroupStage case wildcardBracket + case notImported func emptyLocalizedLabelDescription() -> String { switch self { @@ -94,6 +96,8 @@ struct InscriptionManagerView: View { return "Vous n'avez placé aucune équipe dans le tableau." case .groupStage: return "Vous n'avez placé aucune équipe en poule." + case .notImported: + return "Vous n'avez aucune équipe non importé. Elles proviennent toutes du fichier." } } @@ -113,6 +117,8 @@ struct InscriptionManagerView: View { return "Aucune équipe dans le tableau" case .groupStage: return "Aucune équipe en poule" + case .notImported: + return "Aucune équipe non importée" } } @@ -132,6 +138,8 @@ struct InscriptionManagerView: View { return displayStyle == .wide ? "Forfaits" : "forfait" case .waiting: return displayStyle == .wide ? "Liste d'attente" : "attente" + case .notImported: + return "Non importées" } } } @@ -164,6 +172,10 @@ struct InscriptionManagerView: View { if self.teamsHash == nil, selectedSortedTeams.isEmpty == false { self.teamsHash = _simpleHash(ids: selectedSortedTeams.map { $0.id }) } + self.registrationIssues = nil + Task { + self.registrationIssues = await tournament.registrationIssues() + } } private func _handleHashDiff() { @@ -308,24 +320,47 @@ struct InscriptionManagerView: View { .symbolVariant(filterMode == .all ? .none : .fill) } Menu { + if tournament.inscriptionClosed() == false { + Menu { + _sortingTypePickerView() + } label: { + Text("Méthode de sélection") + Text(tournament.teamSorting.localizedLabel()) + } + Divider() + rankingDateSourcePickerView(showDateInLabel: true) + + Divider() + Button { + tournament.lockRegistration() + _save() + } label: { + Label("Clôturer", systemImage: "lock") + } + + } if tournament.isAnimation() == false { if tournament.inscriptionClosed() == false { - Menu { - _sortingTypePickerView() - } label: { - Text("Méthode de sélection") - Text(tournament.teamSorting.localizedLabel()) - } Divider() - rankingDateSourcePickerView(showDateInLabel: true) + + Section { + Button("+1 en tableau") { + tournament.addWildCard(1, .bracket) + } + + if tournament.groupStageCount > 0 { + Button("+1 en poules") { + tournament.addWildCard(1, .groupStage) + } + } + } header: { + Text("Ajout de wildcards") + } - Divider() - Button { - tournament.lockRegistration() - _save() - } label: { - Label("Clôturer", systemImage: "lock") + Button("Bloquer une place") { + tournament.addEmptyTeamRegistration(1) } + Divider() _sharingTeamsMenuView() Button { @@ -350,7 +385,15 @@ struct InscriptionManagerView: View { } } } else { - rankingDateSourcePickerView(showDateInLabel: true) + Button("Bloquer une place") { + tournament.addEmptyTeamRegistration(1) + } + + Toggle(isOn: $tournament.hideTeamsWeight) { + Text("Masquer les poids des équipes") + } + + //rankingDateSourcePickerView(showDateInLabel: true) Divider() @@ -368,7 +411,7 @@ struct InscriptionManagerView: View { if tournament.inscriptionClosed() == false { LabelOptions() } else { - Label("Clôturer", systemImage: "lock") + Label("Clôturé", systemImage: "lock") } } } @@ -376,19 +419,18 @@ struct InscriptionManagerView: View { .toolbarBackground(.visible, for: .navigationBar) .navigationTitle("Inscriptions") .navigationBarTitleDisplayMode(.inline) + .onChange(of: tournament.hideTeamsWeight) { + _save() + } } private func _sharingTeamsMenuView() -> some View { Menu { - if let teamPaste = teamPaste() { - ShareLink(item: teamPaste) { - Text("En texte") - } + ShareLink(item: teamPaste(), preview: .init("Inscriptions")) { + Text("En texte") } - if let teamPaste = teamPaste(.csv) { - ShareLink(item: teamPaste) { - Text("En csv") - } + ShareLink(item: teamPaste(.csv), preview: .init("Inscriptions")) { + Text("En csv") } } label: { Label("Exporter les paires", systemImage: "square.and.arrow.up") @@ -403,8 +445,8 @@ struct InscriptionManagerView: View { tournament.unsortedTeamsWithoutWO() } - func teamPaste(_ exportFormat: ExportFormat = .rawText) -> URL? { - tournament.pasteDataForImporting(exportFormat).createFile(self.tournament.tournamentTitle(.short), exportFormat) + func teamPaste(_ exportFormat: ExportFormat = .rawText) -> TournamentShareFile { + TournamentShareFile(tournament: tournament, exportFormat: exportFormat) } var unsortedPlayers: [PlayerRegistration] { @@ -433,6 +475,8 @@ struct InscriptionManagerView: View { teams = teams.filter({ $0.inRound() && $0.inGroupStage() == false }) case .groupStage: teams = teams.filter({ $0.inGroupStage() }) + case .notImported: + teams = teams.filter({ $0.isImported() == false }) default: break } @@ -615,6 +659,10 @@ struct InscriptionManagerView: View { case .waiting: let waiting: Int = max(0, unsortedTeamsWithoutWO.count - tournament.teamCount) return waiting.formatted() + case .notImported: + let notImported: Int = max(0, sortedTeams.filter({ $0.isImported() == false }).count) + return notImported.formatted() + } } @@ -668,16 +716,25 @@ struct InscriptionManagerView: View { .fixedSize(horizontal: false, vertical: false) .listRowSeparator(.hidden) - let registrationIssues = tournament.registrationIssues() - if tournament.isAnimation() == false, registrationIssues > 0 { + if tournament.isAnimation() == false { NavigationLink { InscriptionInfoView() .environment(tournament) + .onDisappear { + self.registrationIssues = nil + Task { + self.registrationIssues = await tournament.registrationIssues() + } + } } label: { LabeledContent { - Text(tournament.registrationIssues().formatted()) - .foregroundStyle(.logoRed) - .fontWeight(.bold) + if let registrationIssues { + Text(registrationIssues.formatted()) + .foregroundStyle(.logoRed) + .fontWeight(.bold) + } else { + ProgressView() + } } label: { Text("Problèmes détectés") } @@ -954,3 +1011,55 @@ struct InscriptionManagerView: View { // .environment(Tournament.mock()) // } //} + +struct TournamentRoundShareContent: Transferable { + let tournament: Tournament + + func shareContent() -> String { + print("Generating URL...") + let content = tournament.rounds().compactMap { $0.pasteData() }.joined(separator: "\n\n") + return content + } + + static var transferRepresentation: some TransferRepresentation { + ProxyRepresentation { transferable in + return transferable.shareContent() + } + } +} + +struct TournamentGroupStageShareContent: Transferable { + let tournament: Tournament + + func shareContent() -> String { + print("Generating URL...") + let content = tournament.groupStages().compactMap { $0.pasteData() }.joined(separator: "\n\n") + return content + } + + static var transferRepresentation: some TransferRepresentation { + ProxyRepresentation { transferable in + return transferable.shareContent() + } + } +} + +struct TournamentShareFile: Transferable { + let tournament: Tournament + let exportFormat: ExportFormat + + func shareFile() -> URL { + print("Generating URL...") + return tournament.pasteDataForImporting(exportFormat).createFile(self.tournament.tournamentTitle()+"-inscriptions", exportFormat) + } + + static var transferRepresentation: some TransferRepresentation { + FileRepresentation(exportedContentType: .utf8PlainText) { transferable in + return SentTransferredFile(transferable.shareFile()) + } + + ProxyRepresentation { transferable in + return transferable.shareFile() + } + } +} diff --git a/PadelClub/Views/Tournament/Screen/TableStructureView.swift b/PadelClub/Views/Tournament/Screen/TableStructureView.swift index ad7b39b..b89c2b5 100644 --- a/PadelClub/Views/Tournament/Screen/TableStructureView.swift +++ b/PadelClub/Views/Tournament/Screen/TableStructureView.swift @@ -20,6 +20,7 @@ struct TableStructureView: View { @State private var groupStageAdditionalQualified: Int = 0 @State private var updatedElements: Set = Set() @State private var structurePreset: PadelTournamentStructurePreset = .manual + @State private var buildWildcards: Bool = true @FocusState private var stepperFieldIsFocused: Bool var qualifiedFromGroupStage: Int { @@ -71,20 +72,11 @@ struct TableStructureView: View { Text(structurePreset.localizedDescriptionStructurePresetTitle()) } .onChange(of: structurePreset) { - switch structurePreset { - case .manual: - teamCount = 24 - groupStageCount = 4 - teamsPerGroupStage = 4 - qualifiedPerGroupStage = 1 - groupStageAdditionalQualified = 0 - case .doubleGroupStage: - teamCount = 9 - groupStageCount = 3 - teamsPerGroupStage = 3 - qualifiedPerGroupStage = 0 - groupStageAdditionalQualified = 0 - } + teamCount = structurePreset.tableDimension() + structurePreset.teamsInQualifiers() - structurePreset.qualifiedPerGroupStage() * structurePreset.groupStageCount() + groupStageCount = structurePreset.groupStageCount() + teamsPerGroupStage = structurePreset.teamsPerGroupStage() + qualifiedPerGroupStage = structurePreset.qualifiedPerGroupStage() + groupStageAdditionalQualified = 0 } } @@ -112,7 +104,7 @@ struct TableStructureView: View { Text("Équipes par poule") } - if structurePreset == .manual { + if structurePreset != .doubleGroupStage { LabeledContent { StepperView(count: $qualifiedPerGroupStage, minimum: 0, maximum: (teamsPerGroupStage-1)) } label: { @@ -121,7 +113,7 @@ struct TableStructureView: View { if qualifiedPerGroupStage < teamsPerGroupStage - 1 { LabeledContent { - StepperView(count: $groupStageAdditionalQualified, minimum: 1, maximum: maxMoreQualified) + StepperView(count: $groupStageAdditionalQualified, minimum: 0, maximum: maxMoreQualified) } label: { Text("Qualifié\(groupStageAdditionalQualified.pluralSuffix) supplémentaires") Text(moreQualifiedLabel) @@ -136,7 +128,7 @@ struct TableStructureView: View { } if groupStageCount > 0 && teamsPerGroupStage > 0 { - if structurePreset == .manual { + if structurePreset != .doubleGroupStage { LabeledContent { let mp = teamsPerGroupStage * (teamsPerGroupStage - 1) / 2 Text(mp.formatted()) @@ -179,7 +171,7 @@ struct TableStructureView: View { Section { let tf = max(teamCount - teamsFromGroupStages + qualifiedFromGroupStage + (groupStageCount > 0 ? groupStageAdditionalQualified : 0), 0) if groupStageCount > 0 { - if structurePreset == .manual { + if structurePreset != .doubleGroupStage { LabeledContent { Text(teamsFromGroupStages.formatted()) } label: { @@ -195,13 +187,13 @@ struct TableStructureView: View { } } - if structurePreset == .manual { + if structurePreset != .doubleGroupStage { LabeledContent { Text(tsPure.formatted()) } label: { Text("Nombre de têtes de série") - if tsPure > 0 && (tsPure > teamCount / 2 || tsPure < teamCount / 8 || tsPure < qualifiedFromGroupStage + groupStageAdditionalQualified) { + if groupStageCount > 0 && tsPure > 0 && (tsPure > teamCount / 2 || tsPure < teamCount / 8 || tsPure < qualifiedFromGroupStage + groupStageAdditionalQualified) { Text("Attention !").foregroundStyle(.red) } } @@ -220,17 +212,25 @@ struct TableStructureView: View { } } } footer: { - if tsPure > 0 && structurePreset == .manual { + if tsPure > 0 && structurePreset != .doubleGroupStage, groupStageCount > 0 { if tsPure > teamCount / 2 { - Text("Le nombre de têtes de série ne devrait pas être supérieur à la moitié de l'effectif").foregroundStyle(.red) + Text("Le nombre de têtes de série ne devrait pas être supérieur à la moitié de l'effectif.").foregroundStyle(.red) } else if tsPure < teamCount / 8 { - Text("À partir du moment où vous avez des têtes de série, leur nombre ne devrait pas être inférieur à 1/8ème de l'effectif").foregroundStyle(.red) + Text("À partir du moment où vous avez des têtes de série, leur nombre ne devrait pas être inférieur à 1/8ème de l'effectif.").foregroundStyle(.red) } else if tsPure < qualifiedFromGroupStage + groupStageAdditionalQualified { - Text("Le nombre de têtes de série ne devrait pas être inférieur au nombre de paires qualifiées sortantes").foregroundStyle(.red) + Text("Le nombre de têtes de série ne devrait pas être inférieur au nombre de paires qualifiées sortantes.").foregroundStyle(.red) } } } + if structurePreset.hasWildcards() { + Section { + Toggle("Avec wildcards", isOn: $buildWildcards) + } footer: { + Text("Padel Club réservera des places pour eux dans votre liste d'inscription.") + } + } + if tournament.state() != .initial { Section { @@ -288,6 +288,10 @@ struct TableStructureView: View { } else { updatedElements.remove(.groupStageCount) } + + if structurePreset.isFederalPreset(), groupStageCount == 0 { + teamCount = structurePreset.tableDimension() + } } .onChange(of: teamsPerGroupStage) { if teamsPerGroupStage != tournament.teamsPerGroupStage { @@ -365,6 +369,9 @@ struct TableStructureView: View { groupStage.updateGroupStageState() } } + + _checkGroupStagesTeams() + do { try dataStore.tournaments.addOrUpdate(instance: tournament) } catch { @@ -372,6 +379,21 @@ struct TableStructureView: View { } dismiss() } + + private func _checkGroupStagesTeams() { + if groupStageCount == 0 { + let teams = tournament.unsortedTeams().filter({ $0.inGroupStage() }) + teams.forEach { team in + team.groupStagePosition = nil + team.groupStage = nil + } + do { + try tournament.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams) + } catch { + Logger.error(error) + } + } + } private func _save(rebuildEverything: Bool = false) { _verifyValueIntegrity() @@ -387,10 +409,16 @@ struct TableStructureView: View { if rebuildEverything { tournament.deleteAndBuildEverything(preset: structurePreset) + if structurePreset.hasWildcards(), buildWildcards { + tournament.addWildCardIfNeeded(structurePreset.wildcardBrackets(), .bracket) + tournament.addWildCardIfNeeded(structurePreset.wildcardQualifiers(), .groupStage) + } } else if (rebuildEverything == false && requirements.contains(.groupStage)) { tournament.deleteGroupStages() tournament.buildGroupStages() } + + _checkGroupStagesTeams() try dataStore.tournaments.addOrUpdate(instance: tournament) @@ -437,8 +465,8 @@ struct TableStructureView: View { teamsPerGroupStage = 2 } - if qualifiedPerGroupStage < 1 { - qualifiedPerGroupStage = 1 + if qualifiedPerGroupStage < 0 { + qualifiedPerGroupStage = 0 } if groupStageAdditionalQualified < 0 { diff --git a/PadelClub/Views/Tournament/Screen/TournamentCallView.swift b/PadelClub/Views/Tournament/Screen/TournamentCallView.swift index 8b93bda..16a9af1 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentCallView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentCallView.swift @@ -16,6 +16,7 @@ enum CallDestination: Identifiable, Selectable, Equatable { case teams(Tournament) case seeds(Tournament) case groupStages(Tournament) + case brackets(Tournament) var id: String { switch self { @@ -25,6 +26,8 @@ enum CallDestination: Identifiable, Selectable, Equatable { return "seed" case .groupStages: return "groupStage" + case .brackets: + return "bracket" } } @@ -36,6 +39,8 @@ enum CallDestination: Identifiable, Selectable, Equatable { return "Têtes de série" case .groupStages: return "Poules" + case .brackets: + return "Tableau" } } @@ -47,6 +52,8 @@ enum CallDestination: Identifiable, Selectable, Equatable { case .seeds(let tournament): let allSeedCalled = tournament.seeds().filter({ tournament.isStartDateIsDifferentThanCallDate($0) || $0.callDate == nil }) return allSeedCalled.count + case .brackets(let tournament): + return nil case .groupStages(let tournament): let allSeedCalled = tournament.groupStageTeams().filter({ tournament.isStartDateIsDifferentThanCallDate($0) || $0.callDate == nil }) return allSeedCalled.count @@ -65,6 +72,8 @@ enum CallDestination: Identifiable, Selectable, Equatable { case .seeds(let tournament): let allSeedCalled = tournament.seeds().allSatisfy({ tournament.isStartDateIsDifferentThanCallDate($0) == false }) return allSeedCalled ? .checkmark : nil + case .brackets(let tournament): + return nil case .groupStages(let tournament): let allSeedCalled = tournament.groupStageTeams().allSatisfy({ tournament.isStartDateIsDifferentThanCallDate($0) == false }) return allSeedCalled ? .checkmark : nil @@ -83,16 +92,23 @@ struct TournamentCallView: View { self.tournament = tournament var destinations = [CallDestination]() let groupStageTeams = tournament.groupStageTeams() - if groupStageTeams.isEmpty == false { + let seededTeams = tournament.seededTeams() + if groupStageTeams.isEmpty == false && tournament.groupStageCount > 0 { destinations.append(.groupStages(tournament)) self._selectedDestination = State(wrappedValue: .groupStages(tournament)) } - if tournament.seededTeams().isEmpty == false { + if seededTeams.isEmpty == false { destinations.append(.seeds(tournament)) if groupStageTeams.isEmpty { self._selectedDestination = State(wrappedValue: .seeds(tournament)) } } + if tournament.availableSeeds().isEmpty == false, tournament.rounds().count > 0 { + destinations.append(.brackets(tournament)) + if seededTeams.isEmpty { + self._selectedDestination = State(wrappedValue: .brackets(tournament)) + } + } destinations.append(.teams(tournament)) self.allDestinations = destinations } @@ -109,6 +125,8 @@ struct TournamentCallView: View { TeamsCallingView(teams: tournament.selectedSortedTeams()) case .groupStages: GroupStageCallingView() + case .brackets: + BracketCallingView(tournament: tournament) case .seeds: SeedsCallingView() } diff --git a/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift b/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift index b965fb5..438ade9 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentCashierView.swift @@ -118,21 +118,22 @@ struct TournamentCashierView: View { self.allDestinations = allDestinations - - if tournament.hasEnded() { - if tournament.players().anySatisfy({ $0.hasPaid() == false }) == false { - _selectedDestination = .init(wrappedValue: .summary) - } else { - _selectedDestination = .init(wrappedValue: all) - } - } else { - let gs = tournament.getActiveGroupStage() - if let gs, let destination = allDestinations.first(where: { $0.id == gs.id }) { - _selectedDestination = State(wrappedValue: destination) - } else if let rs = tournament.getActiveRound(withSeeds: true), let destination = allDestinations.first(where: { $0.id == rs.id }) { - _selectedDestination = State(wrappedValue: destination) - } - } + _selectedDestination = .init(wrappedValue: all) + +// if tournament.hasEnded() { +// if tournament.players().anySatisfy({ $0.hasPaid() == false }) == false { +// _selectedDestination = .init(wrappedValue: .summary) +// } else { +// _selectedDestination = .init(wrappedValue: all) +// } +// } else { +// let gs = tournament.getActiveGroupStage() +// if let gs, let destination = allDestinations.first(where: { $0.id == gs.id }) { +// _selectedDestination = State(wrappedValue: destination) +// } else if let rs = tournament.getActiveRound(withSeeds: true), let destination = allDestinations.first(where: { $0.id == rs.id }) { +// _selectedDestination = State(wrappedValue: destination) +// } +// } } var body: some View { diff --git a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift index ba33832..62c5352 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentRankView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentRankView.swift @@ -35,8 +35,8 @@ struct TournamentRankView: View { if editMode?.wrappedValue.isEditing == false { Section { let all = tournament.allMatches() - let runningMatches = tournament.runningMatches(all) - let matchesLeft = tournament.readyMatches(all) + let runningMatches = Tournament.runningMatches(all) + let matchesLeft = Tournament.readyMatches(all) MatchListView(section: "Matchs restant", matches: matchesLeft, hideWhenEmpty: false, isExpanded: false) MatchListView(section: "Matchs en cours", matches: runningMatches, hideWhenEmpty: false, isExpanded: false) @@ -233,8 +233,8 @@ struct TournamentRankView: View { Divider() VStack(alignment: .leading) { - if let name = team.name { - Text(name).foregroundStyle(.secondary) + if let teamName = team.name, teamName.isEmpty == false { + Text(teamName).foregroundStyle(.secondary) } ForEach(team.players()) { player in diff --git a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift index be746d3..e3b862d 100644 --- a/PadelClub/Views/Tournament/Shared/TournamentCellView.swift +++ b/PadelClub/Views/Tournament/Shared/TournamentCellView.swift @@ -76,6 +76,10 @@ struct TournamentCellView: View { // .frame(width: 2) VStack(alignment: .leading, spacing: 0.0) { if let tournament = tournament as? Tournament { +#if targetEnvironment(simulator) + Text(tournament.id) +#endif + HStack { Text(tournament.locationLabel(displayStyle)) .lineLimit(1) @@ -103,7 +107,7 @@ struct TournamentCellView: View { if displayStyle == .wide, tournament.displayAgeAndCategory(forBuild: build) { VStack(alignment: .leading, spacing: 0) { Text(build.category.localizedLabel()) - Text(build.age.localizedLabel()) + Text(build.age.localizedFederalAgeLabel()) } .font(.caption) } @@ -151,7 +155,7 @@ struct TournamentCellView: View { } } else { Text(build.category.localizedLabel()) - Text(build.age.localizedLabel()) + Text(build.age.localizedFederalAgeLabel()) } } } diff --git a/PadelClub/Views/Tournament/Subscription/Guard.swift b/PadelClub/Views/Tournament/Subscription/Guard.swift index dea97c0..0836041 100644 --- a/PadelClub/Views/Tournament/Subscription/Guard.swift +++ b/PadelClub/Views/Tournament/Subscription/Guard.swift @@ -33,10 +33,20 @@ import LeStorage Logger.error(error) } } + + NotificationCenter.default.addObserver(self, selector: #selector(collectionDidLoad), name: NSNotification.Name.CollectionDidLoad, object: nil) + } deinit { self.updateListenerTask?.cancel() + NotificationCenter.default.removeObserver(self) + } + + @objc func collectionDidLoad(notification: Notification) { + if let _ = notification.object as? StoredCollection { + self._updateBestPlan() + } } func productIds() async -> [String] { diff --git a/PadelClub/Views/Tournament/TournamentBuildView.swift b/PadelClub/Views/Tournament/TournamentBuildView.swift index 50dbfca..b76002c 100644 --- a/PadelClub/Views/Tournament/TournamentBuildView.swift +++ b/PadelClub/Views/Tournament/TournamentBuildView.swift @@ -130,6 +130,10 @@ struct TournamentBuildView: View { } } } + } else { + NavigationLink(value: Screen.restingTime) { + Text("Temps de repos") + } } if state == .running || state == .finished { TournamentInscriptionView(tournament: tournament) diff --git a/PadelClub/Views/Tournament/TournamentRunningView.swift b/PadelClub/Views/Tournament/TournamentRunningView.swift index 626a82e..d28620a 100644 --- a/PadelClub/Views/Tournament/TournamentRunningView.swift +++ b/PadelClub/Views/Tournament/TournamentRunningView.swift @@ -17,12 +17,33 @@ struct TournamentRunningView: View { } @ViewBuilder - var body: some View { - MatchListView(section: "en cours", matches: tournament.runningMatches(allMatches), hideWhenEmpty: tournament.hasEnded()) -// MatchListView(section: "à lancer", matches: tournament.readyMatches(allMatches), isExpanded: false) -// MatchListView(section: "disponible", matches: tournament.availableToStart(allMatches), isExpanded: false) - let finishedMatches = tournament.finishedMatches(allMatches, limit: tournament.courtCount) - MatchListView(section: "Dernier\(finishedMatches.count.pluralSuffix) match\(finishedMatches.count.pluralSuffix) terminé\(finishedMatches.count.pluralSuffix)", matches: finishedMatches, isExpanded: tournament.hasEnded()) + var body: some View { + + let runningMatches = Tournament.runningMatches(allMatches) + let matchesLeft = Tournament.matchesLeft(allMatches) + let readyMatches = Tournament.readyMatches(allMatches) + let availableToStart = Tournament.availableToStart(allMatches, in: runningMatches, checkCanPlay: true) + + Section { + MatchListView(section: "prêt à démarrer", matches: availableToStart, hideWhenEmpty: tournament.hasEnded(), isExpanded: true) + } + + Section { + MatchListView(section: "à lancer", matches: readyMatches, hideWhenEmpty: tournament.hasEnded(), isExpanded: availableToStart.isEmpty) + } + + Section { + MatchListView(section: "à venir", matches: matchesLeft, hideWhenEmpty: true, isExpanded: readyMatches.isEmpty) + } + + Section { + MatchListView(section: "en cours", matches: runningMatches, hideWhenEmpty: tournament.hasEnded(), isExpanded: matchesLeft.isEmpty) + } + + Section { + let finishedMatches = Tournament.finishedMatches(allMatches, limit: tournament.courtCount) + MatchListView(section: "Dernier\(finishedMatches.count.pluralSuffix) match\(finishedMatches.count.pluralSuffix) terminé\(finishedMatches.count.pluralSuffix)", matches: finishedMatches, isExpanded: tournament.hasEnded()) + } } } diff --git a/PadelClub/Views/Tournament/TournamentView.swift b/PadelClub/Views/Tournament/TournamentView.swift index ebe7651..12442b3 100644 --- a/PadelClub/Views/Tournament/TournamentView.swift +++ b/PadelClub/Views/Tournament/TournamentView.swift @@ -113,6 +113,8 @@ struct TournamentView: View { PrintSettingsView(tournament: tournament) case .share: ShareModelView(instance: tournament) + case .restingTime: + TeamRestingView() } } .environment(tournament) diff --git a/PadelClub/Views/User/AccountView.swift b/PadelClub/Views/User/AccountView.swift index 07771af..f0d5e5f 100644 --- a/PadelClub/Views/User/AccountView.swift +++ b/PadelClub/Views/User/AccountView.swift @@ -15,6 +15,11 @@ struct AccountView: View { var body: some View { Form { + #if DEBUG + if let purchase = Guard.main.currentBestPurchase, let item = StoreItem(rawValue: purchase.productId) { + PurchaseView(purchaseRow: PurchaseRow(id: purchase.id, name: purchase.productId, item: item)) + } + #endif Section { NavigationLink("Changer de mot de passe") { ChangePasswordView() diff --git a/PadelClub/Views/ViewModifiers/ListRowViewModifier.swift b/PadelClub/Views/ViewModifiers/ListRowViewModifier.swift index bcd0f36..6cfc20a 100644 --- a/PadelClub/Views/ViewModifiers/ListRowViewModifier.swift +++ b/PadelClub/Views/ViewModifiers/ListRowViewModifier.swift @@ -11,6 +11,7 @@ struct ListRowViewModifier: ViewModifier { let isActive: Bool let color: Color var hideColorVariation: Bool = false + let alignment: Alignment func colorVariation() -> Color { hideColorVariation ? Color(uiColor: .systemBackground) : color.variation() @@ -21,7 +22,7 @@ struct ListRowViewModifier: ViewModifier { content .listRowBackground( colorVariation() - .overlay(alignment: .leading, content: { + .overlay(alignment: alignment, content: { color.frame(width: 8) }) ) @@ -32,7 +33,7 @@ struct ListRowViewModifier: ViewModifier { } extension View { - func listRowView(isActive: Bool = false, color: Color, hideColorVariation: Bool = false) -> some View { - modifier(ListRowViewModifier(isActive: isActive, color: color, hideColorVariation: hideColorVariation)) + func listRowView(isActive: Bool = false, color: Color, hideColorVariation: Bool = false, alignment: Alignment = .leading) -> some View { + modifier(ListRowViewModifier(isActive: isActive, color: color, hideColorVariation: hideColorVariation, alignment: alignment)) } } diff --git a/PadelClubTests/ServerDataTests.swift b/PadelClubTests/ServerDataTests.swift index d71c522..60ceaef 100644 --- a/PadelClubTests/ServerDataTests.swift +++ b/PadelClubTests/ServerDataTests.swift @@ -59,6 +59,7 @@ final class ServerDataTests: XCTestCase { assert(inserted_club.phone == club.phone) assert(inserted_club.courtCount == club.courtCount) assert(inserted_club.broadcastCode != nil) + assert(inserted_club.timezone != club.timezone) inserted_club.phone = "123456" inserted_club.lastUpdate = Date() @@ -103,7 +104,7 @@ final class ServerDataTests: XCTestCase { return } - let tournament = Tournament(event: eventId, name: "RG Homme", startDate: Date(), endDate: nil, creationDate: Date(), isPrivate: false, groupStageFormat: MatchFormat.megaTie, roundFormat: MatchFormat.nineGames, loserRoundFormat: MatchFormat.nineGamesDecisivePoint, groupStageSortMode: GroupStageOrderingMode.snake, groupStageCount: 2, rankSourceDate: Date(), dayDuration: 5, teamCount: 3, teamSorting: TeamSortingType.rank, federalCategory: TournamentCategory.mix, federalLevelCategory: TournamentLevel.p1000, federalAgeCategory: FederalTournamentAge.a45, closedRegistrationDate: Date(), groupStageAdditionalQualified: 4, courtCount: 9, prioritizeClubMembers: true, qualifiedPerGroupStage: 1, teamsPerGroupStage: 2, entryFee: 30.0, additionalEstimationDuration: 5, isDeleted: true, publishTeams: true, publishSummons: true, publishGroupStages: true, publishBrackets: true, shouldVerifyBracket: true, shouldVerifyGroupStage: true, hideTeamsWeight: true, publishTournament: true, hidePointsEarned: true, publishRankings: true, loserBracketMode: .manual) + let tournament = Tournament(event: eventId, name: "RG Homme", startDate: Date(), endDate: nil, creationDate: Date(), isPrivate: false, groupStageFormat: MatchFormat.megaTie, roundFormat: MatchFormat.nineGames, loserRoundFormat: MatchFormat.nineGamesDecisivePoint, groupStageSortMode: GroupStageOrderingMode.snake, groupStageCount: 2, rankSourceDate: Date(), dayDuration: 5, teamCount: 3, teamSorting: TeamSortingType.rank, federalCategory: TournamentCategory.mix, federalLevelCategory: TournamentLevel.p1000, federalAgeCategory: FederalTournamentAge.a45, closedRegistrationDate: Date(), groupStageAdditionalQualified: 4, courtCount: 9, prioritizeClubMembers: true, qualifiedPerGroupStage: 1, teamsPerGroupStage: 2, entryFee: 30.0, additionalEstimationDuration: 5, isDeleted: true, publishTeams: true, publishSummons: true, publishGroupStages: true, publishBrackets: true, shouldVerifyBracket: true, shouldVerifyGroupStage: true, hideTeamsWeight: true, publishTournament: true, hidePointsEarned: true, publishRankings: true, loserBracketMode: .manual, initialSeedRound: 8, initialSeedCount: 4) let t = try await StoreCenter.main.service().post(tournament) assert(t.lastUpdate.formatted() == tournament.lastUpdate.formatted()) @@ -145,6 +146,8 @@ final class ServerDataTests: XCTestCase { assert(t.hidePointsEarned == tournament.hidePointsEarned) assert(t.publishRankings == tournament.publishRankings) assert(t.loserBracketMode == tournament.loserBracketMode) + assert(t.initialSeedCount == tournament.initialSeedCount) + assert(t.initialSeedRound == tournament.initialSeedRound) } func testGroupStage() async throws { @@ -394,4 +397,23 @@ final class ServerDataTests: XCTestCase { } + func testDrawLog() async throws { + + let tournament: [Tournament] = try await StoreCenter.main.service().get() + guard let tournamentId = tournament.first?.id else { + assertionFailure("missing tournament in database") + return + } + + let drawLog = DrawLog(tournament: tournamentId, drawSeed: 1, drawMatchIndex: 1, drawTeamPosition: .two, drawType: .court) + let d: DrawLog = try await StoreCenter.main.service().post(drawLog) + + assert(d.tournament == drawLog.tournament) + assert(d.drawDate.formatted() == drawLog.drawDate.formatted()) + assert(d.drawSeed == drawLog.drawSeed) + assert(d.drawTeamPosition == drawLog.drawTeamPosition) + assert(d.drawMatchIndex == drawLog.drawMatchIndex) + assert(d.drawType == drawLog.drawType) + } + }