inscription list management

multistore
Razmig Sarkissian 2 years ago
parent 230181fe31
commit c31062fc70
  1. 64
      PadelClub.xcodeproj/project.pbxproj
  2. 4
      PadelClub/Data/Coredata/PadelClubApp.xcdatamodeld/Model.xcdatamodel/contents
  3. 125
      PadelClub/Data/Federal/FederalPlayer.swift
  4. 4
      PadelClub/Data/GroupStage.swift
  5. 93
      PadelClub/Data/Match.swift
  6. 12
      PadelClub/Data/MockData.swift
  7. 193
      PadelClub/Data/PlayerRegistration.swift
  8. 83
      PadelClub/Data/TeamRegistration.swift
  9. 80
      PadelClub/Data/Tournament.swift
  10. 6
      PadelClub/Extensions/Sequence+Extensions.swift
  11. 80
      PadelClub/Extensions/String+Extensions.swift
  12. 19
      PadelClub/Manager/PadelRule.swift
  13. 6
      PadelClub/ViewModel/SearchViewModel.swift
  14. 2
      PadelClub/Views/Club/ClubsView.swift
  15. 6
      PadelClub/Views/Components/Labels.swift
  16. 19
      PadelClub/Views/GroupStage/GroupStagesView.swift
  17. 110
      PadelClub/Views/Match/MatchDateView.swift
  18. 528
      PadelClub/Views/Match/MatchDetailView.swift
  19. 12
      PadelClub/Views/Match/MatchRowView.swift
  20. 35
      PadelClub/Views/Match/MatchSetupView.swift
  21. 247
      PadelClub/Views/Match/MatchSummaryView.swift
  22. 87
      PadelClub/Views/Match/PlayerBlockView.swift
  23. 11
      PadelClub/Views/Navigation/MainView.swift
  24. 229
      PadelClub/Views/Player/Components/PlayerPopoverView.swift
  25. 48
      PadelClub/Views/Player/Components/PlayerSexPickerView.swift
  26. 28
      PadelClub/Views/Player/PlayerView.swift
  27. 47
      PadelClub/Views/Shared/ImportedPlayerView.swift
  28. 24
      PadelClub/Views/Shared/SelectablePlayerListView.swift
  29. 27
      PadelClub/Views/Team/TeamDetailView.swift
  30. 22
      PadelClub/Views/Team/TeamPickerView.swift
  31. 21
      PadelClub/Views/Team/TeamRowView.swift
  32. 88
      PadelClub/Views/Tournament/Screen/Components/InscriptionTipsView.swift
  33. 350
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  34. 31
      PadelClub/Views/Tournament/TournamentView.swift

@ -32,6 +32,11 @@
C4A47DAD2B85FCCD00ADC637 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47DAC2B85FCCD00ADC637 /* User.swift */; };
C4A47DB12B86375E00ADC637 /* MainUserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47DB02B86375E00ADC637 /* MainUserView.swift */; };
C4A47DB32B86387500ADC637 /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47DB22B86387500ADC637 /* AccountView.swift */; };
FF089EB42BB0020000F0AEC7 /* PlayerSexPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF089EB32BB0020000F0AEC7 /* PlayerSexPickerView.swift */; };
FF089EB62BB00A3800F0AEC7 /* TeamRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF089EB52BB00A3800F0AEC7 /* TeamRowView.swift */; };
FF089EB82BB00ABF00F0AEC7 /* InscriptionTipsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF089EB72BB00ABF00F0AEC7 /* InscriptionTipsView.swift */; };
FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF089EBA2BB0120700F0AEC7 /* PlayerPopoverView.swift */; };
FF089EBD2BB0287D00F0AEC7 /* PlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF089EBC2BB0287D00F0AEC7 /* PlayerView.swift */; };
FF1DC5512BAB351300FD8220 /* ClubDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1DC5502BAB351300FD8220 /* ClubDetailView.swift */; };
FF1DC5532BAB354A00FD8220 /* MockData.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1DC5522BAB354A00FD8220 /* MockData.swift */; };
FF1DC5552BAB36DD00FD8220 /* CreateClubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1DC5542BAB36DD00FD8220 /* CreateClubView.swift */; };
@ -94,6 +99,11 @@
FF967D012BAEF0B400A9A3BD /* MatchSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967D002BAEF0B400A9A3BD /* MatchSummaryView.swift */; };
FF967D032BAEF0C000A9A3BD /* MatchDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967D022BAEF0C000A9A3BD /* MatchDetailView.swift */; };
FF967D042BAEF1C300A9A3BD /* MatchRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967CFF2BAEEF6400A9A3BD /* MatchRowView.swift */; };
FF967D062BAF3C4200A9A3BD /* MatchSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967D052BAF3C4200A9A3BD /* MatchSetupView.swift */; };
FF967D092BAF3D4000A9A3BD /* TeamDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967D082BAF3D4000A9A3BD /* TeamDetailView.swift */; };
FF967D0B2BAF3D4C00A9A3BD /* TeamPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967D0A2BAF3D4C00A9A3BD /* TeamPickerView.swift */; };
FF967D0D2BAF3EB300A9A3BD /* MatchDateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967D0C2BAF3EB200A9A3BD /* MatchDateView.swift */; };
FF967D0F2BAF63B000A9A3BD /* PlayerBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967D0E2BAF63B000A9A3BD /* PlayerBlockView.swift */; };
FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1032BAC28C6008D6F59 /* ClubSearchView.swift */; };
FFC1E1082BAC29FC008D6F59 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */; };
FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */; };
@ -186,6 +196,11 @@
C4A47DAC2B85FCCD00ADC637 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = "<group>"; };
C4A47DB02B86375E00ADC637 /* MainUserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainUserView.swift; sourceTree = "<group>"; };
C4A47DB22B86387500ADC637 /* AccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountView.swift; sourceTree = "<group>"; };
FF089EB32BB0020000F0AEC7 /* PlayerSexPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerSexPickerView.swift; sourceTree = "<group>"; };
FF089EB52BB00A3800F0AEC7 /* TeamRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamRowView.swift; sourceTree = "<group>"; };
FF089EB72BB00ABF00F0AEC7 /* InscriptionTipsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InscriptionTipsView.swift; sourceTree = "<group>"; };
FF089EBA2BB0120700F0AEC7 /* PlayerPopoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerPopoverView.swift; sourceTree = "<group>"; };
FF089EBC2BB0287D00F0AEC7 /* PlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerView.swift; sourceTree = "<group>"; };
FF1DC5502BAB351300FD8220 /* ClubDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubDetailView.swift; sourceTree = "<group>"; };
FF1DC5522BAB354A00FD8220 /* MockData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockData.swift; sourceTree = "<group>"; };
FF1DC5542BAB36DD00FD8220 /* CreateClubView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateClubView.swift; sourceTree = "<group>"; };
@ -246,6 +261,11 @@
FF967CFF2BAEEF6400A9A3BD /* MatchRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchRowView.swift; sourceTree = "<group>"; };
FF967D002BAEF0B400A9A3BD /* MatchSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchSummaryView.swift; sourceTree = "<group>"; };
FF967D022BAEF0C000A9A3BD /* MatchDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchDetailView.swift; sourceTree = "<group>"; };
FF967D052BAF3C4200A9A3BD /* MatchSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchSetupView.swift; sourceTree = "<group>"; };
FF967D082BAF3D4000A9A3BD /* TeamDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamDetailView.swift; sourceTree = "<group>"; };
FF967D0A2BAF3D4C00A9A3BD /* TeamPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamPickerView.swift; sourceTree = "<group>"; };
FF967D0C2BAF3EB200A9A3BD /* MatchDateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchDateView.swift; sourceTree = "<group>"; };
FF967D0E2BAF63B000A9A3BD /* PlayerBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerBlockView.swift; sourceTree = "<group>"; };
FFC1E1032BAC28C6008D6F59 /* ClubSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubSearchView.swift; sourceTree = "<group>"; };
FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = "<group>"; };
FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkFederalService.swift; sourceTree = "<group>"; };
@ -400,6 +420,8 @@
FF1DC54D2BAB34FA00FD8220 /* Club */,
FF967CF92BAEE11500A9A3BD /* GroupStage */,
FF967CFE2BAEEF5A00A9A3BD /* Match */,
FF967D072BAF3D3000A9A3BD /* Team */,
FF089EB92BB011EE00F0AEC7 /* Player */,
FF3F74F72B919F96004CFE0E /* Tournament */,
C4A47D882B7BBB5000ADC637 /* Subscription */,
C4A47D852B7BA33F00ADC637 /* User */,
@ -454,6 +476,24 @@
path = Components;
sourceTree = "<group>";
};
FF089EB02BB001EA00F0AEC7 /* Components */ = {
isa = PBXGroup;
children = (
FF089EB32BB0020000F0AEC7 /* PlayerSexPickerView.swift */,
FF089EBA2BB0120700F0AEC7 /* PlayerPopoverView.swift */,
);
path = Components;
sourceTree = "<group>";
};
FF089EB92BB011EE00F0AEC7 /* Player */ = {
isa = PBXGroup;
children = (
FF089EBC2BB0287D00F0AEC7 /* PlayerView.swift */,
FF089EB02BB001EA00F0AEC7 /* Components */,
);
path = Player;
sourceTree = "<group>";
};
FF1DC54D2BAB34FA00FD8220 /* Club */ = {
isa = PBXGroup;
children = (
@ -604,6 +644,7 @@
FF8F264A2BAE0B4100650388 /* TournamentDatePickerView.swift */,
FF8F26482BAE0B4100650388 /* TournamentFormatSelectionView.swift */,
FF8F26492BAE0B4100650388 /* TournamentLevelPickerView.swift */,
FF089EB72BB00ABF00F0AEC7 /* InscriptionTipsView.swift */,
);
path = Components;
sourceTree = "<group>";
@ -621,12 +662,25 @@
isa = PBXGroup;
children = (
FF967CFF2BAEEF6400A9A3BD /* MatchRowView.swift */,
FF967D052BAF3C4200A9A3BD /* MatchSetupView.swift */,
FF967D002BAEF0B400A9A3BD /* MatchSummaryView.swift */,
FF967D022BAEF0C000A9A3BD /* MatchDetailView.swift */,
FF967D0C2BAF3EB200A9A3BD /* MatchDateView.swift */,
FF967D0E2BAF63B000A9A3BD /* PlayerBlockView.swift */,
);
path = Match;
sourceTree = "<group>";
};
FF967D072BAF3D3000A9A3BD /* Team */ = {
isa = PBXGroup;
children = (
FF967D082BAF3D4000A9A3BD /* TeamDetailView.swift */,
FF967D0A2BAF3D4C00A9A3BD /* TeamPickerView.swift */,
FF089EB52BB00A3800F0AEC7 /* TeamRowView.swift */,
);
path = Team;
sourceTree = "<group>";
};
FFD783FB2B91B919000F62A6 /* Agenda */ = {
isa = PBXGroup;
children = (
@ -836,6 +890,7 @@
C4A47D8A2B7BBB6500ADC637 /* SubscriptionView.swift in Sources */,
FF1DC5572BAB3AED00FD8220 /* ClubsView.swift in Sources */,
FF8F264F2BAE0B9600650388 /* MatchTypeSelectionView.swift in Sources */,
FF967D062BAF3C4200A9A3BD /* MatchSetupView.swift in Sources */,
FF4AB6B52B9248200002987F /* NetworkManager.swift in Sources */,
C4A47DB12B86375E00ADC637 /* MainUserView.swift in Sources */,
FF7091682B90F79F00AB08DA /* TournamentCellView.swift in Sources */,
@ -851,6 +906,7 @@
FF70916C2B91005400AB08DA /* TournamentView.swift in Sources */,
FF1DC5552BAB36DD00FD8220 /* CreateClubView.swift in Sources */,
FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */,
FF089EB62BB00A3800F0AEC7 /* TeamRowView.swift in Sources */,
FF7091622B90F04300AB08DA /* TournamentOrganizerView.swift in Sources */,
FF967CF62BAED51600A9A3BD /* TournamentRunningView.swift in Sources */,
FF8F264D2BAE0B4100650388 /* TournamentDatePickerView.swift in Sources */,
@ -887,9 +943,11 @@
FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */,
FF6EC9092B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift in Sources */,
FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */,
FF089EBB2BB0120700F0AEC7 /* PlayerPopoverView.swift in Sources */,
FF70916E2B9108C600AB08DA /* InscriptionManagerView.swift in Sources */,
FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */,
FF8F26472BAE0ACB00650388 /* TournamentFieldsManagerView.swift in Sources */,
FF967D0B2BAF3D4C00A9A3BD /* TeamPickerView.swift in Sources */,
FF1DC55B2BAB80C400FD8220 /* DisplayContext.swift in Sources */,
C425D4032B6D249D002A7B48 /* ContentView.swift in Sources */,
FF967CFC2BAEE52E00A9A3BD /* GroupStagesView.swift in Sources */,
@ -905,21 +963,27 @@
FFF8ACD22B9238C3008466FA /* FileImportManager.swift in Sources */,
FF967D012BAEF0B400A9A3BD /* MatchSummaryView.swift in Sources */,
FF8F26452BAE0A3400650388 /* TournamentDurationManagerView.swift in Sources */,
FF089EB82BB00ABF00F0AEC7 /* InscriptionTipsView.swift in Sources */,
FF1DC5532BAB354A00FD8220 /* MockData.swift in Sources */,
FF967D092BAF3D4000A9A3BD /* TeamDetailView.swift in Sources */,
FF8F26382BAD523300650388 /* PadelRule.swift in Sources */,
FF967CF42BAECC0B00A9A3BD /* TeamRegistration.swift in Sources */,
FFF8ACDB2B923F48008466FA /* Date+Extensions.swift in Sources */,
FF967CFD2BAEE5F500A9A3BD /* GroupStageView.swift in Sources */,
FF1DC5592BAB767000FD8220 /* Tips.swift in Sources */,
FF59FFB72B90EFBF0061EFF9 /* MainView.swift in Sources */,
FF967D0D2BAF3EB300A9A3BD /* MatchDateView.swift in Sources */,
FFD784022B91C1B4000F62A6 /* WelcomeView.swift in Sources */,
FFF8ACD62B923960008466FA /* URL+Extensions.swift in Sources */,
FF089EBD2BB0287D00F0AEC7 /* PlayerView.swift in Sources */,
FF967D032BAEF0C000A9A3BD /* MatchDetailView.swift in Sources */,
FF967D0F2BAF63B000A9A3BD /* PlayerBlockView.swift in Sources */,
C4A47D922B7BBBEC00ADC637 /* StoreItem.swift in Sources */,
FF4AB6BD2B9256E10002987F /* SelectablePlayerListView.swift in Sources */,
FF8F26512BAE0BAD00650388 /* MatchFormatPickerView.swift in Sources */,
C4A47DA62B83948E00ADC637 /* LoginView.swift in Sources */,
FF967CF82BAEDF0000A9A3BD /* Labels.swift in Sources */,
FF089EB42BB0020000F0AEC7 /* PlayerSexPickerView.swift in Sources */,
FF70916A2B90F95E00AB08DA /* DateBoxView.swift in Sources */,
FFF8ACD42B92392C008466FA /* SourceFileManager.swift in Sources */,
C4A47D912B7BBBEC00ADC637 /* Guard.swift in Sources */,

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22522" systemVersion="23D60" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22757" systemVersion="23E214" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="ImportedPlayer" representedClassName=".ImportedPlayer" syncable="YES" codeGenerationType="class">
<attribute name="assimilation" attributeType="String"/>
<attribute name="canonicalFirstName" optional="YES" attributeType="String" derived="YES" derivationExpression="canonical:(firstName)"/>
<attribute name="canonicalFullName" optional="YES" attributeType="String" derived="YES" derivationExpression="canonical:(lastName)"/>
<attribute name="canonicalFullName" optional="YES" attributeType="String" derived="YES" derivationExpression="canonical:(fullName)"/>
<attribute name="canonicalLastName" optional="YES" attributeType="String" derived="YES" derivationExpression="canonical:(lastName)"/>
<attribute name="clubCode" attributeType="String"/>
<attribute name="clubName" attributeType="String"/>

@ -7,12 +7,106 @@
import Foundation
extension ImportedPlayer {
protocol PlayerHolder {
func getFirstName() -> String
func getLastName() -> String
func formattedRank() -> String
func formattedLicense() -> String
func getPoints() -> Double?
func getRank() -> Int?
func isUnranked() -> Bool
var male: Bool { get }
var tournamentPlayed: Int? { get }
var clubName: String? { get }
var ligueName: String? { get }
var assimilation: String? { get }
}
extension PlayerHolder {
var isAssimilated: Bool {
assimilation == "Oui"
}
}
extension ImportedPlayer: PlayerHolder {
var tournamentPlayed: Int? {
Int(tournamentCount)
}
func getPoints() -> Double? {
self.points
}
func getFirstName() -> String {
self.firstName ?? "prénom inconnu"
}
func getLastName() -> String {
self.lastName ?? "nom inconnu"
}
func formattedLicense() -> String {
if let license { return license.computedLicense }
return "aucune licence"
}
func getRank() -> Int? {
Int(rank)
}
func isUnranked() -> Bool {
false
}
func formattedRank() -> String {
rank.formatted()
}
func isMalePlayer() -> Bool {
male
}
func hitForSearch(_ searchText: String) -> Int {
var trimmedSearchText = searchText.lowercased().trimmingCharacters(in: .whitespaces).folding(options: .diacriticInsensitive, locale: .current)
trimmedSearchText = trimmedSearchText.replaceCharactersFromSet(characterSet: .punctuationCharacters, replacementString: " ")
trimmedSearchText = trimmedSearchText.replaceCharactersFromSet(characterSet: .symbols, replacementString: " ")
if trimmedSearchText.isEmpty { return 0 }
let tokens = trimmedSearchText.components(separatedBy: .whitespacesAndNewlines).filter { $0.isEmpty == false }
if let license, trimmedSearchText.contains(license) {
return 100
}
let label = canonicalFullName!
if tokens.count > 1 {
var wordFound = 0
if trimmedSearchText.lowercased().components(separatedBy: .whitespacesAndNewlines).count > 1 {
let searchFields: Set = Set([firstName!.canonicalVersion.components(separatedBy: .whitespacesAndNewlines), lastName!.canonicalVersion.components(separatedBy: .whitespacesAndNewlines)].flatMap { $0 })
let tokens: Set = Set(trimmedSearchText.components(separatedBy: .whitespacesAndNewlines))
wordFound = searchFields.intersection(tokens).count
}
if wordFound == 2 {
if let first = tokens.pairs().first(where: { a,b in
label.contains(a) && label.contains(b)
}) {
return 2 + first.0.count + first.1.count
}
} else {
return wordFound * 10
}
} else if let first = tokens.first {
if label.contains(first) {
return 1
}
}
return 0
}
}
struct FederalPlayer {
var rank: Int
var lastName: String
@ -91,4 +185,33 @@ struct FederalPlayer {
club = result[10]
}
static func lastRank(mostRecentDateAvailable: Date?, man: Bool) async -> Int? {
let context = PersistenceController.shared.localContainer.newBackgroundContext()
let lastPlayerFetch = ImportedPlayer.fetchRequest()
lastPlayerFetch.sortDescriptors = [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: false)]
var predicate = NSPredicate(format: "male == \(man)")
if let mostRecentDateAvailable {
predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, NSPredicate(format: "importDate == %@", mostRecentDateAvailable as CVarArg)])
}
lastPlayerFetch.predicate = predicate
do {
if let lr = try context.fetch(lastPlayerFetch).first?.rank {
let fetch = ImportedPlayer.fetchRequest()
var rankPredicate = NSPredicate(format: "rank == %i", lr)
if let mostRecentDateAvailable {
rankPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [rankPredicate, NSPredicate(format: "importDate == %@", mostRecentDateAvailable as CVarArg)])
}
fetch.predicate = rankPredicate
let lastPlayersCount = try context.count(for: fetch)
return Int(lr) + Int(lastPlayersCount) - 1
}
} catch {
print("ImportedPlayer.fetchRequest", error)
}
return nil
}
}

@ -36,6 +36,10 @@ class GroupStage: ModelObject, Storable {
self.startDate = startDate
}
func tournamentObject() -> Tournament? {
Store.main.findById(tournament)
}
func title(_ displayStyle: DisplayStyle = .wide) -> String {
switch displayStyle {
case .wide:

@ -43,6 +43,89 @@ class Match: ModelObject, Storable {
self.order = order
}
func title(_ displayStyle: DisplayStyle = .wide) -> String {
switch displayStyle {
case .wide:
return "Match \(index + 1)"
case .short:
return "#\(index + 1)"
}
}
var matchFormat: MatchFormat {
get {
MatchFormat(rawValue: format ?? 0) ?? .defaultFormatForMatchType(.groupStage)
}
set {
format = newValue.rawValue
}
}
func isReady() -> Bool {
teams().count == 2
}
func isEmpty() -> Bool {
teams().isEmpty
}
func hasEnded() -> Bool {
endDate != nil
}
func isGroupStage() -> Bool {
groupStage != nil
}
func isTournamentMatch() -> Bool {
groupStageObject?.tournament != nil
}
func walkoutTeam() -> [TeamRegistration] {
scores().filter({ $0.walkOut != nil }).compactMap { $0.team }
}
func hasWalkoutTeam() -> Bool {
walkoutTeam().isEmpty == false
}
func currentTournament() -> Tournament? {
groupStageObject?.tournamentObject()
}
func scores() -> [TeamScore] {
Store.main.filter(isIncluded: { $0.match == id })
}
func teams() -> [TeamRegistration] {
scores().compactMap({ $0.team }).sorted(by: \.computedPosition)
}
func teamWon(_ team: TeamData) -> Bool {
true
}
func team(_ team: TeamData) -> TeamRegistration? {
switch team {
case .one:
teams().first
case .two:
teams().last
}
}
func teamNames(_ team: TeamData) -> [String]? {
self.team(team)?.players().map { $0.lastName }
}
func teamWalkOut(_ team: TeamData) -> Bool {
false
}
func teamScore(_ team: TeamData) -> TeamScore? {
scores().first(where: { $0.teamRegistration == self.team(team)?.id })
}
func isRunning() -> Bool { // at least a match has started
hasStarted() && hasEnded() == false
}
@ -64,16 +147,14 @@ class Match: ModelObject, Storable {
// }
}
func hasEnded() -> Bool {
endDate != nil
}
var roundObject: Round? {
Store.main.filter { $0.id == self.round }.first
guard let round else { return nil }
return Store.main.findById(round)
}
var groupStageObject: GroupStage? {
Store.main.filter { $0.id == self.groupStage }.first
guard let groupStage else { return nil }
return Store.main.findById(groupStage)
}
var isLoserBracket: Bool {

@ -32,3 +32,15 @@ extension Match {
Match(index: 0, broadcasted: false, order: 0)
}
}
extension TeamRegistration {
static func mock() -> TeamRegistration {
TeamRegistration(tournament: "")
}
}
extension PlayerRegistration {
static func mock() -> PlayerRegistration {
PlayerRegistration(firstName: "Raz", lastName: "Sark", sex: 1)
}
}

@ -13,22 +13,101 @@ class PlayerRegistration: ModelObject, Storable {
static func resourceName() -> String { "player-registrations" }
var id: String = Store.randomId()
var teamRegistration: String
var teamRegistration: String?
var firstName: String
var lastName: String
var licenceId: String?
var rank: Int?
var hasPaid: Bool
var unranked: Bool
var registrationType: Int?
var registrationDate: Date?
var sex: Int
var tournamentPlayed: Int?
var points: Double?
var clubName: String?
var ligueName: String?
var assimilation: String?
// var phoneNumber: String?
// var email: String?
// var birthDate: Date?
// var club: String?
internal init(teamRegistration: String, firstName: String, lastName: String, licenceId: String? = nil, rank: Int? = nil, hasPaid: Bool, unranked: Bool) {
internal init(teamRegistration: String = "", firstName: String, lastName: String, licenceId: String? = nil, rank: Int? = nil, registrationType: Int? = nil, registrationDate: Date? = nil, sex: Int) {
self.teamRegistration = teamRegistration
self.firstName = firstName
self.lastName = lastName
self.licenceId = licenceId
self.rank = rank
self.hasPaid = hasPaid
self.unranked = unranked
self.registrationType = registrationType
self.registrationDate = registrationDate
self.sex = sex
}
internal init(importedPlayer: ImportedPlayer) {
self.teamRegistration = ""
self.firstName = importedPlayer.firstName ?? ""
self.lastName = importedPlayer.lastName ?? ""
self.licenceId = importedPlayer.license ?? nil
self.rank = Int(importedPlayer.rank)
self.sex = importedPlayer.male ? 1 : 0
self.tournamentPlayed = importedPlayer.tournamentPlayed
self.points = importedPlayer.getPoints()
self.clubName = importedPlayer.clubName
self.ligueName = importedPlayer.ligueName
self.assimilation = importedPlayer.assimilation
}
func tournament() -> Tournament? {
guard let tournament = team()?.tournament else { return nil }
return Store.main.findById(tournament)
}
func team() -> TeamRegistration? {
guard let teamRegistration else { return nil }
return Store.main.findById(teamRegistration)
}
func hasPaid() -> Bool {
registrationType != nil
}
var paymentType: PaymentType {
get {
PaymentType(rawValue: registrationType ?? -1) ?? .notPaid
}
set {
registrationType = newValue.rawValue
}
}
func playerLabel(_ displayStyle: DisplayStyle = .wide) -> String {
lastName.trimmed.capitalized + " " + firstName.trimmed.capitalized
}
func rankLabel(_ displayStyle: DisplayStyle = .wide) -> String {
if let rank, rank > 0 {
return rank.ordinalFormatted()
} else {
return "non classé" + (isMalePlayer() ? "" : "e")
}
}
var computedRank: Int {
rank ?? tournament()?.unrankValue(for: isMalePlayer()) ?? Int.max
}
func rank(for tournamentCategory: TournamentCategory, manMax: Int, womanMax: Int) -> Int {
switch tournamentCategory {
case .men:
return isMalePlayer() ? computedRank : computedRank + PlayerRegistration.addon(for: computedRank, manMax: manMax, womanMax: womanMax)
case .women, .mix:
return computedRank
}
}
func isMalePlayer() -> Bool {
sex == 1
}
enum CodingKeys: String, CodingKey {
@ -38,9 +117,105 @@ class PlayerRegistration: ModelObject, Storable {
case _lastName = "lastName"
case _licenceId = "licenceId"
case _rank = "rank"
case _hasPaid = "hasPaid"
case _unranked = "unranked"
case _registrationType = "registrationType"
case _registrationDate = "registrationDate"
case _sex = "sex"
case _tournamentPlayed = "tournamentPlayed"
case _points = "points"
case _clubName = "clubName"
case _ligueName = "ligueName"
case _assimilation = "assimilation"
}
enum PaymentType: Int, CaseIterable, Identifiable {
var id: Self {
self
}
case notPaid = -1
case cash = 0
case lydia = 1
case gift = 2
case check = 3
case paylib = 4
var localizedLabel: String {
switch self {
case .notPaid:
return "Non réglé"
case .check:
return "Chèque"
case .cash:
return "Cash"
case .lydia:
return "Lydia"
case .paylib:
return "Paylib"
case .gift:
return "Offert"
}
}
}
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
case 1...10: return 400
case 11...30: return 1000
case 31...60: return 2000
case 61...100: return 3000
case 101...200: return 8000
case 201...500: return 12000
default:
return 15000
}
}
}
extension PlayerRegistration: Hashable {
static func == (lhs: PlayerRegistration, rhs: PlayerRegistration) -> Bool {
lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
extension PlayerRegistration: PlayerHolder {
func getFirstName() -> String {
firstName
}
func getLastName() -> String {
lastName
}
func getPoints() -> Double? {
self.points
}
func getRank() -> Int? {
rank
}
func isUnranked() -> Bool {
rank == nil
}
func formattedRank() -> String {
self.rankLabel()
}
func formattedLicense() -> String {
if let licenceId { return licenceId.computedLicense }
return "aucune licence"
}
var male: Bool {
isMalePlayer()
}
}

@ -39,16 +39,93 @@ class TeamRegistration: ModelObject, Storable {
self.name = name
}
var computedPosition: Int {
groupStagePosition ?? -1
}
func updatePlayers(_ players: Set<PlayerRegistration>) {
self.players().forEach { player in
if players.contains(player) == false {
try? DataStore.shared.playerRegistrations.delete(instance: player)
}
}
players.forEach { player in
player.teamRegistration = id
try? DataStore.shared.playerRegistrations.addOrUpdate(instance: player)
}
try? DataStore.shared.teamRegistrations.addOrUpdate(instance: self)
}
func qualified() -> Bool {
groupStagePosition != nil && bracketPosition != nil
}
var playerRegistrations: [PlayerRegistration] {
Store.main.filter { $0.teamRegistration == self.id }
typealias AreInIncreasingOrder = (PlayerRegistration, PlayerRegistration) -> Bool
func players() -> [PlayerRegistration] {
Store.main.filter { $0.teamRegistration == self.id }.sorted { (lhs, rhs) in
let predicates: [AreInIncreasingOrder] = [
{ $0.sex < $1.sex },
{ $0.computedRank < $1.computedRank },
{ $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 computedRank() -> Int {
(players().prefix(significantPlayerCount()).map { $0.computedRank } + missing().map { unrankValue(for: $0 == 1 ? true : false ) }).prefix(significantPlayerCount()).reduce(0,+)
}
func significantPlayerCount() -> Int {
tournamentObject()?.significantPlayerCount() ?? 2
}
func mandatoryPlayerType() -> [Int] {
guard let tournamentCategory = tournamentObject()?.tournamentCategory else { return [] }
switch tournamentCategory {
case .mix:
return [0, 1]
case .women:
return [0, 0]
case .men:
return [1, 1]
}
}
func missing() -> [Int] {
let s = players().map { $0.sex }
var missing = mandatoryPlayerType()
s.forEach { i in
if let index = missing.firstIndex(of: i) {
missing.remove(at: index)
}
}
return missing
}
func unrankValue(for malePlayer: Bool) -> Int {
tournamentObject()?.unrankValue(for: malePlayer) ?? Int.max
}
func tournamentObject() -> Tournament? {
Store.main.findById(tournament)
}
override func deleteDependencies() throws {
try Store.main.deleteDependencies(items: self.playerRegistrations)
try Store.main.deleteDependencies(items: self.players())
}
enum CodingKeys: String, CodingKey {

@ -41,14 +41,16 @@ class Tournament : ModelObject, Storable {
var qualifiedPerGroupStage: Int
var teamsPerGroupStage: Int
var entryFee: Double?
var maleUnrankedValue: Int?
var femaleUnrankedValue: Int?
@ObservationIgnored
var navigationPath: [Screen] = []
@ObservationIgnored
var undoManager: Int = 0
internal init(event: String? = nil, creator: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = true, groupStageFormat: Int? = nil, roundFormat: Int? = nil, loserRoundFormat: Int? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, groupStageCourtCount: Int? = nil, seedCount: Int = 8, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil) {
internal init(event: String? = nil, creator: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = true, groupStageFormat: Int? = nil, roundFormat: Int? = nil, loserRoundFormat: Int? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, groupStageCourtCount: Int? = nil, seedCount: Int = 8, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, maleUnrankedValue: Int? = nil, femaleUnrankedValue: Int? = nil) {
self.event = event
self.creator = creator
self.name = name
@ -77,6 +79,8 @@ class Tournament : ModelObject, Storable {
self.qualifiedPerGroupStage = qualifiedPerGroupStage
self.teamsPerGroupStage = teamsPerGroupStage
self.entryFee = entryFee
self.maleUnrankedValue = maleUnrankedValue
self.femaleUnrankedValue = femaleUnrankedValue
}
enum State {
@ -85,24 +89,57 @@ class Tournament : ModelObject, Storable {
}
func state() -> Tournament.State {
if groupStageCount > 0 && groupStages.isEmpty == false {
if groupStageCount > 0 && groupStages().isEmpty == false {
return .build
}
return .initial
}
var groupStages: [GroupStage] {
func groupStages() -> [GroupStage] {
Store.main.filter { $0.tournament == self.id }.sorted(by: \.index)
}
var teamRegistrations: [TeamRegistration] {
Store.main.filter { $0.tournament == self.id }
func teams() -> [TeamRegistration] {
Store.main.filter { $0.tournament == self.id }.sorted {
if $0.computedRank() == $1.computedRank() {
return $0.registrationDate ?? .distantPast < $1.registrationDate ?? .distantPast
} else {
return $0.computedRank() < $1.computedRank()
}
}
}
func players() -> [PlayerRegistration] {
teams().flatMap { $0.players() }
}
func femalePlayers() -> [PlayerRegistration] {
players().filter({ $0.isMalePlayer() == false })
}
func unrankValue(for malePlayer: Bool) -> Int? {
switch tournamentCategory {
case .men:
return maleUnrankedValue
case .women:
return femaleUnrankedValue
case .mix:
return malePlayer ? maleUnrankedValue : femaleUnrankedValue
}
}
var rounds: Int {
4
}
func significantPlayerCount() -> Int {
2
}
func missingUnrankedValue() -> Bool {
maleUnrankedValue == nil || femaleUnrankedValue == nil
}
func title(_ displayStyle: DisplayStyle = .wide) -> String {
[tournamentLevel.localizedLabel(displayStyle), tournamentCategory.localizedLabel(displayStyle), name].compactMap({ $0 }).joined(separator: " ")
}
@ -126,7 +163,7 @@ class Tournament : ModelObject, Storable {
func qualifiedTeams() -> [TeamRegistration] {
teamRegistrations.filter({ $0.qualified() })
teams().filter({ $0.qualified() })
}
func moreQualifiedToDraw() -> Int {
@ -135,7 +172,7 @@ class Tournament : ModelObject, Storable {
func missingQualifiedFromGroupStages() -> [TeamRegistration] {
if groupStageAdditionalQualified > 0 {
return groupStages.filter { $0.hasEnded() }.compactMap { groupStage in
return groupStages().filter { $0.hasEnded() }.compactMap { groupStage in
groupStage.teams[qualifiedPerGroupStage]
}
.filter({ $0.qualified() == false })
@ -145,7 +182,7 @@ class Tournament : ModelObject, Storable {
}
func groupStagesAreOver() -> Bool {
guard groupStages.isEmpty == false else {
guard groupStages().isEmpty == false else {
return true
}
return qualifiedTeams().count == qualifiedFromGroupStage() + groupStageAdditionalQualified
@ -153,7 +190,7 @@ class Tournament : ModelObject, Storable {
func groupStageStatus() -> String {
let runningGroupStages = groupStages.filter({ $0.isRunning() })
let runningGroupStages = groupStages().filter({ $0.isRunning() })
if groupStagesAreOver() { return "terminées" }
if runningGroupStages.isEmpty {
@ -161,7 +198,7 @@ class Tournament : ModelObject, Storable {
if ongoingGroupStages.isEmpty == false {
return "Poule" + ongoingGroupStages.count.pluralSuffix + " " + ongoingGroupStages.map { $0.index.formatted() }.joined(separator: ", ") + " en cours"
}
return groupStages.count.formatted() + " poule" + groupStages.count.pluralSuffix
return groupStages().count.formatted() + " poule" + groupStages().count.pluralSuffix
} else {
return "Poule" + runningGroupStages.count.pluralSuffix + " " + runningGroupStages.map { $0.index.formatted() }.joined(separator: ", ") + " en cours"
}
@ -181,7 +218,7 @@ class Tournament : ModelObject, Storable {
}
func buildGroupStages() {
groupStages.forEach { groupStage in
groupStages().forEach { groupStage in
try? DataStore.shared.groupStages.delete(instance: groupStage)
}
@ -193,7 +230,7 @@ class Tournament : ModelObject, Storable {
try? DataStore.shared.groupStages.append(contentOfs: _groupStages)
groupStages.forEach { $0.buildMatches() }
groupStages().forEach { $0.buildMatches() }
refreshBrackets()
}
@ -225,13 +262,13 @@ class Tournament : ModelObject, Storable {
}
func setBrackets(randomize: Bool) {
let numberOfBracketsAsInt = groupStages.count
let numberOfBracketsAsInt = groupStages().count
// let teamsPerBracket = Int(teamsPerBracket)
if groupStageCount != numberOfBracketsAsInt {
buildGroupStages()
return
}
let max = groupStages.map { $0.size }.reduce(0,+)
let max = groupStages().map { $0.size }.reduce(0,+)
// var chunks = orderedEntries.filter { $0.wcFinalTable == false }.suffix(Int(max)).chunked(into: numberOfBracketsAsInt)
// for (index, _) in chunks.enumerated() {
// if randomize {
@ -253,6 +290,15 @@ class Tournament : ModelObject, Storable {
func isFree() -> Bool {
entryFee == nil || entryFee == 0
}
func addTeam(_ players: Set<PlayerRegistration>) {
let team = TeamRegistration(tournament: id, registrationDate: Date())
try? DataStore.shared.teamRegistrations.addOrUpdate(instance: team)
players.forEach { player in
player.teamRegistration = team.id
}
try? DataStore.shared.playerRegistrations.append(contentOfs: players)
}
var teamSortingType: TeamSortingType {
get {
@ -392,6 +438,8 @@ extension Tournament {
case _qualifiedPerGroupStage = "qualifiedPerGroupStage"
case _teamsPerGroupStage = "teamsPerGroupStage"
case _entryFee = "entryFee"
case _maleUnrankedValue = "maleUnrankedValue"
case _femaleUnrankedValue = "femaleUnrankedValue"
}
}
@ -430,5 +478,7 @@ extension Tournament: Hashable {
hasher.combine(qualifiedPerGroupStage)
hasher.combine(teamsPerGroupStage)
hasher.combine(entryFee)
hasher.combine(maleUnrankedValue)
hasher.combine(femaleUnrankedValue)
}
}

@ -14,3 +14,9 @@ extension Sequence {
}
}
}
extension Sequence {
func pairs() -> AnySequence<(Element, Element)> {
AnySequence(zip(self, self.dropFirst()))
}
}

@ -20,3 +20,83 @@ extension String {
trimmed.replaceCharactersFromSet(characterSet: .punctuationCharacters, replacementString: " ").folding(options: .diacriticInsensitive, locale: .current).lowercased()
}
}
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
}
}
extension String {
func licencesFound() -> [String] {
let matches = self.matches(of: /[1-9][0-9]{5,7}/)
return matches.map { String(self[$0.range]) }
}
}

@ -801,11 +801,20 @@ enum TeamData: Int, Hashable, Codable, CaseIterable {
}
func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
switch self {
case .one:
return "#1"
case .two:
return "#2"
var shortName: String {
switch self {
case .one:
return "#1"
case .two:
return "#2"
}
}
switch displayStyle {
case .wide:
return "Équipe " + shortName
case .short:
return shortName
}
}
}

@ -113,11 +113,11 @@ class SearchViewModel: ObservableObject, Identifiable {
}
func words() -> [String] {
searchText.trimmed.components(separatedBy: .whitespaces)
searchText.canonicalVersion.trimmed.components(separatedBy: .whitespaces)
}
func wordsPredicates() -> NSPredicate? {
let words = words()
let words = words().filter({ $0.isEmpty })
switch words.count {
case 2:
let predicates = [
@ -132,7 +132,7 @@ class SearchViewModel: ObservableObject, Identifiable {
func orPredicate() -> NSPredicate? {
var predicates : [NSPredicate] = []
let searchText = searchText.canonicalVersion
switch tokens.first {
case .none:
if searchText.isEmpty == false {

@ -26,7 +26,7 @@ struct ClubsView: View {
Button(role: .destructive) {
try? dataStore.clubs.delete(instance: club)
} label: {
Label("Effacer", systemImage: "trash")
LabelDelete()
}
}
}

@ -24,3 +24,9 @@ struct LabelSettings: View {
Label("Réglages", systemImage: "slider.horizontal.3")
}
}
struct LabelDelete: View {
var body: some View {
Label("Effacer", systemImage: "trash")
}
}

@ -22,7 +22,7 @@ struct GroupStagesView: View {
case -1:
return "Toutes les poules"
default:
return tournament.groupStages[selectedGroupStageIndex].title()
return tournament.groupStages()[selectedGroupStageIndex].title()
}
}
//
@ -157,14 +157,13 @@ struct GroupStagesView: View {
// }
// }
ForEach(tournament.groupStages) { groupStage in
ForEach(tournament.groupStages()) { groupStage in
if displayGroupStage(groupStage) && groupStage.hasEnded() == false {
GroupStageView(groupStage: groupStage)
if groupStage.matches.isEmpty == false {
Section {
ForEach(groupStage.matches) { match in
MatchRowView(setupSeedContext: false, matchViewStyle: .sectionedStandardStyle)
.environment(match)
MatchRowView(match: match, setupSeedContext: false, matchViewStyle: .sectionedStandardStyle)
}
} header: {
Text("Matchs de la " + groupStage.title())
@ -175,10 +174,10 @@ struct GroupStagesView: View {
}
.toolbar {
ToolbarItem(placement: .principal) {
if tournament.groupStages.count < 6 {
if tournament.groupStages().count < 6 {
Picker(selection: $selectedGroupStageIndex) {
Text("Toutes").tag(-1)
ForEach(tournament.groupStages) { groupStage in
ForEach(tournament.groupStages()) { groupStage in
Text(groupStage.title(.short)).tag(groupStage.index)
}
} label: {
@ -186,10 +185,10 @@ struct GroupStagesView: View {
}
.labelsHidden()
.pickerStyle(.segmented)
} else if tournament.groupStages.count < 8 {
} else if tournament.groupStages().count < 8 {
Picker(selection: $selectedGroupStageIndex) {
Image(systemName: "square.stack").tag(-1)
ForEach(tournament.groupStages) { groupStage in
ForEach(tournament.groupStages()) { groupStage in
Text(groupStage.title(.short)).tag(groupStage.index)
}
} label: {
@ -200,11 +199,11 @@ struct GroupStagesView: View {
} else {
Picker(selection: $selectedGroupStageIndex) {
Text("Voir toutes les poules").tag(-1)
ForEach(tournament.groupStages) { groupStage in
ForEach(tournament.groupStages()) { groupStage in
Text(groupStage.title()).tag(groupStage.index)
}
} label: {
Text("\(tournament.groupStages.count.formatted()) poules")
Text("\(tournament.groupStages().count.formatted()) poules")
}
}
}

@ -0,0 +1,110 @@
//
// MatchDateView.swift
// Padel Tournament
//
// Created by Razmig Sarkissian on 25/11/2023.
//
import SwiftUI
struct MatchDateView: View {
var match: Match
var showPrefix: Bool = false
var body: some View {
Menu {
if match.startDate == nil {
Button("Commencer") {
match.startDate = Date()
save()
}
Button("Échauffement") {
match.startDate = Calendar.current.date(byAdding: .minute, value: 5, to: Date())
save()
}
} else {
Button("Recommencer") {
match.startDate = Date()
match.endDate = nil
save()
}
Button("Remise à zéro") {
match.startDate = nil
match.endDate = nil
save()
}
}
} label: {
label
}
.buttonStyle(.plain)
}
@ViewBuilder
var label: some View {
HStack {
VStack(alignment: .trailing) {
if match.hasWalkoutTeam() == false {
if let startDate = match.startDate, match.endDate == nil {
if startDate.timeIntervalSinceNow < 0 {
if showPrefix {
Text("en cours").font(.footnote).foregroundStyle(.secondary)
}
Text(startDate, style: .timer)
.monospacedDigit()
} else if startDate.timeIntervalSinceNow <= 7200 && showPrefix {
if showPrefix {
Text("démarre dans")
.font(.footnote).foregroundStyle(.secondary)
}
Text(startDate, style: .timer)
.monospacedDigit()
} else {
if showPrefix {
Text("le " + startDate.formatted(date: .abbreviated, time: .omitted))
.font(.footnote).foregroundStyle(.secondary)
Text("à " + startDate.formatted(date: .omitted, time: .shortened))
.monospacedDigit()
} else {
Text(startDate.formatted(date: .abbreviated, time: .shortened))
.monospacedDigit()
}
}
}
if let startDate = match.startDate, let endDate = match.endDate {
let duration = Duration(
secondsComponent: Int64(endDate.timeIntervalSince(startDate)),
attosecondsComponent: 0
).formatted(.units(allowed: [.hours, .minutes], width: .narrow))
if showPrefix {
Text("durée").font(.footnote).foregroundStyle(.secondary)
}
Text(duration)
.monospacedDigit()
}
if match.startDate == nil {
Text("démarrage").font(.footnote).foregroundStyle(.secondary)
Text("non défini")
}
}
}
}
}
func save() {
do {
// match.currentTournament?.objectWillChange.send()
// match.objectWillChange.send()
// try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}

@ -8,16 +8,534 @@
import SwiftUI
struct MatchDetailView: View {
@Environment(Match.self) var match: Match
let setupSeedContext: Bool
@Environment(\.dismiss) var dismiss
let matchViewStyle: MatchViewStyle
@State private var showLiveScore: Bool = false
@State private var editScore: Bool = false
@State private var scoreType: ScoreType?
@State private var shareStat: Bool = false
@State private var startDateSetup: MatchDateSetup = .now
@State private var fieldSetup: MatchFieldSetup = .random
@State private var broadcasted: Bool = false
@State private var startDate: Date = Date()
@State private var endDate: Date = Date()
@State private var isEditing: Bool = false
@State private var showDetails: Bool = false
var match: Match
init(match: Match, matchViewStyle: MatchViewStyle = .standardStyle) {
self.match = match
self.matchViewStyle = matchViewStyle
if match.hasStarted() == false && (match.startDate == nil || match.court == nil) {
_isEditing = State(wrappedValue: true)
}
if let startDate = match.startDate {
_startDateSetup = State(wrappedValue: .customDate)
_startDate = State(wrappedValue: startDate)
} else if match.isReady() == false {
_startDateSetup = State(wrappedValue: .customDate)
}
if let endDate = match.endDate {
_endDate = State(wrappedValue: endDate)
}
if let court = match.court {
_fieldSetup = State(wrappedValue: .field(court))
}
}
// @ViewBuilder
// func entrantView(_ entrant: Entrant) -> some View {
// Section {
// ForEach(entrant.orderedPlayers) { player in
// if player.isPlaying(in: match) {
// playerView(player)
// }
// }
// } header: {
// LabeledContent {
// if let tournament = match.currentTournament, let index = tournament.indexOfEntrant(entrant) {
// Text("#\(index + 1)")
// }
// } label: {
// if let title = entrant.brand?.title {
// Text(title)
// }
// }
// } footer: {
// LabeledContent {
// let weight = entrant.orderedPlayers.filter { $0.isPlaying(in: match) }.map { $0.tournamentRank }.reduce(0, +)
// Text(weight.formatted())
// } label: {
// Text("Poids de la paire")
// }
// }
// .headerProminence(.increased)
// }
// @ViewBuilder
// func playerView(_ player: Player) -> some View {
// VStack(alignment: .leading) {
// HStack {
// Text(player.longLabel)
// Text(player.localizedAge)
// Spacer()
// Text(player.formattedRank)
// }
//
// if let computedClubName = player.computedClubName {
// Text(computedClubName).foregroundStyle(.secondary).font(.caption)
// }
// if let computedLicense = player.computedLicense {
// Text(computedLicense).foregroundStyle(.secondary).font(.caption)
// }
// }
// }
var quickLookHeader: some View {
Section {
HStack {
if match.hasEnded() == false {
Menu {
Button("Non défini") {
match.court = nil
save()
}
// ForEach(1...match.numberOfField, id: \.self) { courtIndex in
// Button("Terrain #\(courtIndex.formatted())") {
// match.fieldIndex = Int64(courtIndex)
// save()
// }
// }
} label: {
VStack(alignment: .leading) {
Text("terrain").font(.footnote).foregroundStyle(.secondary)
if let court = match.court {
Text("#" + court)
} else {
Text("Choisir")
}
}
}
.buttonStyle(.plain)
}
Spacer()
MatchDateView(match: match, showPrefix: true)
}
.font(.title)
} footer: {
// if match.hasWalkoutTeam() == false {
// if let weatherData = match.weatherData {
// HStack {
// WeatherView(weatherData: weatherData)
// }
// }
// }
}
}
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
List {
if match.hasWalkoutTeam() == false {
if match.hasStarted() {
quickLookHeader
} else {
startingOptionView
}
}
Section {
MatchSummaryView(match: match, matchViewStyle: .plainStyle)
} header: {
} footer: {
HStack {
if match.isTournamentMatch() {
if match.isEmpty() == false {
Button {
showDetails = true
} label: {
Text("Détails des joueurs")
}
}
}
Spacer()
if match.isEmpty() == false && match.isTournamentMatch() {
Menu {
//MenuWarnView(warningSender: match)
} label: {
Text("Prévenir")
}
.buttonStyle(.borderless)
}
}
}
Section {
ForEach(match.teams()) { team in
ForEach(team.players().filter({ $0.hasPaid() == false })) { player in
HStack {
Text(player.playerLabel())
Spacer()
//PlayerPayView(player: player)
}
}
}
}
menuView
}
// .sheet(isPresented: $showDetails) {
// NavigationStack {
// List {
// if let entrantOne = match.entrantOne() {
// entrantView(entrantOne)
// }
// if let entrantTwo = match.entrantTwo() {
// entrantView(entrantTwo)
// }
// }
// }
// .presentationDetents([.fraction(0.66)])
// }
// .sheet(item: $scoreType, onDismiss: {
// if match.hasEnded() && match.isTournamentMatch() {
// dismiss()
// }
// }) { scoreType in
// switch scoreType {
// case .edition:
// let matchDescriptor = MatchDescriptor(match: match)
// EditScoreView(matchDescriptor: matchDescriptor)
// case .live:
// if let score = match.score {
// if score.sets.isEmpty {
// SplashView(score: score)
// } else {
// NewLiveScoringView(score: score)
// }
// }
// case .prepare:
// if match.freeMatchTeams.isEmpty == false {
// EditFreeMatchView(match: match)
// } else {
// PrepareMatchView(match: match)
// }
// case .stat:
// if let score = match.score {
// MatchStatView()
// .environmentObject(score)
// }
// case .health:
// HealthKitView(match: match)
// .presentationDetents([.medium])
// case .feeling:
// if let feedbackData = match.feedbackData {
// FeedbackView(feedbackData: feedbackData)
// }
// }
//
// }
// .refreshable {
// if match.isBroadcasted() {
// match.refreshBroadcast()
// }
// }
// .toolbar {
// ToolbarItem(placement: .topBarTrailing) {
// Menu {
// Button {
// scoreType = .live
// } label: {
// Label("Saisie Live", systemImage: "airplayaudio.circle")
// }
//
// Button {
// scoreType = .prepare
// } label: {
// Label("Préparer", systemImage: "calendar")
// }
//
// Divider()
// Menu {
// if match.fieldIndex > 0 {
// Button(role: .destructive) {
// match.currentTournament?.removeField(match.fieldIndex)
// match.fieldIndex = 0
// match.refreshBroadcast()
// save()
// } label: {
// Label("Supprimer le terrain", systemImage: "figure.run")
// }
// }
// Button(role: .destructive) {
// match.restartMatch()
// save()
// } label: {
// Label("Supprimer l'horaire", systemImage: "xmark.circle.fill")
// }
//
// Button(role: .destructive) {
// match.resetScore()
// save()
// } label: {
// Label("Supprimer les scores", systemImage: "xmark.circle.fill")
// }
//
// if match.isFederalTournament == false && match.isFriendlyMatch == false {
// Button(role: .destructive) {
// match.resetMatch()
// save()
// } label: {
// Label("Supprimer les équipes et les scores", systemImage: "xmark.circle.fill")
// }
// }
// } label: {
// Text("Éditer")
// }
//
// } label: {
// Label("Options", systemImage: "ellipsis.circle")
// }
// }
// }
.navigationTitle(match.title())
.navigationBarTitleDisplayMode(.large)
}
enum MatchDateSetup: Hashable, Identifiable {
case inMinutes(Int)
case now
case customDate
var id: Int { hashValue }
}
enum MatchFieldSetup: Hashable, Identifiable {
case random
// case firstAvailable
case field(String)
var id: Int { hashValue }
}
enum ScoreType: Int, Identifiable, Hashable {
var id: Int {
self.rawValue
}
case edition = 0
case live = 1
case prepare = 2
case stat = 3
case feeling = 4
case health = 5
}
var entrantLabelOne: String {
return "match.longLabelTeamOne"
}
var entrantLabelTwo: String {
return "match.longLabelTeamTwo"
}
@ViewBuilder
var menuView: some View {
if match.isReady() {
Section {
inputScoreView
}
}
broadcastView
if match.hasStarted() {
Section {
editionView
}
}
shareView
// if let followUpMatch = match.followUpMatch {
// Section {
// MatchRowView(match: followUpMatch)
// } header: {
// Text("à suivre terrain \(match.fieldIndex)")
// }
// }
}
var inputScoreView: some View {
RowButtonView(title: "Saisir les résultats", systemImage: "list.clipboard") {
scoreType = .edition
}
}
var editionView: some View {
DisclosureGroup(isExpanded: $isEditing) {
startingOptionView
} label: {
if match.isTournamentMatch() {
Text("Modifier l'horaire et le terrain")
} else {
Text("Horaires et terrain")
}
}
}
var startingOptionView: some View {
Section {
if match.hasEnded() == false {
Picker(selection: $startDateSetup) {
if match.isReady() {
Text("Dans 5 minutes").tag(MatchDateSetup.inMinutes(5))
Text("Dans 15 minutes").tag(MatchDateSetup.inMinutes(15))
Text("Prochaine rotation").tag(MatchDateSetup.inMinutes(match.matchFormat.estimatedDuration))
Text("Tout de suite").tag(MatchDateSetup.now)
}
Text("À").tag(MatchDateSetup.customDate)
} label: {
Text("Horaire")
}
.onChange(of: startDateSetup, perform: { value in
switch startDateSetup {
case .customDate:
break
case .now:
startDate = Date()
case .inMinutes(let minutes):
startDate = Date().addingTimeInterval(Double(minutes) * 60)
}
})
}
if match.startDate != nil || startDateSetup == .customDate {
DatePicker(selection: $startDate) {
Label("Début", systemImage: "calendar").labelStyle(.titleOnly)
}
.datePickerStyle(.compact)
}
if match.endDate != nil {
DatePicker(selection: $endDate) {
Label("Fin", systemImage: "calendar").labelStyle(.titleOnly)
}
.datePickerStyle(.compact)
}
Picker(selection: $fieldSetup) {
Text("Au hasard").tag(MatchFieldSetup.random)
//Text("Premier disponible").tag(MatchFieldSetup.firstAvailable)
// ForEach(1...match.numberOfField, id: \.self) { courtIndex in
// let fieldIndex = Int64(courtIndex)
// let fieldIsAvailable : Bool = match.currentTournament?.fieldIsAvailable(fieldIndex) ?? true
// Label("Terrain #\(courtIndex)", systemImage: match.isFieldPreferred(fieldIndex) ? "heart" : "").tag(MatchFieldSetup.field(courtIndex))
// }
} label: {
Text("Choix du terrain")
}
.contextMenu {
NavigationLink {
//FieldDrawView(match: match)
} label: {
Text("Tirage au sort visuel")
}
}
// if match.canBroadcast() == true {
// Picker(selection: $broadcasted) {
// Text("Oui").tag(true)
// Text("Non").tag(false)
// } label: {
// Text("Diffuser automatiquement")
// }
// }
RowButtonView(title: "Valider") {
if match.hasEnded() == false {
match.startDate = startDate
if match.isTournamentMatch() {
// switch fieldSetup {
// case .random:
// let field = match.freeFields.randomElement() ?? match.currentTournament?.freeFields.randomElement() ?? 1
// match.setupFieldAndStartDateIfPossible(field)
// case .field(let courtIndex):
// let fieldIndex = Int64(courtIndex)
// match.setupFieldAndStartDateIfPossible(fieldIndex)
// }
}
} else {
match.startDate = startDate
if match.endDate != nil {
match.endDate = endDate
}
}
if broadcasted {
broadcastAndSave()
} else {
save()
}
isEditing.toggle()
if match.hasStarted() == false {
dismiss()
}
}
}
}
@ViewBuilder
var broadcastView: some View {
Section {
// if match.isBroadcasted() {
// RowButtonView(title: "Arrêter de diffuser") {
// match.stopBroadcast()
// save()
// }
// } else if match.canBroadcast() == true {
// RowButtonView(title: "Diffuser", systemImage: "airplayvideo") {
// broadcastAndSave()
// }
// }
}
}
var shareView: some View {
NavigationLink {
//EditSharingView(match: match)
} label: {
Text("Partage sur les réseaux sociaux")
}
}
private func save() {
}
private func broadcastAndSave() {
Task {
//try? await match.broadcast()
await MainActor.run {
}
}
}
}
#Preview {
MatchDetailView(setupSeedContext: false, matchViewStyle: .standardStyle)
.environment(Match.mock())
MatchDetailView(match: Match.mock(), matchViewStyle: .standardStyle)
}

@ -8,20 +8,19 @@
import SwiftUI
struct MatchRowView: View {
@Environment(Match.self) var match: Match
var match: Match
let setupSeedContext: Bool
let matchViewStyle: MatchViewStyle
@ViewBuilder
var body: some View {
if setupSeedContext {
MatchSummaryView(setupSeedContext: setupSeedContext, matchViewStyle: matchViewStyle)
MatchSetupView(match: match)
} else {
NavigationLink {
MatchDetailView(setupSeedContext: setupSeedContext, matchViewStyle: matchViewStyle)
.environment(match)
MatchDetailView(match: match, matchViewStyle: matchViewStyle)
} label: {
MatchSummaryView(setupSeedContext: setupSeedContext, matchViewStyle: matchViewStyle)
MatchSummaryView(match: match, matchViewStyle: matchViewStyle)
}
//.modifier(BroadcastViewModifier(isBroadcasted: match.isBroadcasted()))
}
@ -30,6 +29,5 @@ struct MatchRowView: View {
#Preview {
MatchRowView(setupSeedContext: false, matchViewStyle: .standardStyle)
.environment(Match.mock())
MatchRowView(match: Match.mock(), setupSeedContext: false, matchViewStyle: .standardStyle)
}

@ -0,0 +1,35 @@
//
// MatchSetupView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 23/03/2024.
//
import SwiftUI
struct MatchSetupView: View {
var match: Match
var body: some View {
HStack {
VStack(alignment: .leading) {
_teamView(match.team(.one), index: 0)
_teamView(match.team(.two), index: 1)
}
}
}
@ViewBuilder
func _teamView(_ team: TeamRegistration?, index: Int) -> some View {
if let team {
TeamDetailView(team: team)
} else {
TeamPickerView(match: match, index: match.index*2 + 1 + index)
.disabled(match.groupStage != nil)
}
}
}
#Preview {
MatchSetupView(match: Match.mock())
}

@ -8,17 +8,254 @@
import SwiftUI
struct MatchSummaryView: View {
@Environment(Match.self) var match: Match
let setupSeedContext: Bool
var match: Match
let matchViewStyle: MatchViewStyle
var entrantLabelOne: String {
"match.longLabelTeamOne"
}
var entrantLabelTwo: String {
"match.longLabelTeamTwo"
}
var color: Color {
matchViewStyle == .plainStyle ? Color(uiColor: .tertiaryLabel) : Color(uiColor: .secondaryLabel)
}
var width: CGFloat {
matchViewStyle == .plainStyle ? 1 : 2
}
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
matchSummaryView
// .contextMenu {
// ForEach(match.teamScores) { entrant in
// if let team = entrant.team, team.orderedPlayers.count > 2 {
// NavigationLink {
// PlayerPickerView(match: match, team: team)
// } label: {
// if let teamTitle = team.entrant?.brand?.title {
// Text(teamTitle).foregroundStyle(.secondary)
// } else {
// let index = match.orderedEntrants.firstIndex(where: { $0 == entrant }) ?? 0
// Text("Équipe \(index + 1)")
// }
// if match.players(from: team).isEmpty {
// Text("Choisir la paire")
// } else {
// Text("Modifier la paire")
// }
// }
// }
// }
// }
}
@ViewBuilder
var matchSummaryView: some View {
VStack(alignment: .leading) {
if matchViewStyle != .plainStyle {
HStack {
if match.isGroupStage() && matchViewStyle != .feedStyle {
if let groupStage = match.groupStageObject, matchViewStyle == .standardStyle {
Text(groupStage.title())
}
// if let index = match.entrantOne()?.bracketPositions?.first, let index2 = match.entrantTwo()?.bracketPositions?.first {
// Text("#\(index) contre #\(index2)")
// }
} else if let currentTournament = match.currentTournament() {
if matchViewStyle == .feedStyle {
//tournamentHeaderView(currentTournament)
} else if matchViewStyle != .sectionedStandardStyle {
Text(match.title(.short))
}
}
if matchViewStyle == .standardStyle || matchViewStyle == .sectionedStandardStyle
{
Spacer()
if let court = match.court, match.hasEnded() == false {
Spacer()
Text("Terrain \(court)")
}
}
}
.lineLimit(1)
}
if matchViewStyle != .feedStyle {
HStack(spacing: 0) {
VStack(alignment: .leading, spacing: matchViewStyle == .plainStyle ? 8 : 0) {
PlayerBlockView(match: match, team: .one, color: color, width: width)
.padding(matchViewStyle == .plainStyle ? 0 : 8)
if width == 1 {
Divider()
} else {
Divider().frame(height: width).overlay(color)
}
PlayerBlockView(match: match, team: .two, color: color, width: width)
.padding(matchViewStyle == .plainStyle ? 0 : 8)
}
}
.overlay {
if matchViewStyle != .plainStyle {
RoundedRectangle(cornerRadius: 8)
.stroke(color, lineWidth: 2)
}
}
if matchViewStyle != .plainStyle {
HStack {
Spacer()
MatchDateView(match: match, showPrefix: matchViewStyle == .tournamentResultStyle ? true : false)
}
}
}
}
.padding(.vertical, matchViewStyle != .plainStyle ? 8 : 0)
.monospacedDigit()
}
@ViewBuilder
func tournamentHeaderView(_ currentTournament: Tournament) -> some View {
VStack(alignment: .leading) {
// HStack {
// ZStack(alignment: .leading) {
// Text("Poule 9").font(.title3).bold().opacity(0)
// Text(currentTournament.tournamentLevel.localizedLabel)
// }
// if let tournamentTitle = currentTournament.title, tournamentTitle.isEmpty == false {
// Text(tournamentTitle)
// }
//
// Spacer()
// if let startDate = match.startDate {
// if let endDate = match.endDate {
// let duration = Duration(
// secondsComponent: Int64(endDate.timeIntervalSince(startDate)),
// attosecondsComponent: 0
// ).formatted(.units(allowed: [.hours, .minutes], width: .narrow))
// Text("durée")
// } else if startDate.timeIntervalSinceNow < 0 {
// Text("en cours")
// } else {
// Text("prévu à")
// }
// } else {
// if let endDate = match.endDate {
// Text("a fini à")
// }
// }
// }
//
// HStack {
// ZStack(alignment: .leading) {
// Text("Poule 9").opacity(0)
// Text(match.shortTitleLabel)
// }
// if let score = match.score, match.hasEnded() {
// Text(score.label)
// } else if match.fieldIndex > 0 {
// Text("terrain #\(match.fieldIndex)")
// }
//
// Spacer()
// if let startDate = match.startDate {
// if let endDate = match.endDate {
// let duration = Duration(
// secondsComponent: Int64(endDate.timeIntervalSince(startDate)),
// attosecondsComponent: 0
// ).formatted(.units(allowed: [.hours, .minutes], width: .narrow))
// Text(duration)
// } else if startDate.timeIntervalSinceNow < 0 {
// Text(startDate, style: .timer)
// } else {
// Text(startDate.formatted(date: .omitted, time: .shortened))
// }
// } else {
// if let endDate = match.endDate {
// Text(endDate.formatted(.dateTime.hour().minute()))
// }
// }
// }
// .font(.title3)
//
// HStack {
// ZStack(alignment: .leading) {
// Text("Poule 9").font(.title3).bold().opacity(0)
// VStack {
// Text(currentTournament.tournamentCategory.localizedLabel)
// }
// }
//
// if let winnerEntrant = match.winnerEntrant {
// Text(winnerEntrant.longLabelPlayerOne)
// } else if match.isBracketMatch {
// Text(match.subtitleLabel)
// } else if let entrantOne = match.entrantOne()?.team?.firstPairLastNames {
// Text(entrantOne)
// }
//
// Spacer()
// if let startDate = match.startDate {
// if let endDate = match.endDate {
// let duration = Duration(
// secondsComponent: Int64(endDate.timeIntervalSince(startDate)),
// attosecondsComponent: 0
// ).formatted(.units(allowed: [.hours, .minutes], width: .narrow))
// Text(startDate.formatted(.dateTime.hour().minute()))
// } else if startDate.timeIntervalSinceNow < 0 {
// Text(startDate.formatted(.dateTime.hour().minute()))
// } else {
// Text(startDate.formatted(.dateTime.day().month()))
// }
// } else {
// if let endDate = match.endDate {
// Text("début")
// }
// }
// }
// .font(.caption)
//
// HStack {
// ZStack(alignment: .leading) {
// Text("Poule 9").font(.title3).bold().opacity(0)
// VStack {
// Text(currentTournament.tournamentAgeLabel)
// }
// }
//
// if let winnerEntrant = match.winnerEntrant {
// Text(winnerEntrant.longLabelPlayerTwo)
// } else if match.isBracketMatch {
// } else if let entrant = match.entrantTwo()?.team?.firstPairLastNames {
// Text(entrant)
// }
//
// Spacer()
// if let startDate = match.startDate {
// if let endDate = match.endDate {
// let duration = Duration(
// secondsComponent: Int64(endDate.timeIntervalSince(startDate)),
// attosecondsComponent: 0
// ).formatted(.units(allowed: [.hours, .minutes], width: .narrow))
// Text(startDate.formatted(.dateTime.day().month().year()))
// } else if startDate.timeIntervalSinceNow < 0 {
// Text(startDate.formatted(.dateTime.day().month().year()))
// } else {
// Text(startDate.formatted(.dateTime.year()))
// }
// } else {
// if let endDate = match.endDate {
// Text("non défini")
// }
// }
// }
// .font(.caption)
}
}
}
#Preview {
MatchSummaryView(setupSeedContext: false, matchViewStyle: .standardStyle)
.environment(Match.mock())
MatchSummaryView(match: Match.mock(), matchViewStyle: .standardStyle)
}

@ -0,0 +1,87 @@
//
// PlayerBlockView.swift
// Padel Tournament
//
// Created by Razmig Sarkissian on 25/11/2023.
//
import SwiftUI
struct PlayerBlockView: View {
var match: Match
let team: TeamData
let color: Color
let width: CGFloat
var names: [String]? {
match.teamNames(team)
}
var hasWon: Bool {
match.teamWon(team)
}
var hideScore: Bool {
match.hasWalkoutTeam()
}
var isWalkOut: Bool {
match.teamWalkOut(team)
}
var scores: [String] {
match.teamScore(team)?.score?.components(separatedBy: ",") ?? []
}
private func _defaultLabel() -> String {
team.localizedLabel()
}
var body: some View {
HStack {
VStack(alignment: .leading) {
if let names {
ForEach(names, id: \.self) { name in
Text(name).lineLimit(1)
}
} else {
ZStack(alignment: .leading) {
VStack {
Text("longLabelPlayerOne").lineLimit(1)
Text("longLabelPlayerTwo").lineLimit(1)
}
.opacity(0)
Text(_defaultLabel()).foregroundStyle(.secondary).lineLimit(1)
}
}
}
.bold(hasWon)
Spacer()
if hasWon {
Image(systemName: "trophy")
} else if isWalkOut {
Text("WO")
}
if hideScore == false {
ForEach(scores.indices, id: \.self) { index in
let string = scores[index]
if string.isEmpty == false {
if width == 1 {
Divider()
} else {
Divider().frame(width: width).overlay(color)
}
Text(string)
.font(.title3)
.frame(maxWidth: 20)
.scaledToFill()
.minimumScaleFactor(0.5)
.lineLimit(1)
}
}
}
}
}
}

@ -15,6 +15,9 @@ struct MainView: View {
@State private var checkingFiles: Bool = false
@AppStorage("lastDataSource") var lastDataSource: String?
@AppStorage("lastDataSourceMaleUnranked") var lastDataSourceMaleUnranked: Int?
@AppStorage("lastDataSourceFemaleUnranked") var lastDataSourceFemaleUnranked: Int?
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
@ -123,10 +126,18 @@ struct MainView: View {
importingFiles = true
Task {
lastDataSource = await FileImportManager.shared.importDataFromFFT()
if let lastDataSource, let mostRecentDate = URL.importDateFormatter.date(from: lastDataSource) {
await _calculateCurrentUnrankedValues(mostRecentDateAvailable: mostRecentDate)
}
importingFiles = false
}
}
private func _calculateCurrentUnrankedValues(mostRecentDateAvailable: Date) async {
lastDataSourceMaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: mostRecentDateAvailable, man: true)
lastDataSourceFemaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: mostRecentDateAvailable, man: false)
}
private func fetchData() async {
if let mostRecent = SourceFile.mostRecentDateAvailable, let current = Calendar.current.date(byAdding: .month, value: 1, to: mostRecent), current > mostRecent {
await fetchData(fromDate: current)

@ -0,0 +1,229 @@
//
// PlayerPopoverView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 24/03/2024.
//
import SwiftUI
struct PlayerPopoverView: View {
enum PlayerCreationField {
case firstName, lastName, license, rank
}
@Environment(\.dismiss) var dismiss
@State private var displayWrongLicenceError: Bool = false
@State private var firstName: String = ""
@State private var lastName: String = ""
@State private var license: String = ""
@State private var rank: Int?
@State var sex: Int = 1
let requiredField: [PlayerCreationField]
let creationCompletionHandler: ((PlayerRegistration) -> Void)
@FocusState private var firstNameIsFocused: Bool
@FocusState private var lastNameIsFocused: Bool
@FocusState private var licenseIsFocused: Bool
@FocusState private var amountIsFocused: Bool
static var source: String?
init(sex: Int, requiredField: [PlayerCreationField] = [.firstName, .lastName], creationCompletionHandler: @escaping (PlayerRegistration) -> Void) {
let source = PlayerPopoverView.source
if let source {
let words = source.components(separatedBy: .whitespaces)
if words.isEmpty == false {
_firstName = State(wrappedValue: words.first?.capitalized ?? "")
if words.count > 1 {
_lastName = State(wrappedValue: words.last?.capitalized ?? "")
}
} else {
_firstName = State(wrappedValue: source)
}
}
_sex = State(wrappedValue: sex)
_rank = State(wrappedValue: nil)
self.requiredField = requiredField
self.creationCompletionHandler = creationCompletionHandler
self.pasteBoard = UIPasteboard.general.string
}
@State private var pasteBoard: String?
var body: some View {
NavigationStack {
List {
if let pasteBoard {
Section {
Text(pasteBoard).foregroundColor(.clear).padding(8)
.frame(maxWidth: .infinity)
.overlay(
TextEditor(text: .constant(pasteBoard))
)
.frame(minHeight: 20.0)
} header: {
HStack {
Spacer()
Button {
self.pasteBoard = ""
} label: {
Text("effacer le contenu du presse-papier")
}
.buttonStyle(.borderless)
}
}
.textCase(nil)
}
Section {
Picker(selection: $sex) {
Text("Homme").tag(1 as Int)
Text("Femme").tag(0 as Int)
} label: {
}
.labelsHidden()
.pickerStyle(.segmented)
HStack {
Text("Prénom").foregroundStyle(.secondary)
Spacer()
TextField("Prénom", text: $firstName)
.submitLabel(.next)
.focused($firstNameIsFocused)
.onSubmit {
firstName = firstName.trimmed
lastNameIsFocused = true
}
.fixedSize()
}
HStack {
Text("Nom").foregroundStyle(.secondary)
Spacer()
TextField("Nom", text: $lastName)
.focused($lastNameIsFocused)
.submitLabel(.next)
.onSubmit {
lastName = lastName.trimmed
licenseIsFocused = true
}
.fixedSize()
}
HStack {
Text("Licence").foregroundStyle(.secondary)
Spacer()
TextField("Licence", text: $license)
.focused($licenseIsFocused)
.keyboardType(.namePhonePad)
.submitLabel(.next)
.onSubmit {
license = license.trimmed
if requiredField.contains(.license) {
if license.isLicenseNumber {
amountIsFocused = true
} else {
displayWrongLicenceError = true
}
} else {
amountIsFocused = true
}
}
.fixedSize()
}
HStack {
Text("Rang").foregroundStyle(.secondary)
Spacer()
TextField("Non classé", value: $rank, format: .number)
.focused($amountIsFocused)
.keyboardType(.asciiCapable)
.submitLabel(.done)
.fixedSize()
}
} header: {
HStack {
Spacer()
Button {
let last = firstName
firstName = lastName
lastName = last
} label: {
Text("inverser nom & prénom")
}.buttonStyle(.borderless)
}
.textCase(nil)
}
.multilineTextAlignment(.trailing)
Section {
RowButtonView(title: "Valider et ajouter un autre") {
createManualPlayer()
lastName = ""
firstName = ""
license = ""
rank = nil
firstNameIsFocused = true
}
}
}
.onAppear {
firstNameIsFocused = true
}
.autocorrectionDisabled()
.navigationTitle(sex == 1 ? "Nouveau joueur" : "Nouvelle joueuse")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("Valider") {
createManualPlayer()
dismiss()
}
.clipShape(Capsule())
.buttonStyle(.bordered)
}
ToolbarItem(placement: .cancellationAction) {
Button("Annuler", role : .cancel) {
dismiss()
}
}
}
.alert("Attention", isPresented: $displayWrongLicenceError) {
Button("OK") {
}
} message: {
Text("La licence n'est pas valide")
}
}
}
func createManualPlayer() {
guard (lastName.isEmpty == false && requiredField.contains(.lastName)) || requiredField.contains(.lastName) == false else {
return
}
guard (license.isEmpty == false && license.isLicenseNumber && requiredField.contains(.license)) || requiredField.contains(.license) == false else {
return
}
guard (firstName.isEmpty == false && requiredField.contains(.firstName)) || requiredField.contains(.firstName) == false else {
return
}
let playerRegistration = PlayerRegistration(firstName: firstName, lastName: lastName, licenceId: license.trimmed.isEmpty ? nil : license, rank: rank, sex: sex)
self.creationCompletionHandler(playerRegistration)
}
}
#Preview {
PlayerPopoverView(sex: 1) { player in
}
}

@ -0,0 +1,48 @@
//
// PlayerSexPickerView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 24/03/2024.
//
import SwiftUI
struct PlayerSexPickerView: View {
@Bindable var player: PlayerRegistration
var body: some View {
HStack {
Text(player.playerLabel())
Spacer()
Picker(selection: $player.sex) {
Text("Homme").tag(1 as Int64)
Text("Femme").tag(0 as Int64)
} label: {
}
.pickerStyle(.segmented)
.fixedSize()
.onChange(of: player.sex) {
save()
}
}
}
func save() {
do {
// player.objectWillChange.send()
// player.team?.entrant?.tournament?.objectWillChange.send()
// try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
#Preview {
PlayerSexPickerView(player: PlayerRegistration.mock())
}

@ -0,0 +1,28 @@
//
// PlayerView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 24/03/2024.
//
import SwiftUI
struct PlayerView: View {
@EnvironmentObject var dataStore: DataStore
let player: PlayerRegistration
var body: some View {
ImportedPlayerView(player: player)
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button(role: .destructive) {
try? dataStore.playerRegistrations.delete(instance: player)
} label: {
LabelDelete()
}
}
}
}
#Preview {
PlayerView(player: PlayerRegistration.mock())
}

@ -8,17 +8,14 @@
import SwiftUI
struct ImportedPlayerView: View {
let player: ImportedPlayer
var hideLigue: Bool = false
var hideClub: Bool = false
var hidePoints: Bool = false
let player: PlayerHolder
var index: Int? = nil
var body: some View {
VStack(alignment: .leading) {
HStack {
Text(player.lastName!.capitalized)
Text(player.firstName!.capitalized)
Text(player.getLastName().capitalized)
Text(player.getFirstName().capitalized)
if index == nil {
Text(player.male ? "" : "")
}
@ -35,37 +32,43 @@ struct ImportedPlayerView: View {
}
}
.font(.title3)
.lineLimit(1)
HStack {
HStack(alignment: .top, spacing: 0) {
Text(player.rank.formatted()).italic(player.isAssimilated)
Text(player.formattedRank()).italic(player.isAssimilated)
.font(.title3)
Text(player.rank.ordinalFormattedSuffix()).italic(player.isAssimilated)
if let rank = player.getRank() {
Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated)
.font(.caption)
}
}
if hidePoints == false {
if let pts = player.getPoints(), pts > 0 {
HStack(alignment: .lastTextBaseline, spacing: 0) {
let pts = player.points
if pts > 0 {
Text(pts.formatted()).font(.title3)
Text(" pts").font(.caption)
}
Text(pts.formatted()).font(.title3)
Text(" pts").font(.caption)
}
}
if let tournamentPlayed = player.tournamentPlayed, tournamentPlayed > 0 {
HStack(alignment: .lastTextBaseline, spacing: 0) {
if player.tournamentCount > 0 {
Text(player.tournamentCount.formatted()).font(.title3)
Text(" tournoi" + player.tournamentCount.pluralSuffix).font(.caption)
}
Text(tournamentPlayed.formatted()).font(.title3)
Text(" tournoi" + tournamentPlayed.pluralSuffix).font(.caption)
}
}
}
Text(player.clubName!)
.font(.caption)
Text(player.ligueName!)
Text(player.formattedLicense())
.font(.caption)
if let clubName = player.clubName {
Text(clubName)
.font(.caption)
}
if let ligueName = player.ligueName {
Text(ligueName)
.font(.caption)
}
}
}
}

@ -161,6 +161,30 @@ struct SelectablePlayerListView: View {
Text("Annuler")
}
}
if searchViewModel.selectedPlayers.isEmpty == false {
ToolbarItem(placement: .bottomBar) {
Button {
searchViewModel.filterSelectionEnabled.toggle()
} label: {
if searchViewModel.filterSelectionEnabled {
Image(systemName: "line.3.horizontal.decrease.circle.fill")
} else {
Image(systemName: "line.3.horizontal.decrease.circle")
}
}
}
ToolbarItem(placement: .status) {
Button {
if let playerSelectionAction {
playerSelectionAction(searchViewModel.selectedPlayers)
}
dismiss()
} label: {
Text("Ajouter le" + searchViewModel.selectedPlayers.count.pluralSuffix + " \(searchViewModel.selectedPlayers.count) joueur" + searchViewModel.selectedPlayers.count.pluralSuffix)
}
}
}
}
}
// .modifierWithCondition(searchViewModel.user != nil) { thisView in

@ -0,0 +1,27 @@
//
// TeamDetailView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 23/03/2024.
//
import SwiftUI
struct TeamDetailView: View {
@EnvironmentObject var dataStore: DataStore
var team: TeamRegistration
var body: some View {
if team.players().isEmpty {
Text("Aucun joueur, espace réservé")
} else {
ForEach(team.players()) { player in
PlayerView(player: player)
}
}
}
}
#Preview {
TeamDetailView(team: TeamRegistration.mock())
}

@ -0,0 +1,22 @@
//
// TeamPickerView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 23/03/2024.
//
import SwiftUI
struct TeamPickerView: View {
var match: Match
var index: Int
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
}
}
#Preview {
TeamPickerView(match: Match.mock(), index: 0)
}

@ -0,0 +1,21 @@
//
// TeamRowView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 24/03/2024.
//
import SwiftUI
struct TeamRowView: View {
@EnvironmentObject var dataStore: DataStore
var team: TeamRegistration
var body: some View {
TeamDetailView(team: team)
}
}
#Preview {
TeamRowView(team: TeamRegistration.mock())
}

@ -0,0 +1,88 @@
//
// InscriptionTipsView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 24/03/2024.
//
import SwiftUI
import TipKit
struct InscriptionTipsView: View {
@Environment(Tournament.self) private var tournament: Tournament
var body: some View {
List {
Section {
let fileTip = InscriptionManagerFileInputTip()
TipView(fileTip) { action in
if action.id == "website" {
} else if action.id == "add-team-file" {
}
}
.tipStyle(tint: nil)
}
Section {
let pasteTip = InscriptionManagerPasteInputTip()
TipView(pasteTip) { action in
if let paste = UIPasteboard.general.string {
//self.pasteField = paste
}
}
.tipStyle(tint: nil)
}
Section {
let searchTip = InscriptionManagerSearchInputTip()
TipView(searchTip) { action in
//presentPlayerCreation = true
}
.tipStyle(tint: nil)
}
Section {
let createTip = InscriptionManagerCreateInputTip()
TipView(createTip) { action in
//presentPlayerSelection = true
}
.tipStyle(tint: nil)
}
Section {
ContentUnavailableView("Aucune équipe", systemImage: "person.2.slash", description:
Text("Vous n'avez encore aucune équipe dans votre liste d'attente.")
)
}
// if let mostRecentDate, let currentRankSourceDate, currentRankSourceDate < mostRecentDate, tournament.isOver == false {
//
// if #available(iOS 17.0, *) {
// Section {
// let tip = InscriptionManagerRankUpdateTip()
// TipView(tip) { action in
// self.currentRankSourceDate = mostRecentDate
// }
// .tipStyle(tint: nil)
// }
// }
//
// rankingDateSourcePickerView(showDateInLabel: false)
// } else if tournament.currentRankSourceDate == nil {
// rankingDateSourcePickerView(showDateInLabel: false)
// }
//
}
}
}
#Preview {
InscriptionTipsView()
.environment(Tournament.mock())
}

@ -6,14 +6,356 @@
//
import SwiftUI
import TipKit
struct InscriptionManagerView: View {
let tournament: Tournament
@EnvironmentObject var dataStore: DataStore
var body: some View {
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \ImportedPlayer.rank, ascending: true)],
animation: .default)
private var fetchPlayers: FetchedResults<ImportedPlayer>
var tournament: Tournament
@State private var searchField: String = ""
@State private var presentPlayerSearch: Bool = false
@State private var presentPlayerCreation: Bool = false
@State private var createdPlayers: Set<PlayerRegistration> = Set()
@State private var testCreatedPlayers: Set<String> = Set()
@State private var editedTeam: TeamRegistration?
@State private var pasteString: String?
let slideToDeleteTip = SlideToDeleteTip()
let inscriptionManagerWomanRankTip = InscriptionManagerWomanRankTip()
private func _pastePredicate(pasteField: String, mostRecentDate: Date?) -> NSPredicate? {
let text = pasteField.canonicalVersion
let nameComponents = text.components(separatedBy: .whitespacesAndNewlines).compactMap { $0.isEmpty ? nil : $0 }.filter({ $0 != "de" && $0 != "la" && $0 != "le" && $0.count > 1 })
var andPredicates = [NSPredicate]()
var orPredicates = [NSPredicate]()
//self.wordsCount = nameComponents.count
if _filterOption() == .male {
andPredicates.append(NSPredicate(format: "male == YES"))
} else if _filterOption() == .female {
andPredicates.append(NSPredicate(format: "male == NO"))
}
if let mostRecentDate {
andPredicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg))
}
if nameComponents.count > 1 {
orPredicates = nameComponents.pairs().map {
return NSPredicate(format: "(firstName contains[cd] %@ AND lastName contains[cd] %@) OR (firstName contains[cd] %@ AND lastName contains[cd] %@)", $0, $1, $1, $0) }
} else {
orPredicates = nameComponents.map { NSPredicate(format: "firstName contains[cd] %@ OR lastName contains[cd] %@", $0,$0) }
}
let matches = text.licencesFound()
let licensesPredicates = matches.map { NSPredicate(format: "license contains[cd] %@", $0) }
orPredicates = orPredicates + licensesPredicates
var predicate = NSCompoundPredicate(andPredicateWithSubpredicates: andPredicates)
if orPredicates.isEmpty == false {
predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, NSCompoundPredicate(orPredicateWithSubpredicates: orPredicates)])
}
return predicate
}
private func _currentSelection() -> Set<PlayerRegistration> {
var currentSelection = Set<PlayerRegistration>()
testCreatedPlayers.compactMap { id in
fetchPlayers.first(where: { id == $0.license })
}.forEach { player in
let player = PlayerRegistration(importedPlayer: player)
currentSelection.insert(player)
}
testCreatedPlayers.compactMap { id in
createdPlayers.first(where: { id == $0.id })
}.forEach {
currentSelection.insert($0)
}
return currentSelection
}
private func _createTeam() {
tournament.addTeam(_currentSelection())
createdPlayers.removeAll()
testCreatedPlayers.removeAll()
pasteString = nil
}
private func _updateTeam() {
editedTeam?.updatePlayers(_currentSelection())
createdPlayers.removeAll()
testCreatedPlayers.removeAll()
pasteString = nil
editedTeam = nil
}
private func _buildingTeamView() -> some View {
List(selection: $testCreatedPlayers) {
Section {
ForEach(testCreatedPlayers.sorted(), id: \.self) { id in
if let p = createdPlayers.first(where: { $0.id == id }) {
PlayerView(player: p).tag(p.id)
}
if let p = fetchPlayers.first(where: { $0.license == id }) {
ImportedPlayerView(player: p).tag(p.license!)
}
}
// ForEach(createdPlayers.sorted(by: \.computedRank)) { player in
// PlayerView(player: player).tag(player.id)
// }
}
if editedTeam == nil {
RowButtonView(title: "Ajouter l'équipe") {
_createTeam()
}
} else {
RowButtonView(title: "Modifier l'équipe") {
_updateTeam()
}
}
if let pasteString {
// if pasteString.licencesFound().count == 2 {
// let hits = fetchPlayers.filter { $0.hitForSearch(pasteString) == 100 }
// if hits.count == 2 {
// ForEach(hits) { hit in
//
// createdPlayers
// }
// }
// }
//
Section {
Text(pasteString)
} footer: {
HStack {
Text("contenu du presse-papier")
Spacer()
Button("effacer", role: .destructive) {
self.pasteString = nil
self.createdPlayers.removeAll()
self.testCreatedPlayers.removeAll()
}
.buttonStyle(.borderless)
}
}
Section {
ForEach(fetchPlayers.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) })) { player in
ImportedPlayerView(player: player).tag(player.license!)
}
} header: {
Text(fetchPlayers.count.formatted() + " résultat" + fetchPlayers.count.pluralSuffix)
}
}
}
.onReceive(fetchPlayers.publisher.count()) { _ in // <-- here
if let pasteString, count == 2 {
fetchPlayers.filter { $0.hitForSearch(pasteString) >= hitTarget }.sorted(by: { $0.hitForSearch(pasteString) > $1.hitForSearch(pasteString) }).forEach { player in
testCreatedPlayers.insert(player.license!)
}
}
}
.environment(\.editMode, Binding.constant(EditMode.active))
}
var count: Int {
return fetchPlayers.filter { $0.hitForSearch(pasteString ?? "") >= hitTarget }.count
}
var hitTarget: Int {
if (pasteString?.matches(of: /[1-9][0-9]{5,7}/).count ?? 0) > 1 {
if fetchPlayers.filter({ $0.hitForSearch(pasteString ?? "") == 100 }).count == 2 { return 100 }
} else {
return 2
}
return 1
}
private func _teamRegisteredView() -> some View {
List {
Text("24")
Section {
if tournament.tournamentCategory == .men && tournament.femalePlayers().isEmpty == false {
TipView(inscriptionManagerWomanRankTip)
.tipStyle(tint: nil)
}
} header: {
Text("Informations")
}
Section {
TipView(slideToDeleteTip)
.tipStyle(tint: nil)
}
ForEach(tournament.teams()) { team in
Section {
TeamRowView(team: team)
} header: {
HStack {
Text("Équipe")
Spacer()
Text(team.computedRank().formatted())
}
} footer: {
HStack {
Spacer()
Menu {
Button("Éditer") {
editedTeam = team
team.players().forEach { player in
createdPlayers.insert(player)
testCreatedPlayers.insert(player.id)
}
}
Divider()
Button(role: .destructive) {
try? dataStore.teamRegistrations.delete(instance: team)
} label: {
LabelDelete()
}
} label: {
LabelOptions()
}
}
}
.headerProminence(.increased)
}
}
.searchable(text: $searchField)
}
var body: some View {
VStack(spacing: 0) {
_managementView()
if testCreatedPlayers.isEmpty == false || pasteString != nil || editedTeam != nil {
_buildingTeamView()
} else if tournament.teams().isEmpty {
InscriptionTipsView()
} else {
_teamRegisteredView()
}
}
.sheet(isPresented: $presentPlayerSearch) {
NavigationStack {
SelectablePlayerListView(allowSelection: -1, filterOption: _filterOption()) { players in
players.forEach { player in
let newPlayer = PlayerRegistration(importedPlayer: player)
createdPlayers.insert(newPlayer)
testCreatedPlayers.insert(newPlayer.id)
}
}
}
}
.sheet(isPresented: $presentPlayerCreation) {
PlayerPopoverView(sex: _addPlayerSex()) { p in
createdPlayers.insert(p)
testCreatedPlayers.insert(p.id)
}
}
.navigationTitle("Inscriptions")
.toolbar {
if testCreatedPlayers.isEmpty == false {
ToolbarItem(placement: .cancellationAction) {
Button("Annuler", role: .cancel) {
createdPlayers.removeAll()
testCreatedPlayers.removeAll()
}
}
}
}
.navigationBarBackButtonHidden(testCreatedPlayers.isEmpty == false)
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Inscription")
.navigationBarTitleDisplayMode(.inline)
}
private func _managementView() -> some View {
HStack {
Button {
presentPlayerCreation = true
} label: {
HStack(spacing: 4) {
Image(systemName: "person.fill.badge.plus")
.resizable()
.scaledToFit()
.frame(width: 20)
Text("Créer")
.font(.headline)
}
.frame(maxWidth: .infinity)
}
PasteButton(payloadType: String.self) { strings in
guard let first = strings.first else { return }
Task {
await MainActor.run() {
fetchPlayers.nsPredicate = _pastePredicate(pasteField: first, mostRecentDate: nil)
pasteString = first
}
}
}
Button {
presentPlayerSearch = true
} label: {
HStack(spacing: 4) {
Image(systemName: "person.fill.viewfinder")
.resizable()
.scaledToFit()
.frame(width: 20)
Text("FFT")
.font(.headline)
}
.frame(maxWidth: .infinity)
}
}
.buttonStyle(.borderedProminent)
.tint(.launchScreenBackground)
.fixedSize(horizontal: false, vertical: true)
.padding(16)
}
private func _addPlayerSex() -> Int {
switch tournament.tournamentCategory {
case .men:
return 1
case .women:
return 0
case .mix:
return 1
}
}
private func _filterOption() -> PlayerFilterOption {
switch tournament.tournamentCategory {
case .men:
return .male
case .women:
return .female
case .mix:
return .all
}
}
}
#Preview {
NavigationStack {
InscriptionManagerView(tournament: Tournament.mock())
.environment(Tournament.mock())
}
}

@ -8,11 +8,40 @@
import SwiftUI
struct TournamentView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
var presentationContext: PresentationContext = .agenda
@AppStorage("lastDataSource") var lastDataSource: String?
var _lastDataSourceDate: Date? {
guard let lastDataSource else { return nil }
return URL.importDateFormatter.date(from: lastDataSource)
}
var body: some View {
List {
if tournament.missingUnrankedValue() {
Button("update NC") {
Task {
tournament.maleUnrankedValue = await FederalPlayer.lastRank(mostRecentDateAvailable: _lastDataSourceDate, man: true)
tournament.femaleUnrankedValue = await FederalPlayer.lastRank(mostRecentDateAvailable: _lastDataSourceDate, man: false)
try? dataStore.tournaments.addOrUpdate(instance: tournament)
}
}
}
NavigationLink(value: Screen.inscription) {
LabeledContent {
Text(tournament.teams().count.formatted())
} label: {
Text("Inscriptions")
}
}
switch tournament.state() {
case .initial:
TournamentInitView()

Loading…
Cancel
Save