Compare commits

..

No commits in common. 'main' and 'sync3' have entirely different histories.
main ... sync3

  1. 4
      PadelClubData/Data/DataStore.swift
  2. 28
      PadelClubData/Data/Event.swift
  3. 9
      PadelClubData/Data/Gen/BaseTournament.swift
  4. 5
      PadelClubData/Data/Gen/Tournament.json
  5. 18
      PadelClubData/Data/GroupStage.swift
  6. 12
      PadelClubData/Data/Match.swift
  7. 34
      PadelClubData/Data/PlayerRegistration.swift
  8. 24
      PadelClubData/Data/Round.swift
  9. 58
      PadelClubData/Data/TeamRegistration.swift
  10. 71
      PadelClubData/Data/Tournament.swift
  11. 12
      PadelClubData/Extensions/Date+Extensions.swift
  12. 93
      PadelClubData/Extensions/String+Extensions.swift
  13. 95
      PadelClubData/Utils/ContactManager.swift
  14. 20
      PadelClubData/Utils/ExportFormat.swift
  15. 67
      PadelClubData/ViewModel/PadelRule.swift

@ -332,7 +332,7 @@ public class DataStore: ObservableObject {
_lastRunningAndNextCheckDate = nil
}
public func runningAndNextMatches(_ selectedTournaments: Set<String> = Set()) -> [Match] {
public func runningAndNextMatches() -> [Match] {
let dateNow : Date = Date()
if let lastCheck = _lastRunningAndNextCheckDate,
let cachedMatches = _cachedRunningAndNextMatches,
@ -340,7 +340,7 @@ public class DataStore: ObservableObject {
return cachedMatches
}
let lastTournaments = self.tournaments.filter { (selectedTournaments.isEmpty || selectedTournaments.contains($0.id) == false) && $0.isDeleted == false && $0.startDate <= dateNow.addingTimeInterval(86_400) && $0.hasEnded() == false }.sorted(by: \Tournament.startDate, order: .descending).prefix(10)
let lastTournaments = self.tournaments.filter { $0.isDeleted == false && $0.startDate <= dateNow && $0.hasEnded() == false }.sorted(by: \Tournament.startDate, order: .descending).prefix(10)
var runningMatches: [Match] = []
for tournament in lastTournaments {

@ -184,34 +184,6 @@ final public class Event: BaseEvent {
return link.compactMap({ $0 }).joined(separator: "\n\n")
}
public func selectedTeams() -> [TeamRegistration] {
confirmedTournaments().flatMap({ $0.selectedSortedTeams() })
}
public func umpireMail() -> [String]? {
confirmedTournaments().first?.umpireMail()
}
public func mailSubject() -> String {
let tournaments = confirmedTournaments()
guard !tournaments.isEmpty else {
return eventTitle()
}
// Get the date range from all confirmed tournaments
let dates = tournaments.compactMap { $0.startDate }
guard let firstDate = dates.min(), let lastDate = dates.max() else {
return eventTitle()
}
let dateRange = firstDate == lastDate
? firstDate.formattedDate(.short)
: "\(firstDate.formatted(.dateTime.day())) au \(lastDate.formatted(.dateTime.day())) \(lastDate.formatted(.dateTime.month(.wide))) \(lastDate.formatted(.dateTime.year()))"
let subject = [eventTitle(), dateRange, clubObject()?.name].compactMap({ $0 }).joined(separator: " | ")
return subject
}
func insertOnServer() throws {
DataStore.shared.events.writeChangeAndInsertOnServer(instance: self)

@ -85,7 +85,6 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
public var clubMemberFeeDeduction: Double? = nil
public var unregisterDeltaInHours: Int = 24
public var currencyCode: String? = nil
public var customClubName: String? = nil
public init(
id: String = Store.randomId(),
@ -159,8 +158,7 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
showTeamsInProg: Bool = false,
clubMemberFeeDeduction: Double? = nil,
unregisterDeltaInHours: Int = 24,
currencyCode: String? = nil,
customClubName: String? = nil
currencyCode: String? = nil
) {
super.init()
self.id = id
@ -235,7 +233,6 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
self.clubMemberFeeDeduction = clubMemberFeeDeduction
self.unregisterDeltaInHours = unregisterDeltaInHours
self.currencyCode = currencyCode
self.customClubName = customClubName
}
required public override init() {
super.init()
@ -316,7 +313,6 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
case _clubMemberFeeDeduction = "clubMemberFeeDeduction"
case _unregisterDeltaInHours = "unregisterDeltaInHours"
case _currencyCode = "currencyCode"
case _customClubName = "customClubName"
}
private static func _decodePayment(container: KeyedDecodingContainer<CodingKeys>) throws -> TournamentPayment? {
@ -460,7 +456,6 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
self.clubMemberFeeDeduction = try container.decodeIfPresent(Double.self, forKey: ._clubMemberFeeDeduction) ?? nil
self.unregisterDeltaInHours = try container.decodeIfPresent(Int.self, forKey: ._unregisterDeltaInHours) ?? 24
self.currencyCode = try container.decodeIfPresent(String.self, forKey: ._currencyCode) ?? nil
self.customClubName = try container.decodeIfPresent(String.self, forKey: ._customClubName) ?? nil
try super.init(from: decoder)
}
@ -538,7 +533,6 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
try container.encode(self.clubMemberFeeDeduction, forKey: ._clubMemberFeeDeduction)
try container.encode(self.unregisterDeltaInHours, forKey: ._unregisterDeltaInHours)
try container.encode(self.currencyCode, forKey: ._currencyCode)
try container.encode(self.customClubName, forKey: ._customClubName)
try super.encode(to: encoder)
}
@ -621,7 +615,6 @@ public class BaseTournament: SyncedModelObject, SyncedStorable {
self.clubMemberFeeDeduction = tournament.clubMemberFeeDeduction
self.unregisterDeltaInHours = tournament.unregisterDeltaInHours
self.currencyCode = tournament.currencyCode
self.customClubName = tournament.customClubName
}
public static func parentRelationships() -> [Relationship] {

@ -372,11 +372,6 @@
"name": "currencyCode",
"type": "String",
"optional": true
},
{
"name": "customClubName",
"type": "String",
"optional": true
}
]
}

@ -337,7 +337,7 @@ final public class GroupStage: BaseGroupStage, SideStorable {
case 5:
order = [3, 5, 8, 2, 6, 1, 9, 4, 7, 0]
case 6:
order = [4, 6, 10, 1, 8, 12, 2, 7, 11, 3, 5, 13, 14, 9, 0]
order = [4, 7, 9, 3, 6, 11, 2, 8, 10, 1, 13, 5, 12, 14, 0]
case 7:
order = [6, 15, 20, 1, 16, 19, 2, 10, 18, 3, 9, 14, 4, 7, 12, 5, 8, 11, 0, 13, 17]
case 8:
@ -629,22 +629,6 @@ final public class GroupStage: BaseGroupStage, SideStorable {
tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams)
}
public func setData(from correspondingGroupStage: GroupStage, tournamentStartDate: Date, previousTournamentStartDate: Date) {
self.matchFormat = correspondingGroupStage.matchFormat
if let correspondingPlannedStartDate = correspondingGroupStage.plannedStartDate {
let offset = correspondingPlannedStartDate.timeIntervalSince(previousTournamentStartDate)
self.startDate = tournamentStartDate.addingTimeInterval(offset)
}
self.size = correspondingGroupStage.size
self.name = correspondingGroupStage.name
let matches = correspondingGroupStage._matches()
for (index, match) in self._matches().enumerated() {
match.setData(from: matches[index], tournamentStartDate: tournamentStartDate, previousTournamentStartDate: previousTournamentStartDate)
}
}
public override func deleteDependencies(store: Store, actionOption: ActionOption) {
store.deleteDependencies(type: Match.self, actionOption: actionOption) { $0.groupStage == self.id }
}

@ -1140,18 +1140,6 @@ defer {
plannedStartDate ?? startDate
}
public func setData(from correspondingMatch: Match, tournamentStartDate: Date, previousTournamentStartDate: Date) {
if let correspondingMatchPlannedStartDate = correspondingMatch.plannedStartDate {
let offset = correspondingMatchPlannedStartDate.timeIntervalSince(previousTournamentStartDate)
self.startDate = tournamentStartDate.addingTimeInterval(offset)
}
self.disabled = correspondingMatch.disabled
self.matchFormat = correspondingMatch.matchFormat
self.courtIndex = correspondingMatch.courtIndex
self.name = correspondingMatch.name
}
func insertOnServer() {
self.tournamentStore?.matches.writeChangeAndInsertOnServer(instance: self)
for teamScore in self.teamScores {

@ -56,23 +56,12 @@ final public class PlayerRegistration: BasePlayerRegistration, SideStorable {
return nil
}
public func pasteData(_ exportFormat: ExportFormat = .rawText, type: ExportType) -> String {
switch type {
case .payment:
switch exportFormat {
case .rawText:
return [firstName.capitalized, lastName.capitalized, hasPaidOnline() ? "Payé [X]" : "Payé  [ ]"].compactMap({ $0 }).joined(separator: exportFormat.separator())
case .csv:
return [lastName.uppercased() + " " + firstName.capitalized].joined(separator: exportFormat.separator())
}
case .sharing:
public func pasteData(_ exportFormat: ExportFormat = .rawText) -> String {
switch exportFormat {
case .rawText:
return [firstName.capitalized, lastName.capitalized, licenceId?.computedLicense].compactMap({ $0 }).joined(separator: exportFormat.separator())
case .csv:
return [lastName.uppercased() + " " + firstName.capitalized, hasPaid() ? "Payé" : "", hasPaidOnline() ? "En ligne" : ""]
.joined(separator: exportFormat.separator())
}
return [lastName.uppercased() + " " + firstName.capitalized].joined(separator: exportFormat.separator())
}
}
@ -81,13 +70,6 @@ final public class PlayerRegistration: BasePlayerRegistration, SideStorable {
}
public func contains(_ searchField: String) -> Bool {
if let paymentId, paymentId.localizedCaseInsensitiveContains(searchField) { return true }
if let email, email.lowercased().localizedCaseInsensitiveContains(searchField) { return true }
if let contactEmail, contactEmail.localizedCaseInsensitiveContains(searchField) { return true }
if let licenceId, licenceId.localizedCaseInsensitiveContains(searchField) { return true }
if searchField.isPhoneNumber(), let phoneNumber, phoneNumber.isSamePhoneNumber(as: searchField) { return true }
if searchField.isPhoneNumber(), let contactPhoneNumber, contactPhoneNumber.isSamePhoneNumber(as: searchField) { return true }
let nameComponents = searchField.canonicalVersion.split(separator: " ")
if nameComponents.count > 1 {
@ -185,7 +167,7 @@ final public class PlayerRegistration: BasePlayerRegistration, SideStorable {
}
public func setComputedRank(in tournament: Tournament) {
let maleUnranked = tournament.unrankValue(for: isMalePlayer()) ?? 92_327
let maleUnranked = tournament.unrankValue(for: isMalePlayer()) ?? 90_415
let femaleUnranked = tournament.unrankValue(for: false) ?? 0
let currentRank = rank ?? maleUnranked
switch tournament.tournamentCategory {
@ -230,16 +212,6 @@ final public class PlayerRegistration: BasePlayerRegistration, SideStorable {
registrationStatus = .confirmed
}
public func hasMail() -> Bool {
let mails = [email, contactEmail].compactMap({ $0 })
return mails.isEmpty == false && mails.anySatisfy({ $0.isValidEmail() })
}
public func hasMobilePhone() -> Bool {
let phones = [phoneNumber, contactPhoneNumber].compactMap({ $0 })
return phones.isEmpty == false && phones.anySatisfy({ $0.isPhoneNumber() })
}
public func paidAmount(_ tournament: Tournament, accountForGiftOrForfeit: Bool = false) -> Double {
if accountForGiftOrForfeit == false, paymentType == .gift {
return 0.0

@ -56,30 +56,6 @@ final public class Round: BaseRound, SideStorable {
return tournamentStore.matches.filter { $0.round == self.id && $0.disabled == true }
}
public func setData(from correspondingRound: Round, tournamentStartDate: Date, previousTournamentStartDate: Date) {
let matches = correspondingRound._matches()
for (index, match) in self._matches().enumerated() {
match.setData(from: matches[index], tournamentStartDate: tournamentStartDate, previousTournamentStartDate: previousTournamentStartDate)
}
self.matchFormat = correspondingRound.matchFormat
if let correspondingPlannedStartDate = correspondingRound.plannedStartDate {
let offset = correspondingPlannedStartDate.timeIntervalSince(previousTournamentStartDate)
self.startDate = tournamentStartDate.addingTimeInterval(offset)
}
self.loserBracketMode = correspondingRound.loserBracketMode
self.groupStageLoserBracket = correspondingRound.groupStageLoserBracket
loserRounds().forEach { round in
if let pRound = correspondingRound.loserRounds().first(where: { r in
r.index == round.index
}) {
round.setData(from: pRound, tournamentStartDate: tournamentStartDate, previousTournamentStartDate: previousTournamentStartDate)
}
}
}
// MARK: -
public var matchFormat: MatchFormat {

@ -171,7 +171,7 @@ final public class TeamRegistration: BaseTeamRegistration, SideStorable {
} else if let roundMatchStartDate = initialMatch()?.startDate {
return roundMatchStartDate
}
return callDate
return nil
}
public var initialWeight: Int {
@ -378,20 +378,14 @@ final public class TeamRegistration: BaseTeamRegistration, SideStorable {
resetBracketPosition()
}
public func pasteData(_ exportFormat: ExportFormat = .rawText, type: ExportType, _ index: Int = 0) -> String {
public func pasteData(_ exportFormat: ExportFormat = .rawText, _ index: Int = 0) -> String {
switch exportFormat {
case .rawText:
switch type {
case .sharing:
return [playersPasteData(exportFormat, type: type), formattedInscriptionDate(exportFormat), name]
.compactMap({ $0 }).joined(separator: exportFormat.newLineSeparator())
case .payment:
return [playersPasteData(exportFormat, type: type), name]
return [playersPasteData(exportFormat), formattedInscriptionDate(exportFormat), name]
.compactMap({ $0 }).joined(separator: exportFormat.newLineSeparator())
}
case .csv:
return [
index.formatted(), playersPasteData(exportFormat, type: type),
index.formatted(), playersPasteData(exportFormat),
isWildCard() ? "WC" : weight.formatted(),
].joined(separator: exportFormat.separator())
}
@ -441,20 +435,15 @@ final public class TeamRegistration: BaseTeamRegistration, SideStorable {
}
}
public func playersPasteData(_ exportFormat: ExportFormat = .rawText, type: ExportType) -> String {
public func playersPasteData(_ exportFormat: ExportFormat = .rawText) -> String {
switch exportFormat {
case .rawText:
return players().map { $0.pasteData(exportFormat, type: type) }.joined(
return players().map { $0.pasteData(exportFormat) }.joined(
separator: exportFormat.newLineSeparator())
case .csv:
return players().map {
switch type {
case .sharing:
[$0.pasteData(exportFormat, type: type), isWildCard() ? "WC" : $0.computedRank.formatted()]
[$0.pasteData(exportFormat), isWildCard() ? "WC" : $0.computedRank.formatted()]
.joined(separator: exportFormat.separator())
case .payment:
$0.pasteData(exportFormat, type: type)
}
}.joined(separator: exportFormat.separator())
}
}
@ -551,7 +540,7 @@ final public class TeamRegistration: BaseTeamRegistration, SideStorable {
}
public func unrankValue(for malePlayer: Bool) -> Int {
return tournamentObject()?.unrankValue(for: malePlayer) ?? 92_327
return tournamentObject()?.unrankValue(for: malePlayer) ?? 90_415
}
public func groupStageObject() -> GroupStage? {
@ -688,37 +677,6 @@ final public class TeamRegistration: BaseTeamRegistration, SideStorable {
return nil
}
public func followingMatches() -> [Match] {
guard let tournamentStore else { return [] }
let allTeamScores = tournamentStore.teamScores.filter({ $0.teamRegistration == self.id })
let ids = allTeamScores.compactMap({ $0.match })
let matches = tournamentStore.matches.filter({ match in
ids.contains(match.id)
})
return matches.sorted(by: \.computedStartDateForSorting)
}
public func nextMatch(in followingMatches: [Match]) -> Match? {
return followingMatches.filter({ $0.hasEnded() == false }).first
}
public func lastMatchPlayed(in followingMatches: [Match]) -> Match? {
return followingMatches.first(where: { $0.hasEnded() })
}
public func numberOfRotation(in followingMatches: [Match]) -> (Int, Int)? {
if let nextMatch = nextMatch(in: followingMatches), let nextMatchPlannedStartDate = nextMatch.plannedStartDate, let lastMatchPlayed = lastMatchPlayed(in: followingMatches), let lastMatchPlayedPlannedStartDate = lastMatchPlayed.plannedStartDate {
let courtCount = self.tournamentStore?.matches.filter({ $0.plannedStartDate == nextMatchPlannedStartDate && $0.disabled == false && $0.hasEnded() == false && $0.confirmed == true && $0.id != nextMatch.id && $0.hasStarted() == false }).count ?? 0
let interval = nextMatchPlannedStartDate.timeIntervalSince(lastMatchPlayedPlannedStartDate)
let matchDuration = lastMatchPlayed.matchFormat.defaultEstimatedDuration * 60
let rotation = Int(interval) / matchDuration
print("numberOfRotation", interval, matchDuration, courtCount, rotation)
return (rotation, (rotation - 1) * lastMatchPlayed.courtCount() + courtCount + 1)
}
return nil
}
func insertOnServer() {
self.tournamentStore?.teamRegistrations.writeChangeAndInsertOnServer(instance: self)
for playerRegistration in self.unsortedPlayers() {

@ -125,16 +125,10 @@ final public class Tournament: BaseTournament {
public func groupStages(atStep step: Int = 0) -> [GroupStage] {
guard let tournamentStore = self.tournamentStore else { return [] }
let groupStages: [GroupStage] = tournamentStore.groupStages.filter { $0.step == step }
let groupStages: [GroupStage] = tournamentStore.groupStages.filter { $0.tournament == self.id && $0.step == step }
return groupStages.sorted(by: \.index)
}
public func hasGroupeStages() -> Bool {
if groupStageCount > 0 { return true }
guard let tournamentStore = self.tournamentStore else { return false }
return tournamentStore.groupStages.isEmpty == false
}
public func allGroupStages() -> [GroupStage] {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.groupStages.sorted(by: \GroupStage.computedOrder)
@ -277,24 +271,17 @@ defer {
return Store.main.findById(event)
}
public func pasteDataForImporting(_ exportFormat: ExportFormat = .rawText, type: ExportType) -> String {
public func pasteDataForImporting(_ exportFormat: ExportFormat = .rawText) -> String {
let _selectedSortedTeams = selectedSortedTeams()
let selectedSortedTeams = _selectedSortedTeams + waitingListSortedTeams(selectedSortedTeams: _selectedSortedTeams)
switch exportFormat {
case .rawText:
let waitingList = waitingListTeams(in: selectedSortedTeams, includingWalkOuts: true)
var stats = [String]()
if type == .payment, isAnimation(), minimumPlayerPerTeam == 1 {
stats += ["\(self.selectedPlayers().count.formatted()) personnes"]
} else {
stats += [selectedSortedTeams.count.formatted() + " équipes"]
}
return (stats + selectedSortedTeams.compactMap { $0.pasteData(exportFormat, type: type) } + (waitingList.isEmpty == false ? ["Liste d'attente"] : []) + waitingList.compactMap { $0.pasteData(exportFormat, type: type) }).joined(separator: exportFormat.newLineSeparator(1))
return (selectedSortedTeams.compactMap { $0.pasteData(exportFormat) } + ["Liste d'attente"] + waitingListTeams(in: selectedSortedTeams, includingWalkOuts: true).compactMap { $0.pasteData(exportFormat) }).joined(separator: exportFormat.newLineSeparator(2))
case .csv:
let headers = ["", "Nom Prénom", "rang", "Nom Prénom", "rang", "poids", "Paire"].joined(separator: exportFormat.separator())
var teamPaste = [headers]
for (index, team) in selectedSortedTeams.enumerated() {
var teamData = team.pasteData(exportFormat, type: type, index + 1)
var teamData = team.pasteData(exportFormat, index + 1)
teamData.append(exportFormat.separator())
teamData.append(team.teamLastNames().joined(separator: " / "))
teamPaste.append(teamData)
@ -721,7 +708,7 @@ defer {
var groupStageTeamCount: Int = groupStageSpots - wcGroupStage.count
if groupStageTeamCount < 0 { groupStageTeamCount = 0 }
if bracketSeeds < 0 { bracketSeeds = 0 }
let clubName = self.clubName
if prioritizeClubMembers {
var bracketTeams: [TeamRegistration] = []
@ -1222,7 +1209,14 @@ defer {
}
public func formattedDate(_ displayStyle: DisplayStyle = .wide) -> String {
startDate.formattedDate(displayStyle)
switch displayStyle {
case .title:
startDate.formatted(.dateTime.weekday(.abbreviated).day().month(.abbreviated).year())
case .wide:
startDate.formatted(date: Date.FormatStyle.DateStyle.complete, time: Date.FormatStyle.TimeStyle.omitted)
case .short:
startDate.formatted(date: .numeric, time: .omitted)
}
}
public func qualifiedFromGroupStage() -> Int {
@ -1472,7 +1466,7 @@ defer {
var _groupStages = [GroupStage]()
for index in 0..<groupStageCount {
let groupStage = GroupStage(tournament: id, index: index, size: teamsPerGroupStage, format: groupStageSmartMatchFormat())
let groupStage = GroupStage(tournament: id, index: index, size: teamsPerGroupStage, format: groupStageFormat)
_groupStages.append(groupStage)
}
@ -1553,9 +1547,6 @@ defer {
public func deleteGroupStages() {
self.tournamentStore?.groupStages.delete(contentOfs: allGroupStages())
if let gs = self.groupStageLoserBracket() {
self.tournamentStore?.rounds.delete(instance: gs)
}
}
public func refreshGroupStages(keepExistingMatches: Bool = false) {
@ -1720,6 +1711,7 @@ defer {
public func groupStageSmartMatchFormat() -> MatchFormat {
let format = tournamentLevel.federalFormatForGroupStage()
if tournamentLevel == .p25 { return .superTie }
if format.rank < groupStageMatchFormat.rank {
return format
} else {
@ -1857,6 +1849,7 @@ defer {
public func roundSmartMatchFormat(_ roundIndex: Int) -> MatchFormat {
let format = tournamentLevel.federalFormatForBracketRound(roundIndex)
if tournamentLevel == .p25 { return .superTie }
if format.rank < matchFormat.rank {
return format
} else {
@ -2124,17 +2117,6 @@ defer {
}
}
public func removeRound(_ round: Round) async {
await MainActor.run {
let teams = round.seeds()
teams.forEach { team in
team.resetBracketPosition()
}
tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams)
tournamentStore?.rounds.delete(instance: round)
}
}
public func addNewRound(_ roundIndex: Int) async {
await MainActor.run {
let round = Round(tournament: id, index: roundIndex, format: matchFormat)
@ -2320,11 +2302,7 @@ defer {
}
public func onlineTeams() -> [TeamRegistration] {
// guard let teamRegistrations = tournamentStore?.teamRegistrations else { return [] }
// return teamRegistrations.cached(key: "online") { collection in
// collection.filter { $0.hasRegisteredOnline() }
// }
return unsortedTeams().filter({ $0.hasRegisteredOnline() })
unsortedTeams().filter({ $0.hasRegisteredOnline() })
}
public func paidOnlineTeams() -> [TeamRegistration] {
@ -2359,7 +2337,7 @@ defer {
}
public func mailSubject() -> String {
let subject = [tournamentTitle(hideSenior: true), formattedDate(.short), customClubName ?? clubName].compactMap({ $0 }).joined(separator: " | ")
let subject = [tournamentTitle(hideSenior: true), formattedDate(.short), clubName].compactMap({ $0 }).joined(separator: " | ")
return subject
}
@ -2464,9 +2442,6 @@ defer {
}
public func addon(for playerRank: Int, manMax: Int, womanMax: Int) -> Int {
if tournamentCategory != .men {
return 0
}
switch playerRank {
case 0: return 0
case womanMax: return manMax - womanMax
@ -2506,16 +2481,6 @@ defer {
self.tournamentStore?.rounds.addOrUpdate(contentOfs: allRounds)
}
public func formatSummary() -> String {
var label = [String]()
if groupStageCount > 0 {
label.append("Poules " + groupStageMatchFormat.format)
}
label.append("Tableau " + matchFormat.format)
label.append("Classement " + loserBracketMatchFormat.format)
return label.joined(separator: ", ")
}
// MARK: -
func insertOnServer() throws {

@ -311,18 +311,6 @@ public extension Date {
let calendar = Calendar.current
return calendar.date(bySetting: .minute, value: 0, of: self)!.withoutSeconds()
}
func formattedDate(_ displayStyle: DisplayStyle = .wide) -> String {
switch displayStyle {
case .title:
self.formatted(.dateTime.weekday(.abbreviated).day().month(.abbreviated).year())
case .wide:
self.formatted(date: Date.FormatStyle.DateStyle.complete, time: Date.FormatStyle.TimeStyle.omitted)
case .short:
self.formatted(date: .numeric, time: .omitted)
}
}
}
public extension Date {

@ -213,96 +213,16 @@ public extension String {
// MARK: - FFT Source Importing
public extension String {
enum RegexStatic {
// Patterns for France only
static let phoneNumber = /^(\+33|0033|33|0)[1-9][0-9]{8}$/
static let phoneNumberWithExtra0 = /^33[0][1-9][0-9]{8}$/
static let mobileNumber = /^(\+33|0033|33|0)[6-7][0-9]{8}$/
static let mobileNumberWithExtra0 = /^33[0][6-7][0-9]{8}$/
}
private func cleanedNumberForValidation() -> String {
// Keep leading '+' if present, remove all other non-digit characters
var cleaned = self.trimmingCharacters(in: .whitespacesAndNewlines)
if cleaned.hasPrefix("+") {
// Preserve '+' at start, remove all other non-digit characters
let digitsOnly = cleaned.dropFirst().components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
cleaned = "+" + digitsOnly
} else {
// Remove all non-digit characters
cleaned = cleaned.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
}
return cleaned
}
// MARK: - Phone Number Validation
/// Validate if the string is a mobile number for the specified locale.
/// - Parameter locale: The locale to validate against. Defaults to `.current`.
/// - Returns: True if the string matches the mobile number pattern for the locale.
func isMobileNumber(locale: Locale = .current) -> Bool {
// TODO: Support additional regions/locales in the future.
switch locale.region?.identifier {
case "FR", "fr", nil:
// French logic for now
let cleaned = cleanedNumberForValidation()
if cleaned.firstMatch(of: RegexStatic.mobileNumber) != nil {
return true
}
if cleaned.firstMatch(of: RegexStatic.mobileNumberWithExtra0) != nil {
return true
}
return false
default:
// For unsupported locales, fallback to checking if the string contains at least 8 digits
// This is a generic minimum length for most countries' phone numbers
let digitsOnly = self.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
return digitsOnly.count >= 8
}
static let mobileNumber = /^(?:\+33|0033|0)[6-7](?:[ .-]?[0-9]{2}){4}$/
static let phoneNumber = /^(?:\+33|0033|0)[1-9](?:[ .-]?[0-9]{2}){4}$/
}
/// Validate if the string is a phone number for the specified locale.
/// - Parameter locale: The locale to validate against. Defaults to `.current`.
/// - Returns: True if the string matches the phone number pattern for the locale.
func isPhoneNumber(locale: Locale = .current) -> Bool {
// TODO: Support additional regions/locales in the future.
switch locale.region?.identifier {
case "FR", "fr", nil:
// French logic for now
let cleaned = cleanedNumberForValidation()
if cleaned.firstMatch(of: RegexStatic.phoneNumber) != nil {
return true
}
if cleaned.firstMatch(of: RegexStatic.phoneNumberWithExtra0) != nil {
return true
}
return false
default:
// For unsupported locales, fallback to checking if the string contains at least 8 digits
// This is a generic minimum length for most countries' phone numbers
let digitsOnly = self.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
return digitsOnly.count >= 8
}
}
func normalize(_ phone: String) -> String {
var normalized = phone.trimmingCharacters(in: .whitespacesAndNewlines)
// Remove all non-digit characters
normalized = normalized.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
// Remove leading country code for France (33) if present
if normalized.hasPrefix("33") {
if normalized.dropFirst(2).hasPrefix("0") {
// Keep as is, don't strip the zero after 33
} else {
normalized = "0" + normalized.dropFirst(2)
}
} else if normalized.hasPrefix("0033") {
normalized = "0" + normalized.dropFirst(4)
}
return normalized
func isMobileNumber() -> Bool {
firstMatch(of: RegexStatic.mobileNumber) != nil
}
func isSamePhoneNumber(as other: String) -> Bool {
return normalize(self) == normalize(other)
func isPhoneNumber() -> Bool {
firstMatch(of: RegexStatic.phoneNumber) != nil
}
func cleanSearchText() -> String {
@ -392,4 +312,3 @@ public extension String {
return self // Return the original string if parsing fails
}
}

@ -55,89 +55,6 @@ public enum ContactManagerError: LocalizedError {
}
}
public enum SummonType: Int, Identifiable {
case contact
case contactWithoutSignature
case summon
case summonWalkoutFollowUp
case summonErrorFollowUp
public func isRecall() -> Bool {
switch self {
case .contact, .contactWithoutSignature:
return false
case .summon:
return false
case .summonWalkoutFollowUp:
return true
case .summonErrorFollowUp:
return true
}
}
public func mainWord() -> String {
switch self {
case .contact:
return "Contacter"
case .contactWithoutSignature:
return "Contacter"
case .summon:
return "Convoquer"
case .summonWalkoutFollowUp:
return "Reconvoquer"
case .summonErrorFollowUp:
return "Reconvoquer"
}
}
public func caption() -> String? {
switch self {
case .contact:
return nil
case .contactWithoutSignature:
return "Sans texte par défaut"
case .summon:
return nil
case .summonWalkoutFollowUp:
return "Suite à un forfait"
case .summonErrorFollowUp:
return "Suite à une erreur"
}
}
public func shouldConfirm() -> Bool {
switch self {
case .contact, .contactWithoutSignature:
return false
case .summon:
return true
case .summonWalkoutFollowUp:
return true
case .summonErrorFollowUp:
return true
}
}
public func intro() -> String {
switch self {
case .contactWithoutSignature:
return ""
case .contact:
return "Vous êtes"
case .summon:
return "Vous êtes"
case .summonWalkoutFollowUp:
return "Suite à des forfaits, vous êtes finalement"
case .summonErrorFollowUp:
return "Suite à une erreur, vous êtes finalement"
}
}
public var id: Int {
self.rawValue
}
}
public enum ContactType: Identifiable {
case mail(date: Date?, recipients: [String]?, bccRecipients: [String]?, body: String?, subject: String?, tournamentBuild: TournamentBuild?)
case message(date: Date?, recipients: [String]?, body: String?, tournamentBuild: TournamentBuild?)
@ -159,7 +76,7 @@ Il est conseillé de vous présenter 10 minutes avant de jouer.\n\nMerci de me c
static func callingCustomMessage(source: String? = nil, tournament: Tournament?, startDate: Date?, roundLabel: String) -> String {
let tournamentCustomMessage = source ?? DataStore.shared.user.summonsMessageBody ?? defaultCustomMessage
let clubName = tournament?.customClubName ?? tournament?.clubName ?? ""
let clubName = tournament?.clubName ?? ""
var text = tournamentCustomMessage
let date = startDate ?? tournament?.startDate ?? Date()
@ -180,10 +97,8 @@ Il est conseillé de vous présenter 10 minutes avant de jouer.\n\nMerci de me c
return text
}
static func callingMessage(tournament: Tournament?, startDate: Date?, roundLabel: String, matchFormat: MatchFormat?, summonType: SummonType = .summon) -> String {
if summonType == .contactWithoutSignature {
return ""
}
static func callingMessage(tournament: Tournament?, startDate: Date?, roundLabel: String, matchFormat: MatchFormat?, reSummon: Bool = false) -> String {
let useFullCustomMessage = DataStore.shared.user.summonsUseFullCustomMessage
if useFullCustomMessage {
@ -192,7 +107,7 @@ Il est conseillé de vous présenter 10 minutes avant de jouer.\n\nMerci de me c
let date = startDate ?? tournament?.startDate ?? Date()
let clubName = tournament?.customClubName ?? tournament?.clubName ?? ""
let clubName = tournament?.clubName ?? ""
let message = DataStore.shared.user.summonsMessageBody ?? defaultCustomMessage
let signature = DataStore.shared.user.getSummonsMessageSignature() ?? DataStore.shared.user.defaultSignature(tournament)
@ -214,7 +129,7 @@ Il est conseillé de vous présenter 10 minutes avant de jouer.\n\nMerci de me c
[entryFeeMessage, message, linkMessage].compacted().map { $0.trimmedMultiline }.joined(separator: "\n\n")
}
let intro = summonType.intro()
let intro = reSummon ? "Suite à des forfaits, vous êtes finalement" : "Vous êtes"
if let tournament {
return "Bonjour,\n\n\(intro) \(localizedCalled) pour jouer en \(roundLabel.lowercased()) du \(tournament.tournamentTitle(.title, hideSenior: true)) au \(clubName) le \(date.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(date.formatted(Date.FormatStyle().hour().minute())).\n\n" + computedMessage + "\n\n\(signature)"

@ -35,23 +35,3 @@ public enum ExportFormat: Int, Identifiable, CaseIterable {
return Array(repeating: "\n", count: count).joined()
}
}
public enum ExportType: Int, Identifiable, CaseIterable {
public var id: Int { self.rawValue }
case sharing
case payment
public func localizedString() -> String {
switch self {
case .sharing:
return "inscriptions"
case .payment:
return "pointage"
}
}
public func newLineSeparator(_ count: Int = 1) -> String {
return Array(repeating: "\n", count: count).joined()
}
}

@ -349,9 +349,7 @@ public enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifi
return 4
} else {
switch level {
case .p25:
return 4
case .p100, .p250:
case .p25, .p100, .p250:
if category == .women {
return 4
}
@ -712,7 +710,7 @@ public enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable
case 13...16:
return [175,150,138,113,100,88,75,63,53,45,38,25,13,8,3]
case 17...20:
return [188,163,150,138,125,113,100,88,75,63,58,50,45,38,30,25,13,8,3]
return [188,163,150,138,123,113,100,88,75,63,58,50,45,38,30,25,13,8,3]
case 21...24:
return [188,175,163,150,138,125,118,108,100,93,83,75,70,63,58,50,45,38,30,25,13,8,3]
case 25...28:
@ -1201,7 +1199,6 @@ public enum TeamPosition: Int, Identifiable, Hashable, Codable, CaseIterable {
public enum SetFormat: Int, Hashable, Codable {
case nine
case four
case three
case six
case superTieBreak
case megaTieBreak
@ -1228,10 +1225,6 @@ public enum SetFormat: Int, Hashable, Codable {
if teamOne == 5 || teamTwo == 5 {
return true
}
case .three:
if teamOne == 4 || teamTwo == 4 {
return true
}
case .six:
if teamOne == 7 || teamTwo == 7 {
return true
@ -1250,8 +1243,6 @@ public enum SetFormat: Int, Hashable, Codable {
return 8
case .four:
return 4
case .three:
return 3
case .six:
return 6
case .superTieBreak, .megaTieBreak:
@ -1269,10 +1260,6 @@ public enum SetFormat: Int, Hashable, Codable {
if teamOneScore == 4 {
return []
}
case .three:
if teamOneScore == 3 {
return []
}
case .six:
if teamOneScore == 6 {
return []
@ -1295,8 +1282,6 @@ public enum SetFormat: Int, Hashable, Codable {
return [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
case .four:
return [5, 4, 3, 2, 1, 0]
case .three:
return [4, 3, 2, 1, 0]
case .six:
return [7, 6, 5, 4, 3, 2, 1, 0]
case .superTieBreak:
@ -1312,8 +1297,6 @@ public enum SetFormat: Int, Hashable, Codable {
return 9
case .four:
return 4
case .three:
return 4
case .six:
return 6
case .superTieBreak:
@ -1381,15 +1364,15 @@ public enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable {
return 2
case .nineGames, .nineGamesDecisivePoint:
return 3
case .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint:
return 4
case .superTie:
return 5
return 4
case .megaTie:
return 6
return 5
case .twoSetsOfSuperTie:
return 7
return 6
case .singleSet, .singleSetDecisivePoint:
return 7
case .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint:
return 8
}
}
@ -1412,15 +1395,15 @@ public enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable {
return 3
case .nineGamesDecisivePoint:
return 3
case .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint:
return 4
case .superTie:
return 5
return 4
case .megaTie:
return 6
return 5
case .twoSetsOfSuperTie:
return 7
return 6
case .singleSet, .singleSetDecisivePoint:
return 7
case .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint:
return 8
}
}
@ -1658,7 +1641,7 @@ public enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable {
case .megaTie:
return "supertie de 15 points"
case .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint:
return "1 set de 4 jeux, tiebreak à 3/3"
return "1 set de 4 jeux, tiebreak à 4/4"
}
}
@ -1687,10 +1670,8 @@ public enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable {
switch self {
case .twoSets, .twoSetsSuperTie, .twoSetsDecisivePoint, .twoSetsDecisivePointSuperTie, .singleSet, .singleSetDecisivePoint:
return .six
case .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint:
case .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint:
return .four
case .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint:
return .three
case .nineGames, .nineGamesDecisivePoint:
return .nine
case .superTie, .twoSetsOfSuperTie:
@ -1869,16 +1850,6 @@ public enum RoundRule {
}
}
public static func cumulatedNumberOfMatches(forTeams teams: Int) -> Int {
var i = teams / 2
var loserTeams = teams / 2
while loserTeams > 1 {
i += Self.cumulatedNumberOfMatches(forTeams: loserTeams)
loserTeams = loserTeams / 2
}
return i
}
public static func teamsInFirstRound(forTeams teams: Int) -> Int {
Int(pow(2.0, ceil(log2(Double(teams)))))
}
@ -1909,16 +1880,12 @@ public enum RoundRule {
}
public static func numberOfMatches(forRoundIndex roundIndex: Int) -> Int {
(1 << roundIndex)
}
public static func baseIndex(forRoundIndex roundIndex: Int) -> Int {
numberOfMatches(forRoundIndex: roundIndex) - 1
Int(pow(2.0, Double(roundIndex)))
}
static func matchIndexWithinRound(fromMatchIndex matchIndex: Int) -> Int {
let roundIndex = roundIndex(fromMatchIndex: matchIndex)
let matchIndexWithinRound = matchIndex - baseIndex(forRoundIndex: roundIndex)
let matchIndexWithinRound = matchIndex - (Int(pow(2.0, Double(roundIndex))) - 1)
return matchIndexWithinRound
}
@ -1942,7 +1909,7 @@ public enum RoundRule {
}
return "Quart de finale"
default:
return "\((1 << roundIndex))ème"
return "\(Int(pow(2.0, Double(roundIndex))))ème"
}
}
}

Loading…
Cancel
Save