first commit

sync3
Laurent 7 months ago
parent 16347065f6
commit 2be7f48215
  1. BIN
      .DS_Store
  2. 544
      PadelClubData.xcodeproj/project.pbxproj
  3. 7
      PadelClubData.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  4. BIN
      PadelClubData/.DS_Store
  5. 27
      PadelClubData/Business/DayPeriod.swift
  6. 64
      PadelClubData/Business/DisplayContext.swift
  7. 37
      PadelClubData/Business/ExportFormat.swift
  8. 156
      PadelClubData/Business/MatchDescriptor.swift
  9. 29
      PadelClubData/Business/MatchSpot.swift
  10. 61
      PadelClubData/Business/OnlineRegistrationStatus.swift
  11. 2101
      PadelClubData/Business/PadelRule.swift
  12. 26
      PadelClubData/Business/Screen.swift
  13. 72
      PadelClubData/Business/SeedInterval.swift
  14. 12
      PadelClubData/Business/SpinDrawable.swift
  15. 71
      PadelClubData/ContactManager.swift
  16. BIN
      PadelClubData/Data/.DS_Store
  17. 111
      PadelClubData/Data/AppSettings.swift
  18. 117
      PadelClubData/Data/Club.swift
  19. 58
      PadelClubData/Data/Court.swift
  20. 268
      PadelClubData/Data/CustomUser.swift
  21. 331
      PadelClubData/Data/DataStore.swift
  22. 76
      PadelClubData/Data/DateInterval.swift
  23. 97
      PadelClubData/Data/DrawLog.swift
  24. 100
      PadelClubData/Data/Event.swift
  25. 149
      PadelClubData/Data/Gen/BaseClub.swift
  26. 92
      PadelClubData/Data/Gen/BaseCourt.swift
  27. 205
      PadelClubData/Data/Gen/BaseCustomUser.swift
  28. 79
      PadelClubData/Data/Gen/BaseDateInterval.swift
  29. 99
      PadelClubData/Data/Gen/BaseDrawLog.swift
  30. 99
      PadelClubData/Data/Gen/BaseEvent.swift
  31. 106
      PadelClubData/Data/Gen/BaseGroupStage.swift
  32. 155
      PadelClubData/Data/Gen/BaseMatch.swift
  33. 162
      PadelClubData/Data/Gen/BaseMatchScheduler.swift
  34. 121
      PadelClubData/Data/Gen/BaseMonthData.swift
  35. 205
      PadelClubData/Data/Gen/BasePlayerRegistration.swift
  36. 98
      PadelClubData/Data/Gen/BasePurchase.swift
  37. 106
      PadelClubData/Data/Gen/BaseRound.swift
  38. 198
      PadelClubData/Data/Gen/BaseTeamRegistration.swift
  39. 98
      PadelClubData/Data/Gen/BaseTeamScore.swift
  40. 533
      PadelClubData/Data/Gen/BaseTournament.swift
  41. 92
      PadelClubData/Data/Gen/Club.json
  42. 42
      PadelClubData/Data/Gen/Court.json
  43. 136
      PadelClubData/Data/Gen/CustomUser.json
  44. 33
      PadelClubData/Data/Gen/DateInterval.json
  45. 47
      PadelClubData/Data/Gen/Drawlog.json
  46. 48
      PadelClubData/Data/Gen/Event.json
  47. 53
      PadelClubData/Data/Gen/GroupStage.json
  48. 83
      PadelClubData/Data/Gen/Match.json
  49. 91
      PadelClubData/Data/Gen/MatchScheduler.json
  50. 71
      PadelClubData/Data/Gen/MonthData.json
  51. 122
      PadelClubData/Data/Gen/PlayerRegistration.json
  52. 51
      PadelClubData/Data/Gen/Purchase.json
  53. 53
      PadelClubData/Data/Gen/Round.json
  54. 118
      PadelClubData/Data/Gen/TeamRegistration.json
  55. 44
      PadelClubData/Data/Gen/TeamScore.json
  56. 310
      PadelClubData/Data/Gen/Tournament.json
  57. 591
      PadelClubData/Data/Gen/generator.py
  58. 684
      PadelClubData/Data/GroupStage.swift
  59. 1094
      PadelClubData/Data/Match.swift
  60. 888
      PadelClubData/Data/MatchScheduler.swift
  61. 66
      PadelClubData/Data/MockData.swift
  62. 33
      PadelClubData/Data/MonthData.swift
  63. 53
      PadelClubData/Data/PlayerPaymentType.swift
  64. 253
      PadelClubData/Data/PlayerRegistration.swift
  65. 95
      PadelClubData/Data/Purchase.swift
  66. 35
      PadelClubData/Data/README.md
  67. 850
      PadelClubData/Data/Round.swift
  68. 712
      PadelClubData/Data/TeamRegistration.swift
  69. 117
      PadelClubData/Data/TeamScore.swift
  70. 2214
      PadelClubData/Data/Tournament.swift
  71. 33
      PadelClubData/Data/TournamentLibrary.swift
  72. 69
      PadelClubData/Data/TournamentStore.swift
  73. 95
      PadelClubData/Extensions/Array+Extensions.swift
  74. 71
      PadelClubData/Extensions/Calendar+Extensions.swift
  75. 45
      PadelClubData/Extensions/CodingContainer+Extensions.swift
  76. 262
      PadelClubData/Extensions/Date+Extensions.swift
  77. 43
      PadelClubData/Extensions/FixedWidthInteger+Extensions.swift
  78. 28
      PadelClubData/Extensions/Locale+Extensions.swift
  79. 20
      PadelClubData/Extensions/NumberFormatter+Extensions.swift
  80. 105
      PadelClubData/Extensions/Sequence+Extensions.swift
  81. 46
      PadelClubData/Extensions/String+Crypto.swift
  82. 272
      PadelClubData/Extensions/String+Extensions.swift
  83. 16
      PadelClubData/Extensions/Tournament+Extensions.swift
  84. 182
      PadelClubData/Extensions/URL+Extensions.swift
  85. 13
      PadelClubData/PadelClubData.docc/PadelClubData.md
  86. 18
      PadelClubData/PadelClubData.h
  87. 143
      PadelClubData/Patcher.swift
  88. 79
      PadelClubData/Selectable.swift
  89. 74
      PadelClubData/SortOption.swift
  90. 250
      PadelClubData/SourceFileManager.swift
  91. 341
      PadelClubData/Subscriptions/Guard.swift
  92. 38
      PadelClubData/Subscriptions/StoreItem.swift
  93. 140
      PadelClubData/Subscriptions/StoreManager.swift
  94. 95
      PadelClubData/URLs.swift
  95. 12
      PadelClubData/Utils/CryptoKey.swift
  96. 27
      PadelClubData/Utils/MySortDescriptor.swift
  97. 102
      PadelClubData/Utils/NetworkManager.swift
  98. 28
      PadelClubData/Utils/NetworkManagerError.swift
  99. 47
      PadelClubData/Utils/PListReader.swift
  100. 66
      PadelClubData/Utils/SetDescriptor.swift
  101. Some files were not shown because too many files have changed in this diff Show More

BIN
.DS_Store vendored

Binary file not shown.

@ -0,0 +1,544 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objects = {
/* Begin PBXBuildFile section */
C4AD86CA2DAEB37900022F97 /* PadelClubData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C4AD86BF2DAEB37900022F97 /* PadelClubData.framework */; };
C4AD86E22DAEB39100022F97 /* LeStorage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C4AD86E12DAEB39100022F97 /* LeStorage.framework */; };
C4AD86E32DAEB39100022F97 /* LeStorage.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C4AD86E12DAEB39100022F97 /* LeStorage.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
C4B685A02DAF99CE0031750D /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = C4B6859F2DAF99CE0031750D /* Algorithms */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
C4AD86CB2DAEB37900022F97 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = C4AD86B62DAEB37900022F97 /* Project object */;
proxyType = 1;
remoteGlobalIDString = C4AD86BE2DAEB37900022F97;
remoteInfo = PadelClubData;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
C4AD86E42DAEB39100022F97 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
C4AD86E32DAEB39100022F97 /* LeStorage.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
C4AD86BF2DAEB37900022F97 /* PadelClubData.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PadelClubData.framework; sourceTree = BUILT_PRODUCTS_DIR; };
C4AD86C92DAEB37900022F97 /* PadelClubDataTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PadelClubDataTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
C4AD86E12DAEB39100022F97 /* LeStorage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LeStorage.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
C4AD86D12DAEB37900022F97 /* Exceptions for "PadelClubData" folder in "PadelClubData" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
publicHeaders = (
PadelClubData.h,
);
target = C4AD86BE2DAEB37900022F97 /* PadelClubData */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
C4AD86C12DAEB37900022F97 /* PadelClubData */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
C4AD86D12DAEB37900022F97 /* Exceptions for "PadelClubData" folder in "PadelClubData" target */,
);
path = PadelClubData;
sourceTree = "<group>";
};
C4AD86CD2DAEB37900022F97 /* PadelClubDataTests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = PadelClubDataTests;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
C4AD86BC2DAEB37900022F97 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
C4B685A02DAF99CE0031750D /* Algorithms in Frameworks */,
C4AD86E22DAEB39100022F97 /* LeStorage.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
C4AD86C62DAEB37900022F97 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
C4AD86CA2DAEB37900022F97 /* PadelClubData.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
C4AD86B52DAEB37900022F97 = {
isa = PBXGroup;
children = (
C4AD86C12DAEB37900022F97 /* PadelClubData */,
C4AD86CD2DAEB37900022F97 /* PadelClubDataTests */,
C4AD86E02DAEB39100022F97 /* Frameworks */,
C4AD86C02DAEB37900022F97 /* Products */,
);
sourceTree = "<group>";
};
C4AD86C02DAEB37900022F97 /* Products */ = {
isa = PBXGroup;
children = (
C4AD86BF2DAEB37900022F97 /* PadelClubData.framework */,
C4AD86C92DAEB37900022F97 /* PadelClubDataTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
C4AD86E02DAEB39100022F97 /* Frameworks */ = {
isa = PBXGroup;
children = (
C4AD86E12DAEB39100022F97 /* LeStorage.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
C4AD86BA2DAEB37900022F97 /* Headers */ = {
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXHeadersBuildPhase section */
/* Begin PBXNativeTarget section */
C4AD86BE2DAEB37900022F97 /* PadelClubData */ = {
isa = PBXNativeTarget;
buildConfigurationList = C4AD86D22DAEB37900022F97 /* Build configuration list for PBXNativeTarget "PadelClubData" */;
buildPhases = (
C4AD86BA2DAEB37900022F97 /* Headers */,
C4AD86BB2DAEB37900022F97 /* Sources */,
C4AD86BC2DAEB37900022F97 /* Frameworks */,
C4AD86BD2DAEB37900022F97 /* Resources */,
C4AD86E42DAEB39100022F97 /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
C4AD86C12DAEB37900022F97 /* PadelClubData */,
);
name = PadelClubData;
packageProductDependencies = (
C4B6859F2DAF99CE0031750D /* Algorithms */,
);
productName = PadelClubData;
productReference = C4AD86BF2DAEB37900022F97 /* PadelClubData.framework */;
productType = "com.apple.product-type.framework";
};
C4AD86C82DAEB37900022F97 /* PadelClubDataTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = C4AD86D72DAEB37900022F97 /* Build configuration list for PBXNativeTarget "PadelClubDataTests" */;
buildPhases = (
C4AD86C52DAEB37900022F97 /* Sources */,
C4AD86C62DAEB37900022F97 /* Frameworks */,
C4AD86C72DAEB37900022F97 /* Resources */,
);
buildRules = (
);
dependencies = (
C4AD86CC2DAEB37900022F97 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
C4AD86CD2DAEB37900022F97 /* PadelClubDataTests */,
);
name = PadelClubDataTests;
packageProductDependencies = (
);
productName = PadelClubDataTests;
productReference = C4AD86C92DAEB37900022F97 /* PadelClubDataTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
C4AD86B62DAEB37900022F97 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1620;
LastUpgradeCheck = 1620;
TargetAttributes = {
C4AD86BE2DAEB37900022F97 = {
CreatedOnToolsVersion = 16.2;
};
C4AD86C82DAEB37900022F97 = {
CreatedOnToolsVersion = 16.2;
};
};
};
buildConfigurationList = C4AD86B92DAEB37900022F97 /* Build configuration list for PBXProject "PadelClubData" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = C4AD86B52DAEB37900022F97;
minimizedProjectReferenceProxies = 1;
packageReferences = (
C4B6859E2DAF99CE0031750D /* XCRemoteSwiftPackageReference "swift-algorithms" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = C4AD86C02DAEB37900022F97 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
C4AD86BE2DAEB37900022F97 /* PadelClubData */,
C4AD86C82DAEB37900022F97 /* PadelClubDataTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
C4AD86BD2DAEB37900022F97 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
C4AD86C72DAEB37900022F97 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
C4AD86BB2DAEB37900022F97 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
C4AD86C52DAEB37900022F97 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
C4AD86CC2DAEB37900022F97 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = C4AD86BE2DAEB37900022F97 /* PadelClubData */;
targetProxy = C4AD86CB2DAEB37900022F97 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
C4AD86D32DAEB37900022F97 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 526E96RFNP;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
ENABLE_MODULE_VERIFIER = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 17.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 1.0;
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
PRODUCT_BUNDLE_IDENTIFIER = com.staxriver.PadelClubData;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_INSTALL_OBJC_HEADER = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
C4AD86D42DAEB37900022F97 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 526E96RFNP;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
ENABLE_MODULE_VERIFIER = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 17.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 1.0;
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
PRODUCT_BUNDLE_IDENTIFIER = com.staxriver.PadelClubData;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_INSTALL_OBJC_HEADER = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
C4AD86D52DAEB37900022F97 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
name = Debug;
};
C4AD86D62DAEB37900022F97 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
VALIDATE_PRODUCT = YES;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
name = Release;
};
C4AD86D82DAEB37900022F97 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 526E96RFNP;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
OTHER_SWIFT_FLAGS = "-DTEST";
PRODUCT_BUNDLE_IDENTIFIER = com.staxriver.PadelClubDataTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
C4AD86D92DAEB37900022F97 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 526E96RFNP;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
OTHER_SWIFT_FLAGS = "-DTEST";
PRODUCT_BUNDLE_IDENTIFIER = com.staxriver.PadelClubDataTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
C4AD86B92DAEB37900022F97 /* Build configuration list for PBXProject "PadelClubData" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C4AD86D52DAEB37900022F97 /* Debug */,
C4AD86D62DAEB37900022F97 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
C4AD86D22DAEB37900022F97 /* Build configuration list for PBXNativeTarget "PadelClubData" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C4AD86D32DAEB37900022F97 /* Debug */,
C4AD86D42DAEB37900022F97 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
C4AD86D72DAEB37900022F97 /* Build configuration list for PBXNativeTarget "PadelClubDataTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C4AD86D82DAEB37900022F97 /* Debug */,
C4AD86D92DAEB37900022F97 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
C4B6859E2DAF99CE0031750D /* XCRemoteSwiftPackageReference "swift-algorithms" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/apple/swift-algorithms.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.2.0;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
C4B6859F2DAF99CE0031750D /* Algorithms */ = {
isa = XCSwiftPackageProductDependency;
package = C4B6859E2DAF99CE0031750D /* XCRemoteSwiftPackageReference "swift-algorithms" */;
productName = Algorithms;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = C4AD86B62DAEB37900022F97 /* Project object */;
}

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

Binary file not shown.

@ -0,0 +1,27 @@
//
// DayPeriod.swift
// PadelClubData
//
// Created by Laurent Morvillier on 15/04/2025.
//
import Foundation
public enum DayPeriod: Int, CaseIterable, Identifiable, Codable {
public var id: Int { self.rawValue }
case all
case weekend
case week
public func localizedDayPeriodLabel() -> String {
switch self {
case .all:
return "n'importe"
case .week:
return "la semaine"
case .weekend:
return "le week-end"
}
}
}

@ -0,0 +1,64 @@
//
// DisplayContext.swift
// PadelClub
//
// Created by Razmig Sarkissian on 20/03/2024.
//
import Foundation
import UIKit
public enum DisplayContext {
case addition
case edition
case lockedForEditing
case selection
}
public enum DisplayStyle {
case title
case wide
case short
}
public enum SummoningDisplayContext {
case footer
case menu
}
public struct DeviceHelper {
public static func isBigScreen() -> Bool {
switch UIDevice.current.userInterfaceIdiom {
case .pad: // iPads
return true
case .phone: // iPhones (you can add more cases here for large vs small phones)
if UIScreen.main.bounds.size.width > 375 { // iPhone X, 11, 12, 13 Pro Max etc.
return true // large phones
} else {
return false // smaller phones
}
default:
return false // Other devices (Apple Watch, TV, etc.)
}
}
public static func maxCharacter() -> Int {
switch UIDevice.current.userInterfaceIdiom {
case .pad: // iPads
return 30
case .phone: // iPhones (you can add more cases here for large vs small phones)
if UIScreen.main.bounds.size.width > 375 { // iPhone X, 11, 12, 13 Pro Max etc.
return 15 // large phones
} else {
return 9 // smaller phones
}
default:
return 9 // Other devices (Apple Watch, TV, etc.)
}
}
public static func charLength() -> Int {
isBigScreen() ? 0 : 15
}
}

@ -0,0 +1,37 @@
//
// ExportFormat.swift
// PadelClub
//
// Created by Razmig Sarkissian on 19/07/2024.
//
import Foundation
public enum ExportFormat: Int, Identifiable, CaseIterable {
public var id: Int { self.rawValue }
case rawText
case csv
var suffix: String {
switch self {
case .rawText:
return "txt"
case .csv:
return "csv"
}
}
func separator() -> String {
switch self {
case .rawText:
return " "
case .csv:
return ";"
}
}
func newLineSeparator(_ count: Int = 1) -> String {
return Array(repeating: "\n", count: count).joined()
}
}

@ -0,0 +1,156 @@
//
// swift
// PadelClub
//
// Created by Razmig Sarkissian on 02/04/2024.
//
import Foundation
import SwiftUI
import Combine
public class MatchDescriptor: ObservableObject {
@Published public var matchFormat: MatchFormat
@Published public var setDescriptors: [SetDescriptor]
public var court: Int = 1
public var title: String = "Titre du match"
public var teamLabelOne: String = ""
public var teamLabelTwo: String = ""
public var startDate: Date = Date()
public var match: Match?
public let colorTeamOne: Color = .teal
public let colorTeamTwo: Color = .indigo
public init(match: Match? = nil) {
self.match = match
if let groupStage = match?.groupStageObject {
self.matchFormat = groupStage.matchFormat
self.setDescriptors = [SetDescriptor(setFormat: groupStage.matchFormat.setFormat)]
} else {
let format = match?.matchFormat ?? match?.currentTournament()?.matchFormat ?? .defaultFormatForMatchType(.groupStage)
self.matchFormat = format
self.setDescriptors = [SetDescriptor(setFormat: format.setFormat)]
}
let teamOne = match?.team(.one)
let teamTwo = match?.team(.two)
self.teamLabelOne = teamOne?.teamLabel(.wide, twoLines: true) ?? ""
self.teamLabelTwo = teamTwo?.teamLabel(.wide, twoLines: true) ?? ""
if let match, let scoresTeamOne = match.teamScore(ofTeam: teamOne)?.score, let scoresTeamTwo = match.teamScore(ofTeam: teamTwo)?.score {
self.setDescriptors = combineArraysIntoTuples(scoresTeamOne.components(separatedBy: ","), scoresTeamTwo.components(separatedBy: ",")).map({ (a:String?, b:String?) in
SetDescriptor(valueTeamOne: a != nil ? Int(a!) : nil, valueTeamTwo: b != nil ? Int(b!) : nil, setFormat: match.matchFormat.setFormat)
})
}
}
public 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
}
public 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
}
public var showSetInputView: Bool {
return setDescriptors.anySatisfy({ $0.showSetInputView })
}
public var showTieBreakInputView: Bool {
return setDescriptors.anySatisfy({ $0.showTieBreakInputView })
}
public var teamOneScores: [String] {
setDescriptors.compactMap { $0.getValue(teamPosition: .one) }
}
public var teamTwoScores: [String] {
setDescriptors.compactMap { $0.getValue(teamPosition: .two) }
}
public var scoreTeamOne: Int { setDescriptors.compactMap { $0.winner }.filter { $0 == .one }.count }
public var scoreTeamTwo: Int { setDescriptors.compactMap { $0.winner }.filter { $0 == .two }.count }
public var hasEnded: Bool {
return matchFormat.hasEnded(scoreTeamOne: scoreTeamOne, scoreTeamTwo: scoreTeamTwo)
}
public func addNewSet() {
if hasEnded == false {
setDescriptors.append(SetDescriptor(setFormat: matchFormat.newSetFormat(setCount: setDescriptors.count)))
}
}
public var winner: TeamPosition {
matchFormat.winner(scoreTeamOne: scoreTeamOne, scoreTeamTwo: scoreTeamTwo)
}
public var winnerLabel: String {
if winner == .one {
return teamLabelOne
} else {
return teamLabelTwo
}
}
}
fileprivate func combineArraysIntoTuples(_ array1: [String], _ array2: [String]) -> [(String?, String?)] {
// Zip the two arrays together and map them to tuples of optional strings
let combined = zip(array1, array2).map { (element1, element2) in
return (element1, element2)
}
// If one array is longer than the other, append the remaining elements
let remainingElements: [(String?, String?)]
if array1.count > array2.count {
let remaining = Array(array1[array2.count...]).map { (element) in
return (element, nil as String?)
}
remainingElements = remaining
} else if array2.count > array1.count {
let remaining = Array(array2[array1.count...]).map { (element) in
return (nil as String?, element)
}
remainingElements = remaining
} else {
remainingElements = []
}
// Concatenate the two arrays
return combined + remainingElements
}

@ -0,0 +1,29 @@
//
// MatchSpot.swift
// PadelClub
//
// Created by razmig on 22/03/2025.
//
public struct MatchSpot: SpinDrawable {
public let match: Match
public let teamPosition: TeamPosition
public func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String] {
[match.roundTitle(), matchTitle(displayStyle: displayStyle)].compactMap { $0 }
}
public func matchTitle(displayStyle: DisplayStyle) -> String {
[match.matchTitle(displayStyle), teamPositionLabel()].joined(separator: " - ")
}
public func teamPositionLabel() -> String {
switch teamPosition {
case .one:
return "haut"
case .two:
return "bas"
}
}
}

@ -0,0 +1,61 @@
//
// OnlineRegistrationStatus.swift
// PadelClubData
//
// Created by Laurent Morvillier on 15/04/2025.
//
import Foundation
public enum OnlineRegistrationStatus: Int {
case open = 1
case notEnabled = 2
case notStarted = 3
case ended = 4
case waitingListPossible = 5
case waitingListFull = 6
case inProgress = 7
case endedWithResults = 8
public var displayName: String {
switch self {
case .open:
return "Open"
case .notEnabled:
return "Not Enabled"
case .notStarted:
return "Not Started"
case .ended:
return "Ended"
case .waitingListPossible:
return "Waiting List Possible"
case .waitingListFull:
return "Waiting List Full"
case .inProgress:
return "In Progress"
case .endedWithResults:
return "Ended with Results"
}
}
public func statusLocalized() -> String {
switch self {
case .open:
return "Inscription ouverte"
case .notEnabled:
return "Inscription désactivée"
case .notStarted:
return "Inscription pas encore ouverte"
case .ended:
return "Inscription terminée"
case .waitingListPossible:
return "Liste d'attente disponible"
case .waitingListFull:
return "Liste d'attente complète"
case .inProgress:
return "Tournoi en cours"
case .endedWithResults:
return "Tournoi terminé"
}
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,26 @@
//
// Screen.swift
// PadelClub
//
// Created by Razmig Sarkissian on 03/03/2024.
//
import Foundation
public enum Screen: String, Codable {
case inscription
case groupStage
case round
case settings
case structure
case schedule
case cashier
case call
case rankings
case broadcast
case event
case print
case share
case restingTime
case stateSettings
}

@ -0,0 +1,72 @@
//
// SeedInterval.swift
// PadelClub
//
// Created by Razmig Sarkissian on 01/04/2024.
//
import Foundation
public struct SeedInterval: Hashable, Comparable {
public let first: Int
public let last: Int
public init(first: Int, last: Int) {
self.first = first
self.last = last
}
public func pointsRange(tournamentLevel: TournamentLevel, teamsCount: Int) -> String {
tournamentLevel.pointsRange(first: first, last: last, teamsCount: teamsCount)
}
public static func <(lhs: SeedInterval, rhs: SeedInterval) -> Bool {
return lhs.first < rhs.first
}
public func isFixed() -> Bool {
first == 1 && last == 2
}
public var count: Int {
dimension
}
private var dimension: Int {
(last - (first - 1))
}
func chunks() -> [SeedInterval]? {
if dimension > 3 {
let split = dimension / 2
if split%2 == 0 {
let firstHalf = SeedInterval(first: first, last: first + split - 1)
let secondHalf = SeedInterval(first: first + split, last: last)
return [firstHalf, secondHalf]
} else {
let firstHalf = SeedInterval(first: first, last: first + split)
let secondHalf = SeedInterval(first: first + split + 1, last: last)
return [firstHalf, secondHalf]
}
} else {
return nil
}
}
public func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
if dimension < 3 {
return "\(first)\(first.ordinalFormattedSuffix()) place"
} else {
return "Place \(first) à \(last)"
}
}
public func localizedInterval(_ displayStyle: DisplayStyle = .wide) -> String {
if dimension < 3 {
return "#\(first) / #\(last)"
} else {
return "#\(first) à #\(last)"
}
}
}

@ -0,0 +1,12 @@
//
// SpinDrawable.swift
// PadelClubData
//
// Created by Laurent Morvillier on 15/04/2025.
//
import Foundation
public protocol SpinDrawable {
func segmentLabel(_ displayStyle: DisplayStyle, hideNames: Bool) -> [String]
}

@ -0,0 +1,71 @@
//
// ContactManager.swift
// Padel Tournament
//
// Created by Razmig Sarkissian on 19/09/2023.
//
import Foundation
import SwiftUI
import MessageUI
import LeStorage
public enum ContactManagerError: LocalizedError {
case mailFailed
case mailNotSent //no network no error
case messageFailed
case messageNotSent //no network no error
case calendarAccessDenied
case calendarEventSaveFailed
case noCalendarAvailable
case uncalledTeams([TeamRegistration])
var localizedDescription: String {
switch self {
case .mailFailed:
return "Le mail n'a pas été envoyé"
case .mailNotSent:
return "Le mail est dans la boîte d'envoi de l'app Mail. Vérifiez son état dans l'app Mail avant d'essayer de le renvoyer."
case .messageFailed:
return "Le SMS n'a pas été envoyé"
case .messageNotSent:
return "Le SMS n'a pas été envoyé"
case .uncalledTeams(let array):
let verb = array.count > 1 ? "peuvent" : "peut"
return "Attention, \(array.count) équipe\(array.count.pluralSuffix) ne \(verb) pas être contacté par la méthode choisie"
case .calendarAccessDenied:
return "Padel Club n'a pas accès à votre calendrier"
case .calendarEventSaveFailed:
return "Padel Club n'a pas réussi à sauver ce tournoi dans votre calendrier"
case .noCalendarAvailable:
return "Padel Club n'a pas réussi à trouver un calendrier pour y inscrire ce tournoi"
}
}
public static func getNetworkErrorMessage(sentError: ContactManagerError?, networkMonitorConnected: Bool) -> String {
var errors: [String] = []
if networkMonitorConnected == false {
errors.append("L'appareil n'est pas connecté à internet.")
}
if let sentError {
errors.append(sentError.localizedDescription)
}
return errors.joined(separator: "\n")
}
}
public enum ContactType: Identifiable {
case mail(date: Date?, recipients: [String]?, bccRecipients: [String]?, body: String?, subject: String?, tournamentBuild: TournamentBuild?)
case message(date: Date?, recipients: [String]?, body: String?, tournamentBuild: TournamentBuild?)
public var id: Int {
switch self {
case .message: return 0
case .mail: return 1
}
}
public static let defaultAvailablePaymentMethods: String = "Règlement possible par chèque ou espèces."
}

Binary file not shown.

@ -0,0 +1,111 @@
//
// AppSettings.swift
// PadelClub
//
// Created by Razmig Sarkissian on 26/03/2024.
//
import Foundation
import LeStorage
import SwiftUI
@Observable
public final class AppSettings: MicroStorable {
public var lastDataSource: String? = nil
public var didCreateAccount: Bool = false
public var didRegisterAccount: Bool = false
//search tournament stuff
public var tournamentCategories: Set<TournamentCategory.ID>
public var tournamentLevels: Set<TournamentLevel.ID>
public var tournamentAges: Set<FederalTournamentAge.ID>
public var tournamentTypes: Set<FederalTournamentType.ID>
public var startDate: Date
public var endDate: Date
public var city: String
public var distance: Double
public var sortingOption: String
public var nationalCup: Bool
public var dayDuration: Int?
public var dayPeriod: DayPeriod
public func lastDataSourceDate() -> Date? {
guard let lastDataSource else { return nil }
return URL.importDateFormatter.date(from: lastDataSource)
}
public func localizedLastDataSource() -> String? {
guard let lastDataSource else { return nil }
guard let date = URL.importDateFormatter.date(from: lastDataSource) else { return nil }
return date.monthYearFormatted
}
public func resetSearch() {
tournamentAges = Set()
tournamentTypes = Set()
tournamentLevels = Set()
tournamentCategories = Set()
city = ""
distance = 30
startDate = Date()
endDate = Calendar.current.date(byAdding: .month, value: 3, to: Date())!
sortingOption = "dateDebut+asc"
nationalCup = false
dayDuration = nil
dayPeriod = .all
}
public required init() {
tournamentAges = Set()
tournamentTypes = Set()
tournamentLevels = Set()
tournamentCategories = Set()
city = ""
distance = 30
startDate = Date()
endDate = Calendar.current.date(byAdding: .month, value: 3, to: Date())!
sortingOption = "dateDebut+asc"
nationalCup = false
dayDuration = nil
dayPeriod = .all
}
public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
lastDataSource = try container.decodeIfPresent(String.self, forKey: ._lastDataSource)
didCreateAccount = try container.decodeIfPresent(Bool.self, forKey: ._didCreateAccount) ?? false
didRegisterAccount = try container.decodeIfPresent(Bool.self, forKey: ._didRegisterAccount) ?? false
tournamentCategories = try container.decodeIfPresent(Set<TournamentCategory.ID>.self, forKey: ._tournamentCategories) ?? Set()
tournamentLevels = try container.decodeIfPresent(Set<TournamentLevel.ID>.self, forKey: ._tournamentLevels) ?? Set()
tournamentAges = try container.decodeIfPresent(Set<FederalTournamentAge.ID>.self, forKey: ._tournamentAges) ?? Set()
tournamentTypes = try container.decodeIfPresent(Set<FederalTournamentType.ID>.self, forKey: ._tournamentTypes) ?? Set()
startDate = try container.decodeIfPresent(Date.self, forKey: ._startDate) ?? Date()
endDate = try container.decodeIfPresent(Date.self, forKey: ._endDate) ?? Calendar.current.date(byAdding: .month, value: 3, to: Date())!
city = try container.decodeIfPresent(String.self, forKey: ._city) ?? ""
distance = try container.decodeIfPresent(Double.self, forKey: ._distance) ?? 30
sortingOption = try container.decodeIfPresent(String.self, forKey: ._sortingOption) ?? "dateDebut+asc"
nationalCup = try container.decodeIfPresent(Bool.self, forKey: ._nationalCup) ?? false
dayDuration = try container.decodeIfPresent(Int.self, forKey: ._dayDuration)
dayPeriod = try container.decodeIfPresent(DayPeriod.self, forKey: ._dayPeriod) ?? .all
}
enum CodingKeys: String, CodingKey {
case _lastDataSource = "lastDataSource"
case _didCreateAccount = "didCreateAccount"
case _didRegisterAccount = "didRegisterAccount"
case _tournamentCategories = "tournamentCategories"
case _tournamentLevels = "tournamentLevels"
case _tournamentAges = "tournamentAges"
case _tournamentTypes = "tournamentTypes"
case _startDate = "startDate"
case _endDate = "endDate"
case _city = "city"
case _distance = "distance"
case _sortingOption = "sortingOption"
case _nationalCup = "nationalCup"
case _dayDuration = "dayDuration"
case _dayPeriod = "dayPeriod"
}
}

@ -0,0 +1,117 @@
//
// Club.swift
// PadelClub
//
// Created by Laurent Morvillier on 02/02/2024.
//
import Foundation
import SwiftUI
import LeStorage
@Observable
public final class Club: BaseClub {
static var copyServerResponse: Bool { return true }
public func clubTitle(_ displayStyle: DisplayStyle = .wide) -> String {
switch displayStyle {
case .wide, .title:
return name
case .short:
return acronym
}
}
public func shareURL() -> URL? {
return URL(string: URLs.main.url.appending(path: "?club=\(id)").absoluteString.removingPercentEncoding!)
}
public var customizedCourts: [Court] {
DataStore.shared.courts.filter { $0.club == self.id }.sorted(by: \.index)
}
public override func deleteDependencies(shouldBeSynchronized: Bool) {
let customizedCourts = self.customizedCourts
for customizedCourt in customizedCourts {
customizedCourt.deleteDependencies(shouldBeSynchronized: shouldBeSynchronized)
}
DataStore.shared.courts.deleteDependencies(customizedCourts, shouldBeSynchronized: shouldBeSynchronized)
}
}
extension Club {
public var isValid: Bool {
name.isEmpty == false && name.count > 3
}
public func automaticShortName() -> String {
name.acronym()
}
public enum AcronymMode: String, CaseIterable {
case automatic = "Automatique"
case custom = "Personalisée"
}
public func shortNameMode() -> AcronymMode {
(acronym.isEmpty || acronym == automaticShortName()) ? .automatic : .custom
}
public func hasTenupId() -> Bool {
code != nil
}
public func federalLink() -> URL? {
guard let code else { return nil }
return URL(string: "https://tenup.fft.fr/club/\(code)")
}
public func courtName(atIndex courtIndex: Int) -> String {
courtNameIfAvailable(atIndex: courtIndex) ?? Court.courtIndexedTitle(atIndex: courtIndex)
}
public func courtNameIfAvailable(atIndex courtIndex: Int) -> String? {
customizedCourts.first(where: { $0.index == courtIndex })?.name
}
public func update(fromClub club: Club) {
self.acronym = club.acronym
self.name = club.name
self.phone = club.phone
self.code = club.code
self.address = club.address
self.city = club.city
self.zipCode = club.zipCode
self.latitude = club.latitude
self.longitude = club.longitude
}
public func hasBeenCreated(by creatorId: String?) -> Bool {
return creatorId == creator || creator == nil || self.relatedUser == creatorId
}
public func isFavorite() -> Bool {
return DataStore.shared.user.clubs.contains(where: { $0 == self.id })
}
public static func findOrCreate(name: String, code: String?, city: String? = nil, zipCode: String? = nil) -> Club {
/*
identify a club : code, name, ??
*/
let club: Club? = DataStore.shared.clubs.first(where: { (code == nil && $0.name == name && $0.city == city && $0.zipCode == zipCode) || code != nil && $0.code == code })
if let club {
return club
} else {
let club = Club(creator: StoreCenter.main.userId, name: name, acronym: name.acronym(), code: code, city: city, zipCode: zipCode)
club.relatedUser = StoreCenter.main.userId
return club
}
}
}

@ -0,0 +1,58 @@
//
// Court.swift
// PadelClub
//
// Created by Razmig Sarkissian on 23/04/2024.
//
import Foundation
import SwiftUI
import LeStorage
@Observable
public final class Court: BaseCourt {
static func == (lhs: Court, rhs: Court) -> Bool {
lhs.id == rhs.id
}
public init(index: Int, club: String, name: String? = nil, exitAllowed: Bool = false, indoor: Bool = false) {
super.init()
self.index = index
self.lastUpdate = Date()
self.club = club
self.name = name
self.exitAllowed = exitAllowed
self.indoor = indoor
}
required init(from decoder: Decoder) throws {
try super.init(from: decoder)
}
required public init() {
super.init()
}
public func courtTitle() -> String {
self.name ?? courtIndexTitle()
}
public func courtIndexTitle() -> String {
Self.courtIndexedTitle(atIndex: index)
}
public static func courtIndexedTitle(atIndex index: Int) -> String {
("Terrain #" + (index + 1).formatted())
}
public func clubObject() -> Club? {
Store.main.findById(club)
}
public override func deleteDependencies(shouldBeSynchronized: Bool) {
}
}

@ -0,0 +1,268 @@
//
// User.swift
// PadelClub
//
// Created by Laurent Morvillier on 21/02/2024.
//
import Foundation
import LeStorage
enum UserRight: Int, Codable {
case none = 0
case edition = 1
case creation = 2
}
@Observable
public class CustomUser: BaseCustomUser, UserBase {
// static func resourceName() -> String { "users" }
// static func tokenExemptedMethods() -> [HTTPMethod] { return [.post] }
// static func filterByStoreIdentifier() -> Bool { return false }
// static var relationshipNames: [String] = []
//
// public var id: String = Store.randomId()
// var lastUpdate: Date
// public var username: String
// public var email: String
// var clubs: [String] = []
// var umpireCode: String?
// var licenceId: String?
// var firstName: String
// var lastName: String
// var phone: String?
// var country: String?
//
// var summonsMessageBody : String? = nil
// var summonsMessageSignature: String? = nil
// var summonsAvailablePaymentMethods: String? = nil
// var summonsDisplayFormat: Bool = false
// var summonsDisplayEntryFee: Bool = false
// var summonsUseFullCustomMessage: Bool = false
// var matchFormatsDefaultDuration: [MatchFormat: Int]? = nil
// var bracketMatchFormatPreference: MatchFormat?
// var groupStageMatchFormatPreference: MatchFormat?
// var loserBracketMatchFormatPreference: MatchFormat?
// var loserBracketMode: LoserBracketMode = .automatic
//
// var deviceId: String?
init(username: String, email: String, firstName: String, lastName: String, phone: String?, country: String?, loserBracketMode: LoserBracketMode = .automatic) {
super.init(username: username, email: email, firstName: firstName, lastName: lastName, phone: phone, country: country, loserBracketMode: loserBracketMode)
// self.lastUpdate = Date()
// self.username = username
// self.firstName = firstName
// self.lastName = lastName
// self.email = email
// self.phone = phone
// self.country = country
// self.loserBracketMode = loserBracketMode
}
required init(from decoder: Decoder) throws {
try super.init(from: decoder)
}
required public init() {
super.init()
}
public func uuid() throws -> UUID {
if let uuid = UUID(uuidString: self.id) {
return uuid
}
throw UUIDError.cantConvertString(string: self.id)
}
public func defaultSignature(_ tournament: Tournament?) -> String {
let fullName = tournament?.umpireCustomContact ?? fullName()
return "Sportivement,\n\(fullName), votre JAP."
}
public func fullName() -> String {
[firstName, lastName].joined(separator: " ")
}
public func hasTenupClubs() -> Bool {
self.clubsObjects().filter({ $0.code != nil }).isEmpty == false
}
public func hasFavoriteClubsAndCreatedClubs() -> Bool {
clubsObjects(includeCreated: true).isEmpty == false
}
public func setUserClub(_ userClub: Club) {
self.clubs.insert(userClub.id, at: 0)
}
public func clubsObjects(includeCreated: Bool = false) -> [Club] {
return DataStore.shared.clubs.filter({ (includeCreated && $0.creator == id) || clubs.contains($0.id) })
}
public func createdClubsObjectsNotFavorite() -> [Club] {
return DataStore.shared.clubs.filter({ ($0.creator == id) && clubs.contains($0.id) == false })
}
public func saveMatchFormatsDefaultDuration(_ matchFormat: MatchFormat, estimatedDuration: Int) {
if estimatedDuration == matchFormat.defaultEstimatedDuration {
matchFormatsDefaultDuration?.removeValue(forKey: matchFormat)
} else {
matchFormatsDefaultDuration = matchFormatsDefaultDuration ?? [MatchFormat: Int]()
matchFormatsDefaultDuration?[matchFormat] = estimatedDuration
}
}
func addClub(_ club: Club) {
if !self.clubs.contains(where: { $0.id == club.id }) {
self.clubs.append(club.id)
}
}
public func getSummonsMessageSignature() -> String? {
if let summonsMessageSignature, summonsMessageSignature.isEmpty == false {
return summonsMessageSignature
} else {
return nil
}
}
// enum CodingKeys: String, CodingKey {
// case _id = "id"
// case _lastUpdate = "lastUpdate"
// case _username = "username"
// case _email = "email"
// case _clubs = "clubs"
// case _umpireCode = "umpireCode"
// case _licenceId = "licenceId"
// case _firstName = "firstName"
// case _lastName = "lastName"
// case _phone = "phone"
// case _country = "country"
// case _summonsMessageBody = "summonsMessageBody"
// case _summonsMessageSignature = "summonsMessageSignature"
// case _summonsAvailablePaymentMethods = "summonsAvailablePaymentMethods"
// case _summonsDisplayFormat = "summonsDisplayFormat"
// case _summonsDisplayEntryFee = "summonsDisplayEntryFee"
// case _summonsUseFullCustomMessage = "summonsUseFullCustomMessage"
// case _matchFormatsDefaultDuration = "matchFormatsDefaultDuration"
// case _bracketMatchFormatPreference = "bracketMatchFormatPreference"
// case _groupStageMatchFormatPreference = "groupStageMatchFormatPreference"
// case _loserBracketMatchFormatPreference = "loserBracketMatchFormatPreference"
// case _deviceId = "deviceId"
// case _loserBracketMode = "loserBracketMode"
// }
//
// public required init(from decoder: Decoder) throws {
// let container = try decoder.container(keyedBy: CodingKeys.self)
//
// // Required properties
// id = try container.decodeIfPresent(String.self, forKey: ._id) ?? Store.randomId()
// lastUpdate = try container.decodeIfPresent(Date.self, forKey: ._lastUpdate) ?? Date()
// username = try container.decode(String.self, forKey: ._username)
// email = try container.decode(String.self, forKey: ._email)
// firstName = try container.decode(String.self, forKey: ._firstName)
// lastName = try container.decode(String.self, forKey: ._lastName)
//
// // Optional properties
// clubs = try container.decodeIfPresent([String].self, forKey: ._clubs) ?? []
// umpireCode = try container.decodeIfPresent(String.self, forKey: ._umpireCode)
// licenceId = try container.decodeIfPresent(String.self, forKey: ._licenceId)
// phone = try container.decodeIfPresent(String.self, forKey: ._phone)
// country = try container.decodeIfPresent(String.self, forKey: ._country)
//
// // Summons-related properties
// summonsMessageBody = try container.decodeIfPresent(String.self, forKey: ._summonsMessageBody)
// summonsMessageSignature = try container.decodeIfPresent(String.self, forKey: ._summonsMessageSignature)
// summonsAvailablePaymentMethods = try container.decodeIfPresent(String.self, forKey: ._summonsAvailablePaymentMethods)
// summonsDisplayFormat = try container.decodeIfPresent(Bool.self, forKey: ._summonsDisplayFormat) ?? false
// summonsDisplayEntryFee = try container.decodeIfPresent(Bool.self, forKey: ._summonsDisplayEntryFee) ?? false
// summonsUseFullCustomMessage = try container.decodeIfPresent(Bool.self, forKey: ._summonsUseFullCustomMessage) ?? false
//
// // Match-related properties
// matchFormatsDefaultDuration = try container.decodeIfPresent([MatchFormat: Int].self, forKey: ._matchFormatsDefaultDuration)
// bracketMatchFormatPreference = try container.decodeIfPresent(MatchFormat.self, forKey: ._bracketMatchFormatPreference)
// groupStageMatchFormatPreference = try container.decodeIfPresent(MatchFormat.self, forKey: ._groupStageMatchFormatPreference)
// loserBracketMatchFormatPreference = try container.decodeIfPresent(MatchFormat.self, forKey: ._loserBracketMatchFormatPreference)
// loserBracketMode = try container.decodeIfPresent(LoserBracketMode.self, forKey: ._loserBracketMode) ?? .automatic
// }
//
// 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(username, forKey: ._username)
// try container.encode(email, forKey: ._email)
// try container.encode(clubs, forKey: ._clubs)
//
// try container.encode(umpireCode, forKey: ._umpireCode)
// try container.encode(licenceId, forKey: ._licenceId)
// try container.encode(firstName, forKey: ._firstName)
// try container.encode(lastName, forKey: ._lastName)
// try container.encode(phone, forKey: ._phone)
// try container.encode(country, forKey: ._country)
// try container.encode(summonsMessageBody, forKey: ._summonsMessageBody)
// try container.encode(summonsMessageSignature, forKey: ._summonsMessageSignature)
// try container.encode(summonsAvailablePaymentMethods, forKey: ._summonsAvailablePaymentMethods)
// try container.encode(summonsDisplayFormat, forKey: ._summonsDisplayFormat)
// try container.encode(summonsDisplayEntryFee, forKey: ._summonsDisplayEntryFee)
// try container.encode(summonsUseFullCustomMessage, forKey: ._summonsUseFullCustomMessage)
//
// try container.encode(matchFormatsDefaultDuration, forKey: ._matchFormatsDefaultDuration)
// try container.encode(bracketMatchFormatPreference, forKey: ._bracketMatchFormatPreference)
// try container.encode(groupStageMatchFormatPreference, forKey: ._groupStageMatchFormatPreference)
// try container.encode(loserBracketMatchFormatPreference, forKey: ._loserBracketMatchFormatPreference)
// try container.encode(deviceId, forKey: ._deviceId)
//
// try container.encode(loserBracketMode, forKey: ._loserBracketMode)
// }
static func placeHolder() -> CustomUser {
return CustomUser(username: "", email: "", firstName: "", lastName: "", phone: nil, country: nil)
}
}
public class UserCreationForm: CustomUser, UserPasswordBase {
public init(user: CustomUser, username: String, password: String, firstName: String, lastName: String, email: String, phone: String?, country: String?) {
self.password = password
super.init(username: username, email: email, firstName: firstName, lastName: lastName, phone: phone, country: country)
self.summonsMessageBody = user.summonsMessageBody
self.summonsMessageSignature = user.summonsMessageSignature
self.summonsAvailablePaymentMethods = user.summonsAvailablePaymentMethods
self.summonsDisplayFormat = user.summonsDisplayFormat
self.summonsDisplayEntryFee = user.summonsDisplayEntryFee
self.summonsUseFullCustomMessage = user.summonsUseFullCustomMessage
self.matchFormatsDefaultDuration = user.matchFormatsDefaultDuration
self.bracketMatchFormatPreference = user.bracketMatchFormatPreference
self.groupStageMatchFormatPreference = user.groupStageMatchFormatPreference
self.loserBracketMatchFormatPreference = user.loserBracketMatchFormatPreference
}
required init(from decoder: Decoder) throws {
fatalError("init(from:) has not been implemented")
}
required public init() {
self.password = ""
super.init()
}
public var password: String
private enum CodingKeys: String, CodingKey {
case password
}
public override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.password, forKey: .password)
}
}

@ -0,0 +1,331 @@
//
// DataStore.swift
// PadelClub
//
// Created by Laurent Morvillier on 02/02/2024.
//
import Foundation
import LeStorage
import SwiftUI
import Combine
public class DataStore: ObservableObject {
public static let shared = DataStore()
@Published public var user: CustomUser = CustomUser.placeHolder() {
didSet {
let loggedUser = StoreCenter.main.isAuthenticated
if loggedUser {
if self.user.id != self.userStorage.item()?.id {
self.userStorage.setItemNoSync(self.user)
StoreCenter.main.initialSynchronization(clear: false)
self._fixMissingClubCreatorIfNecessary(self.clubs)
self._fixMissingEventCreatorIfNecessary(self.events)
}
} else {
self._temporaryLocalUser.item = self.user
}
}
}
public fileprivate(set) var tournaments: SyncedCollection<Tournament>
public fileprivate(set) var clubs: SyncedCollection<Club>
public fileprivate(set) var courts: SyncedCollection<Court>
public fileprivate(set) var events: SyncedCollection<Event>
public fileprivate(set) var monthData: StoredCollection<MonthData>
public fileprivate(set) var dateIntervals: SyncedCollection<DateInterval>
public fileprivate(set) var purchases: SyncedCollection<Purchase>
fileprivate var userStorage: StoredSingleton<CustomUser>
fileprivate var _temporaryLocalUser: OptionalStorage<CustomUser> = OptionalStorage(fileName: "tmp_local_user.json")
public fileprivate(set) var appSettingsStorage: MicroStorage<AppSettings> = MicroStorage(fileName: "appsettings.json")
public var appSettings: AppSettings {
appSettingsStorage.item
}
init() {
let store = Store.main
let indexed: Bool = true
self.clubs = store.registerSynchronizedCollection(indexed: indexed)
self.courts = store.registerSynchronizedCollection(indexed: indexed)
self.tournaments = store.registerSynchronizedCollection(indexed: indexed)
self.events = store.registerSynchronizedCollection(indexed: indexed)
self.dateIntervals = store.registerSynchronizedCollection(indexed: indexed)
self.userStorage = store.registerObject(synchronized: true)
self.purchases = Store.main.registerSynchronizedCollection(inMemory: true)
self.monthData = store.registerCollection(indexed: indexed)
// Load ApiCallCollection, making them restart at launch and deletable on disconnect
StoreCenter.main.loadApiCallCollection(type: GroupStage.self)
StoreCenter.main.loadApiCallCollection(type: Round.self)
StoreCenter.main.loadApiCallCollection(type: PlayerRegistration.self)
StoreCenter.main.loadApiCallCollection(type: TeamRegistration.self)
StoreCenter.main.loadApiCallCollection(type: Match.self)
StoreCenter.main.loadApiCallCollection(type: TeamScore.self)
StoreCenter.main.loadApiCallCollection(type: DrawLog.self)
NotificationCenter.default.addObserver(self, selector: #selector(collectionDidLoad), name: NSNotification.Name.CollectionDidLoad, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(collectionDidUpdate), name: NSNotification.Name.CollectionDidChange, object: nil)
NotificationCenter.default.addObserver(
self,
selector: #selector(_willEnterForegroundNotification),
name: UIScene.willEnterForegroundNotification,
object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
public func saveUser() {
if user.username.count > 0 {
self.userStorage.update()
} else {
self._temporaryLocalUser.item = self.user
}
}
@objc func collectionDidLoad(notification: Notification) {
if let userSingleton: StoredSingleton<CustomUser> = notification.object as? StoredSingleton<CustomUser> {
self.user = userSingleton.item() ?? self._temporaryLocalUser.item ?? CustomUser.placeHolder()
} else if let clubsCollection: SyncedCollection<Club> = notification.object as? SyncedCollection<Club> {
self._fixMissingClubCreatorIfNecessary(clubsCollection)
} else if let eventsCollection: SyncedCollection<Event> = notification.object as? SyncedCollection<Event> {
self._fixMissingEventCreatorIfNecessary(eventsCollection)
}
if Store.main.fileCollectionsAllLoaded() {
AutomaticPatcher.applyAllWhenApplicable()
}
}
fileprivate func _fixMissingClubCreatorIfNecessary(_ clubsCollection: SyncedCollection<Club>) {
for club in clubsCollection {
if let userId = StoreCenter.main.userId, club.creator == nil {
club.creator = userId
self.userStorage.item()?.addClub(club)
self.userStorage.update()
clubsCollection.writeChangeAndInsertOnServer(instance: club)
}
}
}
fileprivate func _fixMissingEventCreatorIfNecessary(_ eventsCollection: SyncedCollection<Event>) {
for event in eventsCollection {
if let userId = StoreCenter.main.userId, event.creator == nil {
event.creator = userId
do {
try event.insertOnServer()
} catch {
Logger.error(error)
}
}
}
}
@objc func collectionDidUpdate(notification: Notification) {
self.objectWillChange.send()
}
@objc func _willEnterForegroundNotification() {
Task {
try await self.purchases.loadDataFromServerIfAllowed(clear: true)
}
}
public func disconnect() {
Task {
if await StoreCenter.main.hasPendingAPICalls() {
// todo qu'est ce qu'on fait des API Call ?
}
do {
let services = try StoreCenter.main.service()
try await services.logout()
} catch {
Logger.error(error)
}
DispatchQueue.main.async {
self._localDisconnect()
}
}
}
public func deleteAccount() {
Task {
do {
let services = try StoreCenter.main.service()
try await services.deleteAccount()
} catch {
Logger.error(error)
}
DispatchQueue.main.async {
self._localDisconnect()
}
}
}
public func deleteTournament(_ tournament: Tournament) {
let event = tournament.eventObject()
let isLastTournament = event?.tournaments.count == 1
self.tournaments.delete(instance: tournament)
if let event, isLastTournament {
self.events.delete(instance: event)
}
StoreCenter.main.destroyStore(identifier: tournament.id)
}
fileprivate func _localDisconnect() {
let tournamendIds: [String] = self.tournaments.map { $0.id }
TournamentLibrary.shared.reset()
self.tournaments.reset()
self.clubs.reset()
self.courts.reset()
self.events.reset()
self.dateIntervals.reset()
self.userStorage.reset()
self.purchases.reset()
Guard.main.disconnect()
StoreCenter.main.disconnect()
for tournament in tournamendIds {
StoreCenter.main.destroyStore(identifier: tournament.id)
}
self.user = self._temporaryLocalUser.item ?? CustomUser.placeHolder()
self.user.clubs.removeAll()
}
// func copyToLocalServer(tournament: Tournament) {
//
// Task {
// do {
//
// if let url = PListReader.readString(plist: "local", key: "local_server"),
// let login = PListReader.readString(plist: "local", key: "username"),
// let pass = PListReader.readString(plist: "local", key: "password") {
// let service = Services(url: url)
// let _: CustomUser = try await service.login(username: login, password: pass)
//
// tournament.event = nil
// _ = try await service.post(tournament)
//
// for groupStage in tournament.groupStages() {
// _ = try await service.post(groupStage)
// }
// for round in tournament.rounds() {
// try await self._insertRoundAndChildren(round: round, service: service)
// }
// for teamRegistration in tournament.unsortedTeams() {
// _ = try await service.post(teamRegistration)
// for playerRegistration in teamRegistration.unsortedPlayers() {
// _ = try await service.post(playerRegistration)
// }
// }
// for groupStage in tournament.groupStages() {
// for match in groupStage._matches() {
// try await self._insertMatch(match: match, service: service)
// }
// }
// for round in tournament.allRounds() {
// for match in round._matches() {
// try await self._insertMatch(match: match, service: service)
// }
// }
//
// }
// } catch {
// Logger.error(error)
// }
// }
//
// }
//
// fileprivate func _insertRoundAndChildren(round: Round, service: Services) async throws {
// _ = try await service.post(round)
// for loserRound in round.loserRounds() {
// try await self._insertRoundAndChildren(round: loserRound, service: service)
// }
// }
//
// fileprivate func _insertMatch(match: Match, service: Services) async throws {
// _ = try await service.post(match)
// for teamScore in match.teamScores {
// _ = try await service.post(teamScore)
// }
//
// }
// MARK: - Convenience
public func runningMatches() -> [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 {
if let store = tournament.tournamentStore {
let matches = store.matches.filter { match in
match.disabled == false && match.isRunning()
}
runningMatches.append(contentsOf: matches)
}
}
return runningMatches
}
public 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 {
if let store = tournament.tournamentStore {
let matches = store.matches.filter { match in
match.disabled == false && match.startDate != nil && match.endDate == nil }
runningMatches.append(contentsOf: matches)
}
}
return runningMatches
}
public 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 {
if let store = tournament.tournamentStore {
let matches = store.matches.filter { match in
match.disabled == false && match.hasEnded() }
runningMatches.append(contentsOf: matches)
}
}
return runningMatches.sorted(by: \.endDate!, order: .descending)
}
}

@ -0,0 +1,76 @@
//
// DateInterval.swift
// PadelClub
//
// Created by Razmig Sarkissian on 19/04/2024.
//
import Foundation
import SwiftUI
import LeStorage
@Observable
public final class DateInterval: BaseDateInterval {
// static func resourceName() -> String { return "date-intervals" }
// 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 courtIndex: Int
// var startDate: Date
// var endDate: Date
public init(event: String, courtIndex: Int, startDate: Date, endDate: Date) {
super.init(event: event, courtIndex: courtIndex, startDate: startDate, endDate: endDate)
// self.lastUpdate = Date()
// self.event = event
// self.courtIndex = courtIndex
// self.startDate = startDate
// self.endDate = endDate
}
required init(from decoder: any Decoder) throws {
try super.init(from: decoder)
}
required public init() {
super.init()
}
var range: Range<Date> {
startDate..<endDate
}
func isSingleDay() -> Bool {
Calendar.current.isDate(startDate, inSameDayAs: endDate)
}
func isDateInside(_ date: Date) -> Bool {
date >= startDate && date <= endDate
}
func isDateOutside(_ date: Date) -> Bool {
date <= startDate && date <= endDate && date >= startDate && date >= endDate
}
public override func deleteDependencies(shouldBeSynchronized: Bool) {
}
// enum CodingKeys: String, CodingKey {
// case _id = "id"
// case _lastUpdate = "lastUpdate"
// case _event = "event"
// case _courtIndex = "courtIndex"
// case _startDate = "startDate"
// case _endDate = "endDate"
// }
func insertOnServer() throws {
DataStore.shared.dateIntervals.writeChangeAndInsertOnServer(instance: self)
}
}

@ -0,0 +1,97 @@
//
// DrawLog.swift
// PadelClub
//
// Created by razmig on 22/10/2024.
//
import Foundation
import SwiftUI
import LeStorage
@Observable
public final class DrawLog: BaseDrawLog, SideStorable {
public func tournamentObject() -> Tournament? {
Store.main.findById(self.tournament)
}
public func computedBracketPosition() -> Int {
drawMatchIndex * 2 + drawTeamPosition.rawValue
}
public func updateTeamBracketPosition(_ team: TeamRegistration) {
guard let match = drawMatch() else { return }
let seedPosition: Int = match.lockAndGetSeedPosition(atTeamPosition: drawTeamPosition)
team.bracketPosition = seedPosition
tournamentObject()?.updateTeamScores(in: seedPosition)
}
public func exportedDrawLog() -> String {
[drawType.localizedDrawType(), drawDate.localizedDate(), localizedDrawLogLabel(), localizedDrawBranch()].filter({ $0.isEmpty == false }).joined(separator: " ")
}
public func localizedDrawSeedLabel() -> String {
return "\(drawType.localizedDrawType()) #\(drawSeed + 1)"
}
public func localizedDrawLogLabel() -> String {
return [localizedDrawSeedLabel(), positionLabel()].filter({ $0.isEmpty == false }).joined(separator: " -> ")
}
public func localizedDrawBranch() -> String {
switch drawType {
case .seed:
return drawTeamPosition.localizedBranchLabel()
default:
return ""
}
}
public 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
}
}
public func positionLabel() -> String {
return drawMatch()?.roundAndMatchTitle() ?? ""
}
public func roundLabel() -> String {
return drawMatch()?.roundTitle() ?? ""
}
public func matchLabel() -> String {
return drawMatch()?.matchTitle() ?? ""
}
public var tournamentStore: TournamentStore? {
return TournamentLibrary.shared.store(tournamentId: self.tournament)
}
public override func deleteDependencies(shouldBeSynchronized: Bool) {
}
}
public 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"
}
}
}

@ -0,0 +1,100 @@
//
// Event_v2.swift
// Padel Tournament
//
// Created by razmig on 10/03/2024.
//
import Foundation
import LeStorage
import SwiftUI
@Observable
public final class Event: BaseEvent {
public init(creator: String? = nil, club: String? = nil, name: String? = nil, tenupId: String? = nil) {
super.init(creator: creator, club: club, name: name, tenupId: tenupId)
self.relatedUser = creator
}
required init(from decoder: Decoder) throws {
try super.init(from: decoder)
}
required public init() {
super.init()
}
public override func deleteDependencies(shouldBeSynchronized: Bool) {
let tournaments = self.tournaments
for tournament in tournaments {
tournament.deleteDependencies(shouldBeSynchronized: shouldBeSynchronized)
}
DataStore.shared.tournaments.deleteDependencies(tournaments, shouldBeSynchronized: shouldBeSynchronized)
let courtsUnavailabilities = self.courtsUnavailability
for courtsUnavailability in courtsUnavailabilities {
courtsUnavailability.deleteDependencies(shouldBeSynchronized: shouldBeSynchronized)
}
DataStore.shared.dateIntervals.deleteDependencies(courtsUnavailabilities, shouldBeSynchronized: shouldBeSynchronized)
}
// MARK: - Computed dependencies
public var tournaments: [Tournament] {
DataStore.shared.tournaments.filter { $0.event == self.id && $0.isDeleted == false }
}
public func clubObject() -> Club? {
guard let club else { return nil }
return Store.main.findById(club)
}
public var courtsUnavailability: [DateInterval] {
DataStore.shared.dateIntervals.filter({ $0.event == id })
}
// MARK: -
public func eventCourtCount() -> Int {
tournaments.map { $0.courtCount }.max() ?? 2
}
public func eventStartDate() -> Date {
tournaments.map { $0.startDate }.min() ?? Date()
}
public func eventDayDuration() -> Int {
tournaments.map { $0.dayDuration }.max() ?? 1
}
public func eventTitle() -> String {
if let name, name.isEmpty == false {
return name
} else {
return "Événement"
}
}
public func existingBuild(_ build: any TournamentBuildHolder) -> Tournament? {
tournaments.first(where: { $0.isSameBuild(build) })
}
public func tournamentsCourtsUsed(exluding tournamentId: String) -> [DateInterval] {
tournaments.filter { $0.id != tournamentId }.flatMap({ tournament in
tournament.getPlayedMatchDateIntervals(in: self)
})
}
func insertOnServer() throws {
DataStore.shared.events.writeChangeAndInsertOnServer(instance: self)
for tournament in self.tournaments {
try tournament.insertOnServer()
}
for dataInterval in self.courtsUnavailability {
try dataInterval.insertOnServer()
}
}
}

@ -0,0 +1,149 @@
// Generated by SwiftModelGenerator
// Do not modify this file manually
import Foundation
import LeStorage
import SwiftUI
@Observable
public class BaseClub: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "clubs" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public var id: String = Store.randomId()
public var creator: String? = nil
public var name: String = ""
public var acronym: String = ""
public var phone: String? = nil
public var code: String? = nil
public var address: String? = nil
public var city: String? = nil
public var zipCode: String? = nil
public var latitude: Double? = nil
public var longitude: Double? = nil
public var courtCount: Int = 2
public var broadcastCode: String? = nil
public var timezone: String? = TimeZone.current.identifier
public init(
id: String = Store.randomId(),
creator: String? = nil,
name: String = "",
acronym: String = "",
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,
timezone: String? = TimeZone.current.identifier
) {
super.init()
self.id = id
self.creator = creator
self.name = name
self.acronym = 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
self.timezone = timezone
}
required public override init() {
super.init()
}
public enum CodingKeys: String, CodingKey {
case _id = "id"
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 _timezone = "timezone"
}
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.creator = try container.decodeIfPresent(String.self, forKey: ._creator) ?? nil
self.name = try container.decodeIfPresent(String.self, forKey: ._name) ?? ""
self.acronym = try container.decodeIfPresent(String.self, forKey: ._acronym) ?? ""
self.phone = try container.decodeIfPresent(String.self, forKey: ._phone) ?? nil
self.code = try container.decodeIfPresent(String.self, forKey: ._code) ?? nil
self.address = try container.decodeIfPresent(String.self, forKey: ._address) ?? nil
self.city = try container.decodeIfPresent(String.self, forKey: ._city) ?? nil
self.zipCode = try container.decodeIfPresent(String.self, forKey: ._zipCode) ?? nil
self.latitude = try container.decodeIfPresent(Double.self, forKey: ._latitude) ?? nil
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)
}
public 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.creator, forKey: ._creator)
try container.encode(self.name, forKey: ._name)
try container.encode(self.acronym, forKey: ._acronym)
try container.encode(self.phone, forKey: ._phone)
try container.encode(self.code, forKey: ._code)
try container.encode(self.address, forKey: ._address)
try container.encode(self.city, forKey: ._city)
try container.encode(self.zipCode, forKey: ._zipCode)
try container.encode(self.latitude, forKey: ._latitude)
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)
}
func creatorValue() -> CustomUser? {
guard let creator = self.creator else { return nil }
return Store.main.findById(creator)
}
public func copy(from other: any Storable) {
guard let club = other as? BaseClub else { return }
self.id = club.id
self.creator = club.creator
self.name = club.name
self.acronym = club.acronym
self.phone = club.phone
self.code = club.code
self.address = club.address
self.city = club.city
self.zipCode = club.zipCode
self.latitude = club.latitude
self.longitude = club.longitude
self.courtCount = club.courtCount
self.broadcastCode = club.broadcastCode
self.timezone = club.timezone
}
public static func relationships() -> [Relationship] {
return [
Relationship(type: CustomUser.self, keyPath: \BaseClub.creator),
]
}
}

@ -0,0 +1,92 @@
// Generated by SwiftModelGenerator
// Do not modify this file manually
import Foundation
import LeStorage
import SwiftUI
@Observable
public class BaseCourt: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "courts" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public var id: String = Store.randomId()
public var index: Int = 0
public var club: String = ""
public var name: String? = nil
public var exitAllowed: Bool = false
public var indoor: Bool = false
public init(
id: String = Store.randomId(),
index: Int = 0,
club: String = "",
name: String? = nil,
exitAllowed: Bool = false,
indoor: Bool = false
) {
super.init()
self.id = id
self.index = index
self.club = club
self.name = name
self.exitAllowed = exitAllowed
self.indoor = indoor
}
required public override init() {
super.init()
}
public enum CodingKeys: String, CodingKey {
case _id = "id"
case _index = "index"
case _club = "club"
case _name = "name"
case _exitAllowed = "exitAllowed"
case _indoor = "indoor"
}
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.index = try container.decodeIfPresent(Int.self, forKey: ._index) ?? 0
self.club = try container.decodeIfPresent(String.self, forKey: ._club) ?? ""
self.name = try container.decodeIfPresent(String.self, forKey: ._name) ?? nil
self.exitAllowed = try container.decodeIfPresent(Bool.self, forKey: ._exitAllowed) ?? false
self.indoor = try container.decodeIfPresent(Bool.self, forKey: ._indoor) ?? false
try super.init(from: decoder)
}
public 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.index, forKey: ._index)
try container.encode(self.club, forKey: ._club)
try container.encode(self.name, forKey: ._name)
try container.encode(self.exitAllowed, forKey: ._exitAllowed)
try container.encode(self.indoor, forKey: ._indoor)
try super.encode(to: encoder)
}
func clubValue() -> Club? {
return Store.main.findById(club)
}
public func copy(from other: any Storable) {
guard let court = other as? BaseCourt else { return }
self.id = court.id
self.index = court.index
self.club = court.club
self.name = court.name
self.exitAllowed = court.exitAllowed
self.indoor = court.indoor
}
public static func relationships() -> [Relationship] {
return [
Relationship(type: Club.self, keyPath: \BaseCourt.club),
]
}
}

@ -0,0 +1,205 @@
// Generated by SwiftModelGenerator
// Do not modify this file manually
import Foundation
import LeStorage
import SwiftUI
@Observable
public class BaseCustomUser: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "users" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [.post] }
public var id: String = Store.randomId()
public var username: String = ""
public var email: String = ""
public var clubs: [String] = []
public var umpireCode: String? = nil
public var licenceId: String? = nil
public var firstName: String = ""
public var lastName: String = ""
public var phone: String? = nil
public var country: String? = nil
public var summonsMessageBody: String? = nil
public var summonsMessageSignature: String? = nil
public var summonsAvailablePaymentMethods: String? = nil
public var summonsDisplayFormat: Bool = false
public var summonsDisplayEntryFee: Bool = false
public var summonsUseFullCustomMessage: Bool = false
public var matchFormatsDefaultDuration: [MatchFormat: Int]? = nil
public var bracketMatchFormatPreference: MatchFormat? = nil
public var groupStageMatchFormatPreference: MatchFormat? = nil
public var loserBracketMatchFormatPreference: MatchFormat? = nil
public var loserBracketMode: LoserBracketMode = .automatic
public var deviceId: String? = nil
public var agents: [String] = []
public init(
id: String = Store.randomId(),
username: String = "",
email: String = "",
clubs: [String] = [],
umpireCode: String? = nil,
licenceId: String? = nil,
firstName: String = "",
lastName: String = "",
phone: String? = nil,
country: String? = nil,
summonsMessageBody: String? = nil,
summonsMessageSignature: String? = nil,
summonsAvailablePaymentMethods: String? = nil,
summonsDisplayFormat: Bool = false,
summonsDisplayEntryFee: Bool = false,
summonsUseFullCustomMessage: Bool = false,
matchFormatsDefaultDuration: [MatchFormat: Int]? = nil,
bracketMatchFormatPreference: MatchFormat? = nil,
groupStageMatchFormatPreference: MatchFormat? = nil,
loserBracketMatchFormatPreference: MatchFormat? = nil,
loserBracketMode: LoserBracketMode = .automatic,
deviceId: String? = nil,
agents: [String] = []
) {
super.init()
self.id = id
self.username = username
self.email = email
self.clubs = clubs
self.umpireCode = umpireCode
self.licenceId = licenceId
self.firstName = firstName
self.lastName = lastName
self.phone = phone
self.country = country
self.summonsMessageBody = summonsMessageBody
self.summonsMessageSignature = summonsMessageSignature
self.summonsAvailablePaymentMethods = summonsAvailablePaymentMethods
self.summonsDisplayFormat = summonsDisplayFormat
self.summonsDisplayEntryFee = summonsDisplayEntryFee
self.summonsUseFullCustomMessage = summonsUseFullCustomMessage
self.matchFormatsDefaultDuration = matchFormatsDefaultDuration
self.bracketMatchFormatPreference = bracketMatchFormatPreference
self.groupStageMatchFormatPreference = groupStageMatchFormatPreference
self.loserBracketMatchFormatPreference = loserBracketMatchFormatPreference
self.loserBracketMode = loserBracketMode
self.deviceId = deviceId
self.agents = agents
}
required public override init() {
super.init()
}
public enum CodingKeys: String, CodingKey {
case _id = "id"
case _username = "username"
case _email = "email"
case _clubs = "clubs"
case _umpireCode = "umpireCode"
case _licenceId = "licenceId"
case _firstName = "firstName"
case _lastName = "lastName"
case _phone = "phone"
case _country = "country"
case _summonsMessageBody = "summonsMessageBody"
case _summonsMessageSignature = "summonsMessageSignature"
case _summonsAvailablePaymentMethods = "summonsAvailablePaymentMethods"
case _summonsDisplayFormat = "summonsDisplayFormat"
case _summonsDisplayEntryFee = "summonsDisplayEntryFee"
case _summonsUseFullCustomMessage = "summonsUseFullCustomMessage"
case _matchFormatsDefaultDuration = "matchFormatsDefaultDuration"
case _bracketMatchFormatPreference = "bracketMatchFormatPreference"
case _groupStageMatchFormatPreference = "groupStageMatchFormatPreference"
case _loserBracketMatchFormatPreference = "loserBracketMatchFormatPreference"
case _loserBracketMode = "loserBracketMode"
case _deviceId = "deviceId"
case _agents = "agents"
}
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.username = try container.decodeIfPresent(String.self, forKey: ._username) ?? ""
self.email = try container.decodeIfPresent(String.self, forKey: ._email) ?? ""
self.clubs = try container.decodeIfPresent([String].self, forKey: ._clubs) ?? []
self.umpireCode = try container.decodeIfPresent(String.self, forKey: ._umpireCode) ?? nil
self.licenceId = try container.decodeIfPresent(String.self, forKey: ._licenceId) ?? nil
self.firstName = try container.decodeIfPresent(String.self, forKey: ._firstName) ?? ""
self.lastName = try container.decodeIfPresent(String.self, forKey: ._lastName) ?? ""
self.phone = try container.decodeIfPresent(String.self, forKey: ._phone) ?? nil
self.country = try container.decodeIfPresent(String.self, forKey: ._country) ?? nil
self.summonsMessageBody = try container.decodeIfPresent(String.self, forKey: ._summonsMessageBody) ?? nil
self.summonsMessageSignature = try container.decodeIfPresent(String.self, forKey: ._summonsMessageSignature) ?? nil
self.summonsAvailablePaymentMethods = try container.decodeIfPresent(String.self, forKey: ._summonsAvailablePaymentMethods) ?? nil
self.summonsDisplayFormat = try container.decodeIfPresent(Bool.self, forKey: ._summonsDisplayFormat) ?? false
self.summonsDisplayEntryFee = try container.decodeIfPresent(Bool.self, forKey: ._summonsDisplayEntryFee) ?? false
self.summonsUseFullCustomMessage = try container.decodeIfPresent(Bool.self, forKey: ._summonsUseFullCustomMessage) ?? false
self.matchFormatsDefaultDuration = try container.decodeIfPresent([MatchFormat: Int].self, forKey: ._matchFormatsDefaultDuration) ?? nil
self.bracketMatchFormatPreference = try container.decodeIfPresent(MatchFormat.self, forKey: ._bracketMatchFormatPreference) ?? nil
self.groupStageMatchFormatPreference = try container.decodeIfPresent(MatchFormat.self, forKey: ._groupStageMatchFormatPreference) ?? nil
self.loserBracketMatchFormatPreference = try container.decodeIfPresent(MatchFormat.self, forKey: ._loserBracketMatchFormatPreference) ?? nil
self.loserBracketMode = try container.decodeIfPresent(LoserBracketMode.self, forKey: ._loserBracketMode) ?? .automatic
self.deviceId = try container.decodeIfPresent(String.self, forKey: ._deviceId) ?? nil
self.agents = try container.decodeIfPresent([String].self, forKey: ._agents) ?? []
try super.init(from: decoder)
}
public 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.username, forKey: ._username)
try container.encode(self.email, forKey: ._email)
try container.encode(self.clubs, forKey: ._clubs)
try container.encode(self.umpireCode, forKey: ._umpireCode)
try container.encode(self.licenceId, forKey: ._licenceId)
try container.encode(self.firstName, forKey: ._firstName)
try container.encode(self.lastName, forKey: ._lastName)
try container.encode(self.phone, forKey: ._phone)
try container.encode(self.country, forKey: ._country)
try container.encode(self.summonsMessageBody, forKey: ._summonsMessageBody)
try container.encode(self.summonsMessageSignature, forKey: ._summonsMessageSignature)
try container.encode(self.summonsAvailablePaymentMethods, forKey: ._summonsAvailablePaymentMethods)
try container.encode(self.summonsDisplayFormat, forKey: ._summonsDisplayFormat)
try container.encode(self.summonsDisplayEntryFee, forKey: ._summonsDisplayEntryFee)
try container.encode(self.summonsUseFullCustomMessage, forKey: ._summonsUseFullCustomMessage)
try container.encode(self.matchFormatsDefaultDuration, forKey: ._matchFormatsDefaultDuration)
try container.encode(self.bracketMatchFormatPreference, forKey: ._bracketMatchFormatPreference)
try container.encode(self.groupStageMatchFormatPreference, forKey: ._groupStageMatchFormatPreference)
try container.encode(self.loserBracketMatchFormatPreference, forKey: ._loserBracketMatchFormatPreference)
try container.encode(self.loserBracketMode, forKey: ._loserBracketMode)
try container.encode(self.deviceId, forKey: ._deviceId)
try container.encode(self.agents, forKey: ._agents)
try super.encode(to: encoder)
}
public func copy(from other: any Storable) {
guard let customuser = other as? BaseCustomUser else { return }
self.id = customuser.id
self.username = customuser.username
self.email = customuser.email
self.clubs = customuser.clubs
self.umpireCode = customuser.umpireCode
self.licenceId = customuser.licenceId
self.firstName = customuser.firstName
self.lastName = customuser.lastName
self.phone = customuser.phone
self.country = customuser.country
self.summonsMessageBody = customuser.summonsMessageBody
self.summonsMessageSignature = customuser.summonsMessageSignature
self.summonsAvailablePaymentMethods = customuser.summonsAvailablePaymentMethods
self.summonsDisplayFormat = customuser.summonsDisplayFormat
self.summonsDisplayEntryFee = customuser.summonsDisplayEntryFee
self.summonsUseFullCustomMessage = customuser.summonsUseFullCustomMessage
self.matchFormatsDefaultDuration = customuser.matchFormatsDefaultDuration
self.bracketMatchFormatPreference = customuser.bracketMatchFormatPreference
self.groupStageMatchFormatPreference = customuser.groupStageMatchFormatPreference
self.loserBracketMatchFormatPreference = customuser.loserBracketMatchFormatPreference
self.loserBracketMode = customuser.loserBracketMode
self.deviceId = customuser.deviceId
self.agents = customuser.agents
}
public static func relationships() -> [Relationship] {
return []
}
}

@ -0,0 +1,79 @@
// Generated by SwiftModelGenerator
// Do not modify this file manually
import Foundation
import LeStorage
import SwiftUI
@Observable
public class BaseDateInterval: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "date-intervals" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public var id: String = Store.randomId()
public var event: String = ""
public var courtIndex: Int = 0
public var startDate: Date = Date()
public var endDate: Date = Date()
public init(
id: String = Store.randomId(),
event: String = "",
courtIndex: Int = 0,
startDate: Date = Date(),
endDate: Date = Date()
) {
super.init()
self.id = id
self.event = event
self.courtIndex = courtIndex
self.startDate = startDate
self.endDate = endDate
}
required public override init() {
super.init()
}
public enum CodingKeys: String, CodingKey {
case _id = "id"
case _event = "event"
case _courtIndex = "courtIndex"
case _startDate = "startDate"
case _endDate = "endDate"
}
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.event = try container.decodeIfPresent(String.self, forKey: ._event) ?? ""
self.courtIndex = try container.decodeIfPresent(Int.self, forKey: ._courtIndex) ?? 0
self.startDate = try container.decodeIfPresent(Date.self, forKey: ._startDate) ?? Date()
self.endDate = try container.decodeIfPresent(Date.self, forKey: ._endDate) ?? Date()
try super.init(from: decoder)
}
public 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.event, forKey: ._event)
try container.encode(self.courtIndex, forKey: ._courtIndex)
try container.encode(self.startDate, forKey: ._startDate)
try container.encode(self.endDate, forKey: ._endDate)
try super.encode(to: encoder)
}
public func copy(from other: any Storable) {
guard let dateinterval = other as? BaseDateInterval else { return }
self.id = dateinterval.id
self.event = dateinterval.event
self.courtIndex = dateinterval.courtIndex
self.startDate = dateinterval.startDate
self.endDate = dateinterval.endDate
}
public static func relationships() -> [Relationship] {
return []
}
}

@ -0,0 +1,99 @@
// Generated by SwiftModelGenerator
// Do not modify this file manually
import Foundation
import LeStorage
import SwiftUI
@Observable
public class BaseDrawLog: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "draw-logs" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public var id: String = Store.randomId()
public var tournament: String = ""
public var drawDate: Date = Date()
public var drawSeed: Int = 0
public var drawMatchIndex: Int = 0
public var drawTeamPosition: TeamPosition = TeamPosition.one
public var drawType: DrawType = DrawType.seed
public 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
}
required public override init() {
super.init()
}
public 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)
}
public 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)
}
public 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
}
public static func relationships() -> [Relationship] {
return [
Relationship(type: Tournament.self, keyPath: \BaseDrawLog.tournament),
]
}
}

@ -0,0 +1,99 @@
// Generated by SwiftModelGenerator
// Do not modify this file manually
import Foundation
import LeStorage
import SwiftUI
@Observable
public class BaseEvent: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "events" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public var id: String = Store.randomId()
public var creator: String? = nil
public var club: String? = nil
public var creationDate: Date = Date()
public var name: String? = nil
public var tenupId: String? = nil
public init(
id: String = Store.randomId(),
creator: String? = nil,
club: String? = nil,
creationDate: Date = Date(),
name: String? = nil,
tenupId: String? = nil
) {
super.init()
self.id = id
self.creator = creator
self.club = club
self.creationDate = creationDate
self.name = name
self.tenupId = tenupId
}
required public override init() {
super.init()
}
public enum CodingKeys: String, CodingKey {
case _id = "id"
case _creator = "creator"
case _club = "club"
case _creationDate = "creationDate"
case _name = "name"
case _tenupId = "tenupId"
}
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.creator = try container.decodeIfPresent(String.self, forKey: ._creator) ?? nil
self.club = try container.decodeIfPresent(String.self, forKey: ._club) ?? nil
self.creationDate = try container.decodeIfPresent(Date.self, forKey: ._creationDate) ?? Date()
self.name = try container.decodeIfPresent(String.self, forKey: ._name) ?? nil
self.tenupId = try container.decodeIfPresent(String.self, forKey: ._tenupId) ?? nil
try super.init(from: decoder)
}
public 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.creator, forKey: ._creator)
try container.encode(self.club, forKey: ._club)
try container.encode(self.creationDate, forKey: ._creationDate)
try container.encode(self.name, forKey: ._name)
try container.encode(self.tenupId, forKey: ._tenupId)
try super.encode(to: encoder)
}
func creatorValue() -> CustomUser? {
guard let creator = self.creator else { return nil }
return Store.main.findById(creator)
}
func clubValue() -> Club? {
guard let club = self.club else { return nil }
return Store.main.findById(club)
}
public func copy(from other: any Storable) {
guard let event = other as? BaseEvent else { return }
self.id = event.id
self.creator = event.creator
self.club = event.club
self.creationDate = event.creationDate
self.name = event.name
self.tenupId = event.tenupId
}
public static func relationships() -> [Relationship] {
return [
Relationship(type: CustomUser.self, keyPath: \BaseEvent.creator),
Relationship(type: Club.self, keyPath: \BaseEvent.club),
]
}
}

@ -0,0 +1,106 @@
// Generated by SwiftModelGenerator
// Do not modify this file manually
import Foundation
import LeStorage
import SwiftUI
@Observable
public class BaseGroupStage: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "group-stages" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public var id: String = Store.randomId()
public var tournament: String = ""
public var index: Int = 0
public var size: Int = 0
public var format: MatchFormat? = nil
public var startDate: Date? = nil
public var name: String? = nil
public var step: Int = 0
public init(
id: String = Store.randomId(),
tournament: String = "",
index: Int = 0,
size: Int = 0,
format: MatchFormat? = nil,
startDate: Date? = nil,
name: String? = nil,
step: Int = 0
) {
super.init()
self.id = id
self.tournament = tournament
self.index = index
self.size = size
self.format = format
self.startDate = startDate
self.name = name
self.step = step
}
required public override init() {
super.init()
}
public enum CodingKeys: String, CodingKey {
case _id = "id"
case _tournament = "tournament"
case _index = "index"
case _size = "size"
case _format = "format"
case _startDate = "startDate"
case _name = "name"
case _step = "step"
}
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.index = try container.decodeIfPresent(Int.self, forKey: ._index) ?? 0
self.size = try container.decodeIfPresent(Int.self, forKey: ._size) ?? 0
self.format = try container.decodeIfPresent(MatchFormat.self, forKey: ._format) ?? nil
self.startDate = try container.decodeIfPresent(Date.self, forKey: ._startDate) ?? nil
self.name = try container.decodeIfPresent(String.self, forKey: ._name) ?? nil
self.step = try container.decodeIfPresent(Int.self, forKey: ._step) ?? 0
try super.init(from: decoder)
}
public 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.index, forKey: ._index)
try container.encode(self.size, forKey: ._size)
try container.encode(self.format, forKey: ._format)
try container.encode(self.startDate, forKey: ._startDate)
try container.encode(self.name, forKey: ._name)
try container.encode(self.step, forKey: ._step)
try super.encode(to: encoder)
}
func tournamentValue() -> Tournament? {
return Store.main.findById(tournament)
}
public func copy(from other: any Storable) {
guard let groupstage = other as? BaseGroupStage else { return }
self.id = groupstage.id
self.tournament = groupstage.tournament
self.index = groupstage.index
self.size = groupstage.size
self.format = groupstage.format
self.startDate = groupstage.startDate
self.name = groupstage.name
self.step = groupstage.step
}
public static func relationships() -> [Relationship] {
return [
Relationship(type: Tournament.self, keyPath: \BaseGroupStage.tournament),
]
}
}

@ -0,0 +1,155 @@
// Generated by SwiftModelGenerator
// Do not modify this file manually
import Foundation
import LeStorage
import SwiftUI
@Observable
public class BaseMatch: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "matches" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public var id: String = Store.randomId()
public var round: String? = nil
public var groupStage: String? = nil
public var startDate: Date? = nil
public var endDate: Date? = nil
public var index: Int = 0
public var format: MatchFormat? = nil
public var servingTeamId: String? = nil
public var winningTeamId: String? = nil
public var losingTeamId: String? = nil
public var name: String? = nil
public var disabled: Bool = false
public var courtIndex: Int? = nil
public var confirmed: Bool = false
public init(
id: String = Store.randomId(),
round: String? = nil,
groupStage: String? = nil,
startDate: Date? = nil,
endDate: Date? = nil,
index: Int = 0,
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()
self.id = id
self.round = round
self.groupStage = groupStage
self.startDate = startDate
self.endDate = endDate
self.index = index
self.format = format
self.servingTeamId = servingTeamId
self.winningTeamId = winningTeamId
self.losingTeamId = losingTeamId
self.name = name
self.disabled = disabled
self.courtIndex = courtIndex
self.confirmed = confirmed
}
required public override init() {
super.init()
}
public enum CodingKeys: String, CodingKey {
case _id = "id"
case _round = "round"
case _groupStage = "groupStage"
case _startDate = "startDate"
case _endDate = "endDate"
case _index = "index"
case _format = "format"
case _servingTeamId = "servingTeamId"
case _winningTeamId = "winningTeamId"
case _losingTeamId = "losingTeamId"
case _name = "name"
case _disabled = "disabled"
case _courtIndex = "courtIndex"
case _confirmed = "confirmed"
}
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.round = try container.decodeIfPresent(String.self, forKey: ._round) ?? nil
self.groupStage = try container.decodeIfPresent(String.self, forKey: ._groupStage) ?? nil
self.startDate = try container.decodeIfPresent(Date.self, forKey: ._startDate) ?? nil
self.endDate = try container.decodeIfPresent(Date.self, forKey: ._endDate) ?? nil
self.index = try container.decodeIfPresent(Int.self, forKey: ._index) ?? 0
self.format = try container.decodeIfPresent(MatchFormat.self, forKey: ._format) ?? nil
self.servingTeamId = try container.decodeIfPresent(String.self, forKey: ._servingTeamId) ?? nil
self.winningTeamId = try container.decodeIfPresent(String.self, forKey: ._winningTeamId) ?? nil
self.losingTeamId = try container.decodeIfPresent(String.self, forKey: ._losingTeamId) ?? nil
self.name = try container.decodeIfPresent(String.self, forKey: ._name) ?? nil
self.disabled = try container.decodeIfPresent(Bool.self, forKey: ._disabled) ?? false
self.courtIndex = try container.decodeIfPresent(Int.self, forKey: ._courtIndex) ?? nil
self.confirmed = try container.decodeIfPresent(Bool.self, forKey: ._confirmed) ?? false
try super.init(from: decoder)
}
public 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.round, forKey: ._round)
try container.encode(self.groupStage, forKey: ._groupStage)
try container.encode(self.startDate, forKey: ._startDate)
try container.encode(self.endDate, forKey: ._endDate)
try container.encode(self.index, forKey: ._index)
try container.encode(self.format, forKey: ._format)
try container.encode(self.servingTeamId, forKey: ._servingTeamId)
try container.encode(self.winningTeamId, forKey: ._winningTeamId)
try container.encode(self.losingTeamId, forKey: ._losingTeamId)
try container.encode(self.name, forKey: ._name)
try container.encode(self.disabled, forKey: ._disabled)
try container.encode(self.courtIndex, forKey: ._courtIndex)
try container.encode(self.confirmed, forKey: ._confirmed)
try super.encode(to: encoder)
}
func roundValue() -> Round? {
guard let round = self.round else { return nil }
return self.store?.findById(round)
}
func groupStageValue() -> GroupStage? {
guard let groupStage = self.groupStage else { return nil }
return self.store?.findById(groupStage)
}
public func copy(from other: any Storable) {
guard let match = other as? BaseMatch else { return }
self.id = match.id
self.round = match.round
self.groupStage = match.groupStage
self.startDate = match.startDate
self.endDate = match.endDate
self.index = match.index
self.format = match.format
self.servingTeamId = match.servingTeamId
self.winningTeamId = match.winningTeamId
self.losingTeamId = match.losingTeamId
self.name = match.name
self.disabled = match.disabled
self.courtIndex = match.courtIndex
self.confirmed = match.confirmed
}
public static func relationships() -> [Relationship] {
return [
Relationship(type: Round.self, keyPath: \BaseMatch.round),
Relationship(type: GroupStage.self, keyPath: \BaseMatch.groupStage),
]
}
}

@ -0,0 +1,162 @@
// Generated by SwiftModelGenerator
// Do not modify this file manually
import Foundation
import LeStorage
import SwiftUI
@Observable
public class BaseMatchScheduler: BaseModelObject, Storable {
public static func resourceName() -> String { return "match-scheduler" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public var id: String = Store.randomId()
public var tournament: String = ""
public var timeDifferenceLimit: Int = 5
public var loserBracketRotationDifference: Int = 0
public var upperBracketRotationDifference: Int = 1
public var accountUpperBracketBreakTime: Bool = true
public var accountLoserBracketBreakTime: Bool = false
public var randomizeCourts: Bool = true
public var rotationDifferenceIsImportant: Bool = false
public var shouldHandleUpperRoundSlice: Bool = false
public var shouldEndRoundBeforeStartingNext: Bool = true
public var groupStageChunkCount: Int? = nil
public var overrideCourtsUnavailability: Bool = false
public var shouldTryToFillUpCourtsAvailable: Bool = false
public var courtsAvailable: Set<Int> = Set<Int>()
public var simultaneousStart: Bool = true
public init(
id: String = Store.randomId(),
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,
groupStageChunkCount: Int? = nil,
overrideCourtsUnavailability: Bool = false,
shouldTryToFillUpCourtsAvailable: Bool = false,
courtsAvailable: Set<Int> = Set<Int>(),
simultaneousStart: Bool = true
) {
super.init()
self.id = id
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
}
required public override init() {
super.init()
}
public 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"
case _courtsAvailable = "courtsAvailable"
case _simultaneousStart = "simultaneousStart"
}
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.timeDifferenceLimit = try container.decodeIfPresent(Int.self, forKey: ._timeDifferenceLimit) ?? 5
self.loserBracketRotationDifference = try container.decodeIfPresent(Int.self, forKey: ._loserBracketRotationDifference) ?? 0
self.upperBracketRotationDifference = try container.decodeIfPresent(Int.self, forKey: ._upperBracketRotationDifference) ?? 1
self.accountUpperBracketBreakTime = try container.decodeIfPresent(Bool.self, forKey: ._accountUpperBracketBreakTime) ?? true
self.accountLoserBracketBreakTime = try container.decodeIfPresent(Bool.self, forKey: ._accountLoserBracketBreakTime) ?? false
self.randomizeCourts = try container.decodeIfPresent(Bool.self, forKey: ._randomizeCourts) ?? true
self.rotationDifferenceIsImportant = try container.decodeIfPresent(Bool.self, forKey: ._rotationDifferenceIsImportant) ?? false
self.shouldHandleUpperRoundSlice = try container.decodeIfPresent(Bool.self, forKey: ._shouldHandleUpperRoundSlice) ?? false
self.shouldEndRoundBeforeStartingNext = try container.decodeIfPresent(Bool.self, forKey: ._shouldEndRoundBeforeStartingNext) ?? true
self.groupStageChunkCount = try container.decodeIfPresent(Int.self, forKey: ._groupStageChunkCount) ?? nil
self.overrideCourtsUnavailability = try container.decodeIfPresent(Bool.self, forKey: ._overrideCourtsUnavailability) ?? false
self.shouldTryToFillUpCourtsAvailable = try container.decodeIfPresent(Bool.self, forKey: ._shouldTryToFillUpCourtsAvailable) ?? false
self.courtsAvailable = try container.decodeIfPresent(Set<Int>.self, forKey: ._courtsAvailable) ?? Set<Int>()
self.simultaneousStart = try container.decodeIfPresent(Bool.self, forKey: ._simultaneousStart) ?? true
try super.init(from: decoder)
}
public 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.timeDifferenceLimit, forKey: ._timeDifferenceLimit)
try container.encode(self.loserBracketRotationDifference, forKey: ._loserBracketRotationDifference)
try container.encode(self.upperBracketRotationDifference, forKey: ._upperBracketRotationDifference)
try container.encode(self.accountUpperBracketBreakTime, forKey: ._accountUpperBracketBreakTime)
try container.encode(self.accountLoserBracketBreakTime, forKey: ._accountLoserBracketBreakTime)
try container.encode(self.randomizeCourts, forKey: ._randomizeCourts)
try container.encode(self.rotationDifferenceIsImportant, forKey: ._rotationDifferenceIsImportant)
try container.encode(self.shouldHandleUpperRoundSlice, forKey: ._shouldHandleUpperRoundSlice)
try container.encode(self.shouldEndRoundBeforeStartingNext, forKey: ._shouldEndRoundBeforeStartingNext)
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)
}
func tournamentValue() -> Tournament? {
return Store.main.findById(tournament)
}
public func copy(from other: any Storable) {
guard let matchscheduler = other as? BaseMatchScheduler else { return }
self.id = matchscheduler.id
self.tournament = matchscheduler.tournament
self.timeDifferenceLimit = matchscheduler.timeDifferenceLimit
self.loserBracketRotationDifference = matchscheduler.loserBracketRotationDifference
self.upperBracketRotationDifference = matchscheduler.upperBracketRotationDifference
self.accountUpperBracketBreakTime = matchscheduler.accountUpperBracketBreakTime
self.accountLoserBracketBreakTime = matchscheduler.accountLoserBracketBreakTime
self.randomizeCourts = matchscheduler.randomizeCourts
self.rotationDifferenceIsImportant = matchscheduler.rotationDifferenceIsImportant
self.shouldHandleUpperRoundSlice = matchscheduler.shouldHandleUpperRoundSlice
self.shouldEndRoundBeforeStartingNext = matchscheduler.shouldEndRoundBeforeStartingNext
self.groupStageChunkCount = matchscheduler.groupStageChunkCount
self.overrideCourtsUnavailability = matchscheduler.overrideCourtsUnavailability
self.shouldTryToFillUpCourtsAvailable = matchscheduler.shouldTryToFillUpCourtsAvailable
self.courtsAvailable = matchscheduler.courtsAvailable
self.simultaneousStart = matchscheduler.simultaneousStart
}
public static func relationships() -> [Relationship] {
return [
Relationship(type: Tournament.self, keyPath: \BaseMatchScheduler.tournament),
]
}
}

@ -0,0 +1,121 @@
// Generated by SwiftModelGenerator
// Do not modify this file manually
import Foundation
import LeStorage
import SwiftUI
@Observable
public class BaseMonthData: BaseModelObject, Storable {
public static func resourceName() -> String { return "month-data" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public var id: String = Store.randomId()
public var monthKey: String = ""
public var creationDate: Date = Date()
public var maleUnrankedValue: Int? = nil
public var femaleUnrankedValue: Int? = nil
public var maleCount: Int? = nil
public var femaleCount: Int? = nil
public var anonymousCount: Int? = nil
public var incompleteMode: Bool = false
public var dataModelIdentifier: String? = nil
public var fileModelIdentifier: String? = nil
public init(
id: String = Store.randomId(),
monthKey: String = "",
creationDate: Date = Date(),
maleUnrankedValue: Int? = nil,
femaleUnrankedValue: Int? = nil,
maleCount: Int? = nil,
femaleCount: Int? = nil,
anonymousCount: Int? = nil,
incompleteMode: Bool = false,
dataModelIdentifier: String? = nil,
fileModelIdentifier: String? = nil
) {
super.init()
self.id = id
self.monthKey = monthKey
self.creationDate = creationDate
self.maleUnrankedValue = maleUnrankedValue
self.femaleUnrankedValue = femaleUnrankedValue
self.maleCount = maleCount
self.femaleCount = femaleCount
self.anonymousCount = anonymousCount
self.incompleteMode = incompleteMode
self.dataModelIdentifier = dataModelIdentifier
self.fileModelIdentifier = fileModelIdentifier
}
required public override init() {
super.init()
}
public enum CodingKeys: String, CodingKey {
case _id = "id"
case _monthKey = "monthKey"
case _creationDate = "creationDate"
case _maleUnrankedValue = "maleUnrankedValue"
case _femaleUnrankedValue = "femaleUnrankedValue"
case _maleCount = "maleCount"
case _femaleCount = "femaleCount"
case _anonymousCount = "anonymousCount"
case _incompleteMode = "incompleteMode"
case _dataModelIdentifier = "dataModelIdentifier"
case _fileModelIdentifier = "fileModelIdentifier"
}
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.monthKey = try container.decodeIfPresent(String.self, forKey: ._monthKey) ?? ""
self.creationDate = try container.decodeIfPresent(Date.self, forKey: ._creationDate) ?? Date()
self.maleUnrankedValue = try container.decodeIfPresent(Int.self, forKey: ._maleUnrankedValue) ?? nil
self.femaleUnrankedValue = try container.decodeIfPresent(Int.self, forKey: ._femaleUnrankedValue) ?? nil
self.maleCount = try container.decodeIfPresent(Int.self, forKey: ._maleCount) ?? nil
self.femaleCount = try container.decodeIfPresent(Int.self, forKey: ._femaleCount) ?? nil
self.anonymousCount = try container.decodeIfPresent(Int.self, forKey: ._anonymousCount) ?? nil
self.incompleteMode = try container.decodeIfPresent(Bool.self, forKey: ._incompleteMode) ?? false
self.dataModelIdentifier = try container.decodeIfPresent(String.self, forKey: ._dataModelIdentifier) ?? nil
self.fileModelIdentifier = try container.decodeIfPresent(String.self, forKey: ._fileModelIdentifier) ?? nil
try super.init(from: decoder)
}
public 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.monthKey, forKey: ._monthKey)
try container.encode(self.creationDate, forKey: ._creationDate)
try container.encode(self.maleUnrankedValue, forKey: ._maleUnrankedValue)
try container.encode(self.femaleUnrankedValue, forKey: ._femaleUnrankedValue)
try container.encode(self.maleCount, forKey: ._maleCount)
try container.encode(self.femaleCount, forKey: ._femaleCount)
try container.encode(self.anonymousCount, forKey: ._anonymousCount)
try container.encode(self.incompleteMode, forKey: ._incompleteMode)
try container.encode(self.dataModelIdentifier, forKey: ._dataModelIdentifier)
try container.encode(self.fileModelIdentifier, forKey: ._fileModelIdentifier)
try super.encode(to: encoder)
}
public func copy(from other: any Storable) {
guard let monthdata = other as? BaseMonthData else { return }
self.id = monthdata.id
self.monthKey = monthdata.monthKey
self.creationDate = monthdata.creationDate
self.maleUnrankedValue = monthdata.maleUnrankedValue
self.femaleUnrankedValue = monthdata.femaleUnrankedValue
self.maleCount = monthdata.maleCount
self.femaleCount = monthdata.femaleCount
self.anonymousCount = monthdata.anonymousCount
self.incompleteMode = monthdata.incompleteMode
self.dataModelIdentifier = monthdata.dataModelIdentifier
self.fileModelIdentifier = monthdata.fileModelIdentifier
}
public static func relationships() -> [Relationship] {
return []
}
}

@ -0,0 +1,205 @@
// Generated by SwiftModelGenerator
// Do not modify this file manually
import Foundation
import LeStorage
import SwiftUI
@Observable
public class BasePlayerRegistration: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "player-registrations" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public var id: String = Store.randomId()
public var teamRegistration: String? = nil
public var firstName: String = ""
public var lastName: String = ""
public var licenceId: String? = nil
public var rank: Int? = nil
public var paymentType: PlayerPaymentType? = nil
public var sex: PlayerSexType? = nil
public var tournamentPlayed: Int? = nil
public var points: Double? = nil
public var clubName: String? = nil
public var ligueName: String? = nil
public var assimilation: String? = nil
public var phoneNumber: String? = nil
public var email: String? = nil
public var birthdate: String? = nil
public var computedRank: Int = 0
public var source: PlayerRegistration.PlayerDataSource? = nil
public var hasArrived: Bool = false
public var coach: Bool = false
public var captain: Bool = false
public var registeredOnline: Bool = false
public init(
id: String = Store.randomId(),
teamRegistration: String? = nil,
firstName: String = "",
lastName: String = "",
licenceId: String? = nil,
rank: Int? = nil,
paymentType: PlayerPaymentType? = nil,
sex: PlayerSexType? = nil,
tournamentPlayed: Int? = nil,
points: Double? = nil,
clubName: String? = nil,
ligueName: String? = nil,
assimilation: String? = nil,
phoneNumber: String? = nil,
email: String? = nil,
birthdate: String? = nil,
computedRank: Int = 0,
source: PlayerRegistration.PlayerDataSource? = nil,
hasArrived: Bool = false,
coach: Bool = false,
captain: Bool = false,
registeredOnline: Bool = false
) {
super.init()
self.id = id
self.teamRegistration = teamRegistration
self.firstName = firstName
self.lastName = lastName
self.licenceId = licenceId
self.rank = rank
self.paymentType = paymentType
self.sex = sex
self.tournamentPlayed = tournamentPlayed
self.points = points
self.clubName = clubName
self.ligueName = ligueName
self.assimilation = assimilation
self.phoneNumber = phoneNumber
self.email = email
self.birthdate = birthdate
self.computedRank = computedRank
self.source = source
self.hasArrived = hasArrived
self.coach = coach
self.captain = captain
self.registeredOnline = registeredOnline
}
required public override init() {
super.init()
}
public enum CodingKeys: String, CodingKey {
case _id = "id"
case _teamRegistration = "teamRegistration"
case _firstName = "firstName"
case _lastName = "lastName"
case _licenceId = "licenceId"
case _rank = "rank"
case _paymentType = "paymentType"
case _sex = "sex"
case _tournamentPlayed = "tournamentPlayed"
case _points = "points"
case _clubName = "clubName"
case _ligueName = "ligueName"
case _assimilation = "assimilation"
case _phoneNumber = "phoneNumber"
case _email = "email"
case _birthdate = "birthdate"
case _computedRank = "computedRank"
case _source = "source"
case _hasArrived = "hasArrived"
case _coach = "coach"
case _captain = "captain"
case _registeredOnline = "registeredOnline"
}
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.teamRegistration = try container.decodeIfPresent(String.self, forKey: ._teamRegistration) ?? nil
self.firstName = try container.decodeIfPresent(String.self, forKey: ._firstName) ?? ""
self.lastName = try container.decodeIfPresent(String.self, forKey: ._lastName) ?? ""
self.licenceId = try container.decodeIfPresent(String.self, forKey: ._licenceId) ?? nil
self.rank = try container.decodeIfPresent(Int.self, forKey: ._rank) ?? nil
self.paymentType = try container.decodeIfPresent(PlayerPaymentType.self, forKey: ._paymentType) ?? nil
self.sex = try container.decodeIfPresent(PlayerSexType.self, forKey: ._sex) ?? nil
self.tournamentPlayed = try container.decodeIfPresent(Int.self, forKey: ._tournamentPlayed) ?? nil
self.points = try container.decodeIfPresent(Double.self, forKey: ._points) ?? nil
self.clubName = try container.decodeIfPresent(String.self, forKey: ._clubName) ?? nil
self.ligueName = try container.decodeIfPresent(String.self, forKey: ._ligueName) ?? nil
self.assimilation = try container.decodeIfPresent(String.self, forKey: ._assimilation) ?? nil
self.phoneNumber = try container.decodeIfPresent(String.self, forKey: ._phoneNumber) ?? nil
self.email = try container.decodeIfPresent(String.self, forKey: ._email) ?? nil
self.birthdate = try container.decodeIfPresent(String.self, forKey: ._birthdate) ?? nil
self.computedRank = try container.decodeIfPresent(Int.self, forKey: ._computedRank) ?? 0
self.source = try container.decodeIfPresent(PlayerRegistration.PlayerDataSource.self, forKey: ._source) ?? nil
self.hasArrived = try container.decodeIfPresent(Bool.self, forKey: ._hasArrived) ?? false
self.coach = try container.decodeIfPresent(Bool.self, forKey: ._coach) ?? false
self.captain = try container.decodeIfPresent(Bool.self, forKey: ._captain) ?? false
self.registeredOnline = try container.decodeIfPresent(Bool.self, forKey: ._registeredOnline) ?? false
try super.init(from: decoder)
}
public 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.teamRegistration, forKey: ._teamRegistration)
try container.encode(self.firstName, forKey: ._firstName)
try container.encode(self.lastName, forKey: ._lastName)
try container.encode(self.licenceId, forKey: ._licenceId)
try container.encode(self.rank, forKey: ._rank)
try container.encode(self.paymentType, forKey: ._paymentType)
try container.encode(self.sex, forKey: ._sex)
try container.encode(self.tournamentPlayed, forKey: ._tournamentPlayed)
try container.encode(self.points, forKey: ._points)
try container.encode(self.clubName, forKey: ._clubName)
try container.encode(self.ligueName, forKey: ._ligueName)
try container.encode(self.assimilation, forKey: ._assimilation)
try container.encode(self.phoneNumber, forKey: ._phoneNumber)
try container.encode(self.email, forKey: ._email)
try container.encode(self.birthdate, forKey: ._birthdate)
try container.encode(self.computedRank, forKey: ._computedRank)
try container.encode(self.source, forKey: ._source)
try container.encode(self.hasArrived, forKey: ._hasArrived)
try container.encode(self.coach, forKey: ._coach)
try container.encode(self.captain, forKey: ._captain)
try container.encode(self.registeredOnline, forKey: ._registeredOnline)
try super.encode(to: encoder)
}
func teamRegistrationValue() -> TeamRegistration? {
guard let teamRegistration = self.teamRegistration else { return nil }
return Store.main.findById(teamRegistration)
}
public func copy(from other: any Storable) {
guard let playerregistration = other as? BasePlayerRegistration else { return }
self.id = playerregistration.id
self.teamRegistration = playerregistration.teamRegistration
self.firstName = playerregistration.firstName
self.lastName = playerregistration.lastName
self.licenceId = playerregistration.licenceId
self.rank = playerregistration.rank
self.paymentType = playerregistration.paymentType
self.sex = playerregistration.sex
self.tournamentPlayed = playerregistration.tournamentPlayed
self.points = playerregistration.points
self.clubName = playerregistration.clubName
self.ligueName = playerregistration.ligueName
self.assimilation = playerregistration.assimilation
self.phoneNumber = playerregistration.phoneNumber
self.email = playerregistration.email
self.birthdate = playerregistration.birthdate
self.computedRank = playerregistration.computedRank
self.source = playerregistration.source
self.hasArrived = playerregistration.hasArrived
self.coach = playerregistration.coach
self.captain = playerregistration.captain
self.registeredOnline = playerregistration.registeredOnline
}
public static func relationships() -> [Relationship] {
return [
Relationship(type: TeamRegistration.self, keyPath: \BasePlayerRegistration.teamRegistration),
]
}
}

@ -0,0 +1,98 @@
// Generated by SwiftModelGenerator
// Do not modify this file manually
import Foundation
import LeStorage
public class BasePurchase: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "purchases" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public var id: UInt64 = 0
public var user: String = ""
public var purchaseDate: Date = Date()
public var productId: String = ""
public var quantity: Int? = nil
public var revocationDate: Date? = nil
public var expirationDate: Date? = nil
public init(
id: UInt64 = 0,
user: String = "",
purchaseDate: Date = Date(),
productId: String = "",
quantity: Int? = nil,
revocationDate: Date? = nil,
expirationDate: Date? = nil
) {
super.init()
self.id = id
self.user = user
self.purchaseDate = purchaseDate
self.productId = productId
self.quantity = quantity
self.revocationDate = revocationDate
self.expirationDate = expirationDate
}
required public override init() {
super.init()
}
public enum CodingKeys: String, CodingKey {
case id = "id"
case user = "user"
case purchaseDate = "purchaseDate"
case productId = "productId"
case quantity = "quantity"
case revocationDate = "revocationDate"
case expirationDate = "expirationDate"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decodeIfPresent(UInt64.self, forKey: .id) ?? 0
self.user = try container.decodeEncrypted(key: .user)
self.purchaseDate = try container.decodeIfPresent(Date.self, forKey: .purchaseDate) ?? Date()
self.productId = try container.decodeIfPresent(String.self, forKey: .productId) ?? ""
self.quantity = try container.decodeIfPresent(Int.self, forKey: .quantity) ?? nil
self.revocationDate = try container.decodeIfPresent(Date.self, forKey: .revocationDate) ?? nil
self.expirationDate = try container.decodeIfPresent(Date.self, forKey: .expirationDate) ?? nil
try super.init(from: decoder)
}
public override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.id, forKey: .id)
try container.encodeAndEncryptIfPresent(self.user.data(using: .utf8), forKey: .user)
try container.encode(self.purchaseDate, forKey: .purchaseDate)
try container.encode(self.productId, forKey: .productId)
try container.encode(self.quantity, forKey: .quantity)
try container.encode(self.revocationDate, forKey: .revocationDate)
try container.encode(self.expirationDate, forKey: .expirationDate)
try super.encode(to: encoder)
}
func userValue() -> CustomUser? {
return Store.main.findById(user)
}
public func copy(from other: any Storable) {
guard let purchase = other as? BasePurchase else { return }
self.id = purchase.id
self.user = purchase.user
self.purchaseDate = purchase.purchaseDate
self.productId = purchase.productId
self.quantity = purchase.quantity
self.revocationDate = purchase.revocationDate
self.expirationDate = purchase.expirationDate
}
public static func relationships() -> [Relationship] {
return [
Relationship(type: CustomUser.self, keyPath: \BasePurchase.user),
]
}
}

@ -0,0 +1,106 @@
// Generated by SwiftModelGenerator
// Do not modify this file manually
import Foundation
import LeStorage
import SwiftUI
@Observable
public class BaseRound: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "rounds" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public var id: String = Store.randomId()
public var tournament: String = ""
public var index: Int = 0
public var parent: String? = nil
public var format: MatchFormat? = nil
public var startDate: Date? = nil
public var groupStageLoserBracket: Bool = false
public var loserBracketMode: LoserBracketMode = .automatic
public init(
id: String = Store.randomId(),
tournament: String = "",
index: Int = 0,
parent: String? = nil,
format: MatchFormat? = nil,
startDate: Date? = nil,
groupStageLoserBracket: Bool = false,
loserBracketMode: LoserBracketMode = .automatic
) {
super.init()
self.id = id
self.tournament = tournament
self.index = index
self.parent = parent
self.format = format
self.startDate = startDate
self.groupStageLoserBracket = groupStageLoserBracket
self.loserBracketMode = loserBracketMode
}
required public override init() {
super.init()
}
public enum CodingKeys: String, CodingKey {
case _id = "id"
case _tournament = "tournament"
case _index = "index"
case _parent = "parent"
case _format = "format"
case _startDate = "startDate"
case _groupStageLoserBracket = "groupStageLoserBracket"
case _loserBracketMode = "loserBracketMode"
}
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.index = try container.decodeIfPresent(Int.self, forKey: ._index) ?? 0
self.parent = try container.decodeIfPresent(String.self, forKey: ._parent) ?? nil
self.format = try container.decodeIfPresent(MatchFormat.self, forKey: ._format) ?? nil
self.startDate = try container.decodeIfPresent(Date.self, forKey: ._startDate) ?? nil
self.groupStageLoserBracket = try container.decodeIfPresent(Bool.self, forKey: ._groupStageLoserBracket) ?? false
self.loserBracketMode = try container.decodeIfPresent(LoserBracketMode.self, forKey: ._loserBracketMode) ?? .automatic
try super.init(from: decoder)
}
public 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.index, forKey: ._index)
try container.encode(self.parent, forKey: ._parent)
try container.encode(self.format, forKey: ._format)
try container.encode(self.startDate, forKey: ._startDate)
try container.encode(self.groupStageLoserBracket, forKey: ._groupStageLoserBracket)
try container.encode(self.loserBracketMode, forKey: ._loserBracketMode)
try super.encode(to: encoder)
}
func tournamentValue() -> Tournament? {
return Store.main.findById(tournament)
}
public func copy(from other: any Storable) {
guard let round = other as? BaseRound else { return }
self.id = round.id
self.tournament = round.tournament
self.index = round.index
self.parent = round.parent
self.format = round.format
self.startDate = round.startDate
self.groupStageLoserBracket = round.groupStageLoserBracket
self.loserBracketMode = round.loserBracketMode
}
public static func relationships() -> [Relationship] {
return [
Relationship(type: Tournament.self, keyPath: \BaseRound.tournament),
]
}
}

@ -0,0 +1,198 @@
// Generated by SwiftModelGenerator
// Do not modify this file manually
import Foundation
import LeStorage
import SwiftUI
@Observable
public class BaseTeamRegistration: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "team-registrations" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public var id: String = Store.randomId()
public var tournament: String = ""
public var groupStage: String? = nil
public var registrationDate: Date? = nil
public var callDate: Date? = nil
public var bracketPosition: Int? = nil
public var groupStagePosition: Int? = nil
public var comment: String? = nil
public var source: String? = nil
public var sourceValue: String? = nil
public var logo: String? = nil
public var name: String? = nil
public var walkOut: Bool = false
public var wildCardBracket: Bool = false
public var wildCardGroupStage: Bool = false
public var weight: Int = 0
public var lockedWeight: Int? = nil
public var confirmationDate: Date? = nil
public var qualified: Bool = false
public var finalRanking: Int? = nil
public var pointsEarned: Int? = nil
public init(
id: String = Store.randomId(),
tournament: String = "",
groupStage: String? = nil,
registrationDate: Date? = nil,
callDate: Date? = nil,
bracketPosition: Int? = nil,
groupStagePosition: Int? = nil,
comment: String? = nil,
source: String? = nil,
sourceValue: String? = nil,
logo: String? = nil,
name: String? = nil,
walkOut: Bool = false,
wildCardBracket: Bool = false,
wildCardGroupStage: Bool = false,
weight: Int = 0,
lockedWeight: Int? = nil,
confirmationDate: Date? = nil,
qualified: Bool = false,
finalRanking: Int? = nil,
pointsEarned: Int? = nil
) {
super.init()
self.id = id
self.tournament = tournament
self.groupStage = groupStage
self.registrationDate = registrationDate
self.callDate = callDate
self.bracketPosition = bracketPosition
self.groupStagePosition = groupStagePosition
self.comment = comment
self.source = source
self.sourceValue = sourceValue
self.logo = logo
self.name = name
self.walkOut = walkOut
self.wildCardBracket = wildCardBracket
self.wildCardGroupStage = wildCardGroupStage
self.weight = weight
self.lockedWeight = lockedWeight
self.confirmationDate = confirmationDate
self.qualified = qualified
self.finalRanking = finalRanking
self.pointsEarned = pointsEarned
}
required public override init() {
super.init()
}
public enum CodingKeys: String, CodingKey {
case _id = "id"
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 _walkOut = "walkOut"
case _wildCardBracket = "wildCardBracket"
case _wildCardGroupStage = "wildCardGroupStage"
case _weight = "weight"
case _lockedWeight = "lockedWeight"
case _confirmationDate = "confirmationDate"
case _qualified = "qualified"
case _finalRanking = "finalRanking"
case _pointsEarned = "pointsEarned"
}
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.groupStage = try container.decodeIfPresent(String.self, forKey: ._groupStage) ?? nil
self.registrationDate = try container.decodeIfPresent(Date.self, forKey: ._registrationDate) ?? nil
self.callDate = try container.decodeIfPresent(Date.self, forKey: ._callDate) ?? nil
self.bracketPosition = try container.decodeIfPresent(Int.self, forKey: ._bracketPosition) ?? nil
self.groupStagePosition = try container.decodeIfPresent(Int.self, forKey: ._groupStagePosition) ?? nil
self.comment = try container.decodeIfPresent(String.self, forKey: ._comment) ?? nil
self.source = try container.decodeIfPresent(String.self, forKey: ._source) ?? nil
self.sourceValue = try container.decodeIfPresent(String.self, forKey: ._sourceValue) ?? nil
self.logo = try container.decodeIfPresent(String.self, forKey: ._logo) ?? nil
self.name = try container.decodeIfPresent(String.self, forKey: ._name) ?? nil
self.walkOut = try container.decodeIfPresent(Bool.self, forKey: ._walkOut) ?? false
self.wildCardBracket = try container.decodeIfPresent(Bool.self, forKey: ._wildCardBracket) ?? false
self.wildCardGroupStage = try container.decodeIfPresent(Bool.self, forKey: ._wildCardGroupStage) ?? false
self.weight = try container.decodeIfPresent(Int.self, forKey: ._weight) ?? 0
self.lockedWeight = try container.decodeIfPresent(Int.self, forKey: ._lockedWeight) ?? nil
self.confirmationDate = try container.decodeIfPresent(Date.self, forKey: ._confirmationDate) ?? nil
self.qualified = try container.decodeIfPresent(Bool.self, forKey: ._qualified) ?? false
self.finalRanking = try container.decodeIfPresent(Int.self, forKey: ._finalRanking) ?? nil
self.pointsEarned = try container.decodeIfPresent(Int.self, forKey: ._pointsEarned) ?? nil
try super.init(from: decoder)
}
public 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.groupStage, forKey: ._groupStage)
try container.encode(self.registrationDate, forKey: ._registrationDate)
try container.encode(self.callDate, forKey: ._callDate)
try container.encode(self.bracketPosition, forKey: ._bracketPosition)
try container.encode(self.groupStagePosition, forKey: ._groupStagePosition)
try container.encode(self.comment, forKey: ._comment)
try container.encode(self.source, forKey: ._source)
try container.encode(self.sourceValue, forKey: ._sourceValue)
try container.encode(self.logo, forKey: ._logo)
try container.encode(self.name, forKey: ._name)
try container.encode(self.walkOut, forKey: ._walkOut)
try container.encode(self.wildCardBracket, forKey: ._wildCardBracket)
try container.encode(self.wildCardGroupStage, forKey: ._wildCardGroupStage)
try container.encode(self.weight, forKey: ._weight)
try container.encode(self.lockedWeight, forKey: ._lockedWeight)
try container.encode(self.confirmationDate, forKey: ._confirmationDate)
try container.encode(self.qualified, forKey: ._qualified)
try container.encode(self.finalRanking, forKey: ._finalRanking)
try container.encode(self.pointsEarned, forKey: ._pointsEarned)
try super.encode(to: encoder)
}
func groupStageValue() -> GroupStage? {
guard let groupStage = self.groupStage else { return nil }
return self.store?.findById(groupStage)
}
public func copy(from other: any Storable) {
guard let teamregistration = other as? BaseTeamRegistration else { return }
self.id = teamregistration.id
self.tournament = teamregistration.tournament
self.groupStage = teamregistration.groupStage
self.registrationDate = teamregistration.registrationDate
self.callDate = teamregistration.callDate
self.bracketPosition = teamregistration.bracketPosition
self.groupStagePosition = teamregistration.groupStagePosition
self.comment = teamregistration.comment
self.source = teamregistration.source
self.sourceValue = teamregistration.sourceValue
self.logo = teamregistration.logo
self.name = teamregistration.name
self.walkOut = teamregistration.walkOut
self.wildCardBracket = teamregistration.wildCardBracket
self.wildCardGroupStage = teamregistration.wildCardGroupStage
self.weight = teamregistration.weight
self.lockedWeight = teamregistration.lockedWeight
self.confirmationDate = teamregistration.confirmationDate
self.qualified = teamregistration.qualified
self.finalRanking = teamregistration.finalRanking
self.pointsEarned = teamregistration.pointsEarned
}
public static func relationships() -> [Relationship] {
return [
Relationship(type: GroupStage.self, keyPath: \BaseTeamRegistration.groupStage),
]
}
}

@ -0,0 +1,98 @@
// Generated by SwiftModelGenerator
// Do not modify this file manually
import Foundation
import LeStorage
import SwiftUI
@Observable
public class BaseTeamScore: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "team-scores" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public var id: String = Store.randomId()
public var match: String = ""
public var teamRegistration: String? = nil
public var score: String? = nil
public var walkOut: Int? = nil
public var luckyLoser: Int? = nil
public init(
id: String = Store.randomId(),
match: String = "",
teamRegistration: String? = nil,
score: String? = nil,
walkOut: Int? = nil,
luckyLoser: Int? = nil
) {
super.init()
self.id = id
self.match = match
self.teamRegistration = teamRegistration
self.score = score
self.walkOut = walkOut
self.luckyLoser = luckyLoser
}
required public override init() {
super.init()
}
public enum CodingKeys: String, CodingKey {
case _id = "id"
case _match = "match"
case _teamRegistration = "teamRegistration"
case _score = "score"
case _walkOut = "walkOut"
case _luckyLoser = "luckyLoser"
}
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.match = try container.decodeIfPresent(String.self, forKey: ._match) ?? ""
self.teamRegistration = try container.decodeIfPresent(String.self, forKey: ._teamRegistration) ?? nil
self.score = try container.decodeIfPresent(String.self, forKey: ._score) ?? nil
self.walkOut = try container.decodeIfPresent(Int.self, forKey: ._walkOut) ?? nil
self.luckyLoser = try container.decodeIfPresent(Int.self, forKey: ._luckyLoser) ?? nil
try super.init(from: decoder)
}
public 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.match, forKey: ._match)
try container.encode(self.teamRegistration, forKey: ._teamRegistration)
try container.encode(self.score, forKey: ._score)
try container.encode(self.walkOut, forKey: ._walkOut)
try container.encode(self.luckyLoser, forKey: ._luckyLoser)
try super.encode(to: encoder)
}
func matchValue() -> Match? {
return self.store?.findById(match)
}
func teamRegistrationValue() -> TeamRegistration? {
guard let teamRegistration = self.teamRegistration else { return nil }
return self.store?.findById(teamRegistration)
}
public func copy(from other: any Storable) {
guard let teamscore = other as? BaseTeamScore else { return }
self.id = teamscore.id
self.match = teamscore.match
self.teamRegistration = teamscore.teamRegistration
self.score = teamscore.score
self.walkOut = teamscore.walkOut
self.luckyLoser = teamscore.luckyLoser
}
public static func relationships() -> [Relationship] {
return [
Relationship(type: Match.self, keyPath: \BaseTeamScore.match),
Relationship(type: TeamRegistration.self, keyPath: \BaseTeamScore.teamRegistration),
]
}
}

@ -0,0 +1,533 @@
// Generated by SwiftModelGenerator
// Do not modify this file manually
import Foundation
import LeStorage
import SwiftUI
@Observable
public class BaseTournament: SyncedModelObject, SyncedStorable {
public static func resourceName() -> String { return "tournaments" }
public static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
public var id: String = Store.randomId()
public var event: String? = nil
public var name: String? = nil
public var startDate: Date = Date()
public var endDate: Date? = nil
public var creationDate: Date = Date()
public var isPrivate: Bool = false
public var groupStageFormat: MatchFormat? = nil
public var roundFormat: MatchFormat? = nil
public var loserRoundFormat: MatchFormat? = nil
public var groupStageSortMode: GroupStageOrderingMode = GroupStageOrderingMode.snake
public var groupStageCount: Int = 0
public var rankSourceDate: Date? = nil
public var dayDuration: Int = 0
public var teamCount: Int = 0
public var teamSorting: TeamSortingType = TeamSortingType.inscriptionDate
public var federalCategory: TournamentCategory = TournamentCategory.men
public var federalLevelCategory: TournamentLevel = TournamentLevel.unlisted
public var federalAgeCategory: FederalTournamentAge = FederalTournamentAge.unlisted
public var closedRegistrationDate: Date? = nil
public var groupStageAdditionalQualified: Int = 0
public var courtCount: Int = 2
public var prioritizeClubMembers: Bool = false
public var qualifiedPerGroupStage: Int = 0
public var teamsPerGroupStage: Int = 0
public var entryFee: Double? = nil
public var payment: TournamentPayment? = nil
public var additionalEstimationDuration: Int = 0
public var isDeleted: Bool = false
public var isCanceled: Bool = false
public var publishTeams: Bool = false
public var publishSummons: Bool = false
public var publishGroupStages: Bool = false
public var publishBrackets: Bool = false
public var shouldVerifyGroupStage: Bool = false
public var shouldVerifyBracket: Bool = false
public var hideTeamsWeight: Bool = false
public var publishTournament: Bool = false
public var hidePointsEarned: Bool = false
public var publishRankings: Bool = false
public var loserBracketMode: LoserBracketMode = .automatic
public var initialSeedRound: Int = 0
public var initialSeedCount: Int = 0
public var enableOnlineRegistration: Bool = false
public var registrationDateLimit: Date? = nil
public var openingRegistrationDate: Date? = nil
public var waitingListLimit: Int? = nil
public var accountIsRequired: Bool = true
public var licenseIsRequired: Bool = true
public var minimumPlayerPerTeam: Int = 2
public var maximumPlayerPerTeam: Int = 2
public var information: String? = nil
public var umpireCustomMail: String? = nil
public var umpireCustomContact: String? = nil
public var umpireCustomPhone: String? = nil
public var hideUmpireMail: Bool = false
public var hideUmpirePhone: Bool = true
public var disableRankingFederalRuling: Bool = false
public var teamCountLimit: Bool = true
public init(
id: String = Store.randomId(),
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 = GroupStageOrderingMode.snake,
groupStageCount: Int = 0,
rankSourceDate: Date? = nil,
dayDuration: Int = 0,
teamCount: Int = 0,
teamSorting: TeamSortingType = TeamSortingType.inscriptionDate,
federalCategory: TournamentCategory = TournamentCategory.men,
federalLevelCategory: TournamentLevel = TournamentLevel.unlisted,
federalAgeCategory: FederalTournamentAge = FederalTournamentAge.unlisted,
closedRegistrationDate: Date? = nil,
groupStageAdditionalQualified: Int = 0,
courtCount: Int = 2,
prioritizeClubMembers: Bool = false,
qualifiedPerGroupStage: Int = 0,
teamsPerGroupStage: Int = 0,
entryFee: Double? = nil,
payment: TournamentPayment? = nil,
additionalEstimationDuration: Int = 0,
isDeleted: Bool = false,
isCanceled: Bool = false,
publishTeams: Bool = false,
publishSummons: Bool = false,
publishGroupStages: Bool = false,
publishBrackets: Bool = false,
shouldVerifyGroupStage: Bool = false,
shouldVerifyBracket: Bool = false,
hideTeamsWeight: Bool = false,
publishTournament: Bool = false,
hidePointsEarned: Bool = false,
publishRankings: Bool = false,
loserBracketMode: LoserBracketMode = .automatic,
initialSeedRound: Int = 0,
initialSeedCount: Int = 0,
enableOnlineRegistration: Bool = false,
registrationDateLimit: Date? = nil,
openingRegistrationDate: Date? = nil,
waitingListLimit: Int? = nil,
accountIsRequired: Bool = true,
licenseIsRequired: Bool = true,
minimumPlayerPerTeam: Int = 2,
maximumPlayerPerTeam: Int = 2,
information: String? = nil,
umpireCustomMail: String? = nil,
umpireCustomContact: String? = nil,
umpireCustomPhone: String? = nil,
hideUmpireMail: Bool = false,
hideUmpirePhone: Bool = true,
disableRankingFederalRuling: Bool = false,
teamCountLimit: Bool = true
) {
super.init()
self.id = id
self.event = event
self.name = name
self.startDate = startDate
self.endDate = endDate
self.creationDate = creationDate
self.isPrivate = isPrivate
self.groupStageFormat = groupStageFormat
self.roundFormat = roundFormat
self.loserRoundFormat = loserRoundFormat
self.groupStageSortMode = groupStageSortMode
self.groupStageCount = groupStageCount
self.rankSourceDate = rankSourceDate
self.dayDuration = dayDuration
self.teamCount = teamCount
self.teamSorting = teamSorting
self.federalCategory = federalCategory
self.federalLevelCategory = federalLevelCategory
self.federalAgeCategory = federalAgeCategory
self.closedRegistrationDate = closedRegistrationDate
self.groupStageAdditionalQualified = groupStageAdditionalQualified
self.courtCount = courtCount
self.prioritizeClubMembers = prioritizeClubMembers
self.qualifiedPerGroupStage = qualifiedPerGroupStage
self.teamsPerGroupStage = teamsPerGroupStage
self.entryFee = entryFee
self.payment = payment
self.additionalEstimationDuration = additionalEstimationDuration
self.isDeleted = isDeleted
self.isCanceled = isCanceled
self.publishTeams = publishTeams
self.publishSummons = publishSummons
self.publishGroupStages = publishGroupStages
self.publishBrackets = publishBrackets
self.shouldVerifyGroupStage = shouldVerifyGroupStage
self.shouldVerifyBracket = shouldVerifyBracket
self.hideTeamsWeight = hideTeamsWeight
self.publishTournament = publishTournament
self.hidePointsEarned = hidePointsEarned
self.publishRankings = publishRankings
self.loserBracketMode = loserBracketMode
self.initialSeedRound = initialSeedRound
self.initialSeedCount = initialSeedCount
self.enableOnlineRegistration = enableOnlineRegistration
self.registrationDateLimit = registrationDateLimit
self.openingRegistrationDate = openingRegistrationDate
self.waitingListLimit = waitingListLimit
self.accountIsRequired = accountIsRequired
self.licenseIsRequired = licenseIsRequired
self.minimumPlayerPerTeam = minimumPlayerPerTeam
self.maximumPlayerPerTeam = maximumPlayerPerTeam
self.information = information
self.umpireCustomMail = umpireCustomMail
self.umpireCustomContact = umpireCustomContact
self.umpireCustomPhone = umpireCustomPhone
self.hideUmpireMail = hideUmpireMail
self.hideUmpirePhone = hideUmpirePhone
self.disableRankingFederalRuling = disableRankingFederalRuling
self.teamCountLimit = teamCountLimit
}
required public override init() {
super.init()
}
public enum CodingKeys: String, CodingKey {
case isCanceled = "isCanceled"
case payment = "payment"
case _id = "id"
case _event = "event"
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 _closedRegistrationDate = "closedRegistrationDate"
case _groupStageAdditionalQualified = "groupStageAdditionalQualified"
case _courtCount = "courtCount"
case _prioritizeClubMembers = "prioritizeClubMembers"
case _qualifiedPerGroupStage = "qualifiedPerGroupStage"
case _teamsPerGroupStage = "teamsPerGroupStage"
case _entryFee = "entryFee"
case _payment = "globalId"
case _additionalEstimationDuration = "additionalEstimationDuration"
case _isDeleted = "isDeleted"
case _isCanceled = "localId"
case _publishTeams = "publishTeams"
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"
case _initialSeedRound = "initialSeedRound"
case _initialSeedCount = "initialSeedCount"
case _enableOnlineRegistration = "enableOnlineRegistration"
case _registrationDateLimit = "registrationDateLimit"
case _openingRegistrationDate = "openingRegistrationDate"
case _waitingListLimit = "waitingListLimit"
case _accountIsRequired = "accountIsRequired"
case _licenseIsRequired = "licenseIsRequired"
case _minimumPlayerPerTeam = "minimumPlayerPerTeam"
case _maximumPlayerPerTeam = "maximumPlayerPerTeam"
case _information = "information"
case _umpireCustomMail = "umpireCustomMail"
case _umpireCustomContact = "umpireCustomContact"
case _umpireCustomPhone = "umpireCustomPhone"
case _hideUmpireMail = "hideUmpireMail"
case _hideUmpirePhone = "hideUmpirePhone"
case _disableRankingFederalRuling = "disableRankingFederalRuling"
case _teamCountLimit = "teamCountLimit"
}
private static func _decodePayment(container: KeyedDecodingContainer<CodingKeys>) throws -> TournamentPayment? {
var data = try container.decodeIfPresent(Data.self, forKey: ._payment)
if data == nil {
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.standard.number(from: String($0))?.intValue }
return TournamentPayment(rawValue: sequence[18])
} catch {
Logger.error(error)
}
}
return nil
}
private func _encodePayment(container: inout KeyedEncodingContainer<CodingKeys>) throws {
guard let payment else {
try container.encodeNil(forKey: ._payment)
return
}
let max: Int = TournamentPayment.allCases.count
var sequence = (1...18).map { _ in Int.random(in: (0..<max)) }
sequence.append(payment.rawValue)
sequence.append(contentsOf: (1...13).map { _ in Int.random(in: (0..<max ))} )
let stringCombo: [String] = sequence.map { $0.formatted() }
let joined: String = stringCombo.joined(separator: "")
if let data = joined.data(using: .utf8) {
let encryped: Data = try data.encrypt(pass: CryptoKey.pass.rawValue)
try container.encodeIfPresent(encryped, forKey: ._payment)
}
}
private static func _decodeIscanceled(container: KeyedDecodingContainer<CodingKeys>) throws -> Bool {
var data = try container.decodeIfPresent(Data.self, forKey: ._isCanceled)
if data == nil {
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.standard.number(from: String($0))?.intValue }
return Bool.decodeInt(sequence[18])
} catch {
Logger.error(error)
}
}
return false
}
private func _encodeIscanceled(container: inout KeyedEncodingContainer<CodingKeys>) throws {
let max: Int = 9
var sequence = (1...18).map { _ in Int.random(in: (0...max)) }
sequence.append(self.isCanceled.encodedValue)
sequence.append(contentsOf: (1...13).map { _ in Int.random(in: (0...max ))} )
let stringCombo: [String] = sequence.map { $0.formatted() }
let joined: String = stringCombo.joined(separator: "")
if let data = joined.data(using: .utf8) {
let encryped: Data = try data.encrypt(pass: CryptoKey.pass.rawValue)
try container.encode(encryped, forKey: ._isCanceled)
}
}
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.event = try container.decodeIfPresent(String.self, forKey: ._event) ?? nil
self.name = try container.decodeIfPresent(String.self, forKey: ._name) ?? nil
self.startDate = try container.decodeIfPresent(Date.self, forKey: ._startDate) ?? Date()
self.endDate = try container.decodeIfPresent(Date.self, forKey: ._endDate) ?? nil
self.creationDate = try container.decodeIfPresent(Date.self, forKey: ._creationDate) ?? Date()
self.isPrivate = try container.decodeIfPresent(Bool.self, forKey: ._isPrivate) ?? false
self.groupStageFormat = try container.decodeIfPresent(MatchFormat.self, forKey: ._groupStageFormat) ?? nil
self.roundFormat = try container.decodeIfPresent(MatchFormat.self, forKey: ._roundFormat) ?? nil
self.loserRoundFormat = try container.decodeIfPresent(MatchFormat.self, forKey: ._loserRoundFormat) ?? nil
self.groupStageSortMode = try container.decodeIfPresent(GroupStageOrderingMode.self, forKey: ._groupStageSortMode) ?? GroupStageOrderingMode.snake
self.groupStageCount = try container.decodeIfPresent(Int.self, forKey: ._groupStageCount) ?? 0
self.rankSourceDate = try container.decodeIfPresent(Date.self, forKey: ._rankSourceDate) ?? nil
self.dayDuration = try container.decodeIfPresent(Int.self, forKey: ._dayDuration) ?? 0
self.teamCount = try container.decodeIfPresent(Int.self, forKey: ._teamCount) ?? 0
self.teamSorting = try container.decodeIfPresent(TeamSortingType.self, forKey: ._teamSorting) ?? TeamSortingType.inscriptionDate
self.federalCategory = try container.decodeIfPresent(TournamentCategory.self, forKey: ._federalCategory) ?? TournamentCategory.men
self.federalLevelCategory = try container.decodeIfPresent(TournamentLevel.self, forKey: ._federalLevelCategory) ?? TournamentLevel.unlisted
self.federalAgeCategory = try container.decodeIfPresent(FederalTournamentAge.self, forKey: ._federalAgeCategory) ?? FederalTournamentAge.unlisted
self.closedRegistrationDate = try container.decodeIfPresent(Date.self, forKey: ._closedRegistrationDate) ?? nil
self.groupStageAdditionalQualified = try container.decodeIfPresent(Int.self, forKey: ._groupStageAdditionalQualified) ?? 0
self.courtCount = try container.decodeIfPresent(Int.self, forKey: ._courtCount) ?? 2
self.prioritizeClubMembers = try container.decodeIfPresent(Bool.self, forKey: ._prioritizeClubMembers) ?? false
self.qualifiedPerGroupStage = try container.decodeIfPresent(Int.self, forKey: ._qualifiedPerGroupStage) ?? 0
self.teamsPerGroupStage = try container.decodeIfPresent(Int.self, forKey: ._teamsPerGroupStage) ?? 0
self.entryFee = try container.decodeIfPresent(Double.self, forKey: ._entryFee) ?? nil
self.payment = try Self._decodePayment(container: container)
self.additionalEstimationDuration = try container.decodeIfPresent(Int.self, forKey: ._additionalEstimationDuration) ?? 0
self.isDeleted = try container.decodeIfPresent(Bool.self, forKey: ._isDeleted) ?? false
self.isCanceled = try Self._decodeIscanceled(container: container)
self.publishTeams = try container.decodeIfPresent(Bool.self, forKey: ._publishTeams) ?? false
self.publishSummons = try container.decodeIfPresent(Bool.self, forKey: ._publishSummons) ?? false
self.publishGroupStages = try container.decodeIfPresent(Bool.self, forKey: ._publishGroupStages) ?? false
self.publishBrackets = try container.decodeIfPresent(Bool.self, forKey: ._publishBrackets) ?? false
self.shouldVerifyGroupStage = try container.decodeIfPresent(Bool.self, forKey: ._shouldVerifyGroupStage) ?? false
self.shouldVerifyBracket = try container.decodeIfPresent(Bool.self, forKey: ._shouldVerifyBracket) ?? false
self.hideTeamsWeight = try container.decodeIfPresent(Bool.self, forKey: ._hideTeamsWeight) ?? false
self.publishTournament = try container.decodeIfPresent(Bool.self, forKey: ._publishTournament) ?? false
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
self.enableOnlineRegistration = try container.decodeIfPresent(Bool.self, forKey: ._enableOnlineRegistration) ?? false
self.registrationDateLimit = try container.decodeIfPresent(Date.self, forKey: ._registrationDateLimit) ?? nil
self.openingRegistrationDate = try container.decodeIfPresent(Date.self, forKey: ._openingRegistrationDate) ?? nil
self.waitingListLimit = try container.decodeIfPresent(Int.self, forKey: ._waitingListLimit) ?? nil
self.accountIsRequired = try container.decodeIfPresent(Bool.self, forKey: ._accountIsRequired) ?? true
self.licenseIsRequired = try container.decodeIfPresent(Bool.self, forKey: ._licenseIsRequired) ?? true
self.minimumPlayerPerTeam = try container.decodeIfPresent(Int.self, forKey: ._minimumPlayerPerTeam) ?? 2
self.maximumPlayerPerTeam = try container.decodeIfPresent(Int.self, forKey: ._maximumPlayerPerTeam) ?? 2
self.information = try container.decodeIfPresent(String.self, forKey: ._information) ?? nil
self.umpireCustomMail = try container.decodeIfPresent(String.self, forKey: ._umpireCustomMail) ?? nil
self.umpireCustomContact = try container.decodeIfPresent(String.self, forKey: ._umpireCustomContact) ?? nil
self.umpireCustomPhone = try container.decodeIfPresent(String.self, forKey: ._umpireCustomPhone) ?? nil
self.hideUmpireMail = try container.decodeIfPresent(Bool.self, forKey: ._hideUmpireMail) ?? false
self.hideUmpirePhone = try container.decodeIfPresent(Bool.self, forKey: ._hideUmpirePhone) ?? true
self.disableRankingFederalRuling = try container.decodeIfPresent(Bool.self, forKey: ._disableRankingFederalRuling) ?? false
self.teamCountLimit = try container.decodeIfPresent(Bool.self, forKey: ._teamCountLimit) ?? true
try super.init(from: decoder)
}
public 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.event, forKey: ._event)
try container.encode(self.name, forKey: ._name)
try container.encode(self.startDate, forKey: ._startDate)
try container.encode(self.endDate, forKey: ._endDate)
try container.encode(self.creationDate, forKey: ._creationDate)
try container.encode(self.isPrivate, forKey: ._isPrivate)
try container.encode(self.groupStageFormat, forKey: ._groupStageFormat)
try container.encode(self.roundFormat, forKey: ._roundFormat)
try container.encode(self.loserRoundFormat, forKey: ._loserRoundFormat)
try container.encode(self.groupStageSortMode, forKey: ._groupStageSortMode)
try container.encode(self.groupStageCount, forKey: ._groupStageCount)
try container.encode(self.rankSourceDate, forKey: ._rankSourceDate)
try container.encode(self.dayDuration, forKey: ._dayDuration)
try container.encode(self.teamCount, forKey: ._teamCount)
try container.encode(self.teamSorting, forKey: ._teamSorting)
try container.encode(self.federalCategory, forKey: ._federalCategory)
try container.encode(self.federalLevelCategory, forKey: ._federalLevelCategory)
try container.encode(self.federalAgeCategory, forKey: ._federalAgeCategory)
try container.encode(self.closedRegistrationDate, forKey: ._closedRegistrationDate)
try container.encode(self.groupStageAdditionalQualified, forKey: ._groupStageAdditionalQualified)
try container.encode(self.courtCount, forKey: ._courtCount)
try container.encode(self.prioritizeClubMembers, forKey: ._prioritizeClubMembers)
try container.encode(self.qualifiedPerGroupStage, forKey: ._qualifiedPerGroupStage)
try container.encode(self.teamsPerGroupStage, forKey: ._teamsPerGroupStage)
try container.encode(self.entryFee, forKey: ._entryFee)
try _encodePayment(container: &container)
try container.encode(self.additionalEstimationDuration, forKey: ._additionalEstimationDuration)
try container.encode(self.isDeleted, forKey: ._isDeleted)
try _encodeIscanceled(container: &container)
try container.encode(self.publishTeams, forKey: ._publishTeams)
try container.encode(self.publishSummons, forKey: ._publishSummons)
try container.encode(self.publishGroupStages, forKey: ._publishGroupStages)
try container.encode(self.publishBrackets, forKey: ._publishBrackets)
try container.encode(self.shouldVerifyGroupStage, forKey: ._shouldVerifyGroupStage)
try container.encode(self.shouldVerifyBracket, forKey: ._shouldVerifyBracket)
try container.encode(self.hideTeamsWeight, forKey: ._hideTeamsWeight)
try container.encode(self.publishTournament, forKey: ._publishTournament)
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 container.encode(self.enableOnlineRegistration, forKey: ._enableOnlineRegistration)
try container.encode(self.registrationDateLimit, forKey: ._registrationDateLimit)
try container.encode(self.openingRegistrationDate, forKey: ._openingRegistrationDate)
try container.encode(self.waitingListLimit, forKey: ._waitingListLimit)
try container.encode(self.accountIsRequired, forKey: ._accountIsRequired)
try container.encode(self.licenseIsRequired, forKey: ._licenseIsRequired)
try container.encode(self.minimumPlayerPerTeam, forKey: ._minimumPlayerPerTeam)
try container.encode(self.maximumPlayerPerTeam, forKey: ._maximumPlayerPerTeam)
try container.encode(self.information, forKey: ._information)
try container.encode(self.umpireCustomMail, forKey: ._umpireCustomMail)
try container.encode(self.umpireCustomContact, forKey: ._umpireCustomContact)
try container.encode(self.umpireCustomPhone, forKey: ._umpireCustomPhone)
try container.encode(self.hideUmpireMail, forKey: ._hideUmpireMail)
try container.encode(self.hideUmpirePhone, forKey: ._hideUmpirePhone)
try container.encode(self.disableRankingFederalRuling, forKey: ._disableRankingFederalRuling)
try container.encode(self.teamCountLimit, forKey: ._teamCountLimit)
try super.encode(to: encoder)
}
func eventValue() -> Event? {
guard let event = self.event else { return nil }
return Store.main.findById(event)
}
public func copy(from other: any Storable) {
guard let tournament = other as? BaseTournament else { return }
self.id = tournament.id
self.event = tournament.event
self.name = tournament.name
self.startDate = tournament.startDate
self.endDate = tournament.endDate
self.creationDate = tournament.creationDate
self.isPrivate = tournament.isPrivate
self.groupStageFormat = tournament.groupStageFormat
self.roundFormat = tournament.roundFormat
self.loserRoundFormat = tournament.loserRoundFormat
self.groupStageSortMode = tournament.groupStageSortMode
self.groupStageCount = tournament.groupStageCount
self.rankSourceDate = tournament.rankSourceDate
self.dayDuration = tournament.dayDuration
self.teamCount = tournament.teamCount
self.teamSorting = tournament.teamSorting
self.federalCategory = tournament.federalCategory
self.federalLevelCategory = tournament.federalLevelCategory
self.federalAgeCategory = tournament.federalAgeCategory
self.closedRegistrationDate = tournament.closedRegistrationDate
self.groupStageAdditionalQualified = tournament.groupStageAdditionalQualified
self.courtCount = tournament.courtCount
self.prioritizeClubMembers = tournament.prioritizeClubMembers
self.qualifiedPerGroupStage = tournament.qualifiedPerGroupStage
self.teamsPerGroupStage = tournament.teamsPerGroupStage
self.entryFee = tournament.entryFee
self.payment = tournament.payment
self.additionalEstimationDuration = tournament.additionalEstimationDuration
self.isDeleted = tournament.isDeleted
self.isCanceled = tournament.isCanceled
self.publishTeams = tournament.publishTeams
self.publishSummons = tournament.publishSummons
self.publishGroupStages = tournament.publishGroupStages
self.publishBrackets = tournament.publishBrackets
self.shouldVerifyGroupStage = tournament.shouldVerifyGroupStage
self.shouldVerifyBracket = tournament.shouldVerifyBracket
self.hideTeamsWeight = tournament.hideTeamsWeight
self.publishTournament = tournament.publishTournament
self.hidePointsEarned = tournament.hidePointsEarned
self.publishRankings = tournament.publishRankings
self.loserBracketMode = tournament.loserBracketMode
self.initialSeedRound = tournament.initialSeedRound
self.initialSeedCount = tournament.initialSeedCount
self.enableOnlineRegistration = tournament.enableOnlineRegistration
self.registrationDateLimit = tournament.registrationDateLimit
self.openingRegistrationDate = tournament.openingRegistrationDate
self.waitingListLimit = tournament.waitingListLimit
self.accountIsRequired = tournament.accountIsRequired
self.licenseIsRequired = tournament.licenseIsRequired
self.minimumPlayerPerTeam = tournament.minimumPlayerPerTeam
self.maximumPlayerPerTeam = tournament.maximumPlayerPerTeam
self.information = tournament.information
self.umpireCustomMail = tournament.umpireCustomMail
self.umpireCustomContact = tournament.umpireCustomContact
self.umpireCustomPhone = tournament.umpireCustomPhone
self.hideUmpireMail = tournament.hideUmpireMail
self.hideUmpirePhone = tournament.hideUmpirePhone
self.disableRankingFederalRuling = tournament.disableRankingFederalRuling
self.teamCountLimit = tournament.teamCountLimit
}
public static func relationships() -> [Relationship] {
return [
Relationship(type: Event.self, keyPath: \BaseTournament.event),
]
}
}

@ -0,0 +1,92 @@
{
"models": [
{
"name": "Club",
"synchronizable": true,
"observable": true,
"properties": [
{
"name": "id",
"type": "String",
"defaultValue": "Store.randomId()"
},
{
"name": "creator",
"type": "String",
"optional": true,
"defaultValue": "nil",
"foreignKey": "CustomUser"
},
{
"name": "name",
"type": "String",
"defaultValue": "\"\""
},
{
"name": "acronym",
"type": "String",
"defaultValue": "\"\""
},
{
"name": "phone",
"type": "String",
"optional": true,
"defaultValue": "nil"
},
{
"name": "code",
"type": "String",
"optional": true,
"defaultValue": "nil"
},
{
"name": "address",
"type": "String",
"optional": true,
"defaultValue": "nil"
},
{
"name": "city",
"type": "String",
"optional": true,
"defaultValue": "nil"
},
{
"name": "zipCode",
"type": "String",
"optional": true,
"defaultValue": "nil"
},
{
"name": "latitude",
"type": "Double",
"optional": true,
"defaultValue": "nil"
},
{
"name": "longitude",
"type": "Double",
"optional": true,
"defaultValue": "nil"
},
{
"name": "courtCount",
"type": "Int",
"defaultValue": "2"
},
{
"name": "broadcastCode",
"type": "String",
"optional": true,
"defaultValue": "nil"
},
{
"name": "timezone",
"type": "String",
"optional": true,
"defaultValue": "TimeZone.current.identifier"
}
]
}
]
}

@ -0,0 +1,42 @@
{
"models": [
{
"name": "Court",
"synchronizable": true,
"observable": true,
"properties": [
{
"name": "id",
"type": "String",
"defaultValue": "Store.randomId()"
},
{
"name": "index",
"type": "Int"
},
{
"name": "club",
"type": "String",
"foreignKey": "Club"
},
{
"name": "name",
"type": "String",
"optional": true,
"defaultValue": "nil"
},
{
"name": "exitAllowed",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "indoor",
"type": "Bool",
"defaultValue": "false"
}
],
"tokenExemptedMethods": []
}
]
}

@ -0,0 +1,136 @@
{
"models": [
{
"name": "CustomUser",
"resource_name": "users",
"synchronizable": true,
"observable": true,
"tokenExemptedMethods": ["post"],
"properties": [
{
"name": "id",
"type": "String",
"defaultValue": "Store.randomId()"
},
{
"name": "username",
"type": "String"
},
{
"name": "email",
"type": "String"
},
{
"name": "clubs",
"type": "[String]",
"defaultValue": "[]"
},
{
"name": "umpireCode",
"type": "String",
"optional": true,
"defaultValue": "nil"
},
{
"name": "licenceId",
"type": "String",
"optional": true,
"defaultValue": "nil"
},
{
"name": "firstName",
"type": "String"
},
{
"name": "lastName",
"type": "String"
},
{
"name": "phone",
"type": "String",
"optional": true,
"defaultValue": "nil"
},
{
"name": "country",
"type": "String",
"optional": true,
"defaultValue": "nil"
},
{
"name": "summonsMessageBody",
"type": "String",
"optional": true,
"defaultValue": "nil"
},
{
"name": "summonsMessageSignature",
"type": "String",
"optional": true,
"defaultValue": "nil"
},
{
"name": "summonsAvailablePaymentMethods",
"type": "String",
"optional": true,
"defaultValue": "nil"
},
{
"name": "summonsDisplayFormat",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "summonsDisplayEntryFee",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "summonsUseFullCustomMessage",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "matchFormatsDefaultDuration",
"type": "[MatchFormat: Int]",
"optional": true,
"defaultValue": "nil"
},
{
"name": "bracketMatchFormatPreference",
"type": "MatchFormat",
"optional": true,
"defaultValue": "nil"
},
{
"name": "groupStageMatchFormatPreference",
"type": "MatchFormat",
"optional": true,
"defaultValue": "nil"
},
{
"name": "loserBracketMatchFormatPreference",
"type": "MatchFormat",
"optional": true,
"defaultValue": "nil"
},
{
"name": "loserBracketMode",
"type": "LoserBracketMode",
"defaultValue": ".automatic"
},
{
"name": "deviceId",
"type": "String",
"optional": true,
"defaultValue": "nil"
},
{
"name": "agents",
"type": "[String]",
"defaultValue": "[]"
}
]
}
]
}

@ -0,0 +1,33 @@
{
"models": [
{
"name": "DateInterval",
"synchronizable": true,
"observable": true,
"properties": [
{
"name": "id",
"type": "String",
"defaultValue": "Store.randomId()"
},
{
"name": "event",
"type": "String"
},
{
"name": "courtIndex",
"type": "Int"
},
{
"name": "startDate",
"type": "Date"
},
{
"name": "endDate",
"type": "Date"
}
],
"tokenExemptedMethods": []
}
]
}

@ -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"
}
]
}
]
}

@ -0,0 +1,48 @@
{
"models": [
{
"name": "Event",
"synchronizable": true,
"observable": true,
"properties": [
{
"name": "id",
"type": "String",
"defaultValue": "Store.randomId()"
},
{
"name": "creator",
"type": "String",
"optional": true,
"defaultValue": "nil",
"foreignKey": "CustomUser"
},
{
"name": "club",
"type": "String",
"optional": true,
"defaultValue": "nil",
"foreignKey": "Club"
},
{
"name": "creationDate",
"type": "Date",
"defaultValue": "Date()"
},
{
"name": "name",
"type": "String",
"optional": true,
"defaultValue": "nil"
},
{
"name": "tenupId",
"type": "String",
"optional": true,
"defaultValue": "nil"
}
],
"tokenExemptedMethods": []
}
]
}

@ -0,0 +1,53 @@
{
"models": [
{
"name": "GroupStage",
"synchronizable": true,
"observable": true,
"properties": [
{
"name": "id",
"type": "String",
"defaultValue": "Store.randomId()"
},
{
"name": "tournament",
"type": "String",
"foreignKey": "Tournament"
},
{
"name": "index",
"type": "Int"
},
{
"name": "size",
"type": "Int"
},
{
"name": "format",
"type": "MatchFormat",
"optional": true,
"defaultValue": "nil"
},
{
"name": "startDate",
"type": "Date",
"optional": true,
"defaultValue": "nil"
},
{
"name": "name",
"type": "String",
"optional": true,
"defaultValue": "nil"
},
{
"name": "step",
"type": "Int",
"defaultValue": "0"
}
],
"tokenExemptedMethods": []
}
]
}

@ -0,0 +1,83 @@
{
"models": [
{
"name": "Match",
"synchronizable": true,
"observable": true,
"tokenExemptedMethods": [],
"properties": [
{
"name": "id",
"type": "String",
"defaultValue": "Store.randomId()"
},
{
"name": "round",
"type": "String",
"optional": true,
"foreignKey": "Round*"
},
{
"name": "groupStage",
"type": "String",
"optional": true,
"foreignKey": "GroupStage*"
},
{
"name": "startDate",
"type": "Date",
"optional": true
},
{
"name": "endDate",
"type": "Date",
"optional": true
},
{
"name": "index",
"type": "Int"
},
{
"name": "format",
"type": "MatchFormat",
"optional": true
},
{
"name": "servingTeamId",
"type": "String",
"optional": true
},
{
"name": "winningTeamId",
"type": "String",
"optional": true
},
{
"name": "losingTeamId",
"type": "String",
"optional": true
},
{
"name": "name",
"type": "String",
"optional": true
},
{
"name": "disabled",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "courtIndex",
"type": "Int",
"optional": true
},
{
"name": "confirmed",
"type": "Bool",
"defaultValue": "false"
}
]
}
]
}

@ -0,0 +1,91 @@
{
"models": [
{
"name": "MatchScheduler",
"resource_name": "match-scheduler",
"synchronizable": false,
"observable": true,
"tokenExemptedMethods": [],
"properties": [
{
"name": "id",
"type": "String",
"defaultValue": "Store.randomId()"
},
{
"name": "tournament",
"type": "String",
"foreignKey": "Tournament"
},
{
"name": "timeDifferenceLimit",
"type": "Int",
"defaultValue": "5"
},
{
"name": "loserBracketRotationDifference",
"type": "Int",
"defaultValue": "0"
},
{
"name": "upperBracketRotationDifference",
"type": "Int",
"defaultValue": "1"
},
{
"name": "accountUpperBracketBreakTime",
"type": "Bool",
"defaultValue": "true"
},
{
"name": "accountLoserBracketBreakTime",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "randomizeCourts",
"type": "Bool",
"defaultValue": "true"
},
{
"name": "rotationDifferenceIsImportant",
"type": "Bool"
},
{
"name": "shouldHandleUpperRoundSlice",
"type": "Bool"
},
{
"name": "shouldEndRoundBeforeStartingNext",
"type": "Bool",
"defaultValue": "true"
},
{
"name": "groupStageChunkCount",
"type": "Int",
"optional": true
},
{
"name": "overrideCourtsUnavailability",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "shouldTryToFillUpCourtsAvailable",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "courtsAvailable",
"type": "Set<Int>",
"defaultValue": "Set<Int>()"
},
{
"name": "simultaneousStart",
"type": "Bool",
"defaultValue": "true"
}
]
}
]
}

@ -0,0 +1,71 @@
{
"models": [
{
"name": "MonthData",
"resource_name": "month-data",
"synchronizable": false,
"observable": true,
"tokenExemptedMethods": [],
"properties": [
{
"name": "id",
"type": "String",
"defaultValue": "Store.randomId()"
},
{
"name": "monthKey",
"type": "String"
},
{
"name": "creationDate",
"type": "Date"
},
{
"name": "maleUnrankedValue",
"type": "Int",
"optional": true,
"defaultValue": "nil"
},
{
"name": "femaleUnrankedValue",
"type": "Int",
"optional": true,
"defaultValue": "nil"
},
{
"name": "maleCount",
"type": "Int",
"optional": true,
"defaultValue": "nil"
},
{
"name": "femaleCount",
"type": "Int",
"optional": true,
"defaultValue": "nil"
},
{
"name": "anonymousCount",
"type": "Int",
"optional": true,
"defaultValue": "nil"
},
{
"name": "incompleteMode",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "dataModelIdentifier",
"type": "String",
"optional": true
},
{
"name": "fileModelIdentifier",
"type": "String",
"optional": true
}
]
}
]
}

@ -0,0 +1,122 @@
{
"models": [
{
"name": "PlayerRegistration",
"synchronizable": true,
"sideStorable": true,
"observable": true,
"relationshipNames": ["teamRegistration"],
"properties": [
{
"name": "id",
"type": "String",
"defaultValue": "Store.randomId()"
},
{
"name": "teamRegistration",
"type": "String",
"optional": true,
"foreignKey": "TeamRegistration"
},
{
"name": "firstName",
"type": "String"
},
{
"name": "lastName",
"type": "String"
},
{
"name": "licenceId",
"type": "String",
"optional": true
},
{
"name": "rank",
"type": "Int",
"optional": true
},
{
"name": "paymentType",
"type": "PlayerPaymentType",
"optional": true
},
{
"name": "sex",
"type": "PlayerSexType",
"optional": true
},
{
"name": "tournamentPlayed",
"type": "Int",
"optional": true
},
{
"name": "points",
"type": "Double",
"optional": true
},
{
"name": "clubName",
"type": "String",
"optional": true
},
{
"name": "ligueName",
"type": "String",
"optional": true
},
{
"name": "assimilation",
"type": "String",
"optional": true
},
{
"name": "phoneNumber",
"type": "String",
"optional": true
},
{
"name": "email",
"type": "String",
"optional": true
},
{
"name": "birthdate",
"type": "String",
"optional": true
},
{
"name": "computedRank",
"type": "Int",
"defaultValue": "0"
},
{
"name": "source",
"type": "PlayerRegistration.PlayerDataSource",
"optional": true
},
{
"name": "hasArrived",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "coach",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "captain",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "registeredOnline",
"type": "Bool",
"defaultValue": "false"
}
]
}
]
}

@ -0,0 +1,51 @@
{
"models": [
{
"name": "Purchase",
"synchronizable": true,
"properties": [
{
"name": "id",
"type": "UInt64",
"defaultValue": "0"
},
{
"name": "user",
"type": "String",
"defaultValue": "\"\"",
"encryption": "standard",
"foreignKey": "CustomUser"
},
{
"name": "purchaseDate",
"type": "Date"
},
{
"name": "productId",
"type": "String",
"defaultValue": "\"\""
},
{
"name": "quantity",
"type": "Int",
"optional": true,
"defaultValue": "nil"
},
{
"name": "revocationDate",
"type": "Date",
"optional": true,
"defaultValue": "nil"
},
{
"name": "expirationDate",
"type": "Date",
"optional": true,
"defaultValue": "nil"
}
],
"tokenExemptedMethods": [],
"relationshipNames": []
}
]
}

@ -0,0 +1,53 @@
{
"models": [
{
"name": "Round",
"synchronizable": true,
"sideStorable": true,
"observable": true,
"relationshipNames": [],
"properties": [
{
"name": "id",
"type": "String",
"defaultValue": "Store.randomId()"
},
{
"name": "tournament",
"type": "String",
"foreignKey": "Tournament"
},
{
"name": "index",
"type": "Int"
},
{
"name": "parent",
"type": "String",
"optional": true
},
{
"name": "format",
"type": "MatchFormat",
"optional": true,
"private": true
},
{
"name": "startDate",
"type": "Date",
"optional": true
},
{
"name": "groupStageLoserBracket",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "loserBracketMode",
"type": "LoserBracketMode",
"defaultValue": ".automatic"
}
]
}
]
}

@ -0,0 +1,118 @@
{
"models": [
{
"name": "TeamRegistration",
"synchronizable": true,
"sideStorable": true,
"observable": true,
"relationshipNames": [],
"properties": [
{
"name": "id",
"type": "String",
"defaultValue": "Store.randomId()"
},
{
"name": "tournament",
"type": "String"
},
{
"name": "groupStage",
"type": "String",
"optional": true,
"foreignKey": "GroupStage*"
},
{
"name": "registrationDate",
"type": "Date",
"optional": true
},
{
"name": "callDate",
"type": "Date",
"optional": true
},
{
"name": "bracketPosition",
"type": "Int",
"optional": true
},
{
"name": "groupStagePosition",
"type": "Int",
"optional": true
},
{
"name": "comment",
"type": "String",
"optional": true
},
{
"name": "source",
"type": "String",
"optional": true
},
{
"name": "sourceValue",
"type": "String",
"optional": true
},
{
"name": "logo",
"type": "String",
"optional": true
},
{
"name": "name",
"type": "String",
"optional": true
},
{
"name": "walkOut",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "wildCardBracket",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "wildCardGroupStage",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "weight",
"type": "Int",
"defaultValue": "0"
},
{
"name": "lockedWeight",
"type": "Int",
"optional": true
},
{
"name": "confirmationDate",
"type": "Date",
"optional": true
},
{
"name": "qualified",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "finalRanking",
"type": "Int",
"optional": true
},
{
"name": "pointsEarned",
"type": "Int",
"optional": true
}
]
}
]
}

@ -0,0 +1,44 @@
{
"models": [
{
"name": "TeamScore",
"synchronizable": true,
"sideStorable": true,
"observable": true,
"relationshipNames": ["match"],
"properties": [
{
"name": "id",
"type": "String",
"defaultValue": "Store.randomId()"
},
{
"name": "match",
"type": "String",
"foreignKey": "Match*"
},
{
"name": "teamRegistration",
"type": "String",
"optional": true,
"foreignKey": "TeamRegistration*"
},
{
"name": "score",
"type": "String",
"optional": true
},
{
"name": "walkOut",
"type": "Int",
"optional": true
},
{
"name": "luckyLoser",
"type": "Int",
"optional": true
}
]
}
]
}

@ -0,0 +1,310 @@
{
"models": [
{
"name": "Tournament",
"synchronizable": true,
"copyable": true,
"observable": true,
"relationshipNames": [],
"properties": [
{
"name": "id",
"type": "String",
"defaultValue": "Store.randomId()"
},
{
"name": "event",
"type": "String",
"optional": true,
"foreignKey": "Event"
},
{
"name": "name",
"type": "String",
"optional": true
},
{
"name": "startDate",
"type": "Date"
},
{
"name": "endDate",
"type": "Date",
"optional": true
},
{
"name": "creationDate",
"type": "Date",
"private": true
},
{
"name": "isPrivate",
"type": "Bool"
},
{
"name": "groupStageFormat",
"type": "MatchFormat",
"optional": true,
"private": true
},
{
"name": "roundFormat",
"type": "MatchFormat",
"optional": true,
"private": true
},
{
"name": "loserRoundFormat",
"type": "MatchFormat",
"optional": true,
"private": true
},
{
"name": "groupStageSortMode",
"type": "GroupStageOrderingMode",
"defaultValue": "GroupStageOrderingMode.snake"
},
{
"name": "groupStageCount",
"type": "Int"
},
{
"name": "rankSourceDate",
"type": "Date",
"optional": true
},
{
"name": "dayDuration",
"type": "Int"
},
{
"name": "teamCount",
"type": "Int"
},
{
"name": "teamSorting",
"type": "TeamSortingType",
"defaultValue": "TeamSortingType.inscriptionDate"
},
{
"name": "federalCategory",
"type": "TournamentCategory",
"defaultValue": "TournamentCategory.men"
},
{
"name": "federalLevelCategory",
"type": "TournamentLevel",
"defaultValue": "TournamentLevel.unlisted"
},
{
"name": "federalAgeCategory",
"type": "FederalTournamentAge",
"defaultValue": "FederalTournamentAge.unlisted"
},
{
"name": "closedRegistrationDate",
"type": "Date",
"optional": true
},
{
"name": "groupStageAdditionalQualified",
"type": "Int"
},
{
"name": "courtCount",
"type": "Int",
"defaultValue": "2"
},
{
"name": "prioritizeClubMembers",
"type": "Bool"
},
{
"name": "qualifiedPerGroupStage",
"type": "Int"
},
{
"name": "teamsPerGroupStage",
"type": "Int"
},
{
"name": "entryFee",
"type": "Double",
"optional": true
},
{
"name": "payment",
"type": "TournamentPayment",
"optional": true,
"defaultValue": "nil",
"encryption": "tournament_payment",
"codingKey": "globalId"
},
{
"name": "additionalEstimationDuration",
"type": "Int",
"defaultValue": "0"
},
{
"name": "isDeleted",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "isCanceled",
"type": "Bool",
"defaultValue": "false",
"encryption": "tournament_iscanceled",
"codingKey": "localId"
},
{
"name": "publishTeams",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "publishSummons",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "publishGroupStages",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "publishBrackets",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "shouldVerifyGroupStage",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "shouldVerifyBracket",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "hideTeamsWeight",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "publishTournament",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "hidePointsEarned",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "publishRankings",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "loserBracketMode",
"type": "LoserBracketMode",
"defaultValue": ".automatic"
},
{
"name": "initialSeedRound",
"type": "Int",
"defaultValue": "0"
},
{
"name": "initialSeedCount",
"type": "Int",
"defaultValue": "0"
},
{
"name": "enableOnlineRegistration",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "registrationDateLimit",
"type": "Date",
"optional": true
},
{
"name": "openingRegistrationDate",
"type": "Date",
"optional": true
},
{
"name": "waitingListLimit",
"type": "Int",
"optional": true
},
{
"name": "accountIsRequired",
"type": "Bool",
"defaultValue": "true"
},
{
"name": "licenseIsRequired",
"type": "Bool",
"defaultValue": "true"
},
{
"name": "minimumPlayerPerTeam",
"type": "Int",
"defaultValue": 2
},
{
"name": "maximumPlayerPerTeam",
"type": "Int",
"defaultValue": 2
},
{
"name": "information",
"type": "String",
"optional": true
},
{
"name": "umpireCustomMail",
"type": "String",
"optional": true
},
{
"name": "umpireCustomContact",
"type": "String",
"optional": true
},
{
"name": "umpireCustomPhone",
"type": "String",
"optional": true
},
{
"name": "hideUmpireMail",
"type": "Bool",
"defaultValue": "false"
},
{
"name": "hideUmpirePhone",
"type": "Bool",
"defaultValue": "true"
},
{
"name": "disableRankingFederalRuling",
"type": "Bool",
"defaultValue": "false",
"optional": false
},
{
"name": "teamCountLimit",
"type": "Bool",
"defaultValue": "true",
"optional": false
}
]
}
]
}

@ -0,0 +1,591 @@
import json
import re
import os
from pathlib import Path
from typing import Dict, List, Any
import argparse
import sys
import logging
from datetime import datetime
import inflect
class SwiftModelGenerator:
def __init__(self, json_data: Dict[str, Any]):
self.data = json_data
self.pluralizer = inflect.engine()
def generate_model(self, model_data: Dict[str, Any]) -> str:
model_name = model_data["name"]
is_sync = model_data.get("synchronizable", False)
is_observable = model_data.get("observable", False)
properties = model_data["properties"]
# Get protocol specific configurations
resource = self.make_resource_name(model_name)
resource_name = model_data.get("resource_name", resource)
token_exempted = model_data.get("tokenExemptedMethods", [])
lines = ["// Generated by SwiftModelGenerator", "// Do not modify this file manually", ""]
# Import statement
lines.append("import Foundation")
lines.append("import LeStorage")
if is_observable:
lines.append("import SwiftUI")
lines.append("")
# Class declaration
if is_observable:
lines.append("@Observable")
protocol = "SyncedStorable" if is_sync else "Storable"
base_class = "SyncedModelObject" if is_sync else "BaseModelObject"
lines.append(f"public class Base{model_name}: {base_class}, {protocol} {{")
lines.append("")
# Add SyncedStorable protocol requirements
lines.extend(self._generate_protocol_requirements(resource_name, token_exempted))
lines.append("")
# Properties
for prop in properties:
swift_type = prop["type"]
if prop.get("optional", False):
swift_type += "?"
default_value = prop.get("defaultValue", "nil" if prop.get("optional", False) else self._get_default_value(swift_type))
lines.append(f" public var {prop['name']}: {swift_type} = {default_value}")
lines.append("")
# Add constructor
lines.extend(self._generate_constructor(model_name, properties))
lines.append("")
# CodingKeys
lines.extend(self._generate_coding_keys(model_name, properties, is_observable))
lines.append("")
# Encryption methods
encrypted_props = [p for p in properties if "encryption" in p]
if encrypted_props:
lines.extend(self._generate_encryption_methods(properties))
lines.append("")
# Codable implementation
lines.extend(self._generate_decoder(model_name, properties, is_observable, is_sync))
lines.append("")
lines.extend(self._generate_encoder(properties, is_observable, is_sync))
lines.append("")
# Foreign Key convenience
foreign_key_methods = self._generate_foreign_key_methods(properties)
if foreign_key_methods:
lines.extend(foreign_key_methods)
# lines.append("")
# Copy method
lines.extend(self._generate_copy_method(model_name, properties))
lines.append("")
# Add relationships function
lines.extend(self._generate_relationships(model_name, properties))
lines.append("")
lines.append("}")
return "\n".join(lines)
def _generate_constructor(self, model_name: str, properties: List[Dict[str, Any]]) -> List[str]:
"""Generate a constructor with all properties as parameters with default values."""
lines = [" public init("]
# Generate parameter list
params = []
for prop in properties:
name = prop['name']
type_name = prop['type']
is_optional = prop.get("optional", False)
# Always include a default value
default_value = prop.get("defaultValue")
if default_value is None:
if is_optional:
default_value = "nil"
else:
default_value = self._get_default_value(type_name)
# Format the parameter with its type and default value
param = f" {name}: {type_name}"
if is_optional:
param += "?"
param += f" = {default_value}"
params.append(param)
# Join parameters with commas
lines.extend([f"{param}," for param in params[:-1]])
lines.append(f"{params[-1]}") # Last parameter without comma
# Constructor body
lines.extend([
" ) {",
" super.init()",
])
# Property assignments
for prop in properties:
name = prop['name']
lines.append(f" self.{name} = {name}")
lines.append(" }")
lines.extend([
" required public override init() {",
" super.init()",
" }",
])
return lines
def _generate_foreign_key_methods(self, properties: List[Dict[str, Any]]) -> List[str]:
lines = []
for prop in properties:
if "foreignKey" in prop:
foreign_key = prop["foreignKey"]
prop_name = prop["name"]
method_name = f"{prop_name}Value"
is_optional = prop.get("optional", False)
lines.extend([f" func {method_name}() -> {foreign_key.rstrip('*')}? {{"])
if is_optional:
lines.extend([
f" guard let {prop_name} = self.{prop_name} else {{ return nil }}"
])
if foreign_key.endswith("*"):
foreign_key = foreign_key[:-1] # Remove the asterisk
lines.extend([
f" return self.store?.findById({prop_name})"
])
else:
lines.extend([
f" return Store.main.findById({prop_name})"
])
lines.extend([" }", ""]) # Close the method and add a blank line
return lines
def _generate_coding_keys(self, model_name: str, properties: List[Dict[str, Any]], is_observable: bool) -> List[str]:
lines = [" public enum CodingKeys: String, CodingKey {"]
if model_name == 'Tournament':
lines.append(" case isCanceled = \"isCanceled\"")
lines.append(" case payment = \"payment\"")
for prop in properties:
name = prop['name']
# Add underscore prefix to case name if observable
case_name = f"_{name}" if is_observable else name
# Use custom codingKey if provided, otherwise use the property name
coding_key_value = prop.get("codingKey", name)
lines.append(f" case {case_name} = \"{coding_key_value}\"")
lines.append(" }")
return lines
def _generate_encryption_methods(self, properties: List[Dict[str, Any]]) -> List[str]:
lines = []
for prop in properties:
if "encryption" in prop:
name = prop['name']
enc_type = prop['encryption']
if enc_type == "tournament_payment":
lines.extend([
f" private static func _decode{name.capitalize()}(container: KeyedDecodingContainer<CodingKeys>) throws -> TournamentPayment? {{",
f" var data = try container.decodeIfPresent(Data.self, forKey: ._{name})",
" if data == nil {",
" 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.standard.number(from: String($0))?.intValue }",
" return TournamentPayment(rawValue: sequence[18])",
" } catch {",
" Logger.error(error)",
" }",
" }",
" return nil",
" }",
"",
f" private func _encode{name.capitalize()}(container: inout KeyedEncodingContainer<CodingKeys>) throws {{",
f" guard let {name} else {{",
f" try container.encodeNil(forKey: ._{name})",
" return",
" }",
" ",
" let max: Int = TournamentPayment.allCases.count",
" var sequence = (1...18).map { _ in Int.random(in: (0..<max)) }",
f" sequence.append({name}.rawValue)",
" sequence.append(contentsOf: (1...13).map { _ in Int.random(in: (0..<max ))} )",
"",
" let stringCombo: [String] = sequence.map { $0.formatted() }",
" let joined: String = stringCombo.joined(separator: \"\")",
" if let data = joined.data(using: .utf8) {",
" let encryped: Data = try data.encrypt(pass: CryptoKey.pass.rawValue)",
f" try container.encodeIfPresent(encryped, forKey: ._{name})",
" }",
" }"
])
elif enc_type == "tournament_iscanceled":
lines.extend([
f" private static func _decode{name.capitalize()}(container: KeyedDecodingContainer<CodingKeys>) throws -> Bool {{",
f" var data = try container.decodeIfPresent(Data.self, forKey: ._{name})",
" if data == nil {",
" 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.standard.number(from: String($0))?.intValue }",
" return Bool.decodeInt(sequence[18])",
" } catch {",
" Logger.error(error)",
" }",
" }",
" return false",
" }",
"",
f" private func _encode{name.capitalize()}(container: inout KeyedEncodingContainer<CodingKeys>) throws {{",
" let max: Int = 9",
" var sequence = (1...18).map { _ in Int.random(in: (0...max)) }",
f" sequence.append(self.{name}.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)",
f" try container.encode(encryped, forKey: ._{name})",
" }",
" }"
])
return lines
def _generate_decoder(self, model_name: str, properties: List[Dict[str, Any]], is_observable: bool, is_sync: bool = False) -> List[str]:
lines = [" required init(from decoder: Decoder) throws {",
" let container = try decoder.container(keyedBy: CodingKeys.self)"]
for prop in properties:
name = prop['name']
type_name = prop['type']
is_optional = prop.get("optional", False)
default_value = prop.get("defaultValue", "nil" if is_optional else self._get_default_value(type_name))
option = prop.get("option")
# Use the correct case reference based on observable flag
case_ref = f"_{name}" if is_observable else name
if "encryption" in prop:
enc_type = prop['encryption']
if enc_type == "standard":
if is_optional:
lines.append(f" self.{name} = try container.decodeEncryptedIfPresent(key: .{case_ref})")
else:
lines.append(f" self.{name} = try container.decodeEncrypted(key: .{case_ref})")
elif enc_type in ["tournament_payment", "tournament_iscanceled"]:
lines.append(f" self.{name} = try Self._decode{name.capitalize()}(container: container)")
else:
# Handle Date with fractional option
if type_name == "Date" and option == "fractional":
if is_optional:
lines.append(f" if let dateString = try container.decodeIfPresent(String.self, forKey: .{case_ref}) {{")
lines.append(f" self.{name} = Date.iso8601FractionalFormatter.date(from: dateString)")
lines.append(" } else {")
lines.append(f" self.{name} = {default_value}")
lines.append(" }")
else:
lines.append(f" let dateString = try container.decode(String.self, forKey: .{case_ref})")
lines.append(f" self.{name} = Date.iso8601FractionalFormatter.date(from: dateString) ?? {default_value}")
else:
lines.append(f" self.{name} = try container.decodeIfPresent({type_name}.self, forKey: .{case_ref}) ?? {default_value}")
lines.append(" try super.init(from: decoder)")
lines.append(" }")
return lines
def _generate_encoder(self, properties: List[Dict[str, Any]], is_observable: bool, is_sync: bool = False) -> List[str]:
lines = [" public override func encode(to encoder: Encoder) throws {",
" var container = encoder.container(keyedBy: CodingKeys.self)"]
for prop in properties:
name = prop['name']
type_name = prop['type']
is_optional = prop.get('optional', False)
option = prop.get("option")
# Use the correct case reference based on observable flag
case_ref = f"_{name}" if is_observable else name
if "encryption" in prop:
enc_type = prop['encryption']
if enc_type == "standard":
if is_optional:
lines.append(f" try container.encodeAndEncryptIfPresent(self.{name}?.data(using: .utf8), forKey: .{case_ref})")
else:
lines.append(f" try container.encodeAndEncryptIfPresent(self.{name}.data(using: .utf8), forKey: .{case_ref})")
elif enc_type in ["tournament_payment", "tournament_iscanceled"]:
lines.append(f" try _encode{name.capitalize()}(container: &container)")
else:
if type_name == "Date" and option == "fractional":
if is_optional:
lines.append(f" if let date = self.{name} {{")
lines.append(f" try container.encode(Date.iso8601FractionalFormatter.string(from: date), forKey: .{case_ref})")
lines.append(" } else {")
lines.append(f" try container.encodeNil(forKey: .{case_ref})")
lines.append(" }")
else:
lines.append(f" try container.encode(Date.iso8601FractionalFormatter.string(from: self.{name}), forKey: .{case_ref})")
else:
lines.append(f" try container.encode(self.{name}, forKey: .{case_ref})")
lines.append(" try super.encode(to: encoder)")
lines.append(" }")
return lines
def _generate_copy_method(self, model_name: str, properties: List[Dict[str, Any]]) -> List[str]:
model_variable = model_name.lower()
lines = [f" public func copy(from other: any Storable) {{"]
lines.append(f" guard let {model_variable} = other as? Base{model_name} else {{ return }}")
for prop in properties:
name = prop['name']
lines.append(f" self.{name} = {model_variable}.{name}")
lines.append(" }")
return lines
def _generate_protocol_requirements(self, resource_name: str, token_exempted: List[str]) -> List[str]:
"""Generate the static functions required by SyncedStorable protocol."""
# Convert HTTP methods to proper format
formatted_methods = [f".{method.lower()}" for method in token_exempted]
methods_str = ", ".join(formatted_methods) if formatted_methods else ""
return [
f" public static func resourceName() -> String {{ return \"{resource_name}\" }}",
f" public static func tokenExemptedMethods() -> [HTTPMethod] {{ return [{methods_str}] }}",
]
def _generate_relationships(self, model_name, properties: List[Dict[str, Any]]) -> List[str]:
# Find all properties with foreign keys
foreign_key_props = [p for p in properties if "foreignKey" in p]
if not foreign_key_props:
# If no foreign keys, return empty array
return [
" public static func relationships() -> [Relationship] {",
" return []",
" }"
]
lines = [
" public static func relationships() -> [Relationship] {",
" return ["
]
# Generate relationship entries
for prop in foreign_key_props:
name = prop["name"]
foreign_key = prop["foreignKey"].rstrip('*') # Remove asterisk if present
lines.append(f" Relationship(type: {foreign_key}.self, keyPath: \\Base{model_name}.{name}),")
# Close the array and function
lines.extend([
" ]",
" }"
])
return lines
def _get_default_value(self, type_name: str) -> str:
"""Get default value for non-optional types"""
if "String" in type_name:
return "\"\""
elif "Int" in type_name:
return "0"
elif "Double" in type_name:
return "0.0"
elif "Bool" in type_name:
return "false"
elif "Date" in type_name:
return "Date()"
else:
return "nil" # For any other type
def get_output_filename(self, model_name: str) -> str:
"""Generate the output filename for a model."""
return f"Base{model_name}.swift"
def make_resource_name(self, text):
p = inflect.engine()
# Split camelCase into words
words = re.findall('[A-Z][^A-Z]*', text)
if not words:
words = [text]
words = [word.lower() for word in words]
words[-1] = p.plural(words[-1])
return '-'.join(words)
def process_directory(input_dir: str, output_dir: str, logger: logging.Logger, dry_run: bool = False) -> int:
"""Process all JSON files in the input directory and generate Swift models."""
try:
input_path = validate_directory(input_dir)
if not dry_run:
output_path = validate_directory(output_dir, create=True)
json_files = list(input_path.glob("*.json"))
if not json_files:
logger.warning(f"No JSON files found in '{input_dir}'")
return 0
logger.info(f"Found {len(json_files)} JSON files to process")
successful_files = 0
for json_file in json_files:
try:
with open(json_file, 'r') as f:
json_data = json.load(f)
generator = SwiftModelGenerator(json_data)
# Generate each model in the JSON file
for model in json_data["models"]:
model_name = model["name"]
swift_code = generator.generate_model(model)
if dry_run:
logger.info(f"Would generate Base{model_name}.swift")
continue
# Write to output file with Base prefix
output_file = output_path / generator.get_output_filename(model_name)
with open(output_file, 'w') as f:
f.write(swift_code)
logger.info(f"Generated Base{model_name}.swift")
successful_files += 1
except json.JSONDecodeError as e:
logger.error(f"Error parsing {json_file.name}: {e}")
except KeyError as e:
logger.error(f"Missing required key in {json_file.name}: {e}")
except Exception as e:
logger.error(f"Error processing {json_file.name}: {e}")
return successful_files
except Exception as e:
logger.error(f"Fatal error: {e}")
return 0
def setup_logging(verbose: bool) -> logging.Logger:
"""Configure logging based on verbosity level."""
logger = logging.getLogger('SwiftModelGenerator')
handler = logging.StreamHandler()
if verbose:
logger.setLevel(logging.DEBUG)
handler.setFormatter(logging.Formatter(
'%(asctime)s - %(levelname)s - %(message)s'
))
else:
logger.setLevel(logging.INFO)
handler.setFormatter(logging.Formatter('%(message)s'))
logger.addHandler(handler)
return logger
def validate_directory(path: str, create: bool = False) -> Path:
"""Validate and optionally create a directory."""
dir_path = Path(path)
if dir_path.exists():
if not dir_path.is_dir():
raise ValueError(f"'{path}' exists but is not a directory")
elif create:
dir_path.mkdir(parents=True)
else:
raise ValueError(f"Directory '{path}' does not exist")
return dir_path
def main():
parser = argparse.ArgumentParser(
description="Generate Swift model classes from JSON definitions",
epilog="Example: %(prog)s -i ./model_definitions -o ../MyProject/Models"
)
parser.add_argument(
"-i", "--input-dir",
required=True,
help="Directory containing JSON model definitions"
)
parser.add_argument(
"-o", "--output-dir",
required=True,
help="Directory where Swift files will be generated"
)
parser.add_argument(
"-v", "--verbose",
action="store_true",
help="Enable verbose output"
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would be generated without actually creating files"
)
parser.add_argument(
"--version",
action="version",
version="%(prog)s 1.0.0"
)
args = parser.parse_args()
# Setup logging
logger = setup_logging(args.verbose)
# Process the directories
start_time = datetime.now()
logger.info("Starting model generation...")
successful_files = process_directory(
args.input_dir,
args.output_dir,
logger,
args.dry_run
)
# Report results
duration = datetime.now() - start_time
logger.info(f"\nGeneration completed in {duration.total_seconds():.2f} seconds")
logger.info(f"Successfully processed {successful_files} files")
# Return appropriate exit code
sys.exit(0 if successful_files > 0 else 1)
if __name__ == "__main__":
main()

@ -0,0 +1,684 @@
//
// GroupStage.swift
// Padel Tournament
//
// Created by razmig on 10/03/2024.
//
import Foundation
import LeStorage
internal import Algorithms
import SwiftUI
@Observable
public final class GroupStage: BaseGroupStage, SideStorable {
public var matchFormat: MatchFormat {
get {
format ?? .defaultFormatForMatchType(.groupStage)
}
set {
format = newValue
}
}
var tournamentStore: TournamentStore? {
return TournamentLibrary.shared.store(tournamentId: self.tournament)
}
// MARK: - Computed dependencies
public func _matches() -> [Match] {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.matches.filter { $0.groupStage == self.id }.sorted(by: \.index)
// Store.main.filter { $0.groupStage == self.id }
}
public func tournamentObject() -> Tournament? {
Store.main.findById(self.tournament)
}
// MARK: -
public func teamAt(groupStagePosition: Int) -> TeamRegistration? {
if step > 0 {
return teams().first(where: { $0.groupStagePositionAtStep(step) == groupStagePosition })
}
return teams().first(where: { $0.groupStagePosition == groupStagePosition })
}
public func groupStageTitle(_ displayStyle: DisplayStyle = .wide) -> String {
if let name { return name }
var stepLabel = ""
if step > 0 {
stepLabel = " (" + (step + 1).ordinalFormatted(feminine: true) + " phase)"
}
switch displayStyle {
case .title:
return "Poule \(index + 1)" + stepLabel
case .wide:
return "Poule \(index + 1)"
case .short:
return "#\(index + 1)"
}
}
var computedOrder: Int {
index + step * 100
}
func isRunning() -> Bool { // at least a match has started
_matches().anySatisfy({ $0.isRunning() })
}
public func hasStarted() -> Bool { // meaning at least one match is over
_matches().filter { $0.hasEnded() }.isEmpty == false
}
public func hasEnded() -> Bool {
let _matches = _matches()
if _matches.isEmpty { return false }
//guard teams().count == size else { return false }
return _matches.anySatisfy { $0.hasEnded() == false } == false
}
fileprivate func _createMatch(index: Int) -> Match {
let match: Match = Match(groupStage: self.id,
index: index,
format: self.matchFormat,
name: self.localizedMatchUpLabel(for: index))
match.store = self.store
// print("_createMatch(index)", index)
return match
}
public func removeReturnMatches(onlyLast: Bool = false) {
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)
}
}
public var matchPhaseCount: Int {
let count = _matches().count
if matchCount > 0 {
return count / matchCount
} else {
return 0
}
}
public func addReturnMatches() {
var teamScores = [TeamScore]()
var matches = [Match]()
let matchPhaseCount = matchPhaseCount
for i in 0..<_numberOfMatchesToBuild() {
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)
}
self.tournamentStore?.matches.addOrUpdate(contentOfs: matches)
self.tournamentStore?.teamScores.addOrUpdate(contentOfs: teamScores)
}
public 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)
}
}
public func playedMatches() -> [Match] {
let ordered = _matches()
let order = _matchOrder()
let matchCount = max(1, matchCount)
let count = ordered.count / matchCount
if ordered.isEmpty == false && ordered.count % order.count == 0 {
let repeatedArray = (0..<count).flatMap { i in
order.map { $0 + i * order.count }
}
let result = repeatedArray.map { ordered[$0] }
return result
} else {
return ordered
}
}
public func orderedIndexOfMatch(_ match: Match) -> Int {
_matchOrder()[safe: match.index] ?? match.index
}
public 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()?.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)
let nextStepGroupStages = tournament.groupStages(atStep: 1)
let groupStagesAreOverAtSecondStep = tournament.groupStagesAreOver(atStep: 1)
if groupStagesAreOverAtFirstStep, nextStepGroupStages.isEmpty || groupStagesAreOverAtSecondStep == true, tournament.groupStageLoserBracketAreOver(), tournament.rounds().isEmpty {
tournament.endDate = Date()
DataStore.shared.tournaments.addOrUpdate(instance: tournament)
}
tournament.updateTournamentState()
}
}
public 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: 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 {
return nil
}
}
// func _score(forGroupStagePosition groupStagePosition: Int, nilIfEmpty: Bool = false) -> TeamGroupStageScore? {
// guard let team = teamAt(groupStagePosition: groupStagePosition) else { return nil }
// let matches = matches(forGroupStagePosition: groupStagePosition).filter({ $0.hasEnded() })
// if matches.isEmpty && nilIfEmpty { return nil }
// 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,+)
// return (team, wins, loses, setDifference, gameDifference)
// /*
// 2 points par rencontre gagnée
// 1 point par rencontre perdue
// -1 point en cas de rencontre perdue par disqualification (scores de 6/0 6/0 attribués aux trois matchs)
// -2 points en cas de rencontre perdu par WO (scores de 6/0 6/0 attribués aux trois matchs)
// */
// }
//
public func matches(forGroupStagePosition groupStagePosition: Int) -> [Match] {
let combos = Array((0..<size).combinations(ofCount: 2))
var matchIndexes = [Int]()
for (index, combo) in combos.enumerated() {
if combo.contains(groupStagePosition) { //team is playing
matchIndexes.append(index)
}
}
return _matches().filter { matchIndexes.contains($0.index % matchCount) }
}
public func initialStartDate(forTeam team: TeamRegistration) -> Date? {
guard let groupStagePosition = team.groupStagePositionAtStep(step) else { return nil }
return matches(forGroupStagePosition: groupStagePosition).compactMap({ $0.startDate }).sorted().first ?? startDate
}
public func matchPlayed(by groupStagePosition: Int, againstPosition: Int) -> Match? {
if groupStagePosition == againstPosition { return nil }
let combos = Array((0..<size).combinations(ofCount: 2))
var matchIndexes = [Int]()
for (index, combo) in combos.enumerated() {
if combo.contains(groupStagePosition) && combo.contains(againstPosition) { //teams are playing
matchIndexes.append(index)
}
}
return _matches().first(where: { matchIndexes.contains($0.index) })
}
public func availableToStart(playedMatches: [Match], in runningMatches: [Match], checkCanPlay: Bool = true) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func group stage availableToStart", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
return playedMatches.filter({ $0.isRunning() == false && $0.canBeStarted(inMatches: runningMatches, checkCanPlay: checkCanPlay) }).sorted(by: \.computedStartDateForSorting)
}
public func runningMatches(playedMatches: [Match]) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func group stage runningMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
return playedMatches.filter({ $0.isRunning() }).sorted(by: \.computedStartDateForSorting)
}
public func readyMatches(playedMatches: [Match]) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func group stage readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
return playedMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false })
}
public func finishedMatches(playedMatches: [Match]) -> [Match] {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func group stage finishedMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
return playedMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed()
}
public func isReturnMatchEnabled() -> Bool {
_matches().count > matchCount
}
private func _matchOrder() -> [Int] {
var order: [Int]
switch size {
case 3:
order = [1, 2, 0]
case 4:
order = [2, 3, 1, 4, 5, 0]
case 5:
order = [3, 5, 8, 2, 6, 1, 9, 4, 7, 0]
case 6:
order = [4, 7, 9, 3, 6, 11, 2, 8, 10, 1, 13, 5, 12, 14, 0]
default:
order = []
}
return order
}
func indexOf(_ matchIndex: Int) -> Int {
_matchOrder().firstIndex(of: matchIndex) ?? matchIndex
}
func _matchUp(for matchIndex: Int) -> [Int] {
let combinations = Array((0..<size).combinations(ofCount: 2))
return combinations[safe: matchIndex%matchCount] ?? []
}
func returnMatchesSuffix(for matchIndex: Int) -> String {
if matchCount > 0 {
let count = _matches().count
if count > matchCount * 2 {
return " - vague \((matchIndex / matchCount) + 1)"
}
if matchIndex >= matchCount {
return " - retour"
}
}
return ""
}
func localizedMatchUpLabel(for matchIndex: Int) -> String {
let matchUp = _matchUp(for: matchIndex)
if let index = matchUp.first, let index2 = matchUp.last {
return "#\(index + 1) vs #\(index2 + 1)" + 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 {
case .one:
return _teams.first ?? nil
case .two:
return _teams.last ?? nil
}
}
private func _teams(for matchIndex: Int) -> [TeamRegistration?] {
let combinations = Array(0..<size).combinations(ofCount: 2).map {$0}
return combinations[safe: matchIndex%matchCount]?.map { teamAt(groupStagePosition: $0) } ?? []
}
private func _removeMatches() {
self.tournamentStore?.matches.delete(contentOfs: _matches())
}
private func _numberOfMatchesToBuild() -> Int {
(size * (size - 1)) / 2
}
public func unsortedPlayers() -> [PlayerRegistration] {
unsortedTeams().flatMap({ $0.unsortedPlayers() })
}
public typealias TeamScoreAreInIncreasingOrder = (TeamGroupStageScore, TeamGroupStageScore) -> Bool
public typealias TeamGroupStageScore = (team: TeamRegistration, wins: Int, loses: Int, setDifference: Int, gameDifference: Int)
fileprivate func _headToHead(_ teamPosition: TeamRegistration, _ otherTeam: TeamRegistration) -> Bool {
let indexes = [teamPosition, otherTeam].compactMap({ $0.groupStagePosition }).sorted()
let combos = Array((0..<size).combinations(ofCount: 2))
let matchIndexes = combos.enumerated().compactMap { $0.element == indexes ? $0.offset : nil }
let matches = _matches().filter { matchIndexes.contains($0.index) }
if matches.count > 1 {
let scoreA = calculateScore(for: teamPosition, matches: matches, groupStagePosition: teamPosition.groupStagePosition!)
let scoreB = calculateScore(for: otherTeam, matches: matches, groupStagePosition: otherTeam.groupStagePosition!)
let teamsSorted = [scoreA, scoreB].sorted { (lhs, rhs) in
let predicates: [TeamScoreAreInIncreasingOrder] = [
{ $0.wins < $1.wins },
{ $0.setDifference < $1.setDifference },
{ $0.gameDifference < $1.gameDifference},
{ [self] in $0.team.groupStagePositionAtStep(self.step)! > $1.team.groupStagePositionAtStep(self.step)! }
]
for predicate in predicates {
if !predicate(lhs, rhs) && !predicate(rhs, lhs) {
continue
}
return predicate(lhs, rhs)
}
return false
}.map({ $0.team })
return teamsSorted.first == teamPosition
} else {
if let matchIndex = combos.firstIndex(of: indexes), let match = _matches().first(where: { $0.index == matchIndex }) {
return teamPosition.id == match.losingTeamId
} else {
return false
}
}
}
public func unsortedTeams() -> [TeamRegistration] {
guard let tournamentStore = self.tournamentStore else { return [] }
if step > 0 {
return tournamentStore.groupStages.filter({ $0.step == step - 1 }).compactMap({ $0.teams(true)[safe: index] })
}
return tournamentStore.teamRegistrations.filter { $0.groupStage == self.id && $0.groupStagePosition != nil }
}
var scoreCache: [Int: TeamGroupStageScore] = [:]
public func computedScore(forTeam team: TeamRegistration, step: Int = 0) -> TeamGroupStageScore? {
guard let groupStagePositionAtStep = team.groupStagePositionAtStep(step) else {
return nil
}
if let cachedScore = scoreCache[groupStagePositionAtStep] {
return cachedScore
} else {
let score = _score(forGroupStagePosition: groupStagePositionAtStep)
if let score = score {
scoreCache[groupStagePositionAtStep] = score
}
return score
}
}
public func teams(_ sortedByScore: Bool = false, scores: [TeamGroupStageScore]? = nil) -> [TeamRegistration] {
if sortedByScore {
return unsortedTeams().compactMap({ team in
// Check cache or use provided scores, otherwise calculate and store in cache
scores?.first(where: { $0.team.id == team.id }) ?? {
return computedScore(forTeam: team, step: step)
}()
}).sorted { (lhs, rhs) in
let predicates: [TeamScoreAreInIncreasingOrder] = [
{ $0.wins < $1.wins },
{ $0.setDifference < $1.setDifference },
{ $0.gameDifference < $1.gameDifference},
{ self._headToHead($0.team, $1.team) },
{ [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 }).reversed()
} else {
return unsortedTeams().sorted(by: \TeamRegistration.groupStagePosition!)
}
}
public func _score(forGroupStagePosition groupStagePosition: Int, nilIfEmpty: Bool = false) -> TeamGroupStageScore? {
// Check if the score for this position is already cached
if let cachedScore = scoreCache[groupStagePosition] {
return cachedScore
}
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,+)
return (team, wins, loses, setDifference, gameDifference)
}
// Clear the cache if necessary, for example when starting a new step or when matches update
func clearScoreCache() {
scoreCache.removeAll()
}
// func teams(_ sortedByScore: Bool = false, scores: [TeamGroupStageScore]? = nil) -> [TeamRegistration] {
// if sortedByScore {
// return unsortedTeams().compactMap({ team in
// scores?.first(where: { $0.team.id == team.id }) ?? _score(forGroupStagePosition: team.groupStagePositionAtStep(step)!)
// }).sorted { (lhs, rhs) in
// // Calculate intermediate values once and reuse them
// let lhsWins = lhs.wins
// let rhsWins = rhs.wins
// let lhsSetDifference = lhs.setDifference
// let rhsSetDifference = rhs.setDifference
// let lhsGameDifference = lhs.gameDifference
// let rhsGameDifference = rhs.gameDifference
// let lhsHeadToHead = self._headToHead(lhs.team, rhs.team)
// let rhsHeadToHead = self._headToHead(rhs.team, lhs.team)
// let lhsGroupStagePosition = lhs.team.groupStagePositionAtStep(self.step)!
// let rhsGroupStagePosition = rhs.team.groupStagePositionAtStep(self.step)!
//
// // Define comparison predicates in the same order
// let predicates: [(Bool, Bool)] = [
// (lhsWins < rhsWins, lhsWins > rhsWins),
// (lhsSetDifference < rhsSetDifference, lhsSetDifference > rhsSetDifference),
// (lhsGameDifference < rhsGameDifference, lhsGameDifference > rhsGameDifference),
// (lhsHeadToHead, rhsHeadToHead),
// (lhsGroupStagePosition > rhsGroupStagePosition, lhsGroupStagePosition < rhsGroupStagePosition)
// ]
//
// // Iterate over predicates and return as soon as a valid comparison is found
// for (lhsPredicate, rhsPredicate) in predicates {
// if lhsPredicate { return true }
// if rhsPredicate { return false }
// }
//
// return false
// }.map({ $0.team }).reversed()
// } else {
// return unsortedTeams().sorted(by: \TeamRegistration.groupStagePosition!)
// }
// }
public func updateMatchFormat(_ updatedMatchFormat: MatchFormat) {
self.matchFormat = updatedMatchFormat
self.updateAllMatchesFormat()
}
public func updateAllMatchesFormat() {
let playedMatches = playedMatches()
playedMatches.forEach { match in
match.matchFormat = matchFormat
}
self.tournamentStore?.matches.addOrUpdate(contentOfs: playedMatches)
}
public func pasteData() -> String {
var data: [String] = []
data.append(self.groupStageTitle())
teams().forEach { team in
data.append(team.teamLabelRanked(displayRank: true, displayTeamName: true))
}
return data.joined(separator: "\n")
}
public func finalPosition(ofTeam team: TeamRegistration) -> Int? {
guard hasEnded() else { return nil }
return teams(true).firstIndex(of: team)
}
public override func deleteDependencies(shouldBeSynchronized: Bool) {
let matches = self._matches()
for match in matches {
match.deleteDependencies(shouldBeSynchronized: shouldBeSynchronized)
}
self.tournamentStore?.matches.deleteDependencies(matches, shouldBeSynchronized: shouldBeSynchronized)
}
// init(from decoder: Decoder) throws {
// let container = try decoder.container(keyedBy: CodingKeys.self)
//
// id = try container.decode(String.self, forKey: ._id)
// storeId = try container.decode(String.self, forKey: ._storeId)
// lastUpdate = try container.decode(Date.self, forKey: ._lastUpdate)
// tournament = try container.decode(String.self, forKey: ._tournament)
// index = try container.decode(Int.self, forKey: ._index)
// size = try container.decode(Int.self, forKey: ._size)
// format = try container.decodeIfPresent(MatchFormat.self, forKey: ._format)
// startDate = try container.decodeIfPresent(Date.self, forKey: ._startDate)
// name = try container.decodeIfPresent(String.self, forKey: ._name)
// step = try container.decodeIfPresent(Int.self, forKey: ._step) ?? 0
// }
//
// 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(index, forKey: ._index)
// try container.encode(size, forKey: ._size)
// try container.encode(format, forKey: ._format)
// try container.encode(startDate, forKey: ._startDate)
// try container.encode(name, forKey: ._name)
// try container.encode(step, forKey: ._step)
// }
func insertOnServer() {
self.tournamentStore?.groupStages.writeChangeAndInsertOnServer(instance: self)
for match in self._matches() {
match.insertOnServer()
}
}
}
extension GroupStage {
enum CodingKeys: String, CodingKey {
case _id = "id"
case _storeId = "storeId"
case _lastUpdate = "lastUpdate"
case _tournament = "tournament"
case _index = "index"
case _size = "size"
case _format = "format"
case _startDate = "startDate"
case _name = "name"
case _step = "step"
}
}
extension GroupStage: Selectable {
public func selectionLabel(index: Int) -> String {
groupStageTitle()
}
public func badgeValue() -> Int? {
return runningMatches(playedMatches: _matches()).count
}
public func badgeValueColor() -> Color? {
return nil
}
public func badgeImage() -> Badge? {
if teams().count < size {
return .xmark
} else {
return hasEnded() ? .checkmark : nil
}
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,888 @@
//
// MatchScheduler.swift
// PadelClub
//
// Created by Razmig Sarkissian on 08/04/2024.
//
import Foundation
import LeStorage
import SwiftUI
@Observable
public final class MatchScheduler: BaseMatchScheduler, SideStorable {
//
// init(tournament: String,
// timeDifferenceLimit: Int = 5,
// loserBracketRotationDifference: Int = 0,
// upperBracketRotationDifference: Int = 1,
// accountUpperBracketBreakTime: Bool = true,
// accountLoserBracketBreakTime: Bool = false,
// randomizeCourts: Bool = true,
// rotationDifferenceIsImportant: Bool = false,
// shouldHandleUpperRoundSlice: Bool = false,
// shouldEndRoundBeforeStartingNext: Bool = true,
//<<<<<<< HEAD
// groupStageChunkCount: Int? = nil, overrideCourtsUnavailability: Bool = false, shouldTryToFillUpCourtsAvailable: Bool = false) {
// super.init()
//=======
// groupStageChunkCount: Int? = nil,
// overrideCourtsUnavailability: Bool = false,
// shouldTryToFillUpCourtsAvailable: Bool = true,
// courtsAvailable: Set<Int> = Set<Int>(),
// simultaneousStart: Bool = true) {
//>>>>>>> main
// self.tournament = tournament
// self.timeDifferenceLimit = timeDifferenceLimit
// self.loserBracketRotationDifference = loserBracketRotationDifference
// self.upperBracketRotationDifference = upperBracketRotationDifference
// self.accountUpperBracketBreakTime = accountUpperBracketBreakTime
// self.accountLoserBracketBreakTime = accountLoserBracketBreakTime
// self.randomizeCourts = randomizeCourts
// self.rotationDifferenceIsImportant = rotationDifferenceIsImportant
// self.shouldHandleUpperRoundSlice = shouldHandleUpperRoundSlice
// self.shouldEndRoundBeforeStartingNext = shouldEndRoundBeforeStartingNext
// self.groupStageChunkCount = groupStageChunkCount
// self.overrideCourtsUnavailability = overrideCourtsUnavailability
// self.shouldTryToFillUpCourtsAvailable = shouldTryToFillUpCourtsAvailable
// self.courtsAvailable = courtsAvailable
// self.simultaneousStart = simultaneousStart
// }
var courtsUnavailability: [DateInterval]? {
guard let event = tournamentObject()?.eventObject() else { return nil }
return event.courtsUnavailability + (overrideCourtsUnavailability ? [] : event.tournamentsCourtsUsed(exluding: tournament))
}
var additionalEstimationDuration : Int {
return tournamentObject()?.additionalEstimationDuration ?? 0
}
var tournamentStore: TournamentStore? {
return TournamentLibrary.shared.store(tournamentId: self.tournament)
// TournamentStore.instance(tournamentId: self.tournament)
}
func tournamentObject() -> Tournament? {
return Store.main.findById(tournament)
}
@discardableResult
public func updateGroupStageSchedule(tournament: Tournament, specificGroupStage: GroupStage? = nil, atStep step: Int = 0, startDate: Date? = nil) -> Date {
let computedGroupStageChunkCount = groupStageChunkCount ?? tournament.getGroupStageChunkValue()
var groupStages: [GroupStage] = tournament.groupStages(atStep: step)
if let specificGroupStage {
groupStages = [specificGroupStage]
}
let matches = groupStages.flatMap { $0._matches() }
matches.forEach({
$0.removeCourt()
$0.startDate = nil
$0.confirmed = false
})
var lastDate : Date = startDate ?? tournament.startDate
let times = Set(groupStages.compactMap { $0.startDate }).sorted()
if let first = times.first {
if first.isEarlierThan(tournament.startDate) {
tournament.startDate = first
do {
try DataStore.shared.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
}
}
times.forEach({ time in
if lastDate.isEarlierThan(time) {
lastDate = time
}
let groups = groupStages.filter({ $0.startDate == time })
let dispatch = groupStageDispatcher(groupStages: groups, startingDate: lastDate)
dispatch.timedMatches.forEach { matchSchedule in
if let match = matches.first(where: { $0.id == matchSchedule.matchID }) {
let estimatedDuration = match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)
let timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(estimatedDuration) * 60
if let startDate = match.groupStageObject?.startDate {
let matchStartDate = startDate.addingTimeInterval(timeIntervalToAdd)
match.startDate = matchStartDate
lastDate = matchStartDate.addingTimeInterval(Double(estimatedDuration) * 60)
}
match.setCourt(matchSchedule.courtIndex)
}
}
})
groupStages.filter({ $0.startDate == nil || times.contains($0.startDate!) == false }).chunked(into: computedGroupStageChunkCount).forEach { groups in
groups.forEach({ $0.startDate = lastDate })
do {
try self.tournamentStore?.groupStages.addOrUpdate(contentOfs: groups)
} catch {
Logger.error(error)
}
let dispatch = groupStageDispatcher(groupStages: groups, startingDate: lastDate)
dispatch.timedMatches.forEach { matchSchedule in
if let match = matches.first(where: { $0.id == matchSchedule.matchID }) {
let estimatedDuration = match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)
let timeIntervalToAdd = (Double(matchSchedule.rotationIndex)) * Double(estimatedDuration) * 60
if let startDate = match.groupStageObject?.startDate {
let matchStartDate = startDate.addingTimeInterval(timeIntervalToAdd)
match.startDate = matchStartDate
lastDate = matchStartDate.addingTimeInterval(Double(estimatedDuration) * 60)
}
match.setCourt(matchSchedule.courtIndex)
}
}
}
do {
try self.tournamentStore?.matches.addOrUpdate(contentOfs: matches)
} catch {
Logger.error(error)
}
return lastDate
}
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
var flattenedMatches = [Match]()
if simultaneousStart {
// Flatten matches in a round-robin order by cycling through each group
flattenedMatches = (0..<maxMatchesCount).flatMap { index in
_groupStages.compactMap { group in
// Safely access matches, return nil if index is out of bounds
let playedMatches = group.playedMatches()
return playedMatches.indices.contains(index) ? playedMatches[index] : nil
}
}
} else {
flattenedMatches = _groupStages.flatMap({ $0.playedMatches() })
}
var slots = [GroupStageTimeMatch]()
var availableMatches = flattenedMatches
var rotationIndex = 0
var teamsPerRotation = [Int: [String]]() // Tracks teams assigned to each rotation
var freeCourtPerRotation = [Int: [Int]]() // Tracks free courts per rotation
var groupLastRotation = [Int: Int]() // Tracks the last rotation each group was involved in
let courtsUnavailability = courtsUnavailability
while slots.count < flattenedMatches.count {
print("Starting rotation \(rotationIndex) with \(availableMatches.count) matches left")
teamsPerRotation[rotationIndex] = []
freeCourtPerRotation[rotationIndex] = []
let previousRotationBracketIndexes = slots.filter { $0.rotationIndex == rotationIndex - 1 }
.map { ($0.groupIndex, 1) }
let counts = Dictionary(previousRotationBracketIndexes, uniquingKeysWith: +)
var rotationMatches = Array(availableMatches.filter({ match in
// Check if all teams from the match are not already scheduled in the current rotation
let teamsAvailable = teamsPerRotation[rotationIndex]!.allSatisfy({ !match.containsTeamIndex($0) })
if !teamsAvailable {
print("Match \(match.roundAndMatchTitle()) has teams already scheduled in rotation \(rotationIndex)")
}
return teamsAvailable
}))
if rotationIndex > 0, simultaneousStart == false {
rotationMatches = rotationMatches.sorted(by: {
if counts[$0.groupStageObject!.index] ?? 0 == counts[$1.groupStageObject!.index] ?? 0 {
return $0.groupStageObject!.index < $1.groupStageObject!.index
} else {
return counts[$0.groupStageObject!.index] ?? 0 < counts[$1.groupStageObject!.index] ?? 0
}
})
}
courtsAvailable.forEach { courtIndex in
print("Checking availability for court \(courtIndex) in rotation \(rotationIndex)")
if let first = rotationMatches.first(where: { match in
let estimatedDuration = match.matchFormat.getEstimatedDuration(additionalEstimationDuration)
let timeIntervalToAdd = Double(rotationIndex) * Double(estimatedDuration) * 60
let rotationStartDate: Date = startingDate.addingTimeInterval(timeIntervalToAdd)
let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: match.matchFormat.getEstimatedDuration(additionalEstimationDuration), courtsUnavailability: courtsUnavailability)
if courtsUnavailable.contains(courtIndex) {
print("Court \(courtIndex) is unavailable at \(rotationStartDate)")
return false
}
let teamsAvailable = teamsPerRotation[rotationIndex]!.allSatisfy({ !match.containsTeamIndex($0) })
if !teamsAvailable {
print("Teams from match \(match.roundAndMatchTitle()) are already scheduled in this rotation")
return false
}
print("Match \(match.roundAndMatchTitle()) is available for court \(courtIndex) at \(rotationStartDate)")
return true
}) {
let timeMatch = GroupStageTimeMatch(matchID: first.id, rotationIndex: rotationIndex, courtIndex: courtIndex, groupIndex: first.groupStageObject!.index)
print("Scheduled match: \(first.roundAndMatchTitle()) on court \(courtIndex) at rotation \(rotationIndex)")
slots.append(timeMatch)
teamsPerRotation[rotationIndex]!.append(contentsOf: first.matchUp())
rotationMatches.removeAll(where: { $0.id == first.id })
availableMatches.removeAll(where: { $0.id == first.id })
if let index = first.groupStageObject?.index {
groupLastRotation[index] = rotationIndex
}
} else {
print("No available matches for court \(courtIndex) in rotation \(rotationIndex), adding to free court list")
freeCourtPerRotation[rotationIndex]!.append(courtIndex)
}
}
rotationIndex += 1
}
print("All matches scheduled. Total rotations: \(rotationIndex)")
// Organize slots and ensure courts are randomized or sorted
var organizedSlots = [GroupStageTimeMatch]()
for i in 0..<rotationIndex {
let courtsSorted: [Int] = slots.filter({ $0.rotationIndex == i }).map { $0.courtIndex }.sorted()
let courts: [Int] = randomizeCourts ? courtsSorted.shuffled() : courtsSorted
var matches = slots.filter({ $0.rotationIndex == i }).sorted(using: .keyPath(\.groupIndex), .keyPath(\.courtIndex))
for j in 0..<matches.count {
matches[j].courtIndex = courts[j]
organizedSlots.append(matches[j])
}
}
return GroupStageMatchDispatcher(
timedMatches: organizedSlots,
freeCourtPerRotation: freeCourtPerRotation,
rotationCount: rotationIndex,
groupLastRotation: groupLastRotation
)
}
func rotationDifference(loserBracket: Bool) -> Int {
if loserBracket {
return loserBracketRotationDifference
} else {
return upperBracketRotationDifference
}
}
func roundMatchCanBePlayed(_ match: Match, roundObject: Round, slots: [TimeMatch], rotationIndex: Int, targetedStartDate: Date, minimumTargetedEndDate: inout Date) -> Bool {
print("Evaluating match: \(match.roundAndMatchTitle()) in round: \(roundObject.roundTitle()) with index: \(match.index)")
if let roundStartDate = roundObject.startDate, targetedStartDate < roundStartDate {
print("Cannot start at \(targetedStartDate), earlier than round start date \(roundStartDate)")
if targetedStartDate == minimumTargetedEndDate {
print("Updating minimumTargetedEndDate to roundStartDate: \(roundStartDate)")
minimumTargetedEndDate = roundStartDate
} else {
print("Setting minimumTargetedEndDate to the earlier of \(roundStartDate) and \(minimumTargetedEndDate)")
minimumTargetedEndDate = min(roundStartDate, minimumTargetedEndDate)
}
print("Returning false: Match cannot start earlier than the round start date.")
return false
}
let previousMatches = roundObject.precedentMatches(ofMatch: match)
if previousMatches.isEmpty {
print("No ancestors matches for this match, returning true. (eg beginning of tournament 1st bracket")
return true
}
let previousMatchSlots = slots.filter { previousMatches.map { $0.id }.contains($0.matchID) }
if previousMatchSlots.isEmpty {
if previousMatches.filter({ !$0.disabled }).allSatisfy({ $0.startDate != nil }) {
print("All previous matches have start dates, returning true.")
return true
}
print("Some previous matches are pending, returning false.")
return false
}
if previousMatches.filter({ !$0.disabled }).count > previousMatchSlots.count {
if previousMatches.filter({ !$0.disabled }).anySatisfy({ $0.startDate != nil }) {
print("Some previous matches started, returning true.")
return true
}
print("Not enough previous matches have started, returning false.")
return false
}
var includeBreakTime = false
if accountLoserBracketBreakTime && roundObject.isLoserBracket() {
includeBreakTime = true
print("Including break time for loser bracket.")
}
if accountUpperBracketBreakTime && !roundObject.isLoserBracket() {
includeBreakTime = true
print("Including break time for upper bracket.")
}
let previousMatchIsInPreviousRotation = previousMatchSlots.allSatisfy {
$0.rotationIndex + rotationDifference(loserBracket: roundObject.isLoserBracket()) < rotationIndex
}
if previousMatchIsInPreviousRotation {
print("All previous matches are from earlier rotations, returning true.")
} else {
print("Some previous matches are from the current rotation.")
}
guard let minimumPossibleEndDate = previousMatchSlots.map({
$0.estimatedEndDate(includeBreakTime: includeBreakTime)
}).max() else {
print("No valid previous match end date, returning \(previousMatchIsInPreviousRotation).")
return previousMatchIsInPreviousRotation
}
if targetedStartDate >= minimumPossibleEndDate {
if rotationDifferenceIsImportant {
print("Targeted start date is after the minimum possible end date and rotation difference is important, returning \(previousMatchIsInPreviousRotation).")
return previousMatchIsInPreviousRotation
} else {
print("Targeted start date is after the minimum possible end date, returning true.")
return true
}
} else {
if targetedStartDate == minimumTargetedEndDate {
print("Updating minimumTargetedEndDate to minimumPossibleEndDate: \(minimumPossibleEndDate)")
minimumTargetedEndDate = minimumPossibleEndDate
} else {
print("Setting minimumTargetedEndDate to the earlier of \(minimumPossibleEndDate) and \(minimumTargetedEndDate)")
minimumTargetedEndDate = min(minimumPossibleEndDate, minimumTargetedEndDate)
}
print("Targeted start date \(targetedStartDate) is before the minimum possible end date, returning false. \(minimumTargetedEndDate)")
return false
}
}
func getNextStartDate(fromPreviousRotationSlots slots: [TimeMatch], includeBreakTime: Bool) -> Date? {
slots.map { $0.estimatedEndDate(includeBreakTime: includeBreakTime) }.min()
}
func getNextEarliestAvailableDate(from slots: [TimeMatch]) -> [(Int, Date)] {
let byCourt = Dictionary(grouping: slots, by: { $0.courtIndex })
return (byCourt.keys.flatMap { courtIndex in
let matchesByCourt = byCourt[courtIndex]?.sorted(by: \.startDate)
let lastMatch = matchesByCourt?.last
var results = [(Int, Date)]()
if let courtFreeDate = lastMatch?.estimatedEndDate(includeBreakTime: false) {
results.append((courtIndex, courtFreeDate))
}
return results
}
)
}
func getAvailableCourts(from matches: [Match]) -> [(Int, Date)] {
let validMatches = matches.filter({ $0.courtIndex != nil && $0.startDate != nil })
let byCourt = Dictionary(grouping: validMatches, by: { $0.courtIndex! })
return (byCourt.keys.flatMap { court in
let matchesByCourt = byCourt[court]?.sorted(by: \.startDate!)
let lastMatch = matchesByCourt?.last
var results = [(Int, Date)]()
if let courtFreeDate = lastMatch?.estimatedEndDate(additionalEstimationDuration) {
results.append((court, courtFreeDate))
}
return results
}
)
}
func roundDispatcher(flattenedMatches: [Match], dispatcherStartDate: Date, initialCourts: [Int]?) -> MatchDispatcher {
var slots = [TimeMatch]()
var _startDate: Date?
var rotationIndex = 0
var availableMatchs = flattenedMatches.filter({ $0.startDate == nil })
let courtsUnavailability = courtsUnavailability
var issueFound: Bool = false
// Log start of the function
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 {
_startDate = match.startDate
} else if match.startDate! > _startDate! {
_startDate = match.startDate
rotationIndex += 1
}
let timeMatch = TimeMatch(matchID: match.id, rotationIndex: rotationIndex, courtIndex: match.courtIndex ?? 0, startDate: match.startDate!, durationLeft: match.matchFormat.getEstimatedDuration(additionalEstimationDuration), minimumBreakTime: match.matchFormat.breakTime.breakTime)
slots.append(timeMatch)
}
if !slots.isEmpty {
rotationIndex += 1
}
var freeCourtPerRotation = [Int: [Int]]()
var courts = initialCourts ?? Array(courtsAvailable)
var shouldStartAtDispatcherDate = rotationIndex > 0
var suitableDate: Date?
while !availableMatchs.isEmpty && !issueFound && rotationIndex < 50 {
freeCourtPerRotation[rotationIndex] = []
let previousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 1 })
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 : Array(courtsAvailable)
}
courts.sort()
// Log courts availability and start date
print("Courts available at rotation \(rotationIndex): \(courts)")
print("Rotation start date: \(rotationStartDate)")
// Check for court availability and break time conflicts
if rotationIndex > 0, let freeCourtPreviousRotation = freeCourtPerRotation[rotationIndex - 1], !freeCourtPreviousRotation.isEmpty {
print("Handling break time conflicts or waiting for free courts")
let previousPreviousRotationSlots = slots.filter { $0.rotationIndex == rotationIndex - 2 && freeCourtPreviousRotation.contains($0.courtIndex) }
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 {
let differenceWithBreak = rotationStartDate.timeIntervalSince(previousEndDate)
let differenceWithoutBreak = rotationStartDate.timeIntervalSince(previousEndDateNoBreak)
print("Difference with break: \(differenceWithBreak), without break: \(differenceWithoutBreak)")
let timeDifferenceLimitInSeconds = Double(timeDifferenceLimit * 60)
var difference = differenceWithBreak
if differenceWithBreak <= 0, accountUpperBracketBreakTime == false {
difference = differenceWithoutBreak
} else if differenceWithBreak > timeDifferenceLimitInSeconds && differenceWithoutBreak > timeDifferenceLimitInSeconds {
difference = noBreakAlreadyTested ? differenceWithBreak : max(differenceWithBreak, differenceWithoutBreak)
}
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
rotationStartDate = rotationStartDate.addingTimeInterval(-difference)
}
}
} else if let firstMatch = availableMatchs.first {
let duration = firstMatch.matchFormat.getEstimatedDuration(additionalEstimationDuration)
let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: duration, courtsUnavailability: courtsUnavailability)
if Array(Set(courtsAvailable).subtracting(Set(courtsUnavailable))).isEmpty {
print("Issue: All courts unavailable in this rotation")
if let courtsUnavailability {
let computedStartDateAndCourts = getFirstFreeCourt(startDate: rotationStartDate, duration: duration, courts: courts, courtsUnavailability: courtsUnavailability)
rotationStartDate = computedStartDateAndCourts.earliestFreeDate
courts = computedStartDateAndCourts.availableCourts
} else {
issueFound = true
}
} else {
courts = Array(Set(courtsAvailable).subtracting(Set(courtsUnavailable)))
}
}
// Dispatch courts and schedule matches
suitableDate = dispatchCourts(courts: courts, availableMatchs: &availableMatchs, slots: &slots, rotationIndex: rotationIndex, rotationStartDate: rotationStartDate, freeCourtPerRotation: &freeCourtPerRotation, courtsUnavailability: courtsUnavailability)
rotationIndex += 1
}
// Organize matches in slots
var organizedSlots = [TimeMatch]()
for i in 0..<rotationIndex {
let courtsSorted = slots.filter { $0.rotationIndex == i }.map { $0.courtIndex }.sorted()
let courts = randomizeCourts ? courtsSorted.shuffled() : courtsSorted
var matches = slots.filter { $0.rotationIndex == i }.sorted(using: .keyPath(\.courtIndex))
for j in 0..<matches.count {
matches[j].courtIndex = courts[j]
organizedSlots.append(matches[j])
}
}
print("Finished roundDispatcher with \(organizedSlots.count) scheduled matches")
return MatchDispatcher(timedMatches: organizedSlots, freeCourtPerRotation: freeCourtPerRotation, rotationCount: rotationIndex, issueFound: issueFound)
}
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
// Log dispatch attempt
print("Dispatching courts for rotation \(rotationIndex) with start date \(rotationStartDate) and available courts \(courts.sorted())")
for (courtPosition, courtIndex) in courts.sorted().enumerated() {
if let firstMatch = availableMatchs.first(where: { match in
print("Trying to find a match for court \(courtIndex) in rotation \(rotationIndex)")
let roundObject = match.roundObject!
let duration = match.matchFormat.getEstimatedDuration(additionalEstimationDuration)
let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: duration, courtsUnavailability: courtsUnavailability)
if courtsUnavailable.contains(courtIndex) {
print("Returning false: Court \(courtIndex) unavailable due to schedule conflicts during \(rotationStartDate).")
return false
}
let canBePlayed = roundMatchCanBePlayed(match, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate)
if !canBePlayed {
print("Returning false: Match \(match.roundAndMatchTitle()) can't be played due to constraints.")
return false
}
let currentRotationSameRoundMatches = matchPerRound[roundObject.id] ?? 0
let roundMatchesCount = roundObject.playedMatches().count
if shouldHandleUpperRoundSlice {
if roundObject.parent == nil, roundObject.index > 1, currentRotationSameRoundMatches == 0, courts.count - matchPerRound.count < 2 {
return false
}
else if roundObject.parent == nil, roundObject.index == 0, matchPerRound.isEmpty == false {
return false
}
else if roundObject.parent == nil, roundObject.index == 1, currentRotationSameRoundMatches >= 0, courtsAvailable.count > 0 {
return true
}
else if roundObject.parent == nil && roundMatchesCount > courts.count && currentRotationSameRoundMatches >= min(roundMatchesCount / 2, courts.count) {
print("Returning false: Too many matches already played in the current rotation for round \(roundObject.roundTitle()).")
return false
}
}
let indexInRound = match.indexInRound()
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
}
}
}
print("Returning true: Match \(match.roundAndMatchTitle()) can be played on court \(courtIndex).")
return canBePlayed
}) {
print("Found match: \(firstMatch.roundAndMatchTitle()) for court \(courtIndex) at \(rotationStartDate)")
matchPerRound[firstMatch.roundObject!.id, default: 0] += 1
let timeMatch = TimeMatch(
matchID: firstMatch.id,
rotationIndex: rotationIndex,
courtIndex: courtIndex,
startDate: rotationStartDate,
durationLeft: firstMatch.matchFormat.getEstimatedDuration(additionalEstimationDuration),
minimumBreakTime: firstMatch.matchFormat.breakTime.breakTime
)
slots.append(timeMatch)
availableMatchs.removeAll(where: { $0.id == firstMatch.id })
} else {
print("No suitable match found for court \(courtIndex) in rotation \(rotationIndex). Adding court to freeCourtPerRotation.")
freeCourtPerRotation[rotationIndex]?.append(courtIndex)
}
}
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 public func updateBracketSchedule(tournament: Tournament, fromRoundId roundId: String?, fromMatchId matchId: String?, startDate: Date) -> Bool {
let upperRounds: [Round] = tournament.rounds()
let allMatches: [Match] = tournament.allMatches().filter({ $0.hasEnded() == false && $0.hasStarted() == false })
var rounds = [Round]()
if let groupStageLoserBracketRound = tournament.groupStageLoserBracket() {
rounds.append(groupStageLoserBracketRound)
}
if shouldEndRoundBeforeStartingNext {
rounds.append(contentsOf: upperRounds.flatMap {
[$0] + $0.loserRoundsAndChildren()
})
} else {
rounds.append(contentsOf: upperRounds.map {
$0
} + upperRounds.flatMap {
$0.loserRoundsAndChildren()
})
}
let flattenedMatches = rounds.flatMap { round in
round._matches().filter({ $0.disabled == false && $0.hasEnded() == false && $0.hasStarted() == false }).sorted(by: \.index)
}
flattenedMatches.forEach({
if (roundId == nil && matchId == nil) || $0.startDate?.isEarlierThan(startDate) == false {
$0.startDate = nil
$0.removeCourt()
$0.confirmed = false
}
})
// if let roundId {
// if let round : Round = Store.main.findById(roundId) {
// let matches = round._matches().filter({ $0.disabled == false }).sorted(by: \.index)
// round.resetFromRoundAllMatchesStartDate()
// flattenedMatches = matches + flattenedMatches
// }
//
// } else if let matchId {
// if let match : Match = Store.main.findById(matchId) {
// if let round = match.roundObject {
// round.resetFromRoundAllMatchesStartDate(from: match)
// }
// flattenedMatches = [match] + flattenedMatches
// }
// }
if let roundId, let matchId {
//todo
if let index = flattenedMatches.firstIndex(where: { $0.round == roundId && $0.id == matchId }) {
flattenedMatches[index...].forEach {
$0.startDate = nil
$0.removeCourt()
$0.confirmed = false
}
}
} else if let roundId {
//todo
if let index = flattenedMatches.firstIndex(where: { $0.round == roundId }) {
flattenedMatches[index...].forEach {
$0.startDate = nil
$0.removeCourt()
$0.confirmed = false
}
}
}
let matches: [Match] = allMatches.filter { $0.startDate?.isEarlierThan(startDate) == true && $0.startDate?.dayInt == startDate.dayInt }
let usedCourts = getAvailableCourts(from: matches)
let initialCourts: [Int] = usedCourts.filter { (court, availableDate) in
availableDate <= startDate
}.sorted(by: \.1).compactMap { $0.0 }
let courts : [Int]? = initialCourts.isEmpty ? nil : initialCourts
print("initial available courts at beginning: \(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 }) {
match.startDate = matchSchedule.startDate
match.setCourt(matchSchedule.courtIndex)
}
}
do {
try self.tournamentStore?.matches.addOrUpdate(contentOfs: allMatches)
} catch {
Logger.error(error)
}
return roundDispatch.issueFound
}
public func courtsUnavailable(startDate: Date, duration: Int, courtsUnavailability: [DateInterval]?) -> [Int] {
let endDate = startDate.addingTimeInterval(Double(duration) * 60)
guard let courtsUnavailability else { return [] }
let groupedBy = Dictionary(grouping: courtsUnavailability, by: { $0.courtIndex })
let courts = groupedBy.keys
return courts.filter {
courtUnavailable(courtIndex: $0, from: startDate, to: endDate, source: courtsUnavailability)
}
}
public func courtUnavailable(courtIndex: Int, from startDate: Date, to endDate: Date, source: [DateInterval]) -> Bool {
let courtLockedSchedule = source.filter({ $0.courtIndex == courtIndex })
return courtLockedSchedule.anySatisfy({ dateInterval in
let range = startDate..<endDate
return dateInterval.range.overlaps(range)
})
}
public 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)
}
}
public 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)
}
if tournament.groupStages(atStep: 1).isEmpty == false {
lastDate = updateGroupStageSchedule(tournament: tournament, atStep: 1, startDate: lastDate)
}
return updateBracketSchedule(tournament: tournament, fromRoundId: nil, fromMatchId: nil, startDate: lastDate)
}
}
struct GroupStageTimeMatch {
let matchID: String
let rotationIndex: Int
var courtIndex: Int
let groupIndex: Int
}
struct TimeMatch {
let matchID: String
let rotationIndex: Int
var courtIndex: Int
var startDate: Date
var durationLeft: Int //in minutes
var minimumBreakTime: Int //in minutes
func estimatedEndDate(includeBreakTime: Bool) -> Date {
let minutesToAdd = Double(durationLeft + (includeBreakTime ? minimumBreakTime : 0))
return startDate.addingTimeInterval(minutesToAdd * 60.0)
}
var computedEndDateForSorting: Date {
estimatedEndDate(includeBreakTime: false)
}
}
struct GroupStageMatchDispatcher {
let timedMatches: [GroupStageTimeMatch]
let freeCourtPerRotation: [Int: [Int]]
let rotationCount: Int
let groupLastRotation: [Int: Int]
}
struct MatchDispatcher {
let timedMatches: [TimeMatch]
let freeCourtPerRotation: [Int: [Int]]
let rotationCount: Int
let issueFound: Bool
}
extension Match {
func teamIds() -> [String] {
return teams().map { $0.id }
}
public func containsTeamId(_ id: String) -> Bool {
return teamIds().contains(id)
}
public 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)" }
}
}

@ -0,0 +1,66 @@
//
// MockData.swift
// PadelClub
//
// Created by Razmig Sarkissian on 20/03/2024.
//
import Foundation
public extension Court {
static func mock() -> Court {
Court(index: 0, club: "", name: "Test")
}
}
public extension Event {
static func mock() -> Event {
Event()
}
}
public extension Club {
static func mock() -> Club {
Club(name: "AUC", acronym: "AUC")
}
static func newEmptyInstance() -> Club {
Club(name: "", acronym: "")
}
}
public extension GroupStage {
static func mock() -> GroupStage {
GroupStage(tournament: "", index: 0, size: 4)
}
}
public extension Round {
static func mock() -> Round {
Round(tournament: "", index: 0)
}
}
public extension Tournament {
static func mock() -> Tournament {
return Tournament(groupStageSortMode: .snake, teamSorting: .inscriptionDate, federalCategory: .men, federalLevelCategory: .p100, federalAgeCategory: .senior)
}
}
public extension Match {
static func mock() -> Match {
return Match(index: 0)
}
}
public extension TeamRegistration {
static func mock() -> TeamRegistration {
return TeamRegistration(tournament: "")
}
}
public extension PlayerRegistration {
static func mock() -> PlayerRegistration {
return PlayerRegistration(firstName: "Raz", lastName: "Shark", sex: .male)
}
}

@ -0,0 +1,33 @@
//
// MonthData.swift
// PadelClub
//
// Created by Razmig Sarkissian on 18/04/2024.
//
import Foundation
import SwiftUI
import LeStorage
@Observable
public final class MonthData: BaseMonthData {
public init(monthKey: String) {
super.init()
self.monthKey = monthKey
self.creationDate = Date()
}
required init(from decoder: Decoder) throws {
try super.init(from: decoder)
}
required public init() {
super.init()
}
public func total() -> Int {
return (maleCount ?? 0) + (femaleCount ?? 0)
}
}

@ -0,0 +1,53 @@
//
// PlayerPaymentType.swift
// PadelClub
//
// Created by Laurent Morvillier on 11/02/2025.
//
import Foundation
public enum PlayerPaymentType: Int, CaseIterable, Identifiable, Codable {
init?(rawValue: Int?) {
guard let value = rawValue else { return nil }
self.init(rawValue: value)
}
public var id: Self {
self
}
case cash = 0
case lydia = 1
case gift = 2
case check = 3
case paylib = 4
case bankTransfer = 5
case clubHouse = 6
case creditCard = 7
case forfeit = 8
public func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
switch self {
case .check:
return "Chèque"
case .cash:
return "Cash"
case .lydia:
return "Lydia"
case .paylib:
return "Paylib"
case .bankTransfer:
return "Virement"
case .clubHouse:
return "Clubhouse"
case .creditCard:
return "CB"
case .forfeit:
return "Forfait"
case .gift:
return "Offert"
}
}
}

@ -0,0 +1,253 @@
//
// PlayerRegistration.swift
// Padel Tournament
//
// Created by razmig on 10/03/2024.
//
import Foundation
import LeStorage
@Observable
public final class PlayerRegistration: BasePlayerRegistration, SideStorable {
public func localizedSourceLabel() -> String {
switch source {
case .frenchFederation:
return "base fédérale"
case .beachPadel:
return "beach-padel"
case nil:
if registeredOnline {
return "à vérifier vous-même"
} else {
return "créé par vous-même"
}
}
}
public init(teamRegistration: String? = nil, firstName: String, lastName: String, licenceId: String? = nil, rank: Int? = nil, paymentType: PlayerPaymentType? = nil, sex: PlayerSexType? = nil, tournamentPlayed: Int? = nil, points: Double? = nil, clubName: String? = nil, ligueName: String? = nil, assimilation: String? = nil, phoneNumber: String? = nil, email: String? = nil, birthdate: String? = nil, computedRank: Int = 0, source: PlayerRegistration.PlayerDataSource? = nil, hasArrived: Bool = false) {
super.init()
self.teamRegistration = teamRegistration
self.firstName = firstName
self.lastName = lastName
self.licenceId = licenceId
self.rank = rank
self.paymentType = paymentType
self.sex = sex
self.tournamentPlayed = tournamentPlayed
self.points = points
self.clubName = clubName
self.ligueName = ligueName
self.assimilation = assimilation
self.phoneNumber = phoneNumber
self.email = email
self.birthdate = birthdate
self.computedRank = computedRank
self.source = source
self.hasArrived = hasArrived
}
required init(from decoder: any Decoder) throws {
try super.init(from: decoder)
}
required public init() {
super.init()
}
public var tournamentStore: TournamentStore? {
guard let storeId else {
fatalError("missing store id for \(String(describing: type(of: self)))")
}
return TournamentLibrary.shared.store(tournamentId: storeId)
// if let store = self.store as? TournamentStore {
// return store
// }
}
public var computedAge: Int? {
if let birthdate {
let components = birthdate.components(separatedBy: "/")
if let age = components.last, let ageInt = Int(age) {
let year = Calendar.current.getSportAge()
if age.count == 2 { //si l'année est sur 2 chiffres dans le fichier
if ageInt < 23 {
return year - 2000 - ageInt
} else {
return year - 2000 + 100 - ageInt
}
} else { //si l'année est représenté sur 4 chiffres
return year - ageInt
}
}
}
return nil
}
public func pasteData(_ exportFormat: ExportFormat = .rawText) -> String {
switch exportFormat {
case .rawText:
return [firstName.capitalized, lastName.capitalized, licenceId].compactMap({ $0 }).joined(separator: exportFormat.separator())
case .csv:
return [lastName.uppercased() + " " + firstName.capitalized].joined(separator: exportFormat.separator())
}
}
public func isPlaying() -> Bool {
team()?.isPlaying() == true
}
public func contains(_ searchField: String) -> Bool {
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)
}
}
}
public func isSameAs(_ player: PlayerRegistration) -> Bool {
firstName.trimmedMultiline.canonicalVersion.localizedCaseInsensitiveCompare(player.firstName.trimmedMultiline.canonicalVersion) == .orderedSame &&
lastName.trimmedMultiline.canonicalVersion.localizedCaseInsensitiveCompare(player.lastName.trimmedMultiline.canonicalVersion) == .orderedSame
}
public func tournament() -> Tournament? {
guard let tournament = team()?.tournament else { return nil }
return Store.main.findById(tournament)
}
public func team() -> TeamRegistration? {
guard let teamRegistration else { return nil }
return self.tournamentStore?.teamRegistrations.findById(teamRegistration)
}
public func isHere() -> Bool {
hasArrived
}
public func hasPaid() -> Bool {
paymentType != nil
}
public func playerLabel(_ displayStyle: DisplayStyle = .wide) -> String {
switch displayStyle {
case .wide, .title:
return lastName.trimmed.capitalized + " " + firstName.trimmed.capitalized
case .short:
let names = lastName.components(separatedBy: .whitespaces)
if lastName.components(separatedBy: .whitespaces).count > 1 {
if let firstLongWord = names.first(where: { $0.count > 3 }) {
return firstLongWord.trimmed.capitalized.trunc(length: 10) + " " + firstName.trimmed.prefix(1).capitalized + "."
}
}
return lastName.trimmed.capitalized.trunc(length: 10) + " " + firstName.trimmed.prefix(1).capitalized + "."
}
}
public func isImported() -> Bool {
source == .beachPadel
}
public func unrankedOrUnknown() -> Bool {
source == nil
}
public func isValidLicenseNumber(year: Int) -> Bool {
guard let licenceId else { return false }
guard licenceId.isLicenseNumber else { return false }
guard licenceId.suffix(6) == "(\(year))" else { return false }
return true
}
@objc
public var canonicalName: String {
playerLabel().folding(options: .diacriticInsensitive, locale: .current).lowercased()
}
public func rankLabel(_ displayStyle: DisplayStyle = .wide) -> String {
if let rank, rank > 0 {
if rank != computedRank {
return computedRank.formatted() + " (" + rank.formatted() + ")"
} else {
return rank.formatted()
}
} else {
return "non classé" + (isMalePlayer() ? "" : "e")
}
}
public func setComputedRank(in tournament: Tournament) {
let currentRank = rank ?? tournament.unrankValue(for: isMalePlayer()) ?? 90_000
switch tournament.tournamentCategory {
case .men:
computedRank = isMalePlayer() ? currentRank : currentRank + PlayerRegistration.addon(for: currentRank, manMax: tournament.maleUnrankedValue ?? 0, womanMax: tournament.femaleUnrankedValue ?? 0)
default:
computedRank = currentRank
}
}
public func isMalePlayer() -> Bool {
sex == .male
}
public func validateLicenceId(_ year: Int) {
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?.computedLicense {
self.licenceId = computedLicense + " (\(year))"
}
}
}
public enum PlayerDataSource: Int, Codable {
case frenchFederation = 0
case beachPadel = 1
}
public static func addon(for playerRank: Int, manMax: Int, womanMax: Int) -> Int {
switch playerRank {
case 0: return 0
case womanMax: return manMax - womanMax
case manMax: return 0
default:
return TournamentCategory.femaleInMaleAssimilationAddition(playerRank)
}
}
func insertOnServer() {
self.tournamentStore?.playerRegistrations.writeChangeAndInsertOnServer(instance: self)
}
}
enum PlayerDataSource: Int, Codable {
case frenchFederation = 0
case beachPadel = 1
}
public enum PlayerSexType: Int, Hashable, CaseIterable, Identifiable, Codable {
init?(rawValue: Int?) {
guard let value = rawValue else { return nil }
self.init(rawValue: value)
}
public var id: Self {
self
}
case female = 0
case male = 1
}

@ -0,0 +1,95 @@
//
// Purchase.swift
// LeStorage
//
// Created by Laurent Morvillier on 12/04/2024.
//
import Foundation
import LeStorage
public class Purchase: BasePurchase {
// static func resourceName() -> String { return "purchases" }
// static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
// static func filterByStoreIdentifier() -> Bool { return false }
// static var relationshipNames: [String] = []
//
// var id: UInt64
// var lastUpdate: Date
// var user: String
// var purchaseDate: Date
// var productId: String
// var quantity: Int?
// var revocationDate: Date? = nil
// var expirationDate: Date? = nil
init(transactionId: UInt64, user: String, purchaseDate: Date, productId: String, quantity: Int? = nil, revocationDate: Date? = nil, expirationDate: Date? = nil) {
super.init(id: transactionId, user: user, purchaseDate: purchaseDate, productId: productId, quantity: quantity, revocationDate: revocationDate, expirationDate: expirationDate)
// self.id = transactionId
// self.lastUpdate = Date()
// self.user = user
// self.purchaseDate = purchaseDate
// self.productId = productId
// self.quantity = quantity
// self.revocationDate = revocationDate
// self.expirationDate = expirationDate
}
required init(from decoder: Decoder) throws {
try super.init(from: decoder)
}
required public init() {
super.init()
}
// enum CodingKeys: String, CodingKey, CaseIterable {
// case id
// case lastUpdate
// case user
// case purchaseDate
// case productId
// case quantity
// case revocationDate
// case expirationDate
// }
func isValid() -> Bool {
guard self.revocationDate == nil else {
return false
}
guard let expiration = self.expirationDate else {
return false
}
return expiration > Date()
}
// func encode(to encoder: Encoder) throws {
// var container = encoder.container(keyedBy: CodingKeys.self)
//
// try container.encode(self.id, forKey: .id)
// try container.encode(self.lastUpdate, forKey: .lastUpdate)
// try container.encodeAndEncryptIfPresent(self.user.data(using: .utf8), forKey: .user)
// try container.encode(self.purchaseDate, forKey: .purchaseDate)
// try container.encode(self.productId, forKey: .productId)
// try container.encode(self.quantity, forKey: .quantity)
// try container.encode(self.revocationDate, forKey: .revocationDate)
// try container.encode(self.expirationDate, forKey: .expirationDate)
// }
//
// required init(from decoder: any Decoder) throws {
// let container = try decoder.container(keyedBy: CodingKeys.self)
//
// self.id = try container.decode(UInt64.self, forKey: .id)
// self.lastUpdate = try container.decodeIfPresent(Date.self, forKey: .lastUpdate) ?? Date()
// self.user = try container.decodeEncrypted(key: .user)
// self.purchaseDate = try container.decode(Date.self, forKey: .purchaseDate)
// self.productId = try container.decode(String.self, forKey: .productId)
// self.quantity = try container.decodeIfPresent(Int.self, forKey: .quantity)
// self.revocationDate = try container.decodeIfPresent(Date.self, forKey: .revocationDate)
// self.expirationDate = try container.decodeIfPresent(Date.self, forKey: .expirationDate)
// }
}

@ -0,0 +1,35 @@
# Procédure de création de classe
Dans Swift:
- Dans Data > Gen > créer un nouveau fichier json pour la classe
- Le paramètre "foreignKey" permet de générer une méthode qui récupère l'objet parent. Ajouter une étoile permet d'indiquer que l'on cherche l'objet dans le Store de l'objet et non le Store.main.
- Pour générer les fichiers, on se place dans le répertoire de generator.py et on lance la commande : python generator.py -i . -o .
- il faut avoir inflect: pip install inflect
Dans Django:
- Les modèles de base doivent étendre BaseModel
- Les modèles stockés dans des répertoires doivent étendre SideStoreModel
- Les classes d'admin doivent étendre SyncedObjectAdmin
- Les ForeignKey doivent toujours avoir on_delete=models.SET_NULL
- Pour se faciliter la vie dans l'admin, on veut que les delete effacent les enfants malgré tout. Il faut donc implémenter la méthode delete_dependencies(self) de la classe
# Procédure d'ajout de champ dans une classe
Dans Swift:
- Ouvrir le fichier .json correspondant à la classe
- Regénérer la classe, voir ci-dessus pour la commande
- 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
- 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
- Faire le *makemigrations* + *migrate*
Note: Les nouvelles classes de model doivent étendre BaseModel ou SideStoreModel
Enfin, revenir dans Xcode, ouvrir ServerDataTests et lancer le test mis à jour

@ -0,0 +1,850 @@
//
// Round.swift
// Padel Tournament
//
// Created by razmig on 10/03/2024.
//
import Foundation
import LeStorage
import SwiftUI
@Observable
public final class Round: BaseRound, SideStorable {
var _cachedSeedInterval: SeedInterval?
public 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)
// self.lastUpdate = Date()
// self.tournament = tournament
// self.index = index
// self.parent = parent
// self.format = matchFormat
// self.startDate = startDate
// self.groupStageLoserBracket = groupStageLoserBracket
// self.loserBracketMode = loserBracketMode
}
required init(from decoder: any Decoder) throws {
try super.init(from: decoder)
}
required public init() {
super.init()
}
// MARK: - Computed dependencies
public var tournamentStore: TournamentStore? {
return TournamentLibrary.shared.store(tournamentId: self.tournament)
}
public func tournamentObject() -> Tournament? {
return Store.main.findById(tournament)
}
public func _matches() -> [Match] {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.matches.filter { $0.round == self.id }.sorted(by: \.index)
}
public func getDisabledMatches() -> [Match] {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.matches.filter { $0.round == self.id && $0.disabled == true }
}
// MARK: -
public var matchFormat: MatchFormat {
get {
format ?? .defaultFormatForMatchType(.bracket)
}
set {
format = newValue
}
}
public func hasStarted() -> Bool {
return playedMatches().anySatisfy({ $0.hasStarted() })
}
public func hasEnded() -> Bool {
if isUpperBracket() {
return playedMatches().anySatisfy({ $0.hasEnded() == false }) == false
} else {
return enabledMatches().anySatisfy({ $0.hasEnded() == false }) == false
}
}
public func upperMatches(ofMatch match: Match) -> [Match] {
if parent != nil, previousRound() == nil, let parentRound {
let matchIndex = match.index
let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex)
return [parentRound.getMatch(atMatchIndexInRound: indexInRound * 2), parentRound.getMatch(atMatchIndexInRound: indexInRound * 2 + 1)].compactMap({ $0 })
}
return []
}
public func previousMatches(ofMatch match: Match) -> [Match] {
guard let previousRound = previousRound() else { return [] }
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.matches.filter {
$0.round == previousRound.id && ($0.index == match.topPreviousRoundMatchIndex() || $0.index == match.bottomPreviousRoundMatchIndex())
}
// return Store.main.filter {
// ($0.index == match.topPreviousRoundMatchIndex() || $0.index == match.bottomPreviousRoundMatchIndex()) && $0.round == previousRound.id
// }
}
public func precedentMatches(ofMatch match: Match) -> [Match] {
let upper = upperMatches(ofMatch: match)
if upper.isEmpty == false {
return upper
}
let previous : [Match] = previousMatches(ofMatch: match)
if previous.isEmpty == false && previous.allSatisfy({ $0.disabled }), let previousRound = previousRound() {
return previous.flatMap({ previousRound.precedentMatches(ofMatch: $0) })
} else {
return previous
}
}
public func team(_ team: TeamPosition, inMatch match: Match, previousRound: Round?) -> TeamRegistration? {
return roundProjectedTeam(team, inMatch: match, previousRound: previousRound)
}
public func seed(_ team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? {
return self.tournamentStore?.teamRegistrations.first(where: {
$0.bracketPosition != nil
&& ($0.bracketPosition! / 2) == matchIndex
&& ($0.bracketPosition! % 2) == team.rawValue
})
}
public func seeds(inMatchIndex matchIndex: Int) -> [TeamRegistration] {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.teamRegistrations.filter {
$0.tournament == tournament
&& $0.bracketPosition != nil
&& ($0.bracketPosition! / 2) == matchIndex
}
// return Store.main.filter(isIncluded: {
// $0.tournament == tournament
// && $0.bracketPosition != nil
// && ($0.bracketPosition! / 2) == matchIndex
// })
}
public func seeds() -> [TeamRegistration] {
guard let tournamentStore = self.tournamentStore else { return [] }
let initialMatchIndex = RoundRule.matchIndex(fromRoundIndex: index)
let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: index)
return tournamentStore.teamRegistrations.filter {
$0.bracketPosition != nil
&& ($0.bracketPosition! / 2) >= initialMatchIndex
&& ($0.bracketPosition! / 2) < initialMatchIndex + numberOfMatches
}
}
public func teamsOrSeeds() -> [TeamRegistration] {
let seeds = seeds()
if seeds.isEmpty {
return playedMatches().flatMap({ $0.teams() })
} else {
return seeds
}
}
public func losers() -> [TeamRegistration] {
let teamIds: [String] = self._matches().compactMap { $0.losingTeamId }
return teamIds.compactMap { self.tournamentStore?.teamRegistrations.findById($0) }
}
public func winners() -> [TeamRegistration] {
let teamIds: [String] = self._matches().compactMap { $0.winningTeamId }
return teamIds.compactMap { self.tournamentStore?.teamRegistrations.findById($0) }
}
public func teams() -> [TeamRegistration] {
return playedMatches().flatMap({ $0.teams() })
}
public func roundProjectedTeam(_ team: TeamPosition, inMatch match: Match, previousRound: Round?) -> TeamRegistration? {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func roundProjectedTeam", team.rawValue, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
if isLoserBracket() == false, let seed = seed(team, inMatchIndex: match.index) {
return seed
}
switch team {
case .one:
if let luckyLoser = match.teamScores.first(where: { $0.luckyLoser == match.index * 2 }) {
return luckyLoser.team
} else if let previousMatch = topPreviousRoundMatch(ofMatch: match, previousRound: previousRound) {
if let teamId = previousMatch.winningTeamId {
return self.tournamentStore?.teamRegistrations.findById(teamId)
} else if previousMatch.disabled {
return previousMatch.teams().first
}
} else if let parent = upperBracketTopMatch(ofMatchIndex: match.index, previousRound: previousRound)?.losingTeamId {
return self.store?.findById(parent)
}
case .two:
if let luckyLoser = match.teamScores.first(where: { $0.luckyLoser == match.index * 2 + 1 }) {
return luckyLoser.team
} else if let previousMatch = bottomPreviousRoundMatch(ofMatch: match, previousRound: previousRound) {
if let teamId = previousMatch.winningTeamId {
return self.tournamentStore?.teamRegistrations.findById(teamId)
} else if previousMatch.disabled {
return previousMatch.teams().first
}
} else if let parent = upperBracketBottomMatch(ofMatchIndex: match.index, previousRound: previousRound)?.losingTeamId {
return self.store?.findById(parent)
}
}
return nil
}
public func upperBracketTopMatch(ofMatchIndex matchIndex: Int, previousRound: Round?) -> Match? {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func upperBracketTopMatch", matchIndex, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
let parentRound = parentRound
if let parentRound, parentRound.parent == nil, groupStageLoserBracket == false, parentRound.loserBracketMode != .automatic {
return nil
}
let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex)
if isLoserBracket(), previousRound == nil, let upperBracketTopMatch = parentRound?.getMatch(atMatchIndexInRound: indexInRound * 2) {
return upperBracketTopMatch
}
return nil
}
public func upperBracketBottomMatch(ofMatchIndex matchIndex: Int, previousRound: Round?) -> Match? {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func upperBracketBottomMatch", matchIndex, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
let parentRound = parentRound
if let parentRound, parentRound.parent == nil, groupStageLoserBracket == false, parentRound.loserBracketMode != .automatic {
return nil
}
let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex)
if isLoserBracket(), previousRound == nil, let upperBracketBottomMatch = parentRound?.getMatch(atMatchIndexInRound: indexInRound * 2 + 1) {
return upperBracketBottomMatch
}
return nil
}
public func topPreviousRoundMatch(ofMatch match: Match, previousRound: Round?) -> Match? {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func topPreviousRoundMatch", match.id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
guard let previousRound else { return nil }
let topPreviousRoundMatchIndex = match.topPreviousRoundMatchIndex()
return self.tournamentStore?.matches.first(where: {
$0.round == previousRound.id && $0.index == topPreviousRoundMatchIndex
})
}
public func bottomPreviousRoundMatch(ofMatch match: Match, previousRound: Round?) -> Match? {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func bottomPreviousRoundMatch", match.id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
guard let previousRound else { return nil }
let bottomPreviousRoundMatchIndex = match.bottomPreviousRoundMatchIndex()
return self.tournamentStore?.matches.first(where: {
$0.round == previousRound.id && $0.index == bottomPreviousRoundMatchIndex
})
}
public func getMatch(atMatchIndexInRound matchIndexInRound: Int) -> Match? {
self.tournamentStore?.matches.first(where: {
let index = RoundRule.matchIndexWithinRound(fromMatchIndex: $0.index)
return $0.round == id && index == matchIndexInRound
})
}
public func enabledMatches() -> [Match] {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.matches.filter { $0.round == self.id && $0.disabled == false }.sorted(by: \.index)
}
// func displayableMatches() -> [Match] {
//#if _DEBUG_TIME //DEBUGING TIME
// let start = Date()
// defer {
// let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
// print("func displayableMatches of round: ", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
// }
//#endif
//
// if index == 0 && isUpperBracket() {
// var matches : [Match?] = [playedMatches().first]
// matches.append(loserRounds().first?.playedMatches().first)
// return matches.compactMap({ $0 })
// } else {
// return playedMatches()
// }
// }
public func playedMatches() -> [Match] {
if isUpperBracket() {
return enabledMatches()
} else {
return _matches()
}
}
public func previousRound() -> Round? {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func previousRound of: ", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
return self.tournamentStore?.rounds.first(where: { $0.parent == parent && $0.index == index + 1 })
}
public func nextRound() -> Round? {
return self.tournamentStore?.rounds.first(where: { $0.parent == parent && $0.index == index - 1 })
}
public func loserRounds(forRoundIndex roundIndex: Int) -> [Round] {
return loserRoundsAndChildren().filter({ $0.index == roundIndex }).sorted(by: \.theoryCumulativeMatchCount)
}
public func loserRounds(forRoundIndex roundIndex: Int, loserRoundsAndChildren: [Round]) -> [Round] {
return loserRoundsAndChildren.filter({ $0.index == roundIndex }).sorted(by: \.theoryCumulativeMatchCount)
}
public func isDisabled() -> Bool {
return _matches().allSatisfy({ $0.disabled })
}
public func isRankDisabled() -> Bool {
return _matches().allSatisfy({ $0.disabled && $0.teamScores.isEmpty })
}
func resetFromRoundAllMatchesStartDate() {
_matches().forEach({
$0.startDate = nil
})
loserRoundsAndChildren().forEach { round in
round.resetFromRoundAllMatchesStartDate()
}
nextRound()?.resetFromRoundAllMatchesStartDate()
}
func resetFromRoundAllMatchesStartDate(from match: Match) {
let matches = _matches()
if let index = matches.firstIndex(where: { $0.id == match.id }) {
matches[index...].forEach { match in
match.startDate = nil
}
}
loserRoundsAndChildren().forEach { round in
round.resetFromRoundAllMatchesStartDate()
}
nextRound()?.resetFromRoundAllMatchesStartDate()
}
public func getActiveLoserRound() -> Round? {
let rounds = loserRounds().filter({ $0.isDisabled() == false }).sorted(by: \.index).reversed()
return rounds.first(where: { $0.hasStarted() && $0.hasEnded() == false }) ?? rounds.first
}
public func enableRound() {
_toggleRound(disable: false)
}
public func disableRound() {
_toggleRound(disable: true)
}
private func _toggleRound(disable: Bool) {
let _matches = _matches()
_matches.forEach { match in
match.disabled = disable
match.resetMatch()
//we need to keep teamscores to handle disable ranking match round stuff
// do {
// try DataStore.shared.teamScores.delete(contentOfs: match.teamScores)
// } catch {
// Logger.error(error)
// }
}
self.tournamentStore?.matches.addOrUpdate(contentOfs: _matches)
}
public var cumulativeMatchCount: Int {
var totalMatches = playedMatches().count
if let parentRound {
totalMatches += parentRound.cumulativeMatchCount
}
return totalMatches
}
public func initialRound() -> Round? {
if let parentRound {
return parentRound.initialRound()
} else {
return self
}
}
public func estimatedEndDate(_ additionalEstimationDuration: Int) -> Date? {
return enabledMatches().last?.estimatedEndDate(additionalEstimationDuration)
}
public func getLoserRoundStartDate() -> Date? {
return loserRoundsAndChildren().first(where: { $0.isDisabled() == false })?.enabledMatches().first?.startDate
}
public func estimatedLoserRoundEndDate(_ additionalEstimationDuration: Int) -> Date? {
let lastMatch = loserRoundsAndChildren().last(where: { $0.isDisabled() == false })?.enabledMatches().last
return lastMatch?.estimatedEndDate(additionalEstimationDuration)
}
public func disabledMatches() -> [Match] {
return _matches().filter({ $0.disabled })
}
public func allLoserRoundMatches() -> [Match] {
loserRoundsAndChildren().flatMap({ $0._matches() })
}
public var theoryCumulativeMatchCount: Int {
var totalMatches = RoundRule.numberOfMatches(forRoundIndex: index)
if let parentRound {
totalMatches += parentRound.theoryCumulativeMatchCount
}
return totalMatches
}
public func correspondingLoserRoundTitle(_ displayStyle: DisplayStyle = .wide) -> String {
if let _cachedSeedInterval { return _cachedSeedInterval.localizedLabel(displayStyle) }
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func correspondingLoserRoundTitle()", duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
let initialMatchIndexFromRoundIndex = RoundRule.matchIndex(fromRoundIndex: index)
var seedsAfterThisRound: [TeamRegistration] = []
if let tournamentStore = tournamentStore {
seedsAfterThisRound = tournamentStore.teamRegistrations.filter {
$0.bracketPosition != nil
&& ($0.bracketPosition! / 2) < initialMatchIndexFromRoundIndex
}
}
// let seedsAfterThisRound : [TeamRegistration] = Store.main.filter(isIncluded: {
// $0.tournament == tournament
// && $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 + seedsCount + 1, last: playedMatches.count * 2 + seedsCount)
_cachedSeedInterval = seedInterval
return seedInterval.localizedLabel(displayStyle)
}
func hasNextRound() -> Bool {
return nextRound()?.isRankDisabled() == false
}
public func pasteData() -> String {
var data: [String] = []
data.append(self.roundTitle())
playedMatches().forEach { match in
data.append(match.matchTitle())
data.append(match.team(.one)?.teamLabelRanked(displayRank: true, displayTeamName: true) ?? "-----")
data.append(match.team(.two)?.teamLabelRanked(displayRank: true, displayTeamName: true) ?? "-----")
}
return data.joined(separator: "\n")
}
public func seedInterval(initialMode: Bool = false) -> SeedInterval? {
if initialMode == false, let _cachedSeedInterval { return _cachedSeedInterval }
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func seedInterval(initialMode)", initialMode, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
if isUpperBracket() {
if index == 0 { return SeedInterval(first: 1, last: 2) }
let initialMatchIndexFromRoundIndex = RoundRule.matchIndex(fromRoundIndex: index)
if initialMode {
let playedMatches = RoundRule.numberOfMatches(forRoundIndex: index)
let seedInterval = SeedInterval(first: playedMatches + 1, last: playedMatches * 2)
//print(seedInterval.localizedLabel())
return seedInterval
} else {
var seedsAfterThisRound : [TeamRegistration] = []
if let tournamentStore = self.tournamentStore {
seedsAfterThisRound = 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() {
if (previousRound.enabledMatches().isEmpty == false || initialMode) {
return previousRound.seedInterval(initialMode: initialMode)?.chunks()?.first
} else {
return previousRound.seedInterval(initialMode: initialMode)
}
} else if let parentRound {
if parentRound.isUpperBracket() {
return parentRound.seedInterval(initialMode: initialMode)
}
return parentRound.seedInterval(initialMode: initialMode)?.chunks()?.last
}
return nil
}
public func roundTitle(_ displayStyle: DisplayStyle = .wide, initialMode: Bool = false) -> String {
if groupStageLoserBracket {
return "Classement Poules"
}
if parent != nil {
if let seedInterval = seedInterval(initialMode: initialMode) {
return seedInterval.localizedLabel(displayStyle)
}
// print("Round pas trouvé", id, parent, index)
return "Match de classement"
}
return RoundRule.roundName(fromRoundIndex: index, displayStyle: displayStyle)
}
public func updateTournamentState() {
let tournamentObject = tournamentObject()
if let tournamentObject, index == 0, isUpperBracket(), hasEnded() {
tournamentObject.endDate = Date()
DataStore.shared.tournaments.addOrUpdate(instance: tournamentObject)
}
tournamentObject?.updateTournamentState()
}
public func roundStatus() -> String {
let hasEnded = hasEnded()
if hasStarted() && hasEnded == false {
return "en cours"
} else if hasEnded {
return "terminée"
} else {
return "à démarrer"
}
}
public func loserRounds() -> [Round] {
guard let tournamentStore = self.tournamentStore else { return [] }
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func loserRounds: ", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
return tournamentStore.rounds.filter( { $0.parent == id }).sorted(by: \.index).reversed()
}
public func loserRoundsAndChildren() -> [Round] {
let loserRounds = loserRounds()
return loserRounds + loserRounds.flatMap({ $0.loserRoundsAndChildren() })
}
public func isUpperBracket() -> Bool {
return parent == nil && groupStageLoserBracket == false
}
public func isLoserBracket() -> Bool {
return parent != nil || groupStageLoserBracket
}
public func deleteLoserBracket() {
let loserRounds = loserRounds()
self.tournamentStore?.rounds.delete(contentOfs: loserRounds)
}
public func buildLoserBracket() {
guard loserRounds().isEmpty else { return }
let currentRoundMatchCount = RoundRule.numberOfMatches(forRoundIndex: index)
guard currentRoundMatchCount > 1 else { return }
let roundCount = RoundRule.numberOfRounds(forTeams: currentRoundMatchCount)
var loserBracketMatchFormat = tournamentObject()?.loserBracketMatchFormat
// if let parentRound {
// loserBracketMatchFormat = tournamentObject()?.loserBracketSmartMatchFormat(parentRound.index)
// }
let rounds = (0..<roundCount).map { //index 0 is the final
let round = Round(tournament: tournament, index: $0, matchFormat: loserBracketMatchFormat)
round.parent = id //parent
return round
}
self.tournamentStore?.rounds.addOrUpdate(contentOfs: rounds)
let matchCount = RoundRule.numberOfMatches(forTeams: currentRoundMatchCount)
let matches = (0..<matchCount).map { //0 is final match
let roundIndex = RoundRule.roundIndex(fromMatchIndex: $0)
let round = rounds[roundIndex]
return Match(round: round.id, index: $0, format: loserBracketMatchFormat, name: round.roundTitle(initialMode: true))
//initial mode let the roundTitle give a name without considering the playable match
}
self.tournamentStore?.matches.addOrUpdate(contentOfs: matches)
loserRounds().forEach { round in
round.buildLoserBracket()
}
}
public var parentRound: Round? {
guard let parent = parent else { return nil }
return self.tournamentStore?.rounds.findById(parent)
}
public func nextRoundsDisableMatches() -> Int {
if parent == nil, index > 0 {
return tournamentObject()?.rounds().suffix(index).flatMap { $0.disabledMatches() }.count ?? 0
} else {
return 0
}
}
public func updateMatchFormat(_ updatedMatchFormat: MatchFormat, checkIfPossible: Bool, andLoserBracket: Bool) {
if updatedMatchFormat.weight < self.matchFormat.weight {
updateMatchFormatAndAllMatches(updatedMatchFormat)
if andLoserBracket {
loserRoundsAndChildren().forEach { round in
round.updateMatchFormat(updatedMatchFormat, checkIfPossible: checkIfPossible, andLoserBracket: true)
}
}
}
}
public func updateMatchFormatAndAllMatches(_ updatedMatchFormat: MatchFormat) {
self.matchFormat = updatedMatchFormat
self.updateMatchFormatOfAllMatches(updatedMatchFormat)
}
public func updateMatchFormatOfAllMatches(_ updatedMatchFormat: MatchFormat) {
let playedMatches = _matches()
playedMatches.forEach { match in
match.matchFormat = updatedMatchFormat
}
self.tournamentStore?.matches.addOrUpdate(contentOfs: playedMatches)
}
public override func deleteDependencies(shouldBeSynchronized: Bool) {
let matches = self._matches()
for match in matches {
match.deleteDependencies(shouldBeSynchronized: shouldBeSynchronized)
}
self.tournamentStore?.matches.deleteDependencies(matches, shouldBeSynchronized: shouldBeSynchronized)
let loserRounds = self.loserRounds()
for round in loserRounds {
round.deleteDependencies(shouldBeSynchronized: shouldBeSynchronized)
}
self.tournamentStore?.rounds.deleteDependencies(loserRounds, shouldBeSynchronized: shouldBeSynchronized)
}
// enum CodingKeys: String, CodingKey {
// case _id = "id"
// case _storeId = "storeId"
// case _lastUpdate = "lastUpdate"
// case _tournament = "tournament"
// case _index = "index"
// case _parent = "parent"
// case _format = "format"
// case _startDate = "startDate"
// case _groupStageLoserBracket = "groupStageLoserBracket"
// case _loserBracketMode = "loserBracketMode"
// }
//
// required init(from decoder: Decoder) throws {
// let container = try decoder.container(keyedBy: CodingKeys.self)
// id = try container.decode(String.self, forKey: ._id)
// storeId = try container.decode(String.self, forKey: ._storeId)
// lastUpdate = try container.decodeIfPresent(Date.self, forKey: ._lastUpdate) ?? Date()
// tournament = try container.decode(String.self, forKey: ._tournament)
// index = try container.decode(Int.self, forKey: ._index)
// parent = try container.decodeIfPresent(String.self, forKey: ._parent)
// format = try container.decodeIfPresent(MatchFormat.self, forKey: ._format)
// startDate = try container.decodeIfPresent(Date.self, forKey: ._startDate)
// groupStageLoserBracket = try container.decodeIfPresent(Bool.self, forKey: ._groupStageLoserBracket) ?? false
// loserBracketMode = try container.decodeIfPresent(LoserBracketMode.self, forKey: ._loserBracketMode) ?? .automatic
// }
//
// 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(storeId, forKey: ._storeId)
// try container.encode(tournament, forKey: ._tournament)
// try container.encode(index, forKey: ._index)
// try container.encode(groupStageLoserBracket, forKey: ._groupStageLoserBracket)
// try container.encode(loserBracketMode, forKey: ._loserBracketMode)
//
// try container.encode(parent, forKey: ._parent)
// try container.encode(format, forKey: ._format)
// try container.encode(startDate, forKey: ._startDate)
//
// }
func insertOnServer() {
self.tournamentStore?.rounds.writeChangeAndInsertOnServer(instance: self)
for match in self._matches() {
match.insertOnServer()
}
}
}
extension Round: Selectable {
public func selectionLabel(index: Int) -> String {
if let parentRound {
return "Tour #\(parentRound.loserRounds().count - index)"
} else {
return roundTitle(.short)
}
}
public func badgeValue() -> Int? {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func badgeValue round of: ", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
if let parentRound {
return parentRound.loserRounds(forRoundIndex: index).flatMap { $0.playedMatches() }.filter({ $0.isRunning() }).count
} else {
return playedMatches().filter({ $0.isRunning() }).count
}
}
public func badgeValueColor() -> Color? {
return nil
}
public func badgeImage() -> Badge? {
#if _DEBUG_TIME //DEBUGING TIME
let start = Date()
defer {
let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000)
print("func badgeImage of round: ", id, duration.formatted(.units(allowed: [.seconds, .milliseconds])))
}
#endif
return hasEnded() ? .checkmark : nil
}
}
public enum LoserBracketMode: Int, CaseIterable, Identifiable, Codable {
public var id: Int { self.rawValue }
case automatic
case manual
public func localizedLoserBracketMode() -> String {
switch self {
case .automatic:
"Automatique"
case .manual:
"Manuelle"
}
}
public func localizedLoserBracketModeDescription() -> String {
switch self {
case .automatic:
"Les perdants du tableau principal sont placés à leur place prévue."
case .manual:
"Aucun placement automatique n'est fait. Vous devez choisir les perdants qui se jouent."
}
}
}

@ -0,0 +1,712 @@
//
// TeamRegistration.swift
// Padel Tournament
//
// Created by razmig on 10/03/2024.
//
import Foundation
import LeStorage
import SwiftUI
@Observable
public final class TeamRegistration: BaseTeamRegistration, SideStorable {
// static func resourceName() -> String { "team-registrations" }
// 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 groupStage: String?
// var registrationDate: Date?
// var callDate: Date?
// var bracketPosition: Int?
// var groupStagePosition: Int?
// var comment: String?
// var source: String?
// var sourceValue: String?
// var logo: String?
// var name: String?
//
// var walkOut: Bool = false
// var wildCardBracket: Bool = false
// var wildCardGroupStage: Bool = false
// var weight: Int = 0
// var lockedWeight: Int?
// var confirmationDate: Date?
// var qualified: Bool = false
// var finalRanking: Int?
// var pointsEarned: Int?
//
// var storeId: String? = nil
public init(
tournament: String, groupStage: String? = nil, registrationDate: Date? = nil,
callDate: Date? = nil, bracketPosition: Int? = nil, groupStagePosition: Int? = nil,
comment: String? = nil, source: String? = nil, sourceValue: String? = nil,
logo: String? = nil, name: String? = nil, walkOut: Bool = false,
wildCardBracket: Bool = false, wildCardGroupStage: Bool = false, weight: Int = 0,
lockedWeight: Int? = nil, confirmationDate: Date? = nil, qualified: Bool = false
) {
super.init()
// self.storeId = tournament
self.tournament = tournament
self.groupStage = groupStage
self.registrationDate = registrationDate ?? Date()
self.callDate = callDate
self.bracketPosition = bracketPosition
self.groupStagePosition = groupStagePosition
self.comment = comment
self.source = source
self.sourceValue = sourceValue
self.logo = logo
self.name = name
self.walkOut = walkOut
self.wildCardBracket = wildCardBracket
self.wildCardGroupStage = wildCardGroupStage
self.weight = weight
self.lockedWeight = lockedWeight
self.confirmationDate = confirmationDate
self.qualified = qualified
}
public func hasRegisteredOnline() -> Bool {
players().anySatisfy({ $0.registeredOnline })
}
public func unrankedOrUnknown() -> Bool {
players().anySatisfy({ $0.source == nil })
}
public func isOutOfTournament() -> Bool {
walkOut
}
required init(from decoder: any Decoder) throws {
try super.init(from: decoder)
}
required public init() {
super.init()
}
public var tournamentStore: TournamentStore? {
return TournamentLibrary.shared.store(tournamentId: self.tournament)
}
// MARK: - Computed dependencies
public func unsortedPlayers() -> [PlayerRegistration] {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.playerRegistrations.filter {
$0.teamRegistration == self.id && $0.coach == false
}
}
// MARK: -
public func deleteTeamScores() {
guard let tournamentStore = self.tournamentStore else { return }
let ts = tournamentStore.teamScores.filter({ $0.teamRegistration == id })
tournamentStore.teamScores.delete(contentOfs: ts)
}
public override func deleteDependencies(shouldBeSynchronized: Bool) {
let unsortedPlayers = unsortedPlayers()
for player in unsortedPlayers {
player.deleteDependencies(shouldBeSynchronized: shouldBeSynchronized)
}
self.tournamentStore?.playerRegistrations.deleteDependencies(unsortedPlayers, shouldBeSynchronized: shouldBeSynchronized)
let teamScores = teamScores()
for teamScore in teamScores {
teamScore.deleteDependencies(shouldBeSynchronized: shouldBeSynchronized)
}
self.tournamentStore?.teamScores.deleteDependencies(teamScores, shouldBeSynchronized: shouldBeSynchronized)
}
public func hasArrived(isHere: Bool = false) {
let unsortedPlayers = unsortedPlayers()
unsortedPlayers.forEach({ $0.hasArrived = !isHere })
self.tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: unsortedPlayers)
}
public func isHere() -> Bool {
let unsortedPlayers = unsortedPlayers()
if unsortedPlayers.isEmpty { return false }
return unsortedPlayers.allSatisfy({ $0.hasArrived })
}
public func isSeedable() -> Bool {
bracketPosition == nil && groupStage == nil
}
public func setSeedPosition(inSpot match: Match, slot: TeamPosition?, opposingSeeding: Bool) {
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
}
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)
}
}
public func expectedSummonDate() -> Date? {
if let groupStageStartDate = groupStageObject()?.startDate {
return groupStageStartDate
} else if let roundMatchStartDate = initialMatch()?.startDate {
return roundMatchStartDate
}
return nil
}
public var initialWeight: Int {
return lockedWeight ?? weight
}
public func called() -> Bool {
return callDate != nil
}
public func confirmed() -> Bool {
return confirmationDate != nil
}
public func getPhoneNumbers() -> [String] {
return players().compactMap { $0.phoneNumber }.filter({ $0.isEmpty == false })
}
public func getMail() -> [String] {
let mails = players().compactMap({ $0.email })
return mails
}
public func isImported() -> Bool {
let unsortedPlayers = unsortedPlayers()
if unsortedPlayers.isEmpty { return false }
return unsortedPlayers.allSatisfy({ $0.isImported() })
}
public func isWildCard() -> Bool {
return wildCardBracket || wildCardGroupStage
}
public func isPlaying() -> Bool {
return currentMatch() != nil
}
func currentMatch() -> Match? {
return teamScores().compactMap { $0.matchObject() }.first(where: { $0.isRunning() })
}
func teamScores() -> [TeamScore] {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.teamScores.filter({ $0.teamRegistration == id })
}
func wins() -> [Match] {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.matches.filter({ $0.winningTeamId == id })
}
func loses() -> [Match] {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.matches.filter({ $0.losingTeamId == id })
}
func matches() -> [Match] {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.matches.filter({ $0.losingTeamId == id || $0.winningTeamId == id })
}
var tournamentCategory: TournamentCategory {
tournamentObject()?.tournamentCategory ?? .men
}
@objc
var canonicalName: String {
players().map { $0.canonicalName }.joined(separator: " ")
}
func hasMemberOfClub(_ codeClubOrClubName: String?) -> Bool {
guard let codeClubOrClubName else { return true }
return unsortedPlayers().anySatisfy({
$0.clubName?.contains(codeClubOrClubName) == true
|| $0.clubName?.contains(codeClubOrClubName) == true
})
}
public func teamLabel(
_ displayStyle: DisplayStyle = .wide, twoLines: Bool = false, separator: String = "&"
) -> String {
if let name { return name }
return players().map { $0.playerLabel(displayStyle) }.joined(
separator: twoLines ? "\n" : " \(separator) ")
}
public func teamLabelRanked(displayRank: Bool, displayTeamName: Bool) -> String {
[
displayTeamName ? name : nil, displayRank ? seedIndex() : nil,
displayTeamName ? (name == nil ? teamLabel() : name) : teamLabel(),
].compactMap({ $0 }).joined(separator: " ")
}
public func seedIndex() -> String? {
guard let tournament = tournamentObject() else { return nil }
guard let index = index(in: tournament.selectedSortedTeams()) else { return nil }
return "(\(index + 1))"
}
public func index(in teams: [TeamRegistration]) -> Int? {
return teams.firstIndex(where: { $0.id == id })
}
public func formattedSeed(in teams: [TeamRegistration]? = nil) -> String {
let selectedSortedTeams = teams ?? tournamentObject()?.selectedSortedTeams() ?? []
if let index = index(in: selectedSortedTeams) {
return "#\(index + 1)"
} else {
return "###"
}
}
public func contains(_ searchField: String) -> Bool {
return unsortedPlayers().anySatisfy({ $0.contains(searchField) })
|| self.name?.localizedCaseInsensitiveContains(searchField) == true
}
public func containsExactlyPlayerLicenses(_ playerLicenses: [String?]) -> Bool {
let arrayOfIds: [String] = unsortedPlayers().compactMap({
$0.licenceId?.strippedLicense?.canonicalVersion
})
let ids: Set<String> = Set<String>(arrayOfIds.sorted())
let searchedIds = Set<String>(
playerLicenses.compactMap({ $0?.strippedLicense?.canonicalVersion }).sorted())
if ids.isEmpty || searchedIds.isEmpty { return false }
return ids.hashValue == searchedIds.hashValue
}
public func includes(players: [PlayerRegistration]) -> Bool {
let unsortedPlayers = unsortedPlayers()
guard players.count == unsortedPlayers.count else { return false }
return players.allSatisfy { player in
unsortedPlayers.anySatisfy { _player in
_player.isSameAs(player)
}
}
}
public func includes(player: PlayerRegistration) -> Bool {
return unsortedPlayers().anySatisfy { _player in
_player.isSameAs(player)
}
}
public func canPlay() -> Bool {
let unsortedPlayers = unsortedPlayers()
if unsortedPlayers.isEmpty { return false }
return matches().isEmpty == false
|| unsortedPlayers.allSatisfy({ $0.hasPaid() || $0.hasArrived })
}
public func availableForSeedPick() -> Bool {
return groupStage == nil && bracketPosition == nil
}
public func inGroupStage() -> Bool {
return groupStagePosition != nil
}
public func inRound() -> Bool {
return bracketPosition != nil
}
public func positionLabel() -> String? {
if groupStagePosition != nil { return "Poule" }
if let initialRound = initialRound() {
return initialRound.roundTitle()
} else {
return nil
}
}
public func resetGroupeStagePosition() {
guard let tournamentStore = self.tournamentStore else { return }
if let groupStage {
let matches = tournamentStore.matches.filter({ $0.groupStage == groupStage }).map {
$0.id
}
let teamScores = tournamentStore.teamScores.filter({
$0.teamRegistration == id && matches.contains($0.match)
})
tournamentStore.teamScores.delete(contentOfs: teamScores)
}
//groupStageObject()?._matches().forEach({ $0.updateTeamScores() })
groupStage = nil
groupStagePosition = nil
}
public func resetBracketPosition() {
guard let tournamentStore = self.tournamentStore else { return }
let matches = tournamentStore.matches.filter({ $0.groupStage == nil }).map { $0.id }
let teamScores = tournamentStore.teamScores.filter({
$0.teamRegistration == id && matches.contains($0.match)
})
tournamentStore.teamScores.delete(contentOfs: teamScores)
self.bracketPosition = nil
}
public func resetPositions() {
resetGroupeStagePosition()
resetBracketPosition()
}
public func pasteData(_ exportFormat: ExportFormat = .rawText, _ index: Int = 0) -> String {
switch exportFormat {
case .rawText:
return [playersPasteData(exportFormat), formattedInscriptionDate(exportFormat), name]
.compactMap({ $0 }).joined(separator: exportFormat.newLineSeparator())
case .csv:
return [
index.formatted(), playersPasteData(exportFormat),
isWildCard() ? "WC" : weight.formatted(),
].joined(separator: exportFormat.separator())
}
}
public var computedRegistrationDate: Date {
return registrationDate ?? .distantFuture
}
public func formattedInscriptionDate(_ exportFormat: ExportFormat = .rawText) -> String? {
guard let registrationDate else { return nil }
let formattedDate = registrationDate.formatted(
.dateTime.weekday().day().month().hour().minute())
let onlineSuffix = hasRegisteredOnline() ? " en ligne" : ""
switch exportFormat {
case .rawText:
return "Inscrit\(onlineSuffix) le \(formattedDate)"
case .csv:
return formattedDate
}
}
public func formattedSummonDate(_ exportFormat: ExportFormat = .rawText) -> String? {
switch exportFormat {
case .rawText:
if let callDate {
return "Convoqué le "
+ callDate.formatted(.dateTime.weekday().day().month().hour().minute())
} else {
return nil
}
case .csv:
if let callDate {
return callDate.formatted(.dateTime.weekday().day().month().hour().minute())
} else {
return nil
}
}
}
public func playersPasteData(_ exportFormat: ExportFormat = .rawText) -> String {
switch exportFormat {
case .rawText:
return players().map { $0.pasteData(exportFormat) }.joined(
separator: exportFormat.newLineSeparator())
case .csv:
return players().map {
[$0.pasteData(exportFormat), isWildCard() ? "WC" : $0.computedRank.formatted()]
.joined(separator: exportFormat.separator())
}.joined(separator: exportFormat.separator())
}
}
public typealias TeamRange = (left: TeamRegistration?, right: TeamRegistration?)
public func replacementRange() -> TeamRange? {
guard let tournamentObject = tournamentObject() else { return nil }
guard let index = tournamentObject.indexOf(team: self) else { return nil }
let selectedSortedTeams = tournamentObject.selectedSortedTeams()
let left = selectedSortedTeams[safe: index - 1]
let right = selectedSortedTeams[safe: index + 1]
return (left: left, right: right)
}
public func replacementRangeExtended() -> TeamRange? {
guard let tournamentObject = tournamentObject() else { return nil }
guard let groupStagePosition else { return nil }
let selectedSortedTeams = tournamentObject.selectedSortedTeams()
var left: TeamRegistration? = nil
if groupStagePosition == 0 {
left = tournamentObject.seeds().last
} else {
let previousHat = selectedSortedTeams.filter({
$0.groupStagePosition == groupStagePosition - 1
}).sorted(by: \.weight)
left = previousHat.last
}
var right: TeamRegistration? = nil
if groupStagePosition == tournamentObject.teamsPerGroupStage - 1 {
right = nil
} else {
let previousHat = selectedSortedTeams.filter({
$0.groupStagePosition == groupStagePosition + 1
}).sorted(by: \.weight)
right = previousHat.first
}
return (left: left, right: right)
}
typealias AreInIncreasingOrder = (PlayerRegistration, PlayerRegistration) -> Bool
public func players() -> [PlayerRegistration] {
self.unsortedPlayers().sorted { (lhs, rhs) in
let predicates: [AreInIncreasingOrder] = [
{ $0.sex?.rawValue ?? 0 < $1.sex?.rawValue ?? 0 },
{ $0.rank ?? Int.max < $1.rank ?? Int.max },
{ $0.lastName < $1.lastName },
{ $0.firstName < $1.firstName },
]
for predicate in predicates {
if !predicate(lhs, rhs) && !predicate(rhs, lhs) {
continue
}
return predicate(lhs, rhs)
}
return false
}
}
func coaches() -> [PlayerRegistration] {
guard let store = self.tournamentStore else { return [] }
return store.playerRegistrations.filter { $0.coach }
}
func significantPlayerCount() -> Int {
return tournamentObject()?.significantPlayerCount() ?? 2
}
func missingPlayerType(inTournamentCategory tournamentCategory: TournamentCategory) -> [Int] {
let players = unsortedPlayers()
if players.count >= 2 { return [] }
let s = players.compactMap { $0.sex?.rawValue }
var missing = tournamentCategory.mandatoryPlayerType()
s.forEach { i in
if let index = missing.firstIndex(of: i) {
missing.remove(at: index)
}
}
return missing
}
func unrankValue(for malePlayer: Bool) -> Int {
return tournamentObject()?.unrankValue(for: malePlayer) ?? 90_000
}
public func groupStageObject() -> GroupStage? {
guard let groupStage else { return nil }
return self.tournamentStore?.groupStages.findById(groupStage)
}
public func initialRound() -> Round? {
guard let bracketPosition else { return nil }
let roundIndex = RoundRule.roundIndex(fromMatchIndex: bracketPosition / 2)
return self.tournamentStore?.rounds.first(where: { $0.index == roundIndex })
}
public func initialMatch() -> Match? {
guard let bracketPosition else { return nil }
guard let initialRoundObject = initialRound() else { return nil }
return self.tournamentStore?.matches.first(where: {
$0.round == initialRoundObject.id && $0.index == bracketPosition / 2
})
}
public func toggleSummonConfirmation() {
if confirmationDate == nil { confirmationDate = Date() } else { confirmationDate = nil }
}
public func didConfirmSummon() -> Bool {
confirmationDate != nil
}
public func tournamentObject() -> Tournament? {
return Store.main.findById(tournament)
}
public func groupStagePositionAtStep(_ step: Int) -> Int? {
guard let groupStagePosition else { return nil }
if step == 0 {
return groupStagePosition
} else if let groupStageObject = groupStageObject(), groupStageObject.hasEnded() {
return groupStageObject.index
}
return nil
}
public func wildcardLabel() -> String? {
if isWildCard() {
let wildcardLabel: String = ["Wildcard", (wildCardBracket ? "Tableau" : "Poule")].joined(separator: " ")
return wildcardLabel
} else {
return nil
}
}
var _cachedRestingTime: (Bool, Date?)?
public 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
}
public func resetRestingTime() {
_cachedRestingTime = nil
}
public var restingTimeForSorting: Date {
restingTime()!
}
public func teamNameLabel() -> String {
if let name, name.isEmpty == false {
return name
} else {
return "Toute l'équipe"
}
}
public func isDifferentPosition(_ drawMatchIndex: Int?) -> Bool {
if let bracketPosition, let drawMatchIndex {
return drawMatchIndex != bracketPosition
} else if bracketPosition != nil {
return true
} else if drawMatchIndex != nil {
return true
}
return false
}
public func shouldDisplayRankAndWeight() -> Bool {
unsortedPlayers().count > 0
}
public func teamInitialPositionBracket() -> String? {
let round = initialMatch()?.roundAndMatchTitle()
if let round {
return round
}
return nil
}
public func teamInitialPositionGroupStage() -> String? {
let groupStage = self.groupStageObject()
let group = groupStage?.groupStageTitle(.title)
var groupPositionLabel: String? = nil
if let finalPosition = groupStage?.finalPosition(ofTeam: self) {
groupPositionLabel = (finalPosition + 1).ordinalFormatted()
} else if let groupStagePosition {
groupPositionLabel = "\(groupStagePosition + 1)"
}
if let group {
if let groupPositionLabel {
return [group, "#\(groupPositionLabel)"].joined(separator: " ")
} else {
return group
}
}
return nil
}
public func qualifiedStatus(hideBracketStatus: Bool = false) -> String? {
let teamInitialPositionBracket = teamInitialPositionBracket()
let groupStageTitle = teamInitialPositionGroupStage()
let base: String? = qualified ? "Qualifié" : nil
if let groupStageTitle, let teamInitialPositionBracket, hideBracketStatus == false {
return [base, groupStageTitle, ">", teamInitialPositionBracket].compactMap({ $0 }).joined(separator: " ")
} else if let groupStageTitle {
return [base, groupStageTitle].compactMap({ $0 }).joined(separator: " ")
} else if hideBracketStatus == false {
return teamInitialPositionBracket
}
return nil
}
func insertOnServer() {
self.tournamentStore?.teamRegistrations.writeChangeAndInsertOnServer(instance: self)
for playerRegistration in self.unsortedPlayers() {
playerRegistration.insertOnServer()
}
}
// MARK: - Refacto
public func setWeight(
from players: [PlayerRegistration],
inTournamentCategory tournamentCategory: TournamentCategory
) {
let significantPlayerCount = significantPlayerCount()
let sortedPlayers = players.sorted(by: \.computedRank, order: .ascending)
weight = (sortedPlayers.prefix(significantPlayerCount).map { $0.computedRank } + missingPlayerType(inTournamentCategory: tournamentCategory).map { unrankValue(for: $0 == 1 ? true : false ) }).prefix(significantPlayerCount).reduce(0,+)
}
}
enum TeamDataSource: Int, Codable {
case beachPadel
}

@ -0,0 +1,117 @@
//
// TeamScore.swift
// Padel Tournament
//
// Created by razmig on 10/03/2024.
//
import Foundation
import LeStorage
@Observable
public final class TeamScore: BaseTeamScore, SideStorable {
// static func resourceName() -> String { "team-scores" }
// static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
// static func filterByStoreIdentifier() -> Bool { return true }
// static var relationshipNames: [String] = ["match"]
//
// var id: String = Store.randomId()
// var lastUpdate: Date
// var match: String
// var teamRegistration: String?
// //var playerRegistrations: [String] = []
// var score: String?
// var walkOut: Int?
// var luckyLoser: Int?
//
// var storeId: String? = nil
init(match: String, teamRegistration: String? = nil, score: String? = nil, walkOut: Int? = nil, luckyLoser: Int? = nil) {
super.init(match: match, teamRegistration: teamRegistration, score: score, walkOut: walkOut, luckyLoser: luckyLoser)
// self.match = match
// self.teamRegistration = teamRegistration
//// self.playerRegistrations = playerRegistrations
// self.score = score
// self.walkOut = walkOut
// self.luckyLoser = luckyLoser
}
init(match: String, team: TeamRegistration?) {
super.init(match: match)
if let team {
self.teamRegistration = team.id
}
}
required init(from decoder: Decoder) throws {
try super.init(from: decoder)
}
required public init() {
super.init()
}
var tournamentStore: TournamentStore? {
guard let storeId else {
fatalError("missing store id for \(String(describing: type(of: self)))")
}
return TournamentLibrary.shared.store(tournamentId: storeId)
//
// if let store = self.store as? TournamentStore {
// return store
// }
// fatalError("missing store for \(String(describing: type(of: self)))")
}
// MARK: - Computed dependencies
func matchObject() -> Match? {
return self.tournamentStore?.matches.findById(self.match)
}
public var team: TeamRegistration? {
guard let teamRegistration else {
return nil
}
return self.tournamentStore?.teamRegistrations.findById(teamRegistration)
}
// MARK: -
public func isWalkOut() -> Bool {
return walkOut != nil
}
// enum CodingKeys: String, CodingKey {
// case _id = "id"
// case _storeId = "storeId"
// case _lastUpdate = "lastUpdate"
// case _match = "match"
// case _teamRegistration = "teamRegistration"
// //case _playerRegistrations = "playerRegistrations"
// case _score = "score"
// case _walkOut = "walkOut"
// case _luckyLoser = "luckyLoser"
// }
//
// 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(match, forKey: ._match)
// try container.encode(teamRegistration, forKey: ._teamRegistration)
// try container.encode(score, forKey: ._score)
// try container.encode(walkOut, forKey: ._walkOut)
// try container.encode(luckyLoser, forKey: ._luckyLoser)
// }
func insertOnServer() {
self.tournamentStore?.teamScores.writeChangeAndInsertOnServer(instance: self)
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,33 @@
//
// TournamentLibrary.swift
// PadelClub
//
// Created by Laurent Morvillier on 11/11/2024.
//
import Foundation
import LeStorage
class TournamentLibrary {
static let shared: TournamentLibrary = TournamentLibrary()
fileprivate var _stores: [String : TournamentStore] = [:]
func store(tournamentId: String) -> TournamentStore? {
guard let tournament = DataStore.shared.tournaments.first(where: { $0.id == tournamentId }) else { return nil }
if let store = self._stores[tournamentId] {
return store
}
let store = StoreCenter.main.store(identifier: tournamentId)
let tournamentStore = TournamentStore(store: store)
self._stores[tournamentId] = tournamentStore
return tournamentStore
}
func reset() {
self._stores.removeAll()
}
}

@ -0,0 +1,69 @@
//
// TournamentStore.swift
// PadelClub
//
// Created by Laurent Morvillier on 26/06/2024.
//
import Foundation
import LeStorage
import SwiftUI
import Combine
public class TournamentStore: ObservableObject {
var store: Store
public fileprivate(set) var groupStages: SyncedCollection<GroupStage> = SyncedCollection.placeholder()
public fileprivate(set) var matches: SyncedCollection<Match> = SyncedCollection.placeholder()
public fileprivate(set) var teamRegistrations: SyncedCollection<TeamRegistration> = SyncedCollection.placeholder()
public fileprivate(set) var playerRegistrations: SyncedCollection<PlayerRegistration> = SyncedCollection.placeholder()
public fileprivate(set) var rounds: SyncedCollection<Round> = SyncedCollection.placeholder()
public fileprivate(set) var teamScores: SyncedCollection<TeamScore> = SyncedCollection.placeholder()
public fileprivate(set) var matchSchedulers: StoredCollection<MatchScheduler> = StoredCollection.placeholder()
public fileprivate(set) var drawLogs: SyncedCollection<DrawLog> = SyncedCollection.placeholder()
// convenience init(tournament: Tournament) {
// let store = StoreCenter.main.store(identifier: tournament.id)
// self.init(store: store)
// self._initialize()
// }
init(store: Store) {
self.store = store
self._initialize()
}
fileprivate func _initialize() {
let indexed: Bool = true
self.groupStages = self.store.registerSynchronizedCollection(indexed: indexed)
self.rounds = self.store.registerSynchronizedCollection(indexed: indexed)
self.teamRegistrations = self.store.registerSynchronizedCollection(indexed: indexed)
self.playerRegistrations = self.store.registerSynchronizedCollection(indexed: indexed)
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.registerSynchronizedCollection(indexed: indexed)
self.store.loadCollectionsFromServerIfNoFile()
NotificationCenter.default.addObserver(
self,
selector: #selector(_leStorageDidSynchronize),
name: NSNotification.Name.LeStorageDidSynchronize,
object: nil)
}
@objc func _leStorageDidSynchronize(notification: Notification) {
// Logger.log("SYNCED > teamRegistrations count = \(self.teamRegistrations.count)")
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}

@ -0,0 +1,95 @@
//
// Array+Extensions.swift
// PadelClub
//
// Created by Razmig Sarkissian on 03/03/2024.
//
import Foundation
public extension Array {
func chunked(into size: Int) -> [[Element]] {
return stride(from: 0, to: count, by: size).map {
Array(self[$0 ..< Swift.min($0 + size, count)])
}
}
func anySatisfy(_ p: (Element) -> Bool) -> Bool {
return first(where: { p($0) }) != nil
//return !self.allSatisfy { !p($0) }
}
// Check if the number of elements in the sequence is even
var isEven: Bool {
return self.count % 2 == 0
}
// Check if the number of elements in the sequence is odd
var isOdd: Bool {
return self.count % 2 != 0
}
}
extension Array where Element: CustomStringConvertible {
func customJoined(separator: String, lastSeparator: String) -> String {
switch count {
case 0:
return ""
case 1:
return "\(self[0])"
case 2:
return "\(self[0]) \(lastSeparator) \(self[1])"
default:
let firstPart = dropLast().map { "\($0)" }.joined(separator: ", ")
let lastPart = "\(lastSeparator) \(last!)"
return "\(firstPart) \(lastPart)"
}
}
}
extension Dictionary where Key == Int, Value == [String] {
mutating func setOrAppend(_ element: String?, at key: Int) {
// Check if the element is nil; do nothing if it is
guard let element = element else {
return
}
// Check if the key exists in the dictionary
if var array = self[key] {
// If it exists, append the element to the array
array.append(element)
self[key] = array
} else {
// If it doesn't exist, create a new array with the element
self[key] = [element]
}
}
}
public extension Array where Element == String {
func formatList(maxDisplay: Int = 2) -> [String] {
// Check if the array has fewer or equal elements than the maximum display limit
if self.count <= maxDisplay {
// Join all elements with commas
return self
} else {
// Join only the first `maxDisplay` elements and add "et plus"
let displayedItems = self.prefix(maxDisplay)
let remainingCount = self.count - maxDisplay
return displayedItems.dropLast() + [displayedItems.last! + " et \(remainingCount) de plus"]
}
}
}
public extension Array {
func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>, order: SortOrder) -> [Element] {
switch order {
case .ascending:
return self.sorted { $0[keyPath: keyPath] < $1[keyPath: keyPath] }
case .descending:
return self.sorted { $0[keyPath: keyPath] > $1[keyPath: keyPath] }
}
}
}

@ -0,0 +1,71 @@
//
// Calendar+Extensions.swift
// PadelClub
//
// Created by Razmig Sarkissian on 28/03/2024.
//
import Foundation
public extension Calendar {
func numberOfDaysBetween(_ from: Date?, and to: Date?) -> Int {
guard let from, let to else { return 0 }
let fromDate = startOfDay(for: from)
let toDate = startOfDay(for: to)
let numberOfDays = dateComponents([.day], from: fromDate, to: toDate)
return numberOfDays.day! // <1>
}
func isSameDay(date1: Date?, date2: Date?) -> Bool {
guard let date1, let date2 else { return false }
return numberOfDaysBetween(date1, and: date2) == 0
}
func getSportAge() -> Int {
let currentDate = Date()
// Get the current year
let currentYear = component(.year, from: currentDate)
// Define the date components for 1st September and 31st December of the current year
let septemberFirstComponents = DateComponents(year: currentYear, month: 9, day: 1)
let decemberThirtyFirstComponents = DateComponents(year: currentYear, month: 12, day: 31)
// Get the actual dates for 1st September and 31st December
let septemberFirst = date(from: septemberFirstComponents)!
let decemberThirtyFirst = date(from: decemberThirtyFirstComponents)!
// Determine the sport year
let sportYear: Int
if currentDate >= septemberFirst && currentDate <= decemberThirtyFirst {
// If after 1st September and before 31st December, use current year + 1
sportYear = currentYear + 1
} else {
// Otherwise, use the current year
sportYear = currentYear
}
return sportYear
}
}
public extension Calendar {
// Add or subtract months from a date
func addMonths(_ months: Int, to date: Date) -> Date {
return self.date(byAdding: .month, value: months, to: date)!
}
// Generate a list of month start dates between two dates
func generateMonthRange(startDate: Date, endDate: Date) -> [Date] {
var dates: [Date] = []
var currentDate = startDate
while currentDate <= endDate {
dates.append(currentDate)
currentDate = self.addMonths(1, to: currentDate)
}
return dates
}
}

@ -0,0 +1,45 @@
//
// KeyedEncodingContainer+Extensions.swift
// PadelClub
//
// Created by Laurent Morvillier on 18/09/2024.
//
import Foundation
import LeStorage
extension KeyedDecodingContainer {
func decodeEncrypted(key: Key) throws -> String {
let data = try self.decode(Data.self, forKey: key)
return try data.decryptData(pass: CryptoKey.pass.rawValue)
}
func decodeEncryptedIfPresent(key: Key) throws -> String? {
let data = try self.decodeIfPresent(Data.self, forKey: key)
if let data {
return try data.decryptData(pass: CryptoKey.pass.rawValue)
}
return nil
}
}
extension KeyedEncodingContainer {
mutating func encodeAndEncrypt(_ value: Data, forKey key: Key) throws {
let encryped: Data = try value.encrypt(pass: CryptoKey.pass.rawValue)
try self.encode(encryped, forKey: key)
}
mutating func encodeAndEncryptIfPresent(_ value: Data?, forKey key: Key) throws {
guard let value else {
try encodeNil(forKey: key)
return
}
try self.encodeAndEncrypt(value, forKey: key)
}
}

@ -0,0 +1,262 @@
//
// Date+Extensions.swift
// PadelClub
//
// Created by Razmig Sarkissian on 01/03/2024.
//
import Foundation
public enum TimeOfDay {
case morning
case noon
case afternoon
case evening
case night
public var hello: String {
switch self {
case .morning, .noon, .afternoon:
return "Bonjour"
case .evening, .night:
return "Bonsoir"
}
}
public var goodbye: String {
switch self {
case .morning, .noon, .afternoon:
return "Bonne journée"
case .evening, .night:
return "Bonne soirée"
}
}
}
public 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()
}
func formattedAsHourMinute() -> String {
formatted(.dateTime.hour().minute())
}
func formattedAsDate() -> String {
formatted(.dateTime.weekday().day(.twoDigits).month().year())
}
var monthYearFormatted: String {
formatted(.dateTime.month(.wide).year(.defaultDigits))
}
var twoDigitsYearFormatted: String {
formatted(Date.FormatStyle(date: .numeric, time: .omitted).locale(Locale(identifier: "fr_FR")).year(.twoDigits))
}
var timeOfDay: TimeOfDay {
let hour = Calendar.current.component(.hour, from: self)
switch hour {
case 6..<12 : return .morning
case 12 : return .noon
case 13..<17 : return .afternoon
case 17..<22 : return .evening
default: return .night
}
}
}
public extension Date {
func isInCurrentYear() -> Bool {
let calendar = Calendar.current
let currentYear = calendar.component(.year, from: Date())
let yearOfDate = calendar.component(.year, from: self)
return currentYear == yearOfDate
}
func get(_ components: Calendar.Component..., calendar: Calendar = Calendar.current) -> DateComponents {
return calendar.dateComponents(Set(components), from: self)
}
func get(_ component: Calendar.Component, calendar: Calendar = Calendar.current) -> Int {
return calendar.component(component, from: self)
}
var tomorrowAtNine: Date {
let currentHour = Calendar.current.component(.hour, from: self)
let startOfDay = Calendar.current.startOfDay(for: self)
if currentHour < 8 {
return Calendar.current.date(byAdding: .hour, value: 9, to: startOfDay)!
} else {
let date = Calendar.current.date(byAdding: .day, value: 1, to: startOfDay)
return Calendar.current.date(byAdding: .hour, value: 9, to: date!)!
}
}
func atBeginningOfDay(hourInt: Int = 9) -> Date {
Calendar.current.date(byAdding: .hour, value: hourInt, to: self.startOfDay)!
}
static var firstDayOfWeek = Calendar.current.firstWeekday
static var capitalizedFirstLettersOfWeekdays: [String] {
let calendar = Calendar.current
// let weekdays = calendar.shortWeekdaySymbols
// return weekdays.map { weekday in
// guard let firstLetter = weekday.first else { return "" }
// return String(firstLetter).capitalized
// }
// Adjusted for the different weekday starts
var weekdays = calendar.veryShortStandaloneWeekdaySymbols
if firstDayOfWeek > 1 {
for _ in 1..<firstDayOfWeek {
if let first = weekdays.first {
weekdays.append(first)
weekdays.removeFirst()
}
}
}
return weekdays.map { $0.capitalized }
}
static var fullMonthNames: [String] {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale.current
return (1...12).compactMap { month in
dateFormatter.setLocalizedDateFormatFromTemplate("MMMM")
let date = Calendar.current.date(from: DateComponents(year: 2000, month: month, day: 1))
return date.map { dateFormatter.string(from: $0) }
}
}
var startOfMonth: Date {
Calendar.current.dateInterval(of: .month, for: self)!.start
}
var endOfMonth: Date {
let lastDay = Calendar.current.dateInterval(of: .month, for: self)!.end
return Calendar.current.date(byAdding: .day, value: -1, to: lastDay)!
}
var startOfPreviousMonth: Date {
let dayInPreviousMonth = Calendar.current.date(byAdding: .month, value: -1, to: self)!
return dayInPreviousMonth.startOfMonth
}
var numberOfDaysInMonth: Int {
Calendar.current.component(.day, from: endOfMonth)
}
// var sundayBeforeStart: Date {
// let startOfMonthWeekday = Calendar.current.component(.weekday, from: startOfMonth)
// let numberFromPreviousMonth = startOfMonthWeekday - 1
// return Calendar.current.date(byAdding: .day, value: -numberFromPreviousMonth, to: startOfMonth)!
// }
// New to accomodate for different start of week days
var firstWeekDayBeforeStart: Date {
let startOfMonthWeekday = Calendar.current.component(.weekday, from: startOfMonth)
let numberFromPreviousMonth = startOfMonthWeekday - Self.firstDayOfWeek
return Calendar.current.date(byAdding: .day, value: -numberFromPreviousMonth, to: startOfMonth)!
}
var calendarDisplayDays: [Date] {
var days: [Date] = []
// Current month days
for dayOffset in 0..<numberOfDaysInMonth {
let newDay = Calendar.current.date(byAdding: .day, value: dayOffset, to: startOfMonth)
days.append(newDay!)
}
// previous month days
for dayOffset in 0..<startOfPreviousMonth.numberOfDaysInMonth {
let newDay = Calendar.current.date(byAdding: .day, value: dayOffset, to: startOfPreviousMonth)
days.append(newDay!)
}
// Fixed to accomodate different weekday starts
return days.filter { $0 >= firstWeekDayBeforeStart && $0 <= endOfMonth }.sorted(by: <)
}
var monthInt: Int {
Calendar.current.component(.month, from: self)
}
var yearInt: Int {
Calendar.current.component(.year, from: self)
}
var dayInt: Int {
Calendar.current.component(.day, from: self)
}
var startOfDay: Date {
Calendar.current.startOfDay(for: self)
}
func endOfDay() -> Date {
let calendar = Calendar.current
return calendar.date(bySettingHour: 23, minute: 59, second: 59, of: self)!
}
func atNine() -> Date {
let calendar = Calendar.current
return calendar.date(bySettingHour: 9, minute: 0, second: 0, of: self)!
}
func atEightAM() -> Date {
let calendar = Calendar.current
return calendar.date(bySettingHour: 8, minute: 0, second: 0, of: self)!
}
}
public extension Date {
func isEarlierThan(_ date: Date) -> Bool {
Calendar.current.compare(self, to: date, toGranularity: .minute) == .orderedAscending
}
}
public extension Date {
func localizedTime() -> String {
self.formattedAsHourMinute()
}
func localizedDay() -> String {
self.formatted(.dateTime.weekday(.wide).day())
}
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
}()
func truncateMinutesAndSeconds() -> Date {
let calendar = Calendar.current
return calendar.date(bySetting: .minute, value: 0, of: self)!.withoutSeconds()
}
}

@ -0,0 +1,43 @@
//
// FixedWidthInteger+Extensions.swift
// PadelClub
//
// Created by Razmig Sarkissian on 03/03/2024.
//
import Foundation
public extension FixedWidthInteger {
func ordinalFormattedSuffix(feminine: Bool = false) -> String {
switch self {
case 1: return feminine ? "ère" : "er"
default: return "ème"
}
}
func ordinalFormatted(feminine: Bool = false) -> String {
return self.formatted() + self.ordinalFormattedSuffix(feminine: feminine)
}
private var isMany: Bool {
self > 1 || self < -1
}
var pluralSuffix: String {
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)
}
}

@ -0,0 +1,28 @@
//
// Locale+Extensions.swift
// PadelClub
//
// Created by Laurent Morvillier on 03/04/2024.
//
import Foundation
public extension Locale {
static func countries() -> [String] {
var countries: [String] = []
for countryCode in Locale.Region.isoRegions {
if let countryName = Locale.current.localizedString(forRegionCode: countryCode.identifier) {
countries.append(countryName)
}
}
return countries.sorted()
}
static func defaultCurrency() -> String {
// return "EUR"
Locale.current.currency?.identifier ?? "EUR"
}
}

@ -0,0 +1,20 @@
//
// NumberFormatter+Extensions.swift
// PadelClub
//
// Created by Razmig Sarkissian on 27/03/2024.
//
import Foundation
public extension NumberFormatter {
static var ordinal: NumberFormatter {
let formatter = NumberFormatter()
formatter.numberStyle = .ordinal
return formatter
}
static var standard: NumberFormatter {
return NumberFormatter()
}
}

@ -0,0 +1,105 @@
//
// Sequence+Extensions.swift
// PadelClub
//
// Created by Razmig Sarkissian on 03/03/2024.
//
import Foundation
public extension Collection {
/// Returns the element at the specified index if it is within bounds, otherwise nil.
subscript (safe index: Index) -> Element? {
return indices.contains(index) ? self[index] : nil
}
}
public extension Sequence {
func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> [Element] {
return sorted { a, b in
return a[keyPath: keyPath] < b[keyPath: keyPath]
}
}
}
public extension Sequence {
func pairs() -> AnySequence<(Element, Element)> {
AnySequence(zip(self, self.dropFirst()))
}
}
public extension Sequence {
func concurrentForEach(
_ operation: @escaping (Element) async throws -> Void
) async throws {
try await withThrowingTaskGroup(of: Void.self) { group in
// First, create all tasks
for element in self {
group.addTask {
try await operation(element)
}
}
// Then wait for all tasks to complete
for try await _ in group {}
}
}
func concurrentForEach(
_ operation: @escaping (Element) async -> Void
) async {
await withTaskGroup(of: Void.self) { group in
// First, add all tasks
for element in self {
group.addTask {
await operation(element)
}
}
// Then wait for all tasks to complete
for await _ in group {}
}
}
}
public enum SortOrder {
case ascending
case descending
}
public extension Sequence {
func sorted(using descriptors: [MySortDescriptor<Element>],
order: SortOrder) -> [Element] {
sorted { valueA, valueB in
for descriptor in descriptors {
let result = descriptor.comparator(valueA, valueB)
switch result {
case .orderedSame:
// Keep iterating if the two elements are equal,
// since that'll let the next descriptor determine
// the sort order:
break
case .orderedAscending:
return order == .ascending
case .orderedDescending:
return order == .descending
}
}
// If no descriptor was able to determine the sort
// order, we'll default to false (similar to when
// using the '<' operator with the built-in API):
return false
}
}
}
public extension Sequence {
func sorted(using descriptors: MySortDescriptor<Element>...) -> [Element] {
sorted(using: descriptors, order: .ascending)
}
}

@ -0,0 +1,46 @@
//
// String+Crypto.swift
// PadelClub
//
// Created by Laurent Morvillier on 30/04/2024.
//
import Foundation
import CryptoKit
enum CryptoError: Error {
case invalidUTF8
case cantConvertUTF8
case invalidBase64String
case nilSeal
}
extension Data {
func encrypt(pass: String) throws -> Data {
let key = try self._createSymmetricKey(fromString: pass)
let sealedBox = try AES.GCM.seal(self, using: key)
if let combined = sealedBox.combined {
return combined
}
throw CryptoError.nilSeal
}
func decryptData(pass: String) throws -> String {
let key = try self._createSymmetricKey(fromString: pass)
let sealedBox = try AES.GCM.SealedBox(combined: self)
let decryptedData = try AES.GCM.open(sealedBox, using: key)
guard let decryptedMessage = String(data: decryptedData, encoding: .utf8) else {
throw CryptoError.invalidUTF8
}
return decryptedMessage
}
fileprivate func _createSymmetricKey(fromString keyString: String) throws -> SymmetricKey {
guard let keyData = Data(base64Encoded: keyString) else {
throw CryptoError.invalidBase64String
}
return SymmetricKey(data: keyData)
}
}

@ -0,0 +1,272 @@
//
// String+Extensions.swift
// PadelClub
//
// Created by Razmig Sarkissian on 01/03/2024.
//
import Foundation
// MARK: - Trimming and stuff
public extension String {
func trunc(length: Int, trailing: String = "") -> String {
if length <= 0 { return self }
return (self.count > length) ? self.prefix(length) + trailing : self
}
func prefixTrimmed(_ length: Int) -> String {
String(trimmed.prefix(length))
}
func prefixMultilineTrimmed(_ length: Int) -> String {
String(trimmedMultiline.prefix(length))
}
var trimmed: String {
replaceCharactersFromSet(characterSet: .newlines, replacementString: " ").trimmingCharacters(in: .whitespacesAndNewlines)
}
var trimmedMultiline: String {
self.trimmingCharacters(in: .whitespacesAndNewlines)
}
func replaceCharactersFromSet(characterSet: CharacterSet, replacementString: String = "") -> String {
components(separatedBy: characterSet).joined(separator:replacementString)
}
var canonicalVersion: String {
trimmed.replaceCharactersFromSet(characterSet: .punctuationCharacters, replacementString: " ").folding(options: .diacriticInsensitive, locale: .current).lowercased()
}
var canonicalVersionWithPunctuation: String {
trimmed.folding(options: .diacriticInsensitive, locale: .current).lowercased()
}
var removingFirstCharacter: String {
String(dropFirst())
}
func isValidEmail() -> Bool {
let emailRegEx = "^[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}$"
let emailPredicate = NSPredicate(format:"SELF MATCHES %@", emailRegEx)
return emailPredicate.evaluate(with: self)
}
}
// MARK: - Club Name
public extension String {
func acronym() -> String {
let acronym = canonicalVersion.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines)
if acronym.count > 10 {
return concatenateFirstLetters().uppercased()
} else {
return acronym.uppercased()
}
}
func concatenateFirstLetters() -> String {
// Split the input into sentences
let sentences = self.components(separatedBy: .whitespacesAndNewlines)
if sentences.count == 1 {
return String(self.prefix(10))
}
// Extract the first character of each sentence
let firstLetters = sentences.compactMap { sentence -> Character? in
let trimmedSentence = sentence.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmedSentence.count > 2 {
if let firstCharacter = trimmedSentence.first {
return firstCharacter
}
}
return nil
}
// Join the first letters together into a string
let result = String(firstLetters)
return String(result.prefix(10))
}
}
// MARK: - FFT License
public extension String {
var computedLicense: String {
if let licenseKey {
return self + licenseKey
} else {
return self
}
}
var strippedLicense: String? {
var dropFirst = 0
if hasPrefix("0") {
dropFirst = 1
}
if let match = self.dropFirst(dropFirst).firstMatch(of: /[0-9]{6,8}/) {
let lic = String(self.dropFirst(dropFirst)[match.range.lowerBound..<match.range.upperBound])
return lic
} else {
return nil
}
}
var isLicenseNumber: Bool {
if let match = self.firstMatch(of: /[0-9]{6,8}[A-Z]/) {
let lic = String(self[match.range.lowerBound..<match.range.upperBound].dropLast(1))
let lastLetter = String(self[match.range.lowerBound..<match.range.upperBound].suffix(1))
if let lkey = lic.licenseKey {
return lkey == lastLetter
}
}
return false
}
var licenseKey: String? {
if let intValue = Int(self) {
var value = intValue
value -= 1
value = value % 23
let v = UnicodeScalar("A").value
let i = Int(v)
if let s = UnicodeScalar(i + value) {
var c = Character(s)
if c >= "I" {
value += 1
if let newS = UnicodeScalar(i + value) {
c = Character(newS)
}
}
if c >= "O" {
value += 1
if let newS = UnicodeScalar(i + value) {
c = Character(newS)
}
}
if c >= "Q" {
value += 1
if let newS = UnicodeScalar(i + value) {
c = Character(newS)
}
}
return String(c)
}
}
return nil
}
func licencesFound() -> [String] {
let matches = self.matches(of: /[1-9][0-9]{5,7}/)
return matches.map { String(self[$0.range]) }
}
}
// MARK: - FFT Source Importing
public extension String {
enum RegexStatic {
static let mobileNumber = /^(?:\+33|0033|0)[6-7](?:[ .-]?[0-9]{2}){4}$/
static let phoneNumber = /^(?:\+33|0033|0)[1-9](?:[ .-]?[0-9]{2}){4}$/
}
func isMobileNumber() -> Bool {
firstMatch(of: RegexStatic.mobileNumber) != nil
}
func isPhoneNumber() -> Bool {
firstMatch(of: RegexStatic.phoneNumber) != nil
}
func cleanSearchText() -> String {
// Create a character set of all punctuation except slashes and hyphens
var punctuationToRemove = CharacterSet.punctuationCharacters
punctuationToRemove.remove(charactersIn: "/-")
// Remove the unwanted punctuation
return self.components(separatedBy: punctuationToRemove)
.joined(separator: " ")
.trimmingCharacters(in: .whitespacesAndNewlines)
}
//april 04-2024 bug with accent characters / adobe / fft
mutating func replace(characters: [(Character, Character)]) {
for (targetChar, replacementChar) in characters {
self = String(self.map { $0 == targetChar ? replacementChar : $0 })
}
}
}
// MARK: - Player Names
public extension StringProtocol {
var firstUppercased: String { prefix(1).uppercased() + dropFirst() }
var firstCapitalized: String { prefix(1).capitalized + dropFirst() }
}
// MARK: - todo clean up ??
public extension LosslessStringConvertible {
var string: String { .init(self) }
}
public extension String {
func createFile(_ withName: String = "temp", _ exportedFormat: ExportFormat = .rawText) -> URL {
let url = FileManager.default.temporaryDirectory
.appendingPathComponent(withName)
.appendingPathExtension(exportedFormat.suffix)
let string = self
try? FileManager.default.removeItem(at: url)
try? string.write(to: url, atomically: true, encoding: .utf8)
return url
}
}
public extension String {
func toInt() -> Int? {
Int(self)
}
}
extension String : @retroactive Identifiable {
public var id: String { self }
}
public extension String {
/// Parses the birthdate string into a `Date` based on multiple formats.
/// - Returns: A `Date` object if parsing is successful, or `nil` if the format is unrecognized.
func parseAsBirthdate() -> Date? {
let dateFormats = [
"yyyy-MM-dd", // Format for "1993-01-31"
"dd/MM/yyyy", // Format for "27/07/1992"
"dd/MM/yy" // Format for "27/07/92"
]
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX") // Ensure consistent parsing
for format in dateFormats {
dateFormatter.dateFormat = format
if let date = dateFormatter.date(from: self) {
return date // Return the parsed date if successful
}
}
return nil // Return nil if no format matches
}
/// Formats the birthdate string into "DD/MM/YYYY".
/// - Returns: A formatted birthdate string, or the original string if parsing fails.
public func formattedAsBirthdate() -> String {
if let parsedDate = self.parseAsBirthdate() {
let outputFormatter = DateFormatter()
outputFormatter.dateFormat = "dd/MM/yyyy" // Desired output format
return outputFormatter.string(from: parsedDate)
}
return self // Return the original string if parsing fails
}
}

@ -0,0 +1,16 @@
//
// Tournament+Extensions.swift
// PadelClubData
//
// Created by Laurent Morvillier on 17/04/2025.
//
import Foundation
extension Tournament {
public static func fake() -> Tournament {
return Tournament(event: "Roland Garros", name: "Magic P100", startDate: Date(), endDate: Date(), creationDate: Date(), isPrivate: false, groupStageFormat: .nineGames, roundFormat: nil, loserRoundFormat: nil, groupStageSortMode: .snake, groupStageCount: 4, rankSourceDate: nil, dayDuration: 2, teamCount: 24, teamSorting: .rank, federalCategory: .men, federalLevelCategory: .p100, federalAgeCategory: .a45, closedRegistrationDate: nil, groupStageAdditionalQualified: 0, courtCount: 4, prioritizeClubMembers: false, qualifiedPerGroupStage: 2, teamsPerGroupStage: 4, entryFee: nil)
}
}

@ -0,0 +1,182 @@
//
// URL+Extensions.swift
// PadelClub
//
// Created by Razmig Sarkissian on 01/03/2024.
//
import Foundation
public extension URL {
public static var savedDateFormatter: DateFormatter = {
let df = DateFormatter()
df.dateFormat = "DD/MM/yyyy"
return df
}()
public static var importDateFormatter: DateFormatter = {
let df = DateFormatter()
df.dateFormat = "MM-yyyy"
return df
}()
var dateFromPath: Date {
let found = deletingPathExtension().path().components(separatedBy: "-").suffix(2).joined(separator: "-")
if let date = URL.importDateFormatter.date(from: found) {
return date
} else {
return Date()
}
}
var index: Int {
if let i = path().dropLast(12).last?.wholeNumberValue {
return i
}
return 0
}
var manData: Bool {
path().contains("MESSIEURS")
}
var womanData: Bool {
path().contains("DAMES")
}
static var seed: URL? {
Bundle.main.url(forResource: "SeedData", withExtension: nil)
}
}
public extension URL {
func creationDate() -> Date? {
// Use FileManager to retrieve the file attributes
do {
let fileAttributes = try FileManager.default.attributesOfItem(atPath: self.path())
// Access the creationDate from the file attributes
if let creationDate = fileAttributes[.creationDate] as? Date {
print("File creationDate: \(creationDate)")
return creationDate
} else {
print("creationDate not found.")
}
} catch {
print("Error retrieving file attributes: \(error.localizedDescription)")
}
return nil
}
func fftImportingStatus() -> Int? {
// Read the contents of the file
guard let fileContents = try? String(contentsOfFile: path(), encoding: .utf8) else {
return nil
}
// Split the contents by newline characters
let lines = fileContents.components(separatedBy: .newlines)
//0 means no need to reimport, just recalc
//1 or missing means re-import
if let line = lines.first(where: {
$0.hasPrefix("import-status:")
}) {
return Int(line.replacingOccurrences(of: "import-status:", with: ""))
}
return nil
}
public func fftImportingMaleUnrankValue() -> Int? {
// Read the contents of the file
guard let fileContents = try? String(contentsOfFile: path(), encoding: .utf8) else {
return nil
}
// Split the contents by newline characters
let lines = fileContents.components(separatedBy: .newlines)
if let line = lines.first(where: {
$0.hasPrefix("unrank-male-value:")
}) {
return Int(line.replacingOccurrences(of: "unrank-male-value:", with: ""))
}
return nil
}
public func fileModelIdentifier() -> String? {
// Read the contents of the file
guard let fileContents = try? String(contentsOfFile: path(), encoding: .utf8) else {
return nil
}
// Split the contents by newline characters
let lines = fileContents.components(separatedBy: .newlines)
if let line = lines.first(where: {
$0.hasPrefix("file-model-version:")
}) {
return line.replacingOccurrences(of: "file-model-version:", with: "")
}
return nil
}
public func fftImportingUncomplete() -> Int? {
// Read the contents of the file
guard let fileContents = try? String(contentsOfFile: path(), encoding: .utf8) else {
return nil
}
// Split the contents by newline characters
let lines = fileContents.components(separatedBy: .newlines)
if let line = lines.first(where: {
$0.hasPrefix("max-players:")
}) {
return Int(line.replacingOccurrences(of: "max-players:", with: ""))
}
return nil
}
func getUnrankedValue() -> Int? {
// Read the contents of the file
guard let fileContents = try? String(contentsOfFile: path(), encoding: .utf8) else {
return nil
}
// Split the contents by newline characters
let lines = fileContents.components(separatedBy: .newlines)
// Get the last non-empty line
var lastLine: String?
for line in lines.reversed() {
let trimmedLine = line.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmedLine.isEmpty {
lastLine = trimmedLine
break
}
}
guard let rankString = lastLine?.components(separatedBy: ";").dropFirst().first, let rank = Int(rankString) else {
return nil
}
// Define the regular expression pattern
let pattern = "\\b\(NSRegularExpression.escapedPattern(for: rankString))\\b"
// Create the regular expression object
guard let regex = try? NSRegularExpression(pattern: pattern) else {
return nil
}
// Get the matches
let matches = regex.matches(in: fileContents, range: NSRange(fileContents.startIndex..., in: fileContents))
// Return the count of matches
return matches.count + rank - 1
}
}

@ -0,0 +1,13 @@
# ``PadelClubData``
<!--@START_MENU_TOKEN@-->Summary<!--@END_MENU_TOKEN@-->
## Overview
<!--@START_MENU_TOKEN@-->Text<!--@END_MENU_TOKEN@-->
## Topics
### <!--@START_MENU_TOKEN@-->Group<!--@END_MENU_TOKEN@-->
- <!--@START_MENU_TOKEN@-->``Symbol``<!--@END_MENU_TOKEN@-->

@ -0,0 +1,18 @@
//
// PadelClubData.h
// PadelClubData
//
// Created by Laurent Morvillier on 15/04/2025.
//
#import <Foundation/Foundation.h>
//! Project version number for PadelClubData.
FOUNDATION_EXPORT double PadelClubDataVersionNumber;
//! Project version string for PadelClubData.
FOUNDATION_EXPORT const unsigned char PadelClubDataVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <PadelClubData/PublicHeader.h>

@ -0,0 +1,143 @@
//
// Patcher.swift
// PadelClub
//
// Created by Laurent Morvillier on 21/06/2024.
//
import Foundation
import LeStorage
public enum ManualPatch: String {
case disconnect
var id: String {
return "padelclub.app.manual.patch.\(self.rawValue)"
}
}
public class ManualPatcher {
public static func patchIfPossible(_ patch: ManualPatch) -> Bool {
if UserDefaults.standard.value(forKey: patch.id) == nil {
do {
Logger.log(">>> Patches \(patch.rawValue)...")
let result = try self._applyPatch(patch)
UserDefaults.standard.setValue(true, forKey: patch.id)
return result
} catch {
Logger.error(error)
}
}
return false
}
fileprivate static func _applyPatch(_ patch: ManualPatch) throws -> Bool {
switch patch {
case .disconnect:
let rawToken = try? StoreCenter.main.rawTokenShouldNotBeUsed()
if StoreCenter.main.userName != nil || StoreCenter.main.userId != nil || rawToken != nil {
DataStore.shared.disconnect()
return true
} else {
return false
}
}
}
}
enum PatchError: Error {
case patchError(message: String)
}
enum Patch: String, CaseIterable {
case cleanLogs
case syncUpgrade
case updateTournaments
case cleanPurchaseApiCalls = "cleanPurchaseApiCalls_2"
var id: String {
return "padelclub.app.patch.\(self.rawValue)"
}
}
public class AutomaticPatcher {
static func applyAllWhenApplicable() {
for patch in Patch.allCases {
self.patchIfPossible(patch)
}
}
static func patchIfPossible(_ patch: Patch) {
if UserDefaults.standard.value(forKey: patch.id) == nil {
do {
Logger.log(">>> Patches \(patch.rawValue)...")
try self._applyPatch(patch)
UserDefaults.standard.setValue(true, forKey: patch.id)
} catch {
Logger.error(error)
}
}
}
fileprivate static func _applyPatch(_ patch: Patch) throws {
switch patch {
case .cleanLogs: self._cleanLogs()
case .syncUpgrade: self._syncUpgrade()
case .updateTournaments: self._updateTournaments()
case .cleanPurchaseApiCalls: self._cleanPurchaseApiCalls()
}
}
fileprivate static func _cleanLogs() {
StoreCenter.main.resetLoggingCollections()
}
fileprivate static func _syncUpgrade() {
for tournament in DataStore.shared.tournaments {
let id = tournament.id
guard let store = TournamentLibrary.shared.store(tournamentId: tournament.id) else { continue }
for round in store.rounds {
round.storeId = id
}
store.rounds.addOrUpdate(contentOfs: store.rounds)
for groupStage in store.groupStages {
groupStage.storeId = id
}
store.groupStages.addOrUpdate(contentOfs: store.groupStages)
for teamRegistration in store.teamRegistrations {
teamRegistration.storeId = id
}
store.teamRegistrations.addOrUpdate(contentOfs: store.teamRegistrations)
for pr in store.playerRegistrations {
pr.storeId = id
}
store.playerRegistrations.addOrUpdate(contentOfs: store.playerRegistrations)
for match in store.matches {
match.storeId = id
}
store.matches.addOrUpdate(contentOfs: store.matches)
for ts in store.teamScores {
ts.storeId = id
}
store.teamScores.addOrUpdate(contentOfs: store.teamScores)
for ms in store.matchSchedulers {
ms.storeId = id
}
store.matchSchedulers.addOrUpdate(contentOfs: store.matchSchedulers)
}
}
fileprivate static func _updateTournaments() {
DataStore.shared.tournaments.addOrUpdate(contentOfs: DataStore.shared.tournaments)
}
fileprivate static func _cleanPurchaseApiCalls() {
StoreCenter.main.resetApiCalls(type: Purchase.self)
}
}

@ -0,0 +1,79 @@
//
// Selectable.swift
// PadelClub
//
// Created by Razmig Sarkissian on 01/04/2024.
//
import Foundation
import SwiftUI
import TipKit
protocol Selectable {
func selectionLabel(index: Int) -> String
func badgeValue() -> Int?
func badgeImage() -> Badge?
func badgeValueColor() -> Color?
func displayImageIfValueZero() -> Bool
func systemImage() -> String?
func associatedTip() -> (any Tip)?
}
extension Selectable {
func associatedTip() -> (any Tip)? {
return nil
}
func systemImage() -> String? {
return nil
}
func displayImageIfValueZero() -> Bool {
return false
}
}
public enum Badge {
case checkmark
case xmark
case custom(systemName: String, color: Color)
public func systemName() -> String {
switch self {
case .checkmark:
return "checkmark.circle.fill"
case .xmark:
return "xmark.circle.fill"
case .custom(let systemName, _):
return systemName
}
}
}
struct SelectionTipViewModifier: ViewModifier {
let selectable: Selectable
let action: () -> Void
func body(content: Content) -> some View {
if let tip = selectable.associatedTip() {
if #available(iOS 18.0, *) {
content
.popoverTip(tip, arrowEdge: .top) { _ in
action()
tip.invalidate(reason: .tipClosed)
}
} else {
content
}
} else {
content
}
}
}
extension View {
func selectableTipViewModifier(selectable: Selectable, action: @escaping () -> Void) -> some View {
modifier(SelectionTipViewModifier(selectable: selectable, action: action))
}
}

@ -0,0 +1,74 @@
//
// SortOption.swift
// PadelClubData
//
// Created by Laurent Morvillier on 15/04/2025.
//
import Foundation
public enum SortOption: Int, CaseIterable, Identifiable {
case name
case rank
case tournamentCount
case points
case progression
public var id: Int { self.rawValue }
public func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
switch self {
case .name:
return "Nom"
case .rank:
return "Rang"
case .tournamentCount:
return "Tournoi"
case .points:
return "Points"
case .progression:
return "Progression"
}
}
// func sortDescriptors(_ ascending: Bool, dataSet: DataSet) -> [SortDescriptor<ImportedPlayer>] {
// switch self {
// case .name:
// return [
// SortDescriptor(\ImportedPlayer.lastName, order: ascending ? .forward : .reverse),
// SortDescriptor(\ImportedPlayer.rank), SortDescriptor(\ImportedPlayer.assimilation),
// ]
// case .rank:
// if dataSet == .national || dataSet == .ligue {
// return [
// SortDescriptor(\ImportedPlayer.rank, order: ascending ? .forward : .reverse)
// ]
// } else {
// return [
// SortDescriptor(\ImportedPlayer.rank, order: ascending ? .forward : .reverse),
// SortDescriptor(\ImportedPlayer.assimilation),
// SortDescriptor(\ImportedPlayer.lastName),
// ]
// }
// case .tournamentCount:
// return [
// SortDescriptor(
// \ImportedPlayer.tournamentCount, order: ascending ? .forward : .reverse),
// SortDescriptor(\ImportedPlayer.rank), SortDescriptor(\ImportedPlayer.assimilation),
// SortDescriptor(\ImportedPlayer.lastName),
// ]
// case .points:
// return [
// SortDescriptor(\ImportedPlayer.points, order: ascending ? .forward : .reverse),
// SortDescriptor(\ImportedPlayer.rank), SortDescriptor(\ImportedPlayer.assimilation),
// SortDescriptor(\ImportedPlayer.lastName),
// ]
// case .progression:
// return [
// SortDescriptor(\ImportedPlayer.progression, order: ascending ? .forward : .reverse),
// SortDescriptor(\ImportedPlayer.rank), SortDescriptor(\ImportedPlayer.assimilation),
// SortDescriptor(\ImportedPlayer.lastName),
// ]
// }
// }
}

@ -0,0 +1,250 @@
//
// SourceFileManager.swift
// PadelClub
//
// Created by Razmig Sarkissian on 01/03/2024.
//
import Foundation
import LeStorage
public class SourceFileManager {
public static let shared = SourceFileManager()
init() {
createDirectoryIfNeeded(directoryURL: rankingSourceDirectory)
#if targetEnvironment(simulator)
createDirectoryIfNeeded(directoryURL: anonymousSourceDirectory)
#endif
}
public let rankingSourceDirectory : URL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appending(path: "rankings")
public let anonymousSourceDirectory : URL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appending(path: "anonymous")
func createDirectoryIfNeeded(directoryURL: URL) {
let fileManager = FileManager.default
do {
// Check if the directory exists
if !fileManager.fileExists(atPath: directoryURL.path) {
// Directory does not exist, create it
try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
// print("Directory created at: \(directoryURL)")
} else {
// print("Directory already exists at: \(directoryURL)")
}
} catch {
print("Error: \(error)")
}
}
var lastDataSource: String? {
DataStore.shared.appSettings.lastDataSource
}
public func lastDataSourceDate() -> Date? {
guard let lastDataSource else { return nil }
return URL.importDateFormatter.date(from: lastDataSource)
}
public func fetchData() async {
await fetchData(fromDate: Date())
// if let mostRecent = mostRecentDateAvailable, let current = Calendar.current.date(byAdding: .month, value: 1, to: mostRecent), current > mostRecent {
// await fetchData(fromDate: current)
// } else {
// }
}
func _removeAllData(fromDate current: Date) {
let lastStringDate = URL.importDateFormatter.string(from: current)
let files = ["MESSIEURS", "MESSIEURS-2", "MESSIEURS-3", "MESSIEURS-4", "DAMES"]
files.forEach { fileName in
NetworkManager.shared.removeRankingData(lastDateString: lastStringDate, fileName: fileName)
}
}
actor SourceFileDownloadTracker {
var _downloadedFileStatus : Int? = nil
func updateIfNecessary(with successState: Int?) {
if successState != nil && (_downloadedFileStatus == nil || _downloadedFileStatus == 0) {
_downloadedFileStatus = successState
}
}
func getDownloadedFileStatus() -> Int? {
return _downloadedFileStatus
}
}
//return nil if no new files
//return 1 if new file to import
//return 0 if new file just to re-calc static data, no need to re-import
@discardableResult
public func fetchData(fromDate current: Date) async -> Int? {
let lastStringDate = URL.importDateFormatter.string(from: current)
let files = ["MESSIEURS", "MESSIEURS-2", "MESSIEURS-3", "MESSIEURS-4", "DAMES"]
let sourceFileDownloadTracker = SourceFileDownloadTracker()
do {
try await withThrowingTaskGroup(of: Void.self) { group in // Mark 1
for file in files {
group.addTask { [sourceFileDownloadTracker] in
let success = try await NetworkManager.shared.downloadRankingData(lastDateString: lastStringDate, fileName: file)
await sourceFileDownloadTracker.updateIfNecessary(with: success)
}
}
try await group.waitForAll()
}
// if current < Date() {
// if let nextCurrent = Calendar.current.date(byAdding: .month, value: 1, to: current) {
// await fetchData(fromDate: nextCurrent)
// }
// }
} catch {
print("downloadRankingData", error)
if mostRecentDateAvailable == nil {
if let previousDate = Calendar.current.date(byAdding: .month, value: -1, to: current) {
await fetchData(fromDate: previousDate)
}
}
}
let downloadedFileStatus = await sourceFileDownloadTracker.getDownloadedFileStatus()
return downloadedFileStatus
}
public func getAllFiles(initialDate: String = "08-2022") async {
let dates = monthsBetweenDates(startDateString: initialDate, endDateString: Date().monthYearFormatted)
.compactMap {
URL.importDateFormatter.date(from: $0)
}
.filter { date in
allFiles.contains(where: { $0.dateFromPath == date }) == false
}
try? await dates.concurrentForEach { date in
await self.fetchData(fromDate: date)
}
}
func monthsBetweenDates(startDateString: String, endDateString: String) -> [String] {
let dateFormatter = URL.importDateFormatter
guard let startDate = dateFormatter.date(from: startDateString),
let endDate = dateFormatter.date(from: endDateString) else {
return []
}
var months: [String] = []
var currentDate = startDate
let calendar = Calendar.current
while currentDate <= endDate {
let monthString = dateFormatter.string(from: currentDate)
months.append(monthString)
guard let nextMonthDate = calendar.date(byAdding: .month, value: 1, to: currentDate) else {
break
}
currentDate = nextMonthDate
}
return months
}
public func getUnrankValue(forMale: Bool, rankSourceDate: Date?) -> Int? {
let _rankSourceDate = rankSourceDate ?? mostRecentDateAvailable
let urls = allFiles(forMale).filter { $0.dateFromPath == _rankSourceDate }
return urls.compactMap { $0.getUnrankedValue() }.sorted().last
}
public var mostRecentDateAvailable: Date? {
allFiles(false).first?.dateFromPath
}
public func removeAllFilesFromServer() {
let allFiles = try! FileManager.default.contentsOfDirectory(at: rankingSourceDirectory, includingPropertiesForKeys: nil)
allFiles.filter { $0.pathExtension == "csv" }.forEach { url in
try? FileManager.default.removeItem(at: url)
}
}
public func anonymousFiles() -> [URL] {
let allJSONFiles = try! FileManager.default.contentsOfDirectory(at: anonymousSourceDirectory, includingPropertiesForKeys: nil).filter({ url in
url.pathExtension == "csv"
})
return allJSONFiles
}
public func jsonFiles() -> [URL] {
let allJSONFiles = try! FileManager.default.contentsOfDirectory(at: rankingSourceDirectory, includingPropertiesForKeys: nil).filter({ url in
url.pathExtension == "json"
})
return allJSONFiles
}
public var allFiles: [URL] {
let allFiles = try! FileManager.default.contentsOfDirectory(at: rankingSourceDirectory, includingPropertiesForKeys: nil).filter({ url in
url.pathExtension == "csv"
})
return (allFiles + (Bundle.main.urls(forResourcesWithExtension: "csv", subdirectory: nil) ?? [])).sorted { $0.dateFromPath == $1.dateFromPath ? $0.index < $1.index : $0.dateFromPath > $1.dateFromPath }
}
public func allFiles(_ isManPlayer: Bool) -> [URL] {
allFiles.filter({ url in
url.path().contains(isManPlayer ? SourceFile.messieurs.rawValue : SourceFile.dames.rawValue)
})
}
public func allFilesSortedByDate(_ isManPlayer: Bool) -> [URL] {
return allFiles(isManPlayer)
}
static func isDateAfterUrlImportDate(date: Date, dateString: String) -> Bool {
guard let importDate = URL.importDateFormatter.date(from: dateString) else {
return false
}
return importDate.isEarlierThan(date)
}
public static func getSortOption() -> [SortOption] {
return SortOption.allCases
}
}
public enum SourceFile: String, CaseIterable {
case dames = "DAMES"
case messieurs = "MESSIEURS"
public var filesFromServer: [URL] {
let rankingSourceDirectory = SourceFileManager.shared.rankingSourceDirectory
let allFiles = try! FileManager.default.contentsOfDirectory(at: rankingSourceDirectory, includingPropertiesForKeys: nil)
return allFiles.filter{$0.pathExtension == "csv" && $0.path().contains(rawValue)}
}
public func currentURLs(importingDate: Date) -> [URL] {
var files = Bundle.main.urls(forResourcesWithExtension: "csv", subdirectory: nil)?.filter({ url in
url.path().contains(rawValue)
}) ?? []
files.append(contentsOf: filesFromServer)
return files.filter({ $0.dateFromPath == importingDate })
}
public var isMan: Bool {
switch self {
case .dames:
return false
default:
return true
}
}
}

@ -0,0 +1,341 @@
//
// Guard.swift
// Poker Analytics 6
//
// Created by Laurent Morvillier on 20/04/2022.
//
import Foundation
import StoreKit
import LeStorage
@available(iOS 15, *)
@objc public class Guard: NSObject {
public static var main: Guard = Guard()
@Published public private(set) var purchasedTransactions = Set<StoreKit.Transaction>()
public var currentBestPurchase: Purchase? = nil
var updateListenerTask: Task<Void, Never>? = nil
fileprivate let _freeTournaments: Int = 3
override init() {
super.init()
self.updateListenerTask = self.listenForTransactions()
Task {
do {
try await self.refreshPurchasedAppleProducts()
} catch {
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<Purchase> {
self._updateBestPlan()
}
}
func productIds() async -> [String] {
var productIds: [String] = []
for await result in Transaction.all {
do {
let verified = try self.checkVerified(result)
productIds.append(verified.productID)
} catch {
Logger.error(error)
}
}
return productIds
}
public func refreshPurchasedAppleProducts() async throws {
// Iterate through the user's purchased products.
for await verificationResult in Transaction.currentEntitlements {
let transaction = try await self.processTransactionResult(verificationResult)
print("processs product id = \(transaction.productID)")
DispatchQueue.main.async {
NotificationCenter.default.post(name: Notification.Name.StoreEventHappened, object: nil)
}
await transaction.finish()
}
}
func listenForTransactions() -> Task<Void, Never> {
return Task(priority: .background) {
//Iterate through any transactions which didn't come from a direct call to `purchase()`.
for await result in Transaction.updates {
// Logger.log(">>> update = \(result)")
do {
let transaction = try self.checkVerified(result)
//Deliver content to the user.
await self.updatePurchasedIdentifiers(transaction)
//Always finish a transaction.
await transaction.finish()
} catch {
Logger.error(error)
//StoreKit has a receipt it can read but it failed verification. Don't deliver content to the user.
print("Transaction failed verification")
}
}
}
}
func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
//Check if the transaction passes StoreKit verification.
switch result {
case .unverified:
//StoreKit has parsed the JWS but failed verification. Don't deliver content to the user.
throw StoreManagerError.failedVerification
case .verified(let safe):
//If the transaction is verified, unwrap and return it.
return safe
}
}
@MainActor
func updatePurchasedIdentifiers(_ transaction: StoreKit.Transaction) async {
// Logger.log("\(transaction.productID) > purchase = \(transaction.originalPurchaseDate), exp date= \(transaction.expirationDate), rev date = \(transaction.revocationDate)")
// Logger.log("purchase date = \(transaction.purchaseDate)")
do {
if transaction.revocationDate == nil {
// If the App Store has not revoked the transaction, add it to the list of `purchasedIdentifiers`.
purchasedTransactions.insert(transaction)
// try self._addPurchaseIfPossible(transaction: transaction)
} else {
// If the App Store has revoked this transaction, remove it from the list of `purchasedIdentifiers`.
purchasedTransactions.remove(transaction)
// try self._updatePurchaseIfPossible(transaction: transaction)
}
try self._updatePurchase(transaction: transaction)
} catch {
Logger.error(error)
}
self._updateBestPlan()
}
fileprivate func _updatePurchase(transaction: StoreKit.Transaction) throws {
let purchases = DataStore.shared.purchases
if let purchase = self._purchaseByTransactionId(transaction.originalID) {
purchase.revocationDate = transaction.revocationDate
purchase.expirationDate = transaction.expirationDate
purchase.purchaseDate = transaction.purchaseDate
purchase.productId = transaction.productID
try purchases.addOrUpdate(instance: purchase)
} else {
let purchase: Purchase = try transaction.purchase()
try purchases.addOrUpdate(instance: purchase)
}
}
// fileprivate func _addPurchaseIfPossible(transaction: StoreKit.Transaction) throws {
//
// let purchases = DataStore.shared.purchases
//
// if self._purchaseByTransactionId(transaction.originalID) == nil {
// let purchase: Purchase = try transaction.purchase()
// try purchases.addOrUpdate(instance: purchase)
// }
// }
//
// fileprivate func _updatePurchaseIfPossible(transaction: StoreKit.Transaction) throws {
// let purchases = DataStore.shared.purchases
// if let existing: Purchase = self._purchaseByTransactionId(transaction.originalID) {
// existing.revocationDate = transaction.revocationDate
// try purchases.addOrUpdate(instance: existing)
// }
// }
fileprivate func _purchaseByTransactionId(_ transactionId: UInt64) -> Purchase? {
let purchases = DataStore.shared.purchases
return purchases.first(where: { $0.id == transactionId })
}
func processTransactionResult(_ result: VerificationResult<StoreKit.Transaction>) async throws -> StoreKit.Transaction {
let transaction = try checkVerified(result)
// Deliver content to the user.
await updatePurchasedIdentifiers(transaction)
return transaction
}
public var currentPlan: StoreItem? {
#if DEBUG
if let plan = PListReader.readString(plist: "local", key: "plan"), !plan.isEmpty {
return StoreItem(rawValue: plan)
}
#elseif TESTFLIGHT
return .monthlyUnlimited
#elseif PRODTEST
return .monthlyUnlimited
#else
if let currentBestPurchase = self.currentBestPurchase, let plan = StoreItem(rawValue: currentBestPurchase.productId) {
return plan
}
#endif
return nil
}
public func userFilteredPurchases() -> [StoreKit.Transaction] {
// Logger.log("self.purchasedTransactions = \(self.purchasedTransactions.count)")
guard let userId = StoreCenter.main.userId, let currentUserUUID: UUID = UUID(uuidString: userId) else {
return []
}
let userTransactions = self.purchasedTransactions.filter { currentUserUUID == $0.appAccountToken || $0.appAccountToken == nil }
let now: Date = Date()
// print("now = \(now)")
return userTransactions.filter { transaction in
if let expirationDate = transaction.expirationDate {
// print("exp = \(expirationDate)")
return expirationDate > now
} else {
return true
}
}
}
/// Update best plan by filtering Apple purchases with registered purchases by the user
fileprivate func _updateBestPlan() {
var purchases: [Purchase] = []
// Make sure the purchase has been done with the logged user
let userPurchases = self.userFilteredPurchases().compactMap { try? $0.purchase() }
purchases.append(contentsOf: userPurchases)
let validPurchases = DataStore.shared.purchases.filter { $0.isValid() }
Logger.log("valid purchases = \(validPurchases.count)")
purchases.append(contentsOf: validPurchases)
if let purchase = purchases.first(where: { $0.productId == StoreItem.monthlyUnlimited.rawValue }) {
self.currentBestPurchase = purchase
} else if let purchase = purchases.first(where: { $0.productId == StoreItem.fivePerMonth.rawValue }) {
self.currentBestPurchase = purchase
}
}
fileprivate func _purchasedTournamentCount() -> Int {
let purchases = DataStore.shared.purchases
let units = purchases.filter { $0.productId == StoreItem.unit.rawValue }
return units.reduce(0) { $0 + ($1.quantity ?? 0) }
// let units = self.userFilteredPurchases().filter { $0.productID == StoreItem.unit.rawValue }
// return units.reduce(0) { $0 + $1.purchasedQuantity }
}
public func paymentForNewTournament() -> TournamentPayment? {
switch self.currentPlan {
case .monthlyUnlimited:
return TournamentPayment.unlimited
case .fivePerMonth:
if let purchaseDate = self.currentBestPurchase?.purchaseDate {
let tournaments = DataStore.shared.tournaments.filter { $0.creationDate > purchaseDate && $0.payment == .subscriptionUnit && $0.isCanceled == false }
if tournaments.count < StoreItem.five {
return TournamentPayment.subscriptionUnit
}
}
return self._paymentWithoutSubscription()
default:
return self._paymentWithoutSubscription()
}
}
fileprivate func _paymentWithoutSubscription() -> TournamentPayment? {
let freelyPayed: Int = DataStore.shared.tournaments.filter { $0.payment == .free && $0.isCanceled == false }.count
if freelyPayed < self._freeTournaments {
return TournamentPayment.free
}
let tournamentCreditCount: Int = self._purchasedTournamentCount()
let unitlyPayed = DataStore.shared.tournaments.filter { $0.payment == .unit && $0.isCanceled == false }.count
if tournamentCreditCount > unitlyPayed {
return TournamentPayment.unit
}
return nil
}
public var remainingTournaments: Int {
let unitlyPayed = DataStore.shared.tournaments.filter { $0.payment == TournamentPayment.unit }.count
let tournamentCreditCount = self._purchasedTournamentCount()
// let notDeletedTournamentCount = DataStore.shared.tournaments.filter { $0.isDeleted == false }.count
// Logger.log("unitlyPayed = \(unitlyPayed), purchased = \(tournamentCreditCount) ")
return tournamentCreditCount - unitlyPayed
}
func disconnect() {
let purchases = DataStore.shared.purchases
purchases.reset()
}
}
public struct PurchaseRow: Identifiable {
public var id: UInt64
public var name: String
public var item: StoreItem
public var quantity: Int?
public init(id: UInt64, name: String, item: StoreItem, quantity: Int? = nil) {
self.id = id
self.name = name
self.item = item
self.quantity = quantity
}
}
fileprivate extension StoreKit.Transaction {
func purchase() throws -> Purchase {
guard let userId = StoreCenter.main.userId else {
throw StoreError.missingUserId
}
return Purchase(transactionId: self.originalID,
user: userId,
purchaseDate: self.purchaseDate,
productId: self.productID,
quantity: self.purchasedQuantity,
revocationDate: self.revocationDate,
expirationDate: self.expirationDate)
}
}

@ -0,0 +1,38 @@
//
// StoreItem.swift
// Padel Club
//
// Created by Laurent Morvillier on 22/04/2024.
//
import Foundation
public enum StoreItem: String, Identifiable, CaseIterable {
case monthlyUnlimited = "app.padelclub.tournament.subscription.unlimited"
case fivePerMonth = "app.padelclub.tournament.subscription.five.per.month"
case unit = "app.padelclub.tournament.unit"
#if DEBUG
static let five: Int = 2
#else
static let five: Int = 5
#endif
public var id: String { return self.rawValue }
public var systemImage: String {
switch self {
case .monthlyUnlimited: return "infinity.circle.fill"
case .fivePerMonth: return "star.circle.fill"
case .unit: return "tennisball.circle.fill"
}
}
public var isConsumable: Bool {
switch self {
case .monthlyUnlimited, .fivePerMonth: return false
case .unit: return true
}
}
}

@ -0,0 +1,140 @@
//
// Store.swift
// Poker Analytics 6
//
// Created by Laurent Morvillier on 20/04/2022.
//
import Foundation
import StoreKit
import LeStorage
public enum StoreManagerError: Error {
case failedVerification
case missingPlan
}
public protocol StoreDelegate {
func productsReceived(products: [Product])
func errorDidOccur(error: Error)
}
public extension Notification.Name {
static let StoreEventHappened = Notification.Name("storePurchaseSucceeded")
}
public class StoreManager {
@Published private(set) var purchasedTransactions = Set<StoreKit.Transaction>()
var delegate: StoreDelegate? = nil
var updateListenerTask: Task<Void, Error>? = nil
public init(delegate: StoreDelegate?) {
self.delegate = delegate
self.updateListenerTask = listenForTransactions()
Task {
//Initialize the store by starting a product request.
await self.requestProducts()
}
}
deinit {
self.updateListenerTask?.cancel()
}
@MainActor
public func requestProducts() async {
do {
// let identifiers: [String] = StoreItem.allCases.map { $0.rawValue }
var products: [Product] = try await Product.products(for: self._productIdentifiers())
products = products.sorted { p1, p2 in
return p2.price > p1.price
}
Logger.log("products = \(products.count)")
self.delegate?.productsReceived(products: products)
} catch {
self.delegate?.errorDidOccur(error: error)
Logger.error(error)
}
}
fileprivate func _productIdentifiers() -> [String] {
var items: [StoreItem] = []
switch Guard.main.currentPlan {
case .fivePerMonth:
items = [StoreItem.unit, StoreItem.monthlyUnlimited]
case .monthlyUnlimited:
break
default:
items = StoreItem.allCases
}
return items.map { $0.rawValue }
}
func listenForTransactions() -> Task<Void, Error> {
return Task.detached {
//Iterate through any transactions which didn't come from a direct call to `purchase()`.
for await result in Transaction.updates {
do {
let transaction = try await Guard.main.processTransactionResult(result)
//Always finish a transaction.
await transaction.finish()
} catch {
self.delegate?.errorDidOccur(error: error)
//StoreKit has a receipt it can read but it failed verification. Don't deliver content to the user.
print("Transaction failed verification")
}
}
}
}
public func purchase(_ product: Product, quantity: Int? = nil) async throws -> StoreKit.Transaction? {
Logger.log("Store purchase started...")
guard let userId = StoreCenter.main.userId, let uuid: UUID = UUID(uuidString: userId) else {
throw StoreError.missingUserId
}
var options: Set<Product.PurchaseOption> = []
let tokenOption = Product.PurchaseOption.appAccountToken(uuid)
options.insert(tokenOption)
if let quantity = quantity {
let quantityOption = Product.PurchaseOption.quantity(quantity)
options.insert(quantityOption)
}
let result = try await product.purchase(options: options)
Logger.log("Store purchase ended with result: \(result)")
switch result {
case .success(let verificationResult):
let transaction = try await Guard.main.processTransactionResult(verificationResult)
// Always finish a transaction.
await transaction.finish()
DispatchQueue.main.asyncAfter(deadline: DispatchTime(uptimeNanoseconds: 100000), execute: {
NotificationCenter.default.post(name: Notification.Name.StoreEventHappened, object: nil)
})
return transaction
case .userCancelled, .pending:
return nil
default:
return nil
}
}
}

@ -0,0 +1,95 @@
//
// URLs.swift
// PadelClub
//
// Created by Laurent Morvillier on 22/04/2024.
//
import Foundation
public enum URLs: String, Identifiable {
// case httpScheme = "https://"
#if DEBUG
case activationHost = "xlr.alwaysdata.net"
case main = "https://xlr.alwaysdata.net/"
// case api = "https://xlr.alwaysdata.net/roads/"
#elseif TESTFLIGHT
case activationHost = "xlr.alwaysdata.net"
case main = "https://xlr.alwaysdata.net/"
// case api = "https://xlr.alwaysdata.net/roads/"
#elseif PRODTEST
case activationHost = "padelclub.app"
case main = "https://padelclub.app/"
// case api = "https://padelclub.app/roads/"
#else
case activationHost = "padelclub.app"
case main = "https://padelclub.app/"
// case api = "https://padelclub.app/roads/"
#endif
case subscriptions = "https://apple.co/2Th4vqI"
case beachPadel = "https://beach-padel.app.fft.fr/beachja/index/"
//case padelClub = "https://padelclub.app"
case tenup = "https://tenup.fft.fr"
case padelCompetitionGeneralGuide = "https://padelclub.app/static/rules/padel-guide-general.pdf"
case padelCompetitionSpecificGuide = "https://padelclub.app/static/rules/padel-guide-cdc.pdf"
case padelCompetitionRankingGuide = "https://padelclub.app/static/rules/padel-guide-rankings.pdf"
case padelRules = "https://padelclub.app/static/rules/padel-rules.pdf"
case restingDischarge = "https://club.fft.fr/tennisfirmidecazeville/60120370_d/data_1/pdf/fo/formlairededechargederesponsabilitetournoidepadel.pdf"
case appReview = "https://apps.apple.com/app/padel-club/id6484163558?mt=8&action=write-review"
case appDescription = "https://padelclub.app/download/"
case instagram = "https://www.instagram.com/padelclub.app?igsh=bmticnV5YWhpMnBn"
case appStore = "https://apps.apple.com/app/padel-club/id6484163558"
case eula = "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/"
case privacy = "https://padelclub.app/terms-of-use"
public var id: String { return self.rawValue }
public var url: URL {
return URL(string: self.rawValue)!
}
public func extend(path: String) -> URL {
return URL(string: self.rawValue + path)!
}
}
public enum PageLink: String, Identifiable, CaseIterable {
case info = "Informations"
case teams = "Équipes"
case summons = "Convocations"
case groupStages = "Poules"
case matches = "Tournoi"
case rankings = "Classement"
case broadcast = "Mode TV (Tournoi)"
case clubBroadcast = "Mode TV (Club)"
public var id: String { self.rawValue }
public func localizedLabel() -> String {
rawValue
}
var path: String {
switch self {
case .matches:
return ""
case .info:
return "info"
case .teams:
return "teams"
case .summons:
return "summons"
case .rankings:
return "rankings"
case .groupStages:
return "group-stages"
case .broadcast:
return "broadcast"
case .clubBroadcast:
return ""
}
}
}

@ -0,0 +1,12 @@
//
// Key.swift
// PadelClub
//
// Created by Laurent Morvillier on 30/04/2024.
//
import Foundation
enum CryptoKey: String {
case pass = "Aa9QDV1G5MP9ijF2FTFasibNbS/Zun4qXrubIL2P+Ik="
}

@ -0,0 +1,27 @@
//
// MySortDescriptor.swift
// PadelClub
//
// Created by Razmig Sarkissian on 26/03/2024.
//
import Foundation
public struct MySortDescriptor<Value> {
var comparator: (Value, Value) -> ComparisonResult
}
public extension MySortDescriptor {
static func keyPath<T: Comparable>(_ keyPath: KeyPath<Value, T>) -> Self {
Self { rootA, rootB in
let valueA = rootA[keyPath: keyPath]
let valueB = rootB[keyPath: keyPath]
guard valueA != valueB else {
return .orderedSame
}
return valueA < valueB ? .orderedAscending : .orderedDescending
}
}
}

@ -0,0 +1,102 @@
//
// NetworkManager.swift
// PadelClub
//
// Created by Razmig Sarkissian on 01/03/2024.
//
import Foundation
class NetworkManager {
static let shared: NetworkManager = NetworkManager()
func removeRankingData(lastDateString: String, fileName: String) {
let dateString = ["CLASSEMENT-PADEL", fileName, lastDateString].joined(separator: "-") + ".csv"
let documentsUrl:URL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first as URL?)!
let destinationFileUrl = documentsUrl.appendingPathComponent("\(dateString)")
try? FileManager.default.removeItem(at: destinationFileUrl)
}
// func headerDataRankingData(lastDateString: String, fileName: String) async throws {
// let dateString = ["CLASSEMENT-PADEL", fileName, lastDateString].joined(separator: "-") + ".csv"
//
// let documentsUrl: URL = SourceFileManager.shared.rankingSourceDirectory
// let destinationFileUrl = documentsUrl.appendingPathComponent("\(dateString)")
// let fileURL = URL(string: "https://xlr.alwaysdata.net/static/rankings/\(dateString)")
//
// var request = URLRequest(url:fileURL!)
// request.httpMethod = "HEAD"
// request.addValue("attachment;filename=\(dateString)", forHTTPHeaderField:"Content-Disposition")
// request.addValue("text/csv", forHTTPHeaderField: "Content-Type")
// let task = try await URLSession.shared.dataTask(with: request)
// if let urlResponse = task.1 as? HTTPURLResponse {
// print(urlResponse.allHeaderFields)
// }
// }
//
func formatDateForHTTPHeader(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss 'GMT'"
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0) // GMT timezone
return formatter.string(from: date)
}
@discardableResult
func downloadRankingData(lastDateString: String, fileName: String) async throws -> Int? {
let dateString = ["CLASSEMENT-PADEL", fileName, lastDateString].joined(separator: "-") + ".csv"
let documentsUrl: URL = SourceFileManager.shared.rankingSourceDirectory
let destinationFileUrl = documentsUrl.appendingPathComponent("\(dateString)")
let fileURL = URLs.main.extend(path: "static/rankings/\(dateString)")
var request = URLRequest(url:fileURL)
request.addValue("attachment;filename=\(dateString)", forHTTPHeaderField:"Content-Disposition")
if FileManager.default.fileExists(atPath: destinationFileUrl.path()), let modificationDate = destinationFileUrl.creationDate() {
request.addValue(formatDateForHTTPHeader(modificationDate), forHTTPHeaderField: "If-Modified-Since")
}
request.addValue("text/csv", forHTTPHeaderField: "Content-Type")
let task = try await URLSession.shared.download(for: request)
if let urlResponse = task.1 as? HTTPURLResponse {
print(dateString, urlResponse.statusCode)
if urlResponse.statusCode == 200 {
//todo à voir si on en a besoin, permet de re-télécharger un csv si on détecte qu'il a été mis à jour
// if FileManager.default.fileExists(atPath: destinationFileUrl.path()) {
// if let creationDate = try checkFileCreationDate(filePath: task.0.path()), let previousCreationDate = try checkFileCreationDate(filePath: destinationFileUrl.path()) {
// print("File creation date:", creationDate)
// print("File previous creation date:", previousCreationDate)
// if previousCreationDate.isEarlierThan(creationDate) {
// try FileManager.default.removeItem(at: destinationFileUrl)
// }
// }
// }
try? FileManager.default.removeItem(at: destinationFileUrl)
try FileManager.default.copyItem(at: task.0, to: destinationFileUrl)
print("dl rank data ok", lastDateString, fileName)
return destinationFileUrl.fftImportingStatus() ?? 1
} else if urlResponse.statusCode == 404 && fileName == "MESSIEURS" {
print("dl rank data failedm fileNotYetAvailable", lastDateString, fileName)
throw NetworkManagerError.fileNotYetAvailable
} else if urlResponse.statusCode == 304 {
print("dl rank data failed, fileNotModified", lastDateString, fileName)
throw NetworkManagerError.fileNotModified
} else {
print("dl rank data failed, fileNotDownloaded", lastDateString, fileName, urlResponse.statusCode)
throw NetworkManagerError.fileNotDownloaded(urlResponse.statusCode)
}
}
return nil
}
func checkFileCreationDate(filePath: String) throws -> Date? {
let fileManager = FileManager.default
let attributes = try fileManager.attributesOfItem(atPath: filePath)
return attributes[.creationDate] as? Date
}
}

@ -0,0 +1,28 @@
//
// NetworkManagerError.swift
// PadelClub
//
// Created by Razmig Sarkissian on 03/03/2024.
//
import Foundation
enum NetworkManagerError: LocalizedError {
case maintenance
case fileNotYetAvailable
case mailFailed
case mailNotSent //no network no error
case messageFailed
case messageNotSent //no network no error
case fileNotModified
case fileNotDownloaded(Int)
var errorDescription: String? {
switch self {
case .maintenance:
return "Le site de la FFT est en maintenance"
default:
return String(describing: self)
}
}
}

@ -0,0 +1,47 @@
//
// PListReader.swift
// PadelClub
//
// Created by Laurent Morvillier on 06/05/2024.
//
import Foundation
public class PListReader {
static func dictionary(plist: String) -> [String: Any]? {
if let plistPath = Bundle.main.path(forResource: plist, ofType: "plist") {
// Read plist file into Data
if let plistData = FileManager.default.contents(atPath: plistPath) {
do {
// Deserialize plist data into a dictionary
if let plistDictionary = try PropertyListSerialization.propertyList(from: plistData, options: [], format: nil) as? [String: Any] {
return plistDictionary
}
} catch {
print("Error reading plist data: \(error)")
}
} else {
print("Failed to read plist file at path: \(plistPath)")
}
} else {
print("Plist file '\(plist)' not found in bundle")
}
return nil
}
public static func readString(plist: String, key: String) -> String? {
if let dictionary = self.dictionary(plist: plist) {
return dictionary[key] as? String
}
return nil
}
public static func readBool(plist: String, key: String) -> Bool? {
if let dictionary = self.dictionary(plist: plist) {
return dictionary[key] as? Bool
}
return nil
}
}

@ -0,0 +1,66 @@
//
// SetDescriptor.swift
// PadelClub
//
// Created by Razmig Sarkissian on 02/04/2024.
//
import Foundation
public struct SetDescriptor: Identifiable, Equatable {
public let id: UUID = UUID()
public var valueTeamOne: Int?
public var valueTeamTwo: Int?
public var tieBreakValueTeamOne: Int?
public var tieBreakValueTeamTwo: Int?
public var setFormat: SetFormat
public var showSetInputView: Bool = true
public var showTieBreakInputView: Bool = false
public var isTeamOneSet: Bool {
return valueTeamOne != nil || tieBreakValueTeamOne != nil
}
public var hasEnded: Bool {
if let valueTeamTwo, let valueTeamOne {
return setFormat.hasEnded(teamOne: valueTeamOne, teamTwo: valueTeamTwo)
} else {
return false
}
}
public var winner: TeamPosition? {
if let valueTeamTwo, let valueTeamOne, valueTeamOne != valueTeamTwo {
return setFormat.winner(teamOne: valueTeamOne, teamTwo: valueTeamTwo)
} else {
return nil
}
}
public var shouldTieBreak: Bool {
setFormat.shouldTiebreak(scoreTeamOne: valueTeamOne ?? 0, scoreTeamTwo: valueTeamTwo ?? 0)
}
public func getValue(teamPosition: TeamPosition) -> String? {
switch teamPosition {
case .one:
if let valueTeamOne {
if let tieBreakValueTeamOne {
return "\(valueTeamOne)-\(tieBreakValueTeamOne)"
} else {
return "\(valueTeamOne)"
}
}
case .two:
if let valueTeamTwo {
if let tieBreakValueTeamTwo {
return "\(valueTeamTwo)-\(tieBreakValueTeamTwo)"
} else {
return "\(valueTeamTwo)"
}
}
}
return nil
}
}

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

Loading…
Cancel
Save