ajout des convocations

multistore
Razmig Sarkissian 2 years ago
parent cb89a322ca
commit 13e09d2163
  1. 48
      PadelClub.xcodeproj/project.pbxproj
  2. 45
      PadelClub/Data/PlayerRegistration.swift
  3. 16
      PadelClub/Data/Round.swift
  4. 9
      PadelClub/Data/TeamRegistration.swift
  5. 58
      PadelClub/Data/Tournament.swift
  6. 4
      PadelClub/Extensions/Date+Extensions.swift
  7. 9
      PadelClub/Extensions/String+Extensions.swift
  8. 195
      PadelClub/Manager/ContactManager.swift
  9. 28
      PadelClub/Manager/NetworkMonitor.swift
  10. 4
      PadelClub/PadelClubApp.swift
  11. 55
      PadelClub/Views/Calling/CallSettingsView.swift
  12. 159
      PadelClub/Views/Calling/CallView.swift
  13. 91
      PadelClub/Views/Calling/GroupStageCallingView.swift
  14. 105
      PadelClub/Views/Calling/SeedsCallingView.swift
  15. 32
      PadelClub/Views/Player/Components/PlayerPayView.swift
  16. 19
      PadelClub/Views/Player/Components/PlayerPopoverView.swift
  17. 7
      PadelClub/Views/Team/TeamRowView.swift
  18. 41
      PadelClub/Views/Tournament/Screen/CashierDetailView.swift
  19. 539
      PadelClub/Views/Tournament/Screen/CashierView.swift
  20. 4
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  21. 2
      PadelClub/Views/Tournament/Screen/Screen.swift
  22. 79
      PadelClub/Views/Tournament/Screen/TournamentCallView.swift
  23. 16
      PadelClub/Views/Tournament/TournamentRunningView.swift
  24. 4
      PadelClub/Views/Tournament/TournamentView.swift

@ -153,6 +153,16 @@
FF8F264F2BAE0B9600650388 /* MatchTypeSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F264E2BAE0B9600650388 /* MatchTypeSelectionView.swift */; };
FF8F26512BAE0BAD00650388 /* MatchFormatPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26502BAE0BAD00650388 /* MatchFormatPickerView.swift */; };
FF8F26542BAE1E4400650388 /* TableStructureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26532BAE1E4400650388 /* TableStructureView.swift */; };
FF9267F82BCE78C70080F940 /* CashierView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF9267F72BCE78C70080F940 /* CashierView.swift */; };
FF9267FA2BCE78EC0080F940 /* CashierDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF9267F92BCE78EB0080F940 /* CashierDetailView.swift */; };
FF9267FC2BCE84870080F940 /* PlayerPayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF9267FB2BCE84870080F940 /* PlayerPayView.swift */; };
FF9267FF2BCE94830080F940 /* CallSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF9267FE2BCE94830080F940 /* CallSettingsView.swift */; };
FF9268012BCE94920080F940 /* SeedsCallingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF9268002BCE94920080F940 /* SeedsCallingView.swift */; };
FF9268032BCE94A30080F940 /* GroupStageCallingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF9268022BCE94A30080F940 /* GroupStageCallingView.swift */; };
FF9268072BCE94D90080F940 /* TournamentCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF9268062BCE94D90080F940 /* TournamentCallView.swift */; };
FF9268092BCEDC2C0080F940 /* CallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF9268082BCEDC2C0080F940 /* CallView.swift */; };
FF92680B2BCEE3E10080F940 /* ContactManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF92680A2BCEE3E10080F940 /* ContactManager.swift */; };
FF92680D2BCEE5EA0080F940 /* NetworkMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF92680C2BCEE5EA0080F940 /* NetworkMonitor.swift */; };
FF967CEA2BAEC70100A9A3BD /* GroupStage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967CE72BAEC70100A9A3BD /* GroupStage.swift */; };
FF967CEC2BAECB9900A9A3BD /* Match.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967CEB2BAECB9900A9A3BD /* Match.swift */; };
FF967CEE2BAECBD700A9A3BD /* Round.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF967CED2BAECBD700A9A3BD /* Round.swift */; };
@ -410,6 +420,16 @@
FF8F264E2BAE0B9600650388 /* MatchTypeSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchTypeSelectionView.swift; sourceTree = "<group>"; };
FF8F26502BAE0BAD00650388 /* MatchFormatPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchFormatPickerView.swift; sourceTree = "<group>"; };
FF8F26532BAE1E4400650388 /* TableStructureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableStructureView.swift; sourceTree = "<group>"; };
FF9267F72BCE78C70080F940 /* CashierView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CashierView.swift; sourceTree = "<group>"; };
FF9267F92BCE78EB0080F940 /* CashierDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CashierDetailView.swift; sourceTree = "<group>"; };
FF9267FB2BCE84870080F940 /* PlayerPayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerPayView.swift; sourceTree = "<group>"; };
FF9267FE2BCE94830080F940 /* CallSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallSettingsView.swift; sourceTree = "<group>"; };
FF9268002BCE94920080F940 /* SeedsCallingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedsCallingView.swift; sourceTree = "<group>"; };
FF9268022BCE94A30080F940 /* GroupStageCallingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupStageCallingView.swift; sourceTree = "<group>"; };
FF9268062BCE94D90080F940 /* TournamentCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentCallView.swift; sourceTree = "<group>"; };
FF9268082BCEDC2C0080F940 /* CallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallView.swift; sourceTree = "<group>"; };
FF92680A2BCEE3E10080F940 /* ContactManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactManager.swift; sourceTree = "<group>"; };
FF92680C2BCEE5EA0080F940 /* NetworkMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.swift; sourceTree = "<group>"; };
FF967CE72BAEC70100A9A3BD /* GroupStage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupStage.swift; sourceTree = "<group>"; };
FF967CEB2BAECB9900A9A3BD /* Match.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Match.swift; sourceTree = "<group>"; };
FF967CED2BAECBD700A9A3BD /* Round.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Round.swift; sourceTree = "<group>"; };
@ -620,6 +640,7 @@
FFCFC00B2BBC39A600B82851 /* Score */,
FF967D072BAF3D3000A9A3BD /* Team */,
FF089EB92BB011EE00F0AEC7 /* Player */,
FF9267FD2BCE94520080F940 /* Calling */,
FFF964512BC2628600EEF017 /* Planning */,
FF3F74F72B919F96004CFE0E /* Tournament */,
C4A47D882B7BBB5000ADC637 /* Subscription */,
@ -682,6 +703,7 @@
children = (
FF089EB32BB0020000F0AEC7 /* PlayerSexPickerView.swift */,
FF089EBA2BB0120700F0AEC7 /* PlayerPopoverView.swift */,
FF9267FB2BCE84870080F940 /* PlayerPayView.swift */,
);
path = Components;
sourceTree = "<group>";
@ -802,6 +824,9 @@
FF8F26422BADFE5B00650388 /* TournamentSettingsView.swift */,
FF8F26532BAE1E4400650388 /* TableStructureView.swift */,
FF0E0B6C2BC254C6005F00A9 /* TournamentScheduleView.swift */,
FF9268062BCE94D90080F940 /* TournamentCallView.swift */,
FF9267F72BCE78C70080F940 /* CashierView.swift */,
FF9267F92BCE78EB0080F940 /* CashierDetailView.swift */,
FF8F26522BAE0E4E00650388 /* Components */,
);
path = Screen;
@ -918,6 +943,17 @@
path = Components;
sourceTree = "<group>";
};
FF9267FD2BCE94520080F940 /* Calling */ = {
isa = PBXGroup;
children = (
FF9267FE2BCE94830080F940 /* CallSettingsView.swift */,
FF9268002BCE94920080F940 /* SeedsCallingView.swift */,
FF9268022BCE94A30080F940 /* GroupStageCallingView.swift */,
FF9268082BCEDC2C0080F940 /* CallView.swift */,
);
path = Calling;
sourceTree = "<group>";
};
FF967CF92BAEE11500A9A3BD /* GroupStage */ = {
isa = PBXGroup;
children = (
@ -1005,6 +1041,8 @@
FF0EC51D2BB16F680056B6D1 /* SwiftParser.swift */,
FF1DC55A2BAB80C400FD8220 /* DisplayContext.swift */,
FFF8ACD32B92392C008466FA /* SourceFileManager.swift */,
FF92680A2BCEE3E10080F940 /* ContactManager.swift */,
FF92680C2BCEE5EA0080F940 /* NetworkMonitor.swift */,
FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */,
FF8F26352BAD523300650388 /* PadelRule.swift */,
FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */,
@ -1251,6 +1289,7 @@
files = (
C4A47D872B7BA36D00ADC637 /* UserCreationView.swift in Sources */,
FF7091662B90F0B000AB08DA /* TabDestination.swift in Sources */,
FF9267F82BCE78C70080F940 /* CashierView.swift in Sources */,
FF8F263F2BAD7D5C00650388 /* Event.swift in Sources */,
FF089EBF2BB0B14600F0AEC7 /* FileImportView.swift in Sources */,
C4A47D9F2B7D0BCE00ADC637 /* StepperView.swift in Sources */,
@ -1265,10 +1304,13 @@
FFB9C8752BBADDF700A0EF4F /* SeedInterval.swift in Sources */,
FFBF065C2BBD2657009D6715 /* GroupStageTeamView.swift in Sources */,
FF5DA1932BB9279B00A33061 /* RoundSettingsView.swift in Sources */,
FF9268012BCE94920080F940 /* SeedsCallingView.swift in Sources */,
FF9268092BCEDC2C0080F940 /* CallView.swift in Sources */,
FF5D0D742BB41DF8005CB568 /* Color+Extensions.swift in Sources */,
C4A47DB12B86375E00ADC637 /* MainUserView.swift in Sources */,
FF7091682B90F79F00AB08DA /* TournamentCellView.swift in Sources */,
FF6EC9042B9479F500EA7F5A /* Sequence+Extensions.swift in Sources */,
FF9267FA2BCE78EC0080F940 /* CashierDetailView.swift in Sources */,
C4A47DB32B86387500ADC637 /* AccountView.swift in Sources */,
FF1CBC1D2BB53DC10036DAAB /* Calendar+Extensions.swift in Sources */,
FF967CF22BAECC0B00A9A3BD /* TeamScore.swift in Sources */,
@ -1289,8 +1331,10 @@
FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */,
FFCFC00E2BBC3D4600B82851 /* PointSelectionView.swift in Sources */,
FF089EB62BB00A3800F0AEC7 /* TeamRowView.swift in Sources */,
FF92680B2BCEE3E10080F940 /* ContactManager.swift in Sources */,
FFCFC00C2BBC3D1E00B82851 /* EditScoreView.swift in Sources */,
FF7091622B90F04300AB08DA /* TournamentOrganizerView.swift in Sources */,
FF92680D2BCEE5EA0080F940 /* NetworkMonitor.swift in Sources */,
FF967CF62BAED51600A9A3BD /* TournamentRunningView.swift in Sources */,
FF8F264D2BAE0B4100650388 /* TournamentDatePickerView.swift in Sources */,
FF967D042BAEF1C300A9A3BD /* MatchRowView.swift in Sources */,
@ -1328,12 +1372,14 @@
FFCFC0162BBC5A4C00B82851 /* SetInputView.swift in Sources */,
FF5D0D892BB4935C005CB568 /* ClubRowView.swift in Sources */,
FF1DC5512BAB351300FD8220 /* ClubDetailView.swift in Sources */,
FF9268032BCE94A30080F940 /* GroupStageCallingView.swift in Sources */,
FFFCDE0E2BCC833600317DEF /* LoserRoundScheduleEditorView.swift in Sources */,
C4A47D632B6D3D6500ADC637 /* Club.swift in Sources */,
FF6EC90B2B947AC000EA7F5A /* Array+Extensions.swift in Sources */,
FF59FFB92B90EFD70061EFF9 /* ToolboxView.swift in Sources */,
FFF8ACD92B923F3C008466FA /* String+Extensions.swift in Sources */,
FFC2DCB22BBE75D40046DB9F /* LoserBracketView.swift in Sources */,
FF9267FC2BCE84870080F940 /* PlayerPayView.swift in Sources */,
FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */,
FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */,
FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */,
@ -1351,6 +1397,7 @@
FF967D0B2BAF3D4C00A9A3BD /* TeamPickerView.swift in Sources */,
FFA6D7872BB0B7A2003A31F3 /* CloudConvert.swift in Sources */,
FF1DC55B2BAB80C400FD8220 /* DisplayContext.swift in Sources */,
FF9268072BCE94D90080F940 /* TournamentCallView.swift in Sources */,
C425D4032B6D249D002A7B48 /* ContentView.swift in Sources */,
FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */,
FF967CFC2BAEE52E00A9A3BD /* GroupStagesView.swift in Sources */,
@ -1399,6 +1446,7 @@
C4A47DA62B83948E00ADC637 /* LoginView.swift in Sources */,
FF967CF82BAEDF0000A9A3BD /* Labels.swift in Sources */,
FF089EB42BB0020000F0AEC7 /* PlayerSexPickerView.swift in Sources */,
FF9267FF2BCE94830080F940 /* CallSettingsView.swift in Sources */,
FF5D0D852BB48997005CB568 /* RankCalculatorView.swift in Sources */,
FF70916A2B90F95E00AB08DA /* DateBoxView.swift in Sources */,
FF5D0D722BB3EFA5005CB568 /* LearnMoreSheetView.swift in Sources */,

@ -18,7 +18,7 @@ class PlayerRegistration: ModelObject, Storable {
var lastName: String
var licenceId: String?
var rank: Int?
var registrationType: Int?
var registrationType: PaymentType?
var registrationDate: Date?
var sex: Int
@ -37,7 +37,7 @@ class PlayerRegistration: ModelObject, Storable {
var hasArrived: Bool = false
internal init(teamRegistration: String? = nil, firstName: String, lastName: String, licenceId: String? = nil, rank: Int? = nil, registrationType: Int? = nil, registrationDate: Date? = nil, sex: Int, source: PlayerDataSource? = nil) {
internal init(teamRegistration: String? = nil, firstName: String, lastName: String, licenceId: String? = nil, rank: Int? = nil, registrationType: PaymentType? = nil, registrationDate: Date? = nil, sex: Int, source: PlayerDataSource? = nil) {
self.teamRegistration = teamRegistration
self.firstName = firstName
self.lastName = lastName
@ -117,15 +117,6 @@ class PlayerRegistration: ModelObject, Storable {
func hasPaid() -> Bool {
registrationType != nil
}
var paymentType: PaymentType {
get {
PaymentType(rawValue: registrationType ?? -1) ?? .notPaid
}
set {
registrationType = newValue.rawValue
}
}
func playerLabel(_ displayStyle: DisplayStyle = .wide) -> String {
switch displayStyle {
@ -233,6 +224,16 @@ class PlayerRegistration: ModelObject, Storable {
sex == 1
}
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 {
self.licenceId = computedLicense + " (\(year))"
}
}
}
enum CodingKeys: String, CodingKey {
case _id = "id"
case _teamRegistration = "teamRegistration"
@ -262,21 +263,27 @@ class PlayerRegistration: ModelObject, Storable {
case beachPadel
}
enum PaymentType: Int, CaseIterable, Identifiable {
enum PaymentType: Int, CaseIterable, Identifiable, Codable {
init?(rawValue: Int?) {
guard let value = rawValue else { return nil }
self.init(rawValue: value)
}
var id: Self {
self
}
case notPaid = -1
case cash = 0
case lydia = 1
case gift = 2
case check = 3
case paylib = 4
case bankTransfer = 5
case clubHouse = 6
case creditCard = 7
var localizedLabel: String {
func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
switch self {
case .notPaid:
return "Non réglé"
case .check:
return "Chèque"
case .cash:
@ -285,6 +292,12 @@ class PlayerRegistration: ModelObject, Storable {
return "Lydia"
case .paylib:
return "Paylib"
case .bankTransfer:
return "Virement"
case .clubHouse:
return "Clubhouse"
case .creditCard:
return "CB"
case .gift:
return "Offert"
}

@ -99,6 +99,22 @@ class Round: ModelObject, Storable {
})
}
func seeds(inMatchIndex matchIndex: Int) -> [TeamRegistration] {
return Store.main.filter(isIncluded: {
$0.tournament == tournament && $0.bracketPosition != nil
}).filter({
($0.bracketPosition! / 2) == matchIndex
})
}
func seeds() -> [TeamRegistration] {
return Store.main.filter(isIncluded: {
$0.tournament == tournament && $0.bracketPosition != nil
}).filter({
($0.bracketPosition! / 2) >= RoundRule.matchIndex(fromRoundIndex: index) && ($0.bracketPosition! / 2) < RoundRule.matchIndex(fromRoundIndex: index) + RoundRule.numberOfMatches(forRoundIndex: index)
})
}
func losers() -> [TeamRegistration] {
_matches().compactMap { $0.losingTeamId }.compactMap { Store.main.findById($0) }
}

@ -70,6 +70,15 @@ class TeamRegistration: ModelObject, Storable {
func confirmed() -> Bool {
confirmationDate != nil
}
func getPhoneNumbers() -> [String] {
return players().compactMap { $0.phoneNumber }.filter({ $0.isMobileNumber() })
}
func getMail() -> [String] {
let mails = players().compactMap({ $0.email })
return mails
}
func isImported() -> Bool {
unsortedPlayers().allSatisfy({ $0.isImported() })

@ -155,7 +155,14 @@ class Tournament : ModelObject, Storable {
return .initial
}
func seededTeams() -> [TeamRegistration] {
selectedSortedTeams().filter({ $0.bracketPosition != nil && $0.groupStagePosition == nil })
}
func groupStageTeams() -> [TeamRegistration] {
selectedSortedTeams().filter({ $0.bracketPosition == nil && $0.groupStagePosition != nil })
}
func seeds() -> [TeamRegistration] {
let seeds = max(teamCount - groupStageCount * teamsPerGroupStage, 0)
return Array(selectedSortedTeams().prefix(seeds))
@ -416,6 +423,10 @@ class Tournament : ModelObject, Storable {
unsortedTeams().flatMap { $0.unsortedPlayers() }
}
func selectedPlayers() -> [PlayerRegistration] {
selectedSortedTeams().flatMap { $0.unsortedPlayers() }.sorted(by: \.weight)
}
func players() -> [PlayerRegistration] {
unsortedTeams().flatMap { $0.unsortedPlayers() }.sorted(by: \.weight)
}
@ -609,11 +620,56 @@ class Tournament : ModelObject, Storable {
//return qualifiedTeams().count == qualifiedFromGroupStage() + groupStageAdditionalQualified
}
var entryFeeMessage: String {
if let entryFee {
return "Inscription: " + entryFee.formatted(.currency(code: "EUR")) + "."
} else {
return "Inscription: gratuite."
}
}
func umpireMail() -> [String]? {
if let mail = UserDefaults.standard.string(forKey: "umpireMail"), mail.isEmpty == false {
return [mail]
} else {
return nil
}
// if let umpireMail = federalTournament?.courrielEngagement {
// return [umpireMail]
// } else {
// }
}
func earnings() -> Double {
if let entryFee {
return Double(selectedPlayers().filter { $0.hasPaid() }.count) * entryFee
} else {
return 0.0
}
}
func paidCompletion() -> Double {
let selectedPlayers = selectedPlayers()
if selectedPlayers.isEmpty { return 0 }
return Double(selectedPlayers.filter { $0.hasPaid() }.count) / Double(selectedPlayers.count)
}
func cashierStatus() -> String {
//todo
return "todo"
}
func scheduleStatus() -> String {
//todo
return "todo"
}
func callStatus() -> String {
//todo
return "todo"
}
func bracketStatus() -> String {
if let round = getActiveRound() {
return [round.roundTitle(), round.roundStatus()].joined(separator: " ")

@ -36,6 +36,10 @@ enum TimeOfDay {
extension Date {
func localizedDate() -> String {
self.formatted(.dateTime.weekday().day().month()) + " à " + self.formatted(.dateTime.hour().minute())
}
var monthYearFormatted: String {
formatted(.dateTime.month(.wide).year(.defaultDigits))
}

@ -59,6 +59,15 @@ extension String {
}
extension String {
enum RegexStatic {
static let mobileNumber = /^0[6-7]/
//static let mobileNumber = /^(?:(?:\+|00)33[\s.-]{0,3}(?:\(0\)[\s.-]{0,3})?|0)[1-9](?:(?:[\s.-]?\d{2}){4}|\d{2}(?:[\s.-]?\d{3}){2})$/
}
func isMobileNumber() -> Bool {
firstMatch(of: RegexStatic.mobileNumber) != nil
}
var computedLicense: String {
if let licenseKey {
return self + licenseKey

@ -0,0 +1,195 @@
//
// ContactManager.swift
// Padel Tournament
//
// Created by Razmig Sarkissian on 19/09/2023.
//
import Foundation
import SwiftUI
import MessageUI
enum ContactManagerError: LocalizedError {
case mailFailed
case mailNotSent //no network no error
case messageFailed
case messageNotSent //no network no error
}
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?)
var id: Int {
switch self {
case .message: return 0
case .mail: return 1
}
}
}
extension ContactType {
static let defaultCustomMessage = "Il est conseillé de vous présenter 10 minutes avant de jouer.\nMerci de confirmer en répondant à ce message et de prévenir votre partenaire."
static let defaultSignature = ""
static func callingGroupStageCustomMessage(tournament: Tournament?, startDate: Date?, roundLabel: String) -> String {
let tournamentCustomMessage = UserDefaults.standard.string(forKey: "customMessage") ?? defaultCustomMessage
let clubName = tournament?.clubName ?? ""
var text = tournamentCustomMessage
let date = startDate ?? tournament?.startDate ?? Date()
if let tournament {
text = text.replacingOccurrences(of: "#titre", with: tournament.tournamentTitle())
}
text = text.replacingOccurrences(of: "#club", with: clubName)
text = text.replacingOccurrences(of: "#round", with: roundLabel)
text = text.replacingOccurrences(of: "#jour", with: "\(date.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide)))")
text = text.replacingOccurrences(of: "#horaire", with: "\(date.formatted(Date.FormatStyle().hour().minute()))")
let signature = UserDefaults.standard.string(forKey: "mySelf") ?? defaultSignature
text = text.replacingOccurrences(of: "#signature", with: signature)
return text
}
static func callingGroupStageMessage(tournament: Tournament?, startDate: Date?, roundLabel: String, matchFormat: MatchFormat?) -> String {
let useFullCustomMessage = UserDefaults.standard.bool(forKey: "useFullCustomMessage")
if useFullCustomMessage {
return callingGroupStageCustomMessage(tournament: tournament, startDate: startDate, roundLabel: roundLabel)
}
let date = startDate ?? tournament?.startDate ?? Date()
let clubName = tournament?.clubName ?? ""
let message = UserDefaults.standard.string(forKey: "customMessage") ?? defaultCustomMessage
let signature = UserDefaults.standard.string(forKey: "mySelf") ?? defaultSignature
let localizedCalled = "convoqué" + (tournament?.tournamentCategory == .women ? "e" : "") + "s"
var formatMessage: String? {
UserDefaults.standard.bool(forKey: "displayFormat") ? matchFormat?.computedLongLabel.appending(".") : nil
}
var entryFeeMessage: String? {
UserDefaults.standard.bool(forKey: "displayEntryFee") ? tournament?.entryFeeMessage : nil
}
var computedMessage: String {
[formatMessage, entryFeeMessage, message].compacted().map { $0.trimmed }.joined(separator: "\n")
}
if let tournament {
return "Bonjour,\n\nVous êtes \(localizedCalled) pour jouer en \(roundLabel.lowercased()) du \(tournament.tournamentTitle()) au \(clubName) le \(date.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(date.formatted(Date.FormatStyle().hour().minute())).\n\n" + computedMessage + "\n\n\(signature)"
} else {
return "Bonjour,\n\nVous êtes \(localizedCalled) \(roundLabel) au \(clubName) le \(date.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(date.formatted(Date.FormatStyle().hour().minute())).\n\nMerci de confirmer en répondant à ce message et de prévenir votre partenaire !\n\n\(signature)"
}
}
}
struct MessageComposeView: UIViewControllerRepresentable {
typealias Completion = (_ result: MessageComposeResult) -> Void
static var canSendText: Bool { MFMessageComposeViewController.canSendText() }
let recipients: [String]?
let body: String?
let completion: Completion?
func makeUIViewController(context: Context) -> UIViewController {
guard Self.canSendText else {
let errorView = ContentUnavailableView("Aucun compte de messagerie", systemImage: "xmark", description: Text("Aucun compte de messagerie n'est configuré sur cet appareil."))
return UIHostingController(rootView: errorView)
}
let controller = MFMessageComposeViewController()
controller.messageComposeDelegate = context.coordinator
controller.recipients = recipients
controller.body = body
return controller
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(completion: self.completion)
}
class Coordinator: NSObject, MFMessageComposeViewControllerDelegate {
private let completion: Completion?
public init(completion: Completion?) {
self.completion = completion
}
public func messageComposeViewController(_ controller: MFMessageComposeViewController, didFinishWith result: MessageComposeResult) {
controller.dismiss(animated: true, completion: {
self.completion?(result)
})
}
}
}
struct MailComposeView: UIViewControllerRepresentable {
typealias Completion = (_ result: MFMailComposeResult) -> Void
static var canSendMail: Bool {
if let mailURL = URL(string: "mailto:?to=jap@padelclub.com") {
let mailConfigured = UIApplication.shared.canOpenURL(mailURL)
return mailConfigured && MFMailComposeViewController.canSendMail()
} else {
return MFMailComposeViewController.canSendMail()
}
}
let recipients: [String]?
let bccRecipients: [String]?
let body: String?
let subject: String?
let completion: Completion?
func makeUIViewController(context: Context) -> UIViewController {
guard Self.canSendMail else {
let errorView = ContentUnavailableView("Aucun compte mail", systemImage: "xmark", description: Text("Aucun compte mail n'est configuré sur cet appareil."))
return UIHostingController(rootView: errorView)
}
let controller = MFMailComposeViewController()
controller.mailComposeDelegate = context.coordinator
controller.setToRecipients(recipients)
controller.setBccRecipients(bccRecipients)
if let body {
controller.setMessageBody(body, isHTML: false)
}
if let subject {
controller.setSubject(subject)
}
return controller
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(completion: self.completion)
}
class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
private let completion: Completion?
public init(completion: Completion?) {
self.completion = completion
}
public func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
controller.dismiss(animated: true, completion: {
self.completion?(result)
})
}
}
}

@ -0,0 +1,28 @@
//
// NetworkMonitor.swift
// PadelClub
//
// Created by Razmig Sarkissian on 16/04/2024.
//
import Foundation
import Network
class NetworkMonitor: ObservableObject {
let monitor = NWPathMonitor()
let queue = DispatchQueue(label: "Monitor")
@Published private(set) var connected: Bool = false
func checkConnection() {
monitor.pathUpdateHandler = { path in
DispatchQueue.main.async {
if path.status == .satisfied {
self.connected = true
} else {
self.connected = false
}
}
}
monitor.start(queue: queue)
}
}

@ -13,10 +13,12 @@ import TipKit
struct PadelClubApp: App {
let persistenceController = PersistenceController.shared
@State private var navigationViewModel = NavigationViewModel()
@StateObject var networkMonitor: NetworkMonitor = NetworkMonitor()
var body: some Scene {
WindowGroup {
MainView()
.environmentObject(networkMonitor)
.environment(navigationViewModel)
.accentColor(.master)
.onAppear {

@ -0,0 +1,55 @@
//
// CallSettingsView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 16/04/2024.
//
import SwiftUI
struct CallSettingsView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
var body: some View {
List {
Section {
NavigationLink {
} label: {
Text("Modifier le message de convocation")
}
}
Section {
RowButtonView("Annuler toutes les convocations") {
let teams = tournament.unsortedTeams()
teams.forEach { team in
team.callDate = nil
}
try? dataStore.teamRegistrations.addOrUpdate(contentOfs: teams)
}
}
Section {
RowButtonView("Envoyer un message à tout le monde") {
}
}
Section {
RowButtonView("Tout le monde a été convoqué") {
let teams = tournament.unsortedTeams()
teams.forEach { team in
team.callDate = Date()
}
try? dataStore.teamRegistrations.addOrUpdate(contentOfs: teams)
}
}
}
}
}
#Preview {
CallSettingsView()
}

@ -0,0 +1,159 @@
//
// CallView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 16/04/2024.
//
import SwiftUI
struct CallView: View {
struct CallStatusView: View {
let count: Int
let total: Int
let startDate: Date?
var body: some View {
VStack(spacing: 0) {
HStack {
if let startDate {
Text(startDate.formatted(.dateTime.hour().minute()))
} else {
Text("Aucun horaire")
}
Spacer()
Text(count.formatted() + "/" + total.formatted())
}
.font(.largeTitle)
HStack {
if let startDate {
Text(startDate.formatted(.dateTime.weekday().day(.twoDigits).month().year()))
}
Spacer()
Text("paires convoquées")
}
.font(.caption)
.foregroundColor(.secondary)
}
}
}
struct TeamView: View {
let team: TeamRegistration
var body: some View {
TeamRowView(team: team, displayCallDate: true)
}
}
@EnvironmentObject var dataStore: DataStore
@EnvironmentObject var networkMonitor: NetworkMonitor
@Environment(Tournament.self) var tournament: Tournament
var teams: [TeamRegistration]
let callDate: Date
let matchFormat: MatchFormat
let roundLabel: String
@State private var contactType: ContactType? = nil
@State private var sentError: ContactManagerError? = nil
var messageSentFailed: Binding<Bool> {
Binding {
sentError != nil
} set: { newValue in
if newValue == false {
sentError = nil
}
}
}
private func _called(_ success: Bool) {
if success {
teams.forEach { team in
team.callDate = callDate
}
try? dataStore.teamRegistrations.addOrUpdate(contentOfs: teams)
}
}
var finalMessage: String {
ContactType.callingGroupStageMessage(tournament: tournament, startDate: callDate, roundLabel: roundLabel, matchFormat: matchFormat)
}
var body: some View {
let callWord = teams.allSatisfy({ $0.called() }) ? "Reconvoquer" : "Convoquer"
HStack {
if teams.count == 1 {
Text(callWord + " cette paire par")
} else {
Text(callWord + " ces \(teams.count) paires par")
}
Button {
contactType = .message(date: callDate, recipients: teams.flatMap { $0.getPhoneNumbers() }, body: finalMessage, tournamentBuild: nil)
} label: {
Text("sms")
.underline()
}
Text("ou")
Button {
contactType = .mail(date: callDate, recipients: tournament.umpireMail(), bccRecipients: teams.flatMap { $0.getMail() }, body: finalMessage, subject: tournament.tournamentTitle(), tournamentBuild: nil)
} label: {
Text("mail")
.underline()
}
}
.font(.subheadline)
.buttonStyle(.borderless)
.alert("Un problème est survenu", isPresented: messageSentFailed) {
Button("OK") {
}
} message: {
let message = [networkMonitor.connected == false ? "L'appareil n'est pas connecté à internet." as String? : nil, sentError == .mailNotSent ? "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." as String? : nil, (sentError == .messageFailed || sentError == .messageNotSent) ? "Le SMS n'a pas été envoyé" as String? : nil, sentError == .mailFailed ? "Le mail n'a pas été envoyé" as String? : nil].compacted().joined(separator: "\n")
Text(message)
}
.sheet(item: $contactType) { contactType in
switch contactType {
case .message(_, let recipients, let body, _):
MessageComposeView(recipients: recipients, body: body) { result in
switch result {
case .cancelled:
_called(true)
break
case .failed:
self.sentError = .messageFailed
case .sent:
if networkMonitor.connected == false {
self.sentError = .messageNotSent
} else {
_called(true)
}
@unknown default:
break
}
}
case .mail(_, let recipients, let bccRecipients, let body, let subject, _):
MailComposeView(recipients: recipients, bccRecipients: bccRecipients, body: body, subject: subject) { result in
switch result {
case .cancelled, .saved:
self.contactType = nil
_called(true)
case .failed:
self.contactType = nil
self.sentError = .mailFailed
case .sent:
if networkMonitor.connected == false {
self.contactType = nil
self.sentError = .mailNotSent
} else {
_called(true)
}
@unknown default:
break
}
}
}
}
}
}

@ -0,0 +1,91 @@
//
// GroupStageCallingView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 16/04/2024.
//
import SwiftUI
struct GroupStageCallingView: View {
@Environment(Tournament.self) var tournament: Tournament
@State private var displayByTeam: Bool = false
var body: some View {
let groupStages = tournament.groupStages()
List {
_sameTimeGroupStageView(groupStages: groupStages)
ForEach(groupStages) { groupStage in
let seeds = groupStage.teams()
let callSeeds = seeds.filter({ $0.callDate != nil })
if seeds.isEmpty == false {
Section {
NavigationLink {
_groupStageView(groupStage: groupStage)
.environment(tournament)
} label: {
CallView.CallStatusView(count: callSeeds.count, total: seeds.count, startDate: groupStage.startDate)
}
} header: {
Text(groupStage.groupStageTitle())
} footer: {
if let startDate = groupStage.startDate {
CallView(teams: seeds, callDate: startDate, matchFormat: groupStage.matchFormat, roundLabel: "poule")
}
}
}
}
}
.headerProminence(.increased)
}
@ViewBuilder
func _sameTimeGroupStageView(groupStages: [GroupStage]) -> some View {
let times = Dictionary(grouping: groupStages) { groupStage in
groupStage.startDate
}
let keys = times.keys.compactMap { $0 }.sorted()
ForEach(keys, id: \.self) { key in
if let _groupStages = times[key], _groupStages.count > 1 {
let teams = _groupStages.flatMap { $0.teams() }
Section {
CallView.CallStatusView(count: teams.filter({ $0.callDate != nil }).count, total: teams.count, startDate: key)
} header: {
Text(groupStages.map { $0.groupStageTitle() }.joined(separator: ", "))
} footer: {
CallView(teams: teams, callDate: key, matchFormat: tournament.groupStageMatchFormat, roundLabel: "poule")
}
}
}
}
@ViewBuilder
private func _groupStageView(groupStage: GroupStage) -> some View {
let teams = groupStage.teams()
List {
if let startDate = groupStage.startDate {
ForEach(teams) { team in
Section {
CallView.TeamView(team: team)
} header: {
Text(startDate.localizedDate())
} footer: {
CallView(teams: [team], callDate: startDate, matchFormat: groupStage.matchFormat, roundLabel: "poule")
}
}
}
}
.headerProminence(.increased)
.navigationTitle(groupStage.groupStageTitle())
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
}
}
#Preview {
GroupStageCallingView()
}

@ -0,0 +1,105 @@
//
// SeedsCallingView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 16/04/2024.
//
import SwiftUI
struct SeedsCallingView: View {
@Environment(Tournament.self) var tournament: Tournament
@State private var displayByMatch: Bool = false
var body: some View {
List {
ForEach(tournament.rounds()) { round in
let seeds = round.seeds()
let callSeeds = seeds.filter({ $0.callDate != nil })
if seeds.isEmpty == false {
Section {
NavigationLink {
_roundView(round: round)
.environment(tournament)
} label: {
CallView.CallStatusView(count: callSeeds.count, total: seeds.count, startDate: round.playedMatches().first?.startDate)
}
} header: {
Text(round.roundTitle())
}
}
}
}
.headerProminence(.increased)
}
@ViewBuilder
private func _roundView(round: Round) -> some View {
let roundMatches = round.playedMatches()
let times = Dictionary(grouping: roundMatches) { match in
match.startDate
}
let keys = times.keys.compactMap { $0 }.sorted()
List {
if displayByMatch == false {
ForEach(keys, id: \.self) { time in
if let matches = times[time] {
let teams = matches.flatMap { round.seeds(inMatchIndex: $0.index) }
Section {
ForEach(teams) { team in
TeamRowView(team: team)
}
} header: {
HStack {
Text(time.localizedDate())
Spacer()
Text(teams.count.formatted() + " paire" + teams.count.pluralSuffix)
}
} footer: {
CallView(teams: teams, callDate: time, matchFormat: round.matchFormat, roundLabel: round.roundTitle())
}
}
}
} else {
ForEach(roundMatches) { match in
let teams = round.seeds(inMatchIndex: match.index)
Section {
ForEach(teams) { team in
CallView.TeamView(team: team)
}
} header: {
HStack {
if let startDate = match.startDate {
Text(startDate.localizedDate())
} else {
Text("Aucun horaire")
}
Spacer()
Text(match.matchTitle())
}
} footer: {
if let startDate = match.startDate {
CallView(teams: teams, callDate: startDate, matchFormat: match.matchFormat, roundLabel: round.roundTitle())
}
}
}
}
}
.headerProminence(.increased)
.navigationTitle(round.roundTitle())
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button(displayByMatch ? "par horaire" : "par match") {
displayByMatch.toggle()
}
.buttonBorderShape(.capsule)
}
}
}
}
#Preview {
SeedsCallingView()
}

@ -0,0 +1,32 @@
//
// PlayerPayView.swift
// Padel Tournament
//
// Created by Razmig Sarkissian on 22/11/2023.
//
import SwiftUI
struct PlayerPayView: View {
@EnvironmentObject var dataStore: DataStore
@Bindable var player: PlayerRegistration
var body: some View {
Picker(selection: $player.registrationType) {
Text("Non réglé").tag(nil as PlayerRegistration.PaymentType?)
ForEach(PlayerRegistration.PaymentType.allCases) { type in
Text(type.localizedLabel()).tag(type as PlayerRegistration.PaymentType?)
}
} label: {
}
.pickerStyle(.menu)
.fixedSize()
.onChange(of: player.registrationType) {
_save()
}
}
private func _save() {
try? dataStore.playerRegistrations.addOrUpdate(instance: player)
}
}

@ -174,6 +174,7 @@ struct PlayerPopoverView: View {
rank = nil
firstNameIsFocused = true
}
.disabled(isPlayerValid() == false)
}
}
.onAppear {
@ -191,8 +192,9 @@ struct PlayerPopoverView: View {
}
.clipShape(Capsule())
.buttonStyle(.bordered)
.disabled(isPlayerValid() == false)
}
ToolbarItem(placement: .cancellationAction) {
Button("Annuler", role : .cancel) {
dismiss()
@ -229,20 +231,23 @@ struct PlayerPopoverView: View {
}
}
}
func createManualPlayer() {
func isPlayerValid() -> Bool {
guard (lastName.isEmpty == false && requiredField.contains(.lastName)) || requiredField.contains(.lastName) == false else {
return
return false
}
guard (license.isEmpty == false && license.isLicenseNumber && requiredField.contains(.license)) || requiredField.contains(.license) == false else {
return
return false
}
guard (firstName.isEmpty == false && requiredField.contains(.firstName)) || requiredField.contains(.firstName) == false else {
return
return false
}
return true
}
func createManualPlayer() {
let playerRegistration = PlayerRegistration(firstName: firstName, lastName: lastName, licenceId: license.trimmed.isEmpty ? nil : license, rank: rank, sex: sex)
self.creationCompletionHandler(playerRegistration)
}

@ -10,6 +10,7 @@ import SwiftUI
struct TeamRowView: View {
var team: TeamRegistration
var teamPosition: TeamPosition? = nil
var displayCallDate: Bool = false
var body: some View {
LabeledContent {
@ -30,6 +31,12 @@ struct TeamRowView: View {
}
} label: {
Text(team.teamLabel(.short))
if let callDate = team.callDate {
Text("Déjà convoquée \(callDate.localizedDate())")
.foregroundStyle(.red)
.italic()
.font(.caption)
}
}
}
}

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

@ -0,0 +1,539 @@
//
// CashierView.swift
// Padel Tournament
//
// Created by Razmig Sarkissian on 04/03/2023.
//
import SwiftUI
import Combine
struct CashierView: View {
@EnvironmentObject var dataStore: DataStore
var tournaments : [Tournament]
@Environment(\.dismiss) private var dismiss
@State var licenseCheck: Bool?
@State private var sortOption: SortOption = .callDate
@State private var filterOption: FilterOption = .all
@State private var sortOrder: SortOrder = .ascending
@State private var searchText = ""
@State private var licenseToEdit = ""
@State private var editPlayer: PlayerRegistration?
let licenseMode: Bool
init(event: Event, licenseCheck: Bool? = nil, sortOption: SortOption = .callDate) {
_licenseCheck = State(wrappedValue: licenseCheck)
_sortOption = State(wrappedValue: sortOption)
licenseMode = licenseCheck != nil
self.tournaments = event.tournaments
}
init(tournament: Tournament, licenseCheck: Bool? = nil, sortOption: SortOption = .callDate) {
_licenseCheck = State(wrappedValue: licenseCheck)
_sortOption = State(wrappedValue: sortOption)
licenseMode = licenseCheck != nil
self.tournaments = [tournament]
}
func somePlayerToEdit() -> Binding<Bool> {
Binding {
editPlayer != nil
} set: { _ in
}
}
enum SortOption: Int, Identifiable, CaseIterable {
case round
case team
case alphabeticalLastName
case alphabeticalFirstName
case rank
case age
case callDate
var id: Int { self.rawValue }
func localizedLabel() -> String {
switch self {
case .round, .callDate:
return "Convocation"
case .team:
return "Équipe"
case .alphabeticalLastName:
return "Nom"
case .alphabeticalFirstName:
return "Prénom"
case .rank:
return "Rang"
case .age:
return "Âge"
}
}
}
enum FilterOption: Int, Identifiable, CaseIterable {
case all
case didPay
case didNotPay
var id: Int { self.rawValue }
func localizedLabel() -> String {
switch self {
case .all:
return "Tous"
case .didPay:
return "Réglé"
case .didNotPay:
return "Non réglé"
}
}
func shouldDisplayPlayer(_ player: PlayerRegistration) -> Bool {
switch self {
case .all:
return true
case .didPay:
return player.hasPaid()
case .didNotPay:
return player.hasPaid() == false
}
}
}
var orderedPlayers: [PlayerRegistration] {
if sortOrder == .ascending {
return sortedPlayers
} else {
return sortedPlayers.reversed()
}
}
var orderedTeams: [TeamRegistration] {
if sortOrder == .ascending {
return sortedTeams
} else {
return sortedTeams.reversed()
}
}
var sortedTeams: [TeamRegistration] {
tournaments.flatMap({ $0.selectedSortedTeams() })
}
func playerHasValidLicense(_ player: PlayerRegistration) -> Bool {
return true
//todo
// if player.isImported() {
// if let licenseCheck, let licenseYearValidity = event.licenseYearValidity {
// return player.isValidLicenseNumber(year: licenseYearValidity) == licenseCheck
// } else {
// return true
// }
// } else {
// return true
// }
}
var searchedPlayers: [PlayerRegistration] {
tournaments.flatMap({ $0.unsortedPlayers() })
// if searchText.trimmed.isEmpty {
// return event.orderedPlayers.filter { playerHasValidLicense($0) }
// } else {
// let search = searchText.trimmed.folding(options: .diacriticInsensitive, locale: .current).lowercased()
// return event.orderedPlayers.filter { $0.canonicalName.contains(search) && playerHasValidLicense($0) }
// }
}
var filteredPlayers: [PlayerRegistration] {
if licenseMode == false {
return searchedPlayers.filter { filterOption.shouldDisplayPlayer($0) }
} else {
return searchedPlayers
}
}
var sortedPlayers: [PlayerRegistration] {
switch sortOption {
case .callDate, .team, .round:
return filteredPlayers.sorted(using: .keyPath(\.lastName), .keyPath(\.firstName))
case .alphabeticalFirstName:
return filteredPlayers.sorted(using: .keyPath(\.firstName), .keyPath(\.lastName))
case .alphabeticalLastName:
return filteredPlayers.sorted(using: .keyPath(\.lastName), .keyPath(\.firstName))
case .rank:
return filteredPlayers.sorted(using: .keyPath(\.weight), .keyPath(\.lastName), .keyPath(\.firstName))
case .age:
return filteredPlayers.sorted(using: .keyPath(\.weight), .keyPath(\.lastName), .keyPath(\.firstName))
}
}
func save() {
// do {
// event.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)")
// }
}
func bracketPlayers(in tournament: Tournament) -> [PlayerRegistration] {
tournament.selectedSortedTeams().filter { $0.groupStagePosition != nil }.flatMap { $0.unsortedPlayers() }.filter { orderedPlayers.contains($0) }
}
func playersForRound(in round: Int, tournament: Tournament) -> [PlayerRegistration] {
tournament.selectedSortedTeams().filter { RoundRule.roundIndex(fromMatchIndex: 0) == round && $0.groupStagePosition == nil }.flatMap { $0.unsortedPlayers() }.filter { orderedPlayers.contains($0) }
}
var displayOptionView: some View {
DisclosureGroup {
HStack {
Text("Voir")
Spacer()
Picker(selection: $licenseCheck) {
Text("Tous les joueurs").tag(nil as Bool?)
Text("Avec licence valide").tag(true as Bool?)
Text("Sans licence valide").tag(false as Bool?)
} label: {
}
}
HStack {
Text("Filtre")
Spacer()
Picker(selection: $filterOption) {
ForEach(FilterOption.allCases) { filterOption in
Text(filterOption.localizedLabel()).tag(filterOption)
}
} label: {
}
}
HStack {
Text("Tri")
Spacer()
Picker(selection: $sortOption) {
ForEach(SortOption.allCases) { sortOption in
Text(sortOption.localizedLabel()).tag(sortOption)
}
} label: {
}
}
HStack {
Text("Ordre")
Spacer()
Picker(selection: $sortOrder) {
Text("Croissant").tag(SortOrder.ascending)
Text("Décroissant").tag(SortOrder.descending)
} label: {
}
}
} label: {
Text("Options d'affichage")
}
}
@ViewBuilder
var sortedPlayersView: some View {
if orderedPlayers.isEmpty {
ContentUnavailableView.search(text: searchText)
} else {
Section {
ForEach(orderedPlayers) { player in
computedPlayerView(player)
}
} header: {
HStack {
Text(orderedPlayers.count.formatted() + " joueurs")
}
}
}
}
var body: some View {
List {
Section {
NavigationLink {
CashierDetailView(tournaments: tournaments)
} label: {
Text("Résumé")
}
}
Section {
ForEach(tournaments) { tournament in
HStack {
Text(tournament.tournamentTitle())
Spacer()
Text(tournament.earnings().formatted(.currency(code: "EUR").precision(.fractionLength(0))))
Text(tournament.paidCompletion().formatted(.percent.precision(.fractionLength(0)))).foregroundStyle(.secondary)
}
}
// HStack {
// Text("Total")
// Spacer()
// Text(event.earnings.formatted(.currency(code: "EUR").precision(.fractionLength(0))))
// Text(event.paidCompletion.formatted(.percent.precision(.fractionLength(0)))).foregroundStyle(.secondary)
// }
} header: {
Text("Encaissement")
}
if licenseMode == false {
Section {
Picker(selection: $filterOption) {
ForEach(FilterOption.allCases) { filterOption in
Text(filterOption.localizedLabel()).tag(filterOption)
}
} label: {
Text("Statut du règlement")
}
Picker(selection: $sortOption) {
ForEach(SortOption.allCases) { sortOption in
Text(sortOption.localizedLabel()).tag(sortOption)
}
} label: {
Text("Affichage par")
}
if sortOption != .round {
Picker(selection: $sortOrder) {
Text("Croissant").tag(SortOrder.ascending)
Text("Décroissant").tag(SortOrder.descending)
} label: {
Text(sortOption == .team ? "Tri par rang" : "Tri")
}
}
} header: {
Text("Options d'affichage")
}
}
if searchText.isEmpty == false {
sortedPlayersView
} else {
if sortOption == .team && licenseMode == false {
if orderedTeams.isEmpty {
ContentUnavailableView.search(text: searchText)
} else {
ForEach(orderedTeams) { team in
Section {
ForEach(team.players()) { player in
computedPlayerView(player)
}
}
}
}
} else if sortOption == .round && licenseMode == false {
if orderedPlayers.isEmpty {
ContentUnavailableView.search(text: searchText)
} else {
ForEach(tournaments) { tournament in
let bracketPlayers = bracketPlayers(in: tournament)
Section {
DisclosureGroup {
ForEach(bracketPlayers) { player in
computedPlayerView(player)
}
} label: {
HStack {
Text("Poules")
Spacer()
Text(bracketPlayers.count.formatted())
}
}
} header: {
Text(tournament.tournamentTitle())
}
}
// ForEach(1...event.rounds) { round in
// ForEach(tournaments) { tournament in
// if tournament.isRoundHidden(round) == false {
// let players = playersForRound(in: round, tournament: tournament)
//
// if players.isEmpty == false {
// Section {
// DisclosureGroup {
// ForEach(players) { player in
// computedPlayerView(player)
// }
// } label: {
// HStack {
// Text(RoundLabel.labels[tournament.rounds - round])
// Spacer()
// Text(players.count.formatted())
// }
// }
// } header: {
// Text(tournament.localizedTitle)
// }
// }
// }
// }
// }
}
} else if sortOption == .callDate && licenseMode == false {
_byCallDateView()
} else {
sortedPlayersView
}
}
}
// .alert("Licence", isPresented: somePlayerToEdit(), actions: {
// TextField("Licence", text: $licenseToEdit)
// .keyboardType(.asciiCapable)
// .autocorrectionDisabled(true)
// .textContentType(.init(rawValue: ""))
// Button("OK") {
// editPlayer?.license = licenseToEdit
// licenseToEdit = ""
// editPlayer?.objectWillChange.send()
// editPlayer = nil
// save()
// }
// Button("Annuler", role : .cancel) {
// licenseToEdit = ""
// editPlayer = nil
// }
// })
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Menu {
// Button {
// event.orderedPlayers.forEach { player in
// if let registration = player.orderedRegistrations.first(where: { $0.tournament?.tournamentID == player.tournament?.tournamentID }) {
// if registration.paymentType == .notPaid {
// registration.paymentType = .gift
// }
// } else {
// let registration = Registration(context: viewContext)
// registration.paymentType = .gift
// registration.tournament = player.tournament
// player.addToRegistrations(registration)
// }
// save()
// }
// } label: {
// Text("Tout le monde a réglé")
// }
//
// Button {
// event.orderedPlayers.forEach { player in
// if let registration = player.orderedRegistrations.first(where: { $0.tournament?.tournamentID == player.tournament?.tournamentID }) {
// registration.paymentType = .notPaid
// } else {
// let registration = Registration(context: viewContext)
// registration.paymentType = .notPaid
// registration.tournament = player.tournament
// player.addToRegistrations(registration)
// }
// save()
// }
// } label: {
// Text("Personne n'a réglé")
// }
//
} label: {
LabelOptions()
}
}
}
.navigationTitle("Joueurs")
.navigationBarTitleDisplayMode(.large)
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: Text("Chercher un joueur"))
}
@ViewBuilder
func computedPlayerView(_ player: PlayerRegistration) -> some View {
VStack(alignment: .leading) {
ImportedPlayerView(player: player)
HStack {
Menu {
if let number = player.phoneNumber?.replacingOccurrences(of: " ", with: ""), let url = URL(string: "tel:\(number)") {
Link(destination: url) {
Label("Appeler", systemImage: "phone")
}
}
if let number = player.phoneNumber?.replacingOccurrences(of: " ", with: ""), let url = URL(string: "sms:\(number)") {
Link(destination: url) {
Label("SMS", systemImage: "message")
}
}
Divider()
if let licenseCheck, let licenseYearValidity = player.tournament()?.licenseYearValidity(), licenseCheck == false {
Button {
player.validateLicenceId(licenseYearValidity)
save()
if filteredPlayers.isEmpty {
dismiss()
}
} label: {
Text("Valider la licence \(licenseYearValidity)")
}
}
if let license = player.licenceId?.strippedLicense {
Button {
let pasteboard = UIPasteboard.general
pasteboard.string = license
} label: {
Label("Copier la licence", systemImage: "doc.on.doc")
}
}
Section {
Button {
licenseToEdit = player.licenceId ?? ""
editPlayer = player
} label: {
if player.licenceId == nil {
Text("Ajouter la licence")
} else {
Text("Modifier la licence")
}
}
PasteButton(payloadType: String.self) { strings in
guard let first = strings.first else { return }
player.licenceId = first
}
} header: {
Text("Modification de licence")
}
} label: {
Text("Options")
}
Spacer()
PlayerPayView(player: player)
}
}
}
private func _byCallDateView() -> some View {
ForEach(tournaments) { tournament in
let teams = tournament.selectedSortedTeams()
let players = teams.filter({ $0.callDate != nil }).sorted(using: .keyPath(\.callDate!)).flatMap({ $0.players() }) + teams.filter({ $0.callDate == nil }).flatMap({ $0.players() })
Section {
ForEach(players) { player in
computedPlayerView(player)
}
} header: {
Text(tournament.tournamentTitle())
}
.headerProminence(.increased)
}
}
}

@ -143,6 +143,10 @@ struct InscriptionManagerView: View {
if tournament.teamSorting == .inscriptionDate {
Divider()
_prioritizeClubMembersButton()
Button("Bloquer une place") {
_createTeam()
}
}
Divider()
Button {

@ -14,4 +14,6 @@ enum Screen: String, Codable {
case settings
case structure
case schedule
case cashier
case call
}

@ -0,0 +1,79 @@
//
// TournamentCallView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 16/04/2024.
//
import SwiftUI
enum CallDestination: String, Identifiable, Selectable {
case seeds
case groupStages
var id: String { self.rawValue }
func selectionLabel() -> String {
switch self {
case .seeds:
return "Têtes de série"
case .groupStages:
return "Poules"
}
}
func badgeValue() -> Int? {
nil
}
}
struct TournamentCallView: View {
var tournament: Tournament
@State private var selectedDestination: CallDestination?
let allDestinations: [CallDestination]
init(tournament: Tournament) {
self.tournament = tournament
var destinations = [CallDestination]()
let groupStageTeams = tournament.groupStageTeams()
if groupStageTeams.isEmpty == false {
destinations.append(.groupStages)
self._selectedDestination = State(wrappedValue: .groupStages)
}
if tournament.seededTeams().isEmpty == false {
destinations.append(.seeds)
if groupStageTeams.isEmpty {
self._selectedDestination = State(wrappedValue: .seeds)
}
}
self.allDestinations = destinations
}
var body: some View {
VStack(spacing: 0) {
GenericDestinationPickerView(selectedDestination: $selectedDestination, destinations: allDestinations, nilDestinationIsValid: true)
switch selectedDestination {
case .none:
CallSettingsView()
.navigationTitle("Réglages")
case .some(let selectedCall):
switch selectedCall {
case .groupStages:
GroupStageCallingView()
case .seeds:
SeedsCallingView()
}
}
}
.environment(tournament)
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Convocations")
}
}
#Preview {
TournamentCallView(tournament: Tournament.mock())
}

@ -20,6 +20,22 @@ struct TournamentRunningView: View {
Text("Horaires")
}
}
NavigationLink(value: Screen.call) {
LabeledContent {
Text(tournament.callStatus())
} label: {
Text("Convocations")
}
}
NavigationLink(value: Screen.cashier) {
LabeledContent {
Text(tournament.cashierStatus())
} label: {
Text("Encaissement")
}
}
}
if tournament.groupStages().isEmpty == false {

@ -80,6 +80,10 @@ struct TournamentView: View {
RoundsView(tournament: tournament)
case .schedule:
TournamentScheduleView(tournament: tournament)
case .cashier:
CashierView(tournament: tournament)
case .call:
TournamentCallView(tournament: tournament)
}
}
.environment(tournament)

Loading…
Cancel
Save