add premise of court availability

fix issue with scheduler
fix issue on seeding
add the ability to export / import quickly a list of players from padel club export
multistore
Razmig Sarkissian 2 years ago
parent b9f2048546
commit df56b384e0
  1. 8
      PadelClub.xcodeproj/project.pbxproj
  2. 24
      PadelClub/Data/Federal/FederalPlayer.swift
  3. 10
      PadelClub/Data/Match.swift
  4. 13
      PadelClub/Data/MockData.swift
  5. 4
      PadelClub/Data/PlayerRegistration.swift
  6. 35
      PadelClub/Data/Tournament.swift
  7. 19
      PadelClub/Extensions/Date+Extensions.swift
  8. 260
      PadelClub/Manager/FileImportManager.swift
  9. 87
      PadelClub/Manager/PadelRule.swift
  10. 32
      PadelClub/ViewModel/DateInterval.swift
  11. 104
      PadelClub/ViewModel/MatchScheduler.swift
  12. 2
      PadelClub/ViewModel/SearchViewModel.swift
  13. 22
      PadelClub/ViewModel/SeedInterval.swift
  14. 1
      PadelClub/Views/Event/EventCreationView.swift
  15. 3
      PadelClub/Views/Match/MatchDetailView.swift
  16. 157
      PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift
  17. 23
      PadelClub/Views/Planning/PlanningSettingsView.swift
  18. 3
      PadelClub/Views/Planning/PlanningView.swift
  19. 16
      PadelClub/Views/Planning/RoundScheduleEditorView.swift
  20. 4
      PadelClub/Views/Player/Components/EditablePlayerView.swift
  21. 16
      PadelClub/Views/Shared/ImportedPlayerView.swift
  22. 15
      PadelClub/Views/Shared/SelectablePlayerListView.swift
  23. 15
      PadelClub/Views/Tournament/FileImportView.swift
  24. 2
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  25. 6
      PadelClub/Views/Tournament/Shared/TournamentCellView.swift

@ -231,6 +231,8 @@
FFDB1C6D2BB2A02000F1E467 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */; };
FFDB1C732BB2CFE900F1E467 /* MySortDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDB1C722BB2CFE900F1E467 /* MySortDescriptor.swift */; };
FFDDD40C2B93B2BB00C91A49 /* DeferredViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */; };
FFF116E12BD2A9B600A33B06 /* DateInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF116E02BD2A9B600A33B06 /* DateInterval.swift */; };
FFF116E32BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF116E22BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift */; };
FFF527D62BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */; };
FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACCC2B92367B008466FA /* FederalPlayer.swift */; };
FFF8ACD42B92392C008466FA /* SourceFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF8ACD32B92392C008466FA /* SourceFileManager.swift */; };
@ -518,6 +520,8 @@
FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
FFDB1C722BB2CFE900F1E467 /* MySortDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MySortDescriptor.swift; sourceTree = "<group>"; };
FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeferredViewModifier.swift; sourceTree = "<group>"; };
FFF116E02BD2A9B600A33B06 /* DateInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateInterval.swift; sourceTree = "<group>"; };
FFF116E22BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourtAvailabilitySettingsView.swift; sourceTree = "<group>"; };
FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchScheduleEditorView.swift; sourceTree = "<group>"; };
FFF8ACCC2B92367B008466FA /* FederalPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalPlayer.swift; sourceTree = "<group>"; };
FFF8ACD32B92392C008466FA /* SourceFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceFileManager.swift; sourceTree = "<group>"; };
@ -955,6 +959,7 @@
FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */,
FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */,
FF3B60A22BC49BBC008C2E66 /* MatchScheduler.swift */,
FFF116E02BD2A9B600A33B06 /* DateInterval.swift */,
);
path = ViewModel;
sourceTree = "<group>";
@ -1168,6 +1173,7 @@
FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */,
FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */,
FFF9645A2BC2D53B00EEF017 /* GroupStageScheduleEditorView.swift */,
FFF116E22BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift */,
FF1162882BD0523B000C4809 /* Components */,
);
path = Planning;
@ -1433,6 +1439,7 @@
FF92680D2BCEE5EA0080F940 /* NetworkMonitor.swift in Sources */,
FF967CF62BAED51600A9A3BD /* TournamentRunningView.swift in Sources */,
FF8F264D2BAE0B4100650388 /* TournamentDatePickerView.swift in Sources */,
FFF116E32BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift in Sources */,
FF967D042BAEF1C300A9A3BD /* MatchRowView.swift in Sources */,
C44B79112BBDA63A00906534 /* Locale+Extensions.swift in Sources */,
FF967CEA2BAEC70100A9A3BD /* GroupStage.swift in Sources */,
@ -1464,6 +1471,7 @@
FF11627A2BCF8109000C4809 /* CallMessageCustomizationView.swift in Sources */,
FF025ADB2BD0C2D000A86CF8 /* MatchTeamDetailView.swift in Sources */,
FF5DA1952BB927E800A33061 /* GenericDestinationPickerView.swift in Sources */,
FFF116E12BD2A9B600A33B06 /* DateInterval.swift in Sources */,
FF8F26542BAE1E4400650388 /* TableStructureView.swift in Sources */,
C45BAE442BCA753E002EEC8A /* Purchase.swift in Sources */,
FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */,

@ -22,6 +22,7 @@ protocol PlayerHolder {
var ligueName: String? { get }
var assimilation: String? { get }
var computedAge: Int? { get }
func getAssimilatedAsMaleRank() -> Int?
}
extension PlayerHolder {
@ -30,8 +31,31 @@ extension PlayerHolder {
}
}
fileprivate extension Int {
var femaleInMaleAssimilation: Int {
self + femaleInMaleAssimilationAddition
}
var femaleInMaleAssimilationAddition: Int {
switch self {
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 ImportedPlayer: PlayerHolder {
func getAssimilatedAsMaleRank() -> Int? {
guard male == false else { return nil }
return getRank()?.femaleInMaleAssimilation
}
var computedAge: Int? { nil }
var tournamentPlayed: Int? {

@ -184,7 +184,15 @@ class Match: ModelObject, Storable {
}
func next() -> Match? {
Store.main.filter(isIncluded: { $0.round == round && $0.index == index + 1 }).first
Store.main.filter(isIncluded: { $0.round == round && $0.index > index }).sorted(by: \.index).first
}
func getDuration() -> Int {
if let tournament = currentTournament() {
matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)
} else {
matchFormat.getEstimatedDuration()
}
}
func roundTitle() -> String? {

@ -42,15 +42,12 @@ extension Tournament {
}
let rankSourceDate = _mostRecentDateAvailable
let tournaments : [Tournament] = DataStore.shared.tournaments.filter { $0.endDate != nil }.sorted(by: \.startDate).reversed()
let tournamentLevel = TournamentLevel.mostUsed(inTournaments: tournaments)
let tournamentCategory = TournamentCategory.mostUsed(inTournaments: tournaments)
let federalTournamentAge = FederalTournamentAge.mostUsed(inTournaments: tournaments)
//todo
/*
tournament.tournamentLevel = TournamentLevel.mostUsed(tournaments: tournaments)
tournament.tournamentCategory = TournamentCategory.mostUsed(tournaments: tournaments)
tournament.federalTournamentAge = FederalTournamentAge.mostUsed(tournaments: tournaments)
*/
return Tournament(groupStageSortMode: .snake, rankSourceDate: rankSourceDate, teamSorting: .inscriptionDate, federalCategory: .men, federalLevelCategory: .p100, federalAgeCategory: .senior)
return Tournament(groupStageSortMode: .snake, rankSourceDate: rankSourceDate, teamSorting: tournamentLevel.defaultTeamSortingType, federalCategory: tournamentCategory, federalLevelCategory: tournamentLevel, federalAgeCategory: federalTournamentAge)
}
}

@ -343,7 +343,6 @@ class PlayerRegistration: ModelObject, Storable {
return 15000
}
}
}
extension PlayerRegistration: Hashable {
@ -357,6 +356,9 @@ extension PlayerRegistration: Hashable {
}
extension PlayerRegistration: PlayerHolder {
func getAssimilatedAsMaleRank() -> Int? {
nil
}
func getFirstName() -> String {
firstName

@ -44,6 +44,8 @@ class Tournament : ModelObject, Storable {
var payment: TournamentPayment = .free
var additionalEstimationDuration: Int = 0
var courtsUnavailability: [Int: [DateInterval]]? = nil
@ObservationIgnored
var navigationPath: [Screen] = []
@ -257,12 +259,16 @@ class Tournament : ModelObject, Storable {
let availableSeedSpot = availableSeedSpot(inRoundIndex: roundIndex)
let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex)
if availableSeeds.count == availableSeedSpot.count {
if availableSeeds.count == availableSeedSpot.count && availableSeedGroup.dimension == availableSeeds.count {
return availableSeedGroup
} else if (availableSeeds.count == availableSeedOpponentSpot.count && availableSeeds.count == self.availableSeeds().count) {
} else if (availableSeeds.count == availableSeedOpponentSpot.count && availableSeeds.count == self.availableSeeds().count) && availableSeedGroup.dimension == availableSeedOpponentSpot.count {
return availableSeedGroup
} else if let chunk = availableSeedGroup.chunk() {
return seedGroupAvailable(atRoundIndex: roundIndex, availableSeedGroup: chunk)
} else if let chunks = availableSeedGroup.chunks() {
if let chunk = chunks.first(where: { seedInterval in
seedInterval.first >= self.seededTeams().count
}) {
return seedGroupAvailable(atRoundIndex: roundIndex, availableSeedGroup: chunk)
}
}
}
@ -500,7 +506,9 @@ class Tournament : ModelObject, Storable {
func playersWithoutValidLicense(in players: [PlayerRegistration]) -> [PlayerRegistration] {
let licenseYearValidity = licenseYearValidity()
return players.filter({ ($0.isImported() && $0.isValidLicenseNumber(year: licenseYearValidity) == false) || ($0.isImported() == false && ($0.licenceId == nil || $0.licenceId?.isLicenseNumber == false || $0.licenceId?.isEmpty == true)) })
return players.filter({
($0.isImported() && $0.isValidLicenseNumber(year: licenseYearValidity) == false) || ($0.isImported() == false && ($0.licenceId == nil || $0.formattedLicense().isLicenseNumber == false || $0.licenceId?.isEmpty == true))
})
}
func getStartDate(ofSeedIndex seedIndex: Int?) -> Date? {
@ -515,7 +523,7 @@ class Tournament : ModelObject, Storable {
previousTeam.updatePlayers(team.players)
teamsToImport.append(previousTeam)
} else {
let newTeam = addTeam(team.players)
let newTeam = addTeam(team.players, registrationDate: team.registrationDate)
teamsToImport.append(newTeam)
}
}
@ -905,8 +913,8 @@ class Tournament : ModelObject, Storable {
selectedSortedTeams().firstIndex(where: { $0.id == team.id })
}
func addTeam(_ players: Set<PlayerRegistration>) -> TeamRegistration {
let team = TeamRegistration(tournament: id, registrationDate: Date())
func addTeam(_ players: Set<PlayerRegistration>, registrationDate: Date? = nil) -> TeamRegistration {
let team = TeamRegistration(tournament: id, registrationDate: registrationDate ?? Date())
team.tournamentCategory = tournamentCategory
team.setWeight(from: Array(players))
players.forEach { player in
@ -1005,7 +1013,14 @@ class Tournament : ModelObject, Storable {
return groupStageMatchFormat
}
}
func setupFederalSettings() {
teamSorting = tournamentLevel.defaultTeamSortingType
groupStageMatchFormat = groupStageSmartMatchFormat()
loserBracketMatchFormat = loserBracketSmartMatchFormat(1)
matchFormat = roundSmartMatchFormat(1)
}
func roundSmartMatchFormat(_ roundIndex: Int) -> MatchFormat {
let format = tournamentLevel.federalFormatForBracketRound(roundIndex)
if matchFormat.rank > format.rank {
@ -1129,6 +1144,4 @@ extension Tournament: TournamentBuildHolder {
var age: FederalTournamentAge {
federalTournamentAge
}
}

@ -89,6 +89,10 @@ extension Date {
}
}
func atBeginningOfDay(hourInt: Int = 9) -> Date {
Calendar.current.date(byAdding: .hour, value: hourInt, to: self.startOfDay)!
}
static var firstDayOfWeek = Calendar.current.firstWeekday
static var capitalizedFirstLettersOfWeekdays: [String] {
let calendar = Calendar.current
@ -180,9 +184,15 @@ extension Date {
var dayInt: Int {
Calendar.current.component(.day, from: self)
}
var startOfDay: Date {
Calendar.current.startOfDay(for: self)
}
func endOfDay() -> Date {
let calendar = Calendar.current
return calendar.date(bySettingHour: 23, minute: 59, second: 59, of: self)!
}
}
extension Date {
@ -191,3 +201,12 @@ extension Date {
}
}
extension Date {
func localizedTime() -> String {
self.formatted(.dateTime.hour().minute())
}
func localizedDay() -> String {
self.formatted(.dateTime.weekday(.wide).day())
}
}

@ -44,10 +44,13 @@ class FileImportManager {
var id: Self { self }
case frenchFederation
case padelClub
case unknown
var localizedLabel: String {
switch self {
case .padelClub:
return "Padel Club"
case .frenchFederation:
return "FFT"
case .unknown:
@ -58,24 +61,20 @@ class FileImportManager {
struct TeamHolder: Identifiable {
let id: UUID = UUID()
let playerOne: PlayerRegistration
let playerTwo: PlayerRegistration
let players: Set<PlayerRegistration>
let weight: Int
let tournamentCategory: TournamentCategory
let previousTeam: TeamRegistration?
init(playerOne: PlayerRegistration, playerTwo: PlayerRegistration, tournamentCategory: TournamentCategory, previousTeam: TeamRegistration?) {
self.playerOne = playerOne
self.playerTwo = playerTwo
var registrationDate: Date? = nil
init(players: [PlayerRegistration], tournamentCategory: TournamentCategory, previousTeam: TeamRegistration?, registrationDate: Date? = nil) {
self.players = Set(players)
self.tournamentCategory = tournamentCategory
self.previousTeam = previousTeam
self.weight = playerOne.weight + playerTwo.weight
}
var players: Set<PlayerRegistration> {
Set([playerOne, playerTwo])
self.weight = players.map { $0.weight }.reduce(0,+)
self.registrationDate = registrationDate
}
func index(in teams: [TeamHolder]) -> Int? {
teams.firstIndex(where: { $0.id == id })
}
@ -100,6 +99,60 @@ class FileImportManager {
static let FFT_ASSIMILATION_WOMAN_IN_MAN = "A calculer selon la pondération en vigueur"
func createTeams(from fileContent: String, tournament: Tournament, fileProvider: FileProvider = .frenchFederation) async -> [TeamHolder] {
switch fileProvider {
case .frenchFederation:
return await _getFederalTeams(from: fileContent, tournament: tournament)
case .padelClub:
return await _getPadelClubTeams(from: fileContent, tournament: tournament)
case .unknown:
return await _getPadelBusinessLeagueTeams(from: fileContent, tournament: tournament)
}
}
func importDataFromFFT() async -> String? {
if let importingDate = SourceFileManager.shared.mostRecentDateAvailable {
for source in SourceFile.allCases {
for fileURL in source.currentURLs {
let p = readCSV(inputFile: fileURL)
await importingChunkOfPlayers(p, importingDate: importingDate)
}
}
return URL.importDateFormatter.string(from: importingDate)
}
return nil
}
func readCSV(inputFile: URL) -> [FederalPlayer] {
do {
let fileContent = try String(contentsOf: inputFile)
return loadFromCSV(fileContent: fileContent, isMale: inputFile.manData)
} catch {
print("error: \(error)") // to do deal with errors
}
return []
}
func loadFromCSV(fileContent: String, isMale: Bool) -> [FederalPlayer] {
let lines = fileContent.components(separatedBy: "\n")
return lines.compactMap { line in
if line.components(separatedBy: ";").count < 10 {
} else {
let data = line.components(separatedBy: ";").joined(separator: "\n")
return FederalPlayer(data, isMale: isMale)
}
return nil
}
}
func importingChunkOfPlayers(_ players: [FederalPlayer], importingDate: Date) async {
for chunk in players.chunked(into: 1000) {
await PersistenceController.shared.batchInsertPlayers(chunk, importingDate: importingDate)
}
}
private func _getFederalTeams(from fileContent: String, tournament: Tournament) async -> [TeamHolder] {
let lines = fileContent.components(separatedBy: "\n")
guard let firstLine = lines.first else { return [] }
var separator = ","
@ -108,58 +161,7 @@ class FileImportManager {
}
let headerCount = firstLine.components(separatedBy: separator).count
var results: [TeamHolder] = []
if headerCount == 23 && fileProvider == .unknown { //PBL
let fetchRequest = ImportedPlayer.fetchRequest()
let federalContext = PersistenceController.shared.localContainer.viewContext
lines.dropFirst().forEach { line in
let data = line.components(separatedBy: separator)
if data.count == 23 {
// let team = Team(context: context)
// let brand = Brand(context: context)
// brand.title = data[2].trimmed
// brand.qualifier = data[0].trimmed
// brand.country = data[1].trimmed
// brand.lineOfBusiness = data[3].trimmed
// if brand.lineOfBusiness == "Bâtiment / Immo" { //quick fix
// brand.lineOfBusiness = "Bâtiment / Immo / Transport"
// }
// brand.name = data[4].trimmed
// team.brand = brand
//
// for i in 0...5 {
// let sex = data[i*3+5]
// let lastName = data[i*3+6].trimmed
// let firstName = data[i*3+7].trimmed
// if lastName.isEmpty == false {
// let playerOne = Player(context: context)
// let predicate = NSPredicate(format: "(canonicalLastName matches[cd] %@ OR canonicalLastName matches[cd] %@) AND (canonicalFirstName matches[cd] %@ OR canonicalFirstName matches[cd] %@)", lastName, lastName.removePunctuationAndHyphens, firstName, firstName.removePunctuationAndHyphens)
// fetchRequest.predicate = predicate
// if let playerFound = try? federalContext.fetch(fetchRequest).first {
// playerOne.updateWithImportedPlayer(playerFound)
// } else {
// playerOne.lastName = lastName
// playerOne.firstName = firstName
// playerOne.sex = sex == "H" ? 1 : sex == "F" ? 0 : -1
// playerOne.currentRank = tournament?.lastRankMan ?? 0
// }
// team.addToPlayers(playerOne)
// }
// }
// team.category = TournamentCategory.men.importingRawValue
//
// if let players = team.players, players.count > 0 {
// results.append(team)
// } else {
// context.delete(team)
// }
}
}
return results
} else if headerCount <= 18 && fileProvider == .frenchFederation {
if headerCount <= 18 {
Array(lines.dropFirst()).chunked(into: 2).forEach { teamLines in
if teamLines.count == 2 {
let dataOne = teamLines[0].replacingOccurrences(of: "\"", with: "").components(separatedBy: separator)
@ -211,13 +213,13 @@ class FileImportManager {
playerOne.setWeight(in: tournament)
let playerTwo = PlayerRegistration(federalData: Array(resultTwo[0...7]), sex: sexPlayerTwo, sexUnknown: sexUnknown)
playerTwo.setWeight(in: tournament)
let team = TeamHolder(playerOne: playerOne, playerTwo: playerTwo, tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo]))
let team = TeamHolder(players: [playerOne, playerTwo], tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo]))
results.append(team)
}
}
}
return results
} else if headerCount > 18 && fileProvider == .frenchFederation {
} else {
lines.dropFirst().forEach { line in
let data = line.components(separatedBy: separator)
if data.count > 18 {
@ -244,7 +246,7 @@ class FileImportManager {
case .mix: return 1
}
}
var sexPlayerTwo : Int {
switch tournamentCategory {
case .men: return 1
@ -257,56 +259,108 @@ class FileImportManager {
playerOne.setWeight(in: tournament)
let playerTwo = PlayerRegistration(federalData: Array(result[8...]), sex: sexPlayerTwo, sexUnknown: sexUnknown)
playerTwo.setWeight(in: tournament)
let team = TeamHolder(playerOne: playerOne, playerTwo: playerTwo, tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo]))
let team = TeamHolder(players: [playerOne, playerTwo], tournamentCategory: tournamentCategory, previousTeam: tournament.findTeam([playerOne, playerTwo]))
results.append(team)
}
}
return results
} else {
return []
}
}
func importDataFromFFT() async -> String? {
if let importingDate = SourceFileManager.shared.mostRecentDateAvailable {
for source in SourceFile.allCases {
for fileURL in source.currentURLs {
let p = readCSV(inputFile: fileURL)
await importingChunkOfPlayers(p, importingDate: importingDate)
private func _getPadelClubTeams(from fileContent: String, tournament: Tournament) async -> [TeamHolder] {
let lines = fileContent.components(separatedBy: "\n\n")
var results: [TeamHolder] = []
let fetchRequest = ImportedPlayer.fetchRequest()
let federalContext = PersistenceController.shared.localContainer.viewContext
lines.forEach { team in
let data = team.components(separatedBy: "\n")
let players = team.licencesFound()
fetchRequest.predicate = NSPredicate(format: "license IN %@", players)
let found = try? federalContext.fetch(fetchRequest)
let registeredPlayers = found?.map({ importedPlayer in
let player = PlayerRegistration(importedPlayer: importedPlayer)
player.setWeight(in: tournament)
return player
})
if let registeredPlayers, registeredPlayers.isEmpty == false {
var registrationDate: Date? {
if let registrationDateData = data[safe:2]?.replacingOccurrences(of: "inscrit le ", with: "") {
return try? Date(registrationDateData, strategy: .dateTime.weekday().day().month().hour().minute())
}
return nil
}
let team = TeamHolder(players: registeredPlayers, tournamentCategory: tournament.tournamentCategory, previousTeam: tournament.findTeam(registeredPlayers), registrationDate: registrationDate)
results.append(team)
}
return URL.importDateFormatter.string(from: importingDate)
}
return nil
}
func readCSV(inputFile: URL) -> [FederalPlayer] {
do {
let fileContent = try String(contentsOf: inputFile)
return loadFromCSV(fileContent: fileContent, isMale: inputFile.manData)
} catch {
print("error: \(error)") // to do deal with errors
}
return []
return results
}
func loadFromCSV(fileContent: String, isMale: Bool) -> [FederalPlayer] {
private func _getPadelBusinessLeagueTeams(from fileContent: String, tournament: Tournament) async -> [TeamHolder] {
let lines = fileContent.components(separatedBy: "\n")
return lines.compactMap { line in
if line.components(separatedBy: ";").count < 10 {
} else {
let data = line.components(separatedBy: ";").joined(separator: "\n")
return FederalPlayer(data, isMale: isMale)
}
return nil
guard let firstLine = lines.first else { return [] }
var separator = ","
if firstLine.contains(";") {
separator = ";"
}
}
func importingChunkOfPlayers(_ players: [FederalPlayer], importingDate: Date) async {
for chunk in players.chunked(into: 1000) {
await PersistenceController.shared.batchInsertPlayers(chunk, importingDate: importingDate)
let headerCount = firstLine.components(separatedBy: separator).count
var results: [TeamHolder] = []
if headerCount == 23 {
//todo
let fetchRequest = ImportedPlayer.fetchRequest()
let federalContext = PersistenceController.shared.localContainer.viewContext
lines.dropFirst().forEach { line in
let data = line.components(separatedBy: separator)
if data.count == 23 {
// let team = Team(context: context)
// let brand = Brand(context: context)
// brand.title = data[2].trimmed
// brand.qualifier = data[0].trimmed
// brand.country = data[1].trimmed
// brand.lineOfBusiness = data[3].trimmed
// if brand.lineOfBusiness == "Bâtiment / Immo" { //quick fix
// brand.lineOfBusiness = "Bâtiment / Immo / Transport"
// }
// brand.name = data[4].trimmed
// team.brand = brand
//
// for i in 0...5 {
// let sex = data[i*3+5]
// let lastName = data[i*3+6].trimmed
// let firstName = data[i*3+7].trimmed
// if lastName.isEmpty == false {
// let playerOne = Player(context: context)
// let predicate = NSPredicate(format: "(canonicalLastName matches[cd] %@ OR canonicalLastName matches[cd] %@) AND (canonicalFirstName matches[cd] %@ OR canonicalFirstName matches[cd] %@)", lastName, lastName.removePunctuationAndHyphens, firstName, firstName.removePunctuationAndHyphens)
// fetchRequest.predicate = predicate
// if let playerFound = try? federalContext.fetch(fetchRequest).first {
// playerOne.updateWithImportedPlayer(playerFound)
// } else {
// playerOne.lastName = lastName
// playerOne.firstName = firstName
// playerOne.sex = sex == "H" ? 1 : sex == "F" ? 0 : -1
// playerOne.currentRank = tournament?.lastRankMan ?? 0
// }
// team.addToPlayers(playerOne)
// }
// }
// team.category = TournamentCategory.men.importingRawValue
//
// if let players = team.players, players.count > 0 {
// results.append(team)
// } else {
// context.delete(team)
// }
}
}
return results
}
return []
}
}

@ -159,20 +159,18 @@ enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifiable {
case a45 = 450
case a55 = 550
static func mostRecent(tournaments: [Tournament] = []) -> Self {
.senior
// return tournaments.first?.federalTournamentAge ?? .a11_12
static func mostRecent(inTournaments tournaments: [Tournament]) -> Self {
return tournaments.first?.federalTournamentAge ?? .senior
}
static func mostUsed(tournaments: [Tournament] = []) -> Self {
// let countedSet = NSCountedSet(array: tournaments.map { $0.federalTournamentAge })
// let mostFrequent = countedSet.max { countedSet.count(for: $0) < countedSet.count(for: $1) }
// if mostFrequent != nil {
// return mostFrequent as! FederalTournamentAge
// } else {
// return mostRecent(tournaments: tournaments)
// }
.senior
static func mostUsed(inTournaments tournaments: [Tournament]) -> Self {
let countedSet = NSCountedSet(array: tournaments.map { $0.federalTournamentAge })
let mostFrequent = countedSet.max { countedSet.count(for: $0) < countedSet.count(for: $1) }
if mostFrequent != nil {
return mostFrequent as! FederalTournamentAge
} else {
return mostRecent(inTournaments: tournaments)
}
}
var id: Int { self.rawValue }
@ -236,22 +234,20 @@ enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable {
case p1500 = 1500
case p2000 = 2000
static func mostRecent(tournaments: [Tournament] = []) -> Self {
//return tournaments.first?.tournamentLevel ?? .p25
.p100
static func mostRecent(inTournaments tournaments: [Tournament]) -> Self {
return tournaments.first?.tournamentLevel ?? .p100
}
static func mostUsed(tournaments: [Tournament] = []) -> Self {
// let countedSet = NSCountedSet(array: tournaments.map { $0.tournamentLevel })
// let mostFrequent = countedSet.max { countedSet.count(for: $0) < countedSet.count(for: $1) }
// if mostFrequent != nil {
// return mostFrequent as! TournamentLevel
// } else {
// return mostRecent(tournaments: tournaments)
// }
.p100
static func mostUsed(inTournaments tournaments: [Tournament]) -> Self {
let countedSet = NSCountedSet(array: tournaments.map { $0.tournamentLevel })
let mostFrequent = countedSet.max { countedSet.count(for: $0) < countedSet.count(for: $1) }
if mostFrequent != nil {
return mostFrequent as! TournamentLevel
} else {
return mostRecent(inTournaments: tournaments)
}
}
var id: Int { self.rawValue }
func maximumDuration() -> Double {
@ -631,20 +627,27 @@ enum TournamentCategory: Int, Hashable, Codable, CaseIterable, Identifiable {
}
}
static func mostRecent(tournaments: [Tournament] = []) -> Self {
//return tournaments.first?.tournamentCategory ?? .mix
.men
var showFemaleInMaleAssimilation: Bool {
switch self {
case .men:
return true
default:
return false
}
}
static func mostRecent(inTournaments tournaments: [Tournament]) -> Self {
return tournaments.first?.tournamentCategory ?? .men
}
static func mostUsed(tournaments: [Tournament] = []) -> Self {
// let countedSet = NSCountedSet(array: tournaments.map { $0.tournamentCategory })
// let mostFrequent = countedSet.max { countedSet.count(for: $0) < countedSet.count(for: $1) }
// if mostFrequent != nil {
// return mostFrequent as! TournamentCategory
// } else {
// return mostRecent(tournaments: tournaments)
// }
.men
static func mostUsed(inTournaments tournaments: [Tournament]) -> Self {
let countedSet = NSCountedSet(array: tournaments.map { $0.tournamentCategory })
let mostFrequent = countedSet.max { countedSet.count(for: $0) < countedSet.count(for: $1) }
if mostFrequent != nil {
return mostFrequent as! TournamentCategory
} else {
return mostRecent(inTournaments: tournaments)
}
}
var next: TournamentCategory {
@ -1019,10 +1022,14 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
}
static func defaultFormatForMatchType(_ matchType: MatchType) -> MatchFormat {
if UserDefaults.standard.object(forKey: matchType.rawValue + "MatchFormatPreference") == nil {
return .nineGamesDecisivePoint
switch matchType {
case .bracket:
MatchFormat(rawValue: DataStore.shared.appSettings.bracketMatchFormatPreference) ?? .nineGamesDecisivePoint
case .groupStage:
MatchFormat(rawValue: DataStore.shared.appSettings.groupStageMatchFormatPreference) ?? .nineGamesDecisivePoint
case .loserBracket:
MatchFormat(rawValue: DataStore.shared.appSettings.loserBracketMatchFormatPreference) ?? .nineGamesDecisivePoint
}
return MatchFormat(rawValue: UserDefaults.standard.integer(forKey: matchType.rawValue + "MatchFormatPreference")) ?? .nineGamesDecisivePoint
}
static var allCases: [MatchFormat] {

@ -0,0 +1,32 @@
//
// DateInterval.swift
// PadelClub
//
// Created by Razmig Sarkissian on 19/04/2024.
//
import Foundation
import LeStorage
struct DateInterval: Identifiable, Codable {
var id: String = Store.randomId()
let startDate: Date
let endDate: Date
var range: Range<Date> {
startDate..<endDate
}
func isSingleDay() -> Bool {
Calendar.current.isDate(startDate, inSameDayAs: endDate)
}
func isDateInside(_ date: Date) -> Bool {
date >= startDate && date <= endDate
}
func isDateOutside(_ date: Date) -> Bool {
date <= startDate && date <= endDate && date >= startDate && date >= endDate
}
}

@ -67,6 +67,7 @@ class MatchScheduler {
var timeDifferenceLimit: Double = 300.0
var loserBracketRotationDifference: Int = 0
var upperBracketRotationDifference: Int = 1
var courtsUnavailability: [Int: [DateInterval]]? = nil
func shouldHandleUpperRoundSlice() -> Bool {
options.contains(.shouldHandleUpperRoundSlice)
@ -176,7 +177,18 @@ class MatchScheduler {
}
func roundMatchCanBePlayed(_ match: Match, roundObject: Round, slots: [TimeMatch], rotationIndex: Int, targetedStartDate: Date, minimumTargetedEndDate: inout Date) -> Bool {
//print(roundObject.roundTitle(), match.matchTitle())
print(roundObject.roundTitle(), match.matchTitle())
if let roundStartDate = roundObject.startDate, targetedStartDate < roundStartDate {
print("can't start \(targetedStartDate) earlier than \(roundStartDate)")
if targetedStartDate == minimumTargetedEndDate {
minimumTargetedEndDate = roundStartDate
} else {
minimumTargetedEndDate = min(roundStartDate, minimumTargetedEndDate)
}
return false
}
let previousMatches = roundObject.precedentMatches(ofMatch: match)
if previousMatches.isEmpty { return true }
@ -361,33 +373,55 @@ class MatchScheduler {
func dispatchCourts(availableCourts: Int, courts: [Int], availableMatchs: inout [Match], slots: inout [TimeMatch], rotationIndex: Int, rotationStartDate: Date, freeCourtPerRotation: inout [Int: [Int]]) {
var matchPerRound = [Int: Int]()
var minimumTargetedEndDate: Date = rotationStartDate
courts.forEach { courtIndex in
//print(mt.map { ($0.bracket!.index.intValue, counts[$0.bracket!.index.intValue]) })
print("dispatchCourts", courts.sorted(), rotationStartDate, rotationIndex)
courts.sorted().forEach { courtIndex in
print("trying to find a match for \(courtIndex) in \(rotationIndex)")
if let first = availableMatchs.first(where: { match in
let roundObject = match.roundObject!
let courtsUnavailable = courtsUnavailable(startDate: rotationStartDate, duration: match.matchFormat.getEstimatedDuration(additionalEstimationDuration))
print("courtsUnavailable \(courtsUnavailable)")
if courtIndex >= availableCourts - courtsUnavailable {
return false
}
let canBePlayed = roundMatchCanBePlayed(match, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate)
let currentRotationSameRoundMatches = matchPerRound[roundObject.index] ?? 0
if shouldHandleUpperRoundSlice() {
let roundMatchesCount = roundObject.playedMatches().count
print("shouldHandleUpperRoundSlice \(roundMatchesCount)")
if roundObject.loser == nil && roundMatchesCount > courts.count {
if currentRotationSameRoundMatches >= min(roundMatchesCount / 2, courts.count) { return false }
print("roundMatchesCount \(roundMatchesCount) > \(courts.count)")
if currentRotationSameRoundMatches >= min(roundMatchesCount / 2, courts.count) {
print("return false, \(currentRotationSameRoundMatches) >= \(min(roundMatchesCount / 2, courts.count))")
return false
}
}
}
if roundObject.loser == nil && roundObject.index > 0, match.indexInRound() == 0, courts.count > 1, let nextMatch = match.next() {
let indexInRound = match.indexInRound()
print("Upper Round, index > 0, first Match of round \(indexInRound) and more than one court available; looking for next match (same round) \(indexInRound + 1)")
if roundObject.loser == nil && roundObject.index > 0, indexInRound == 0, courts.count > 1, let nextMatch = match.next() {
if canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate) {
print("next match and this match can be played, returning true")
return true
} else {
print("next match and this match can not be played at the same time, returning false")
return false
}
}
print("\(currentRotationSameRoundMatches) modulo \(currentRotationSameRoundMatches%2) same round match is even, index of round is not 0 and upper bracket. If it's not the last court available \(courtIndex) == \(courts.count - 1)")
if currentRotationSameRoundMatches%2 == 0 && roundObject.index != 0 && roundObject.loser == nil && courtIndex == courts.count - 1 {
print("we return false")
return false
}
return canBePlayed
}) {
@ -409,11 +443,17 @@ class MatchScheduler {
}
if freeCourtPerRotation[rotationIndex]!.count == availableCourts {
print("no match found to be put in this rotation, check if we can put anything to another date")
freeCourtPerRotation[rotationIndex] = []
let courtsUsed = getNextEarliestAvailableDate(from: slots)
let freeCourts = courtsUsed.filter { (courtIndex, availableDate) in
availableDate <= minimumTargetedEndDate
}.sorted(by: \.1).map { $0.0 }
var freeCourts: [Int] = []
if courtsUsed.isEmpty {
freeCourts = (0..<availableCourts).map { $0 }
} else {
freeCourts = courtsUsed.filter { (courtIndex, availableDate) in
availableDate <= minimumTargetedEndDate
}.sorted(by: \.1).map { $0.0 }
}
dispatchCourts(availableCourts: availableCourts, courts: freeCourts, availableMatchs: &availableMatchs, slots: &slots, rotationIndex: rotationIndex, rotationStartDate: minimumTargetedEndDate, freeCourtPerRotation: &freeCourtPerRotation)
}
@ -424,9 +464,27 @@ class MatchScheduler {
let upperRounds = tournament.rounds()
let allMatches = tournament.allMatches()
var roundIndex = 0
// if dayOne < tournament.courtCount {
// let startOfDay = startDate.startOfDay
// let endOfday = Calendar.current.dateInterval(of: .day, for: startOfDay)!.end
// let dateInterval = DateInterval(startDate: startOfDay, endDate: endOfday)
// let _courtsUnavailability = courtsUnavailability ?? [:]()
// let data = _courtsUnavailability[courtCount - 1] ?? [DateInterval]()
// data.append(dateInterval)
// courtsUnavailability = _courtsUnavailability
// }
//
// if dayTwo < tournament.courtCount {
// let startOfDay = startDate.startOfDay
// let endOfday = Calendar.current.dateInterval(of: .day, for: startOfDay)!.end
// let dateInterval = DateInterval(startDate: startOfDay, endDate: endOfday)
// let _courtsUnavailability = courtsUnavailability ?? [:]()
// let data = _courtsUnavailability[courtCount - 1] ?? [DateInterval]()
// data.append(dateInterval)
// courtsUnavailability = _courtsUnavailability
// }
let rounds = upperRounds.map {
$0
} + upperRounds.flatMap {
@ -459,12 +517,14 @@ class MatchScheduler {
}
}
let usedCourts = getAvailableCourts(from: allMatches.filter({ $0.startDate?.isEarlierThan(startDate) == true }))
let usedCourts = getAvailableCourts(from: allMatches.filter({ $0.startDate?.isEarlierThan(startDate) == true && $0.startDate?.dayInt == startDate.dayInt }))
let initialCourts = usedCourts.filter { (court, availableDate) in
availableDate <= startDate
}.sorted(by: \.1).compactMap { tournament.getCourtIndex($0.0) }
let courts : [Int]? = initialCourts.isEmpty ? nil : initialCourts
print("initial available courts at beginning: \(courts)")
let roundDispatch = self.roundDispatcher(numberOfCourtsAvailablePerRotation: tournament.courtCount, flattenedMatches: flattenedMatches, dispatcherStartDate: startDate, initialCourts: courts)
@ -477,5 +537,21 @@ class MatchScheduler {
try? DataStore.shared.matches.addOrUpdate(contentOfs: allMatches)
}
}
func courtsUnavailable(startDate: Date, duration: Int) -> Int {
let endDate = startDate.addingTimeInterval(Double(duration) * 60)
guard let courtsUnavailability else { return 0 }
let courts = courtsUnavailability.keys
return courts.filter {
courtUnavailable(courtIndex: $0, from: startDate, to: endDate)
}.count
}
func courtUnavailable(courtIndex: Int, from startDate: Date, to endDate: Date) -> Bool {
guard let courtLockedSchedule = courtsUnavailability?[courtIndex] else { return true }
return courtLockedSchedule.anySatisfy({ dateInterval in
dateInterval.isDateInside(startDate) || dateInterval.isDateInside(endDate)
})
}
}

@ -14,6 +14,8 @@ class SearchViewModel: ObservableObject, Identifiable {
var codeClub: String? = nil
var clubName: String? = nil
var ligueName: String? = nil
var showFemaleInMaleAssimilation: Bool = false
@Published var debouncableText: String = ""
@Published var searchText: String = ""
@Published var task: DispatchWorkItem?

@ -15,10 +15,26 @@ struct SeedInterval: Hashable, Comparable {
return lhs.first < rhs.first
}
var dimension: Int {
(last - first)
}
func chunks() -> [SeedInterval]? {
if dimension > 3 {
let split = dimension / 2
let firstHalf = SeedInterval(first: first, last: first + split - 1)
let secondHalf = SeedInterval(first: first + split, last: last)
return [firstHalf, secondHalf]
} else {
return nil
}
}
func chunk() -> SeedInterval? {
if (last - first) / 2 > 0 {
if last - (last - first) / 2 > first {
return SeedInterval(first: first, last: last - (last - first) / 2)
if dimension / 2 > 0 {
let halfDimension = last - dimension / 2
if halfDimension > first {
return SeedInterval(first: first, last: halfDimension - 1)
}
}
return nil

@ -100,6 +100,7 @@ struct EventCreationView: View {
tournaments.forEach { tournament in
tournament.startDate = startingDate
tournament.dayDuration = duration
tournament.setupFederalSettings()
}
try? dataStore.tournaments.addOrUpdate(contentOfs: tournaments)

@ -9,7 +9,6 @@ import SwiftUI
struct MatchDetailView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
@Environment(\.dismiss) var dismiss
let matchViewStyle: MatchViewStyle
@ -321,7 +320,7 @@ struct MatchDetailView: View {
Section {
if match.hasEnded() == false {
let rotationDuration = match.matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)
let rotationDuration = match.getDuration()
Picker(selection: $startDateSetup) {
if match.isReady() {
Text("Dans 5 minutes").tag(MatchDateSetup.inMinutes(5))

@ -0,0 +1,157 @@
//
// CourtAvailabilitySettingsView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 19/04/2024.
//
import SwiftUI
struct CourtAvailabilitySettingsView: View {
@Environment(Tournament.self) var tournament: Tournament
@State private var courtsUnavailability: [Int: [DateInterval]] = [Int:[DateInterval]]()
@State private var showingPopover: Bool = false
@State private var courtIndex: Int = 0
@State private var startDate: Date = Date()
@State private var endDate: Date = Date()
var body: some View {
List {
let keys = courtsUnavailability.keys.sorted(by: \.self)
ForEach(keys, id: \.self) { key in
if let dates = courtsUnavailability[key] {
Section {
ForEach(dates) { dateInterval in
HStack {
VStack(alignment: .leading, spacing: 0) {
Text(dateInterval.startDate.localizedTime()).font(.largeTitle)
Text(dateInterval.startDate.localizedDay()).font(.caption)
}
Spacer()
Image(systemName: "arrowshape.forward.fill")
.tint(.master)
Spacer()
VStack(alignment: .trailing, spacing: 0) {
Text(dateInterval.endDate.localizedTime()).font(.largeTitle)
Text(dateInterval.endDate.localizedDay()).font(.caption)
}
}
.contextMenu(menuItems: {
Button("dupliquer") {
}
Button("éditer") {
}
Button("effacer") {
}
})
.swipeActions {
Button(role: .destructive) {
courtsUnavailability[key]?.removeAll(where: { $0.id == dateInterval.id })
} label: {
LabelDelete()
}
}
}
} header: {
Text("Terrain #\(key + 1)")
}
.headerProminence(.increased)
}
}
}
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
showingPopover = true
} label: {
Image(systemName: "plus.circle.fill")
.resizable()
.scaledToFit()
.frame(minHeight: 28)
}
}
}
.onDisappear {
tournament.courtsUnavailability = courtsUnavailability
}
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Créneaux")
.popover(isPresented: $showingPopover) {
NavigationStack {
Form {
Section {
CourtPicker(title: "Terrain", selection: $courtIndex, maxCourt: 3)
}
Section {
DatePicker("Début", selection: $startDate)
DatePicker("Fin", selection: $endDate)
} footer: {
Button("jour entier") {
startDate = startDate.startOfDay
endDate = endDate.endOfDay()
}
.buttonStyle(.borderless)
.underline()
}
}
.toolbar {
Button("Valider") {
let dateInterval = DateInterval(startDate: startDate, endDate: endDate)
var courtUnavailability = courtsUnavailability[courtIndex] ?? [DateInterval]()
courtUnavailability.append(dateInterval)
courtsUnavailability[courtIndex] = courtUnavailability
showingPopover = false
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Nouveau créneau")
}
.onAppear {
UIDatePicker.appearance().minuteInterval = 5
}
.onDisappear {
UIDatePicker.appearance().minuteInterval = 1
}
}
}
}
struct CourtPicker: View {
let title: String
@Binding var selection: Int
let maxCourt: Int
var body: some View {
Picker(title, selection: $selection) {
ForEach(0..<maxCourt, id: \.self) {
Text("Terrain #\($0 + 1)")
}
}
}
}
#Preview {
CourtAvailabilitySettingsView()
}
/*
LabeledContent {
// switch dayIndex {
// case 1:
// StepperView(count: $dayTwo, maximum: tournament.courtCount)
// case 2:
// StepperView(count: $dayThree, maximum: tournament.courtCount)
// default:
// StepperView(count: $dayOne, maximum: tournament.courtCount)
// }
// } label: {
// Text("Terrains maximum")
// Text(tournament.startDate.formatted(.dateTime.weekday(.wide)) + " + \(dayIndex)")
// }
*/

@ -60,10 +60,28 @@ struct PlanningSettingsView: View {
}
NavigationLink {
CourtAvailabilitySettingsView()
.environment(tournament)
} label: {
Text("Disponibilité des terrains")
Text("Préciser la disponibilité des terrains")
}
// if tournament.dayDuration > 1 {
// ForEach(0..<tournament.dayDuration, id: \.self) { dayIndex in
// LabeledContent {
// switch dayIndex {
// case 1:
// StepperView(count: $dayTwo, maximum: tournament.courtCount)
// case 2:
// StepperView(count: $dayThree, maximum: tournament.courtCount)
// default:
// StepperView(count: $dayOne, maximum: tournament.courtCount)
// }
// } label: {
// Text("Terrains maximum")
// Text(tournament.startDate.formatted(.dateTime.weekday(.wide)) + " + \(dayIndex)")
// }
// }
// }
}
Section {
@ -156,6 +174,7 @@ struct PlanningSettingsView: View {
let groupStages = tournament.groupStages()
let numberOfCourtsAvailablePerRotation: Int = tournament.courtCount
let matchScheduler = MatchScheduler.shared
matchScheduler.courtsUnavailability = tournament.courtsUnavailability
matchScheduler.options.removeAll()
if randomCourtDistribution {

@ -9,7 +9,8 @@ import SwiftUI
struct PlanningView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
let matches: [Match]
@State private var timeSlots: [Date:[Match]]
@State private var days: [Date]

@ -28,8 +28,20 @@ struct RoundScheduleEditorView: View {
Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline)
}
} footer: {
DateUpdateManagerView(startDate: $startDate) {
_updateSchedule()
HStack {
DateUpdateManagerView(startDate: $startDate) {
_updateSchedule()
}
Spacer()
if let roundStartDate = round.startDate {
Button("horaire automatique") {
round.startDate = nil
}
.underline()
.buttonStyle(.borderless)
}
}
}

@ -36,6 +36,10 @@ struct EditablePlayerView: View {
func computedPlayerView(_ player: PlayerRegistration) -> some View {
VStack(alignment: .leading) {
ImportedPlayerView(player: player)
HStack {
Text(player.isImported() ? "importé" : "non importé")
Text(player.formattedLicense().isLicenseNumber ? "valide" : "non valide")
}
HStack {
Menu {
if let number = player.phoneNumber?.replacingOccurrences(of: " ", with: ""), let url = URL(string: "tel:\(number)") {

@ -10,6 +10,7 @@ import SwiftUI
struct ImportedPlayerView: View {
let player: PlayerHolder
var index: Int? = nil
var showFemaleInMaleAssimilation: Bool = false
var body: some View {
VStack(alignment: .leading) {
@ -39,7 +40,7 @@ struct ImportedPlayerView: View {
.font(.title3)
if let rank = player.getRank() {
Text(rank.ordinalFormattedSuffix()).italic(player.isAssimilated)
.font(.caption)
.font(.caption)
}
}
@ -58,6 +59,19 @@ struct ImportedPlayerView: View {
}
}
if let assimilatedAsMaleRank = player.getAssimilatedAsMaleRank(), showFemaleInMaleAssimilation {
HStack(alignment: .top, spacing: 2) {
Text("(")
Text(assimilatedAsMaleRank.formatted())
VStack(alignment: .leading, spacing: 0) {
Text("équivalence")
Text("messieurs")
}
.font(.caption)
}
Text(")").font(.title3)
}
HStack {
Text(player.formattedLicense())
if let computedAge = player.computedAge {

@ -34,12 +34,13 @@ struct SelectablePlayerListView: View {
return URL.importDateFormatter.date(from: lastDataSource)
}
init(allowSelection: Int = 0, searchField: String? = nil, user: User? = nil, dataSet: DataSet = .national, filterOption: PlayerFilterOption = .all, hideAssimilation: Bool = false, ascending: Bool = true, sortOption: SortOption = .rank, fromPlayer: FederalPlayer? = nil, codeClub: String? = nil, ligue: String? = nil, playerSelectionAction: PlayerSelectionAction? = nil, contentUnavailableAction: ContentUnavailableAction? = nil) {
init(allowSelection: Int = 0, searchField: String? = nil, user: User? = nil, dataSet: DataSet = .national, filterOption: PlayerFilterOption = .all, hideAssimilation: Bool = false, ascending: Bool = true, sortOption: SortOption = .rank, fromPlayer: FederalPlayer? = nil, codeClub: String? = nil, ligue: String? = nil, showFemaleInMaleAssimilation: Bool = false, playerSelectionAction: PlayerSelectionAction? = nil, contentUnavailableAction: ContentUnavailableAction? = nil) {
self.allowSelection = allowSelection
// self.searchText = searchField ?? ""
self.playerSelectionAction = playerSelectionAction
self.contentUnavailableAction = contentUnavailableAction
let searchViewModel = SearchViewModel()
searchViewModel.showFemaleInMaleAssimilation = showFemaleInMaleAssimilation
searchViewModel.searchText = searchField ?? ""
searchViewModel.isPresented = allowSelection != 0
searchViewModel.user = user
@ -292,7 +293,7 @@ struct MySearchView: View {
let array = Array(searchViewModel.selectedPlayers)
Section {
ForEach(array) { player in
ImportedPlayerView(player: player)
ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation)
}
.onDelete { indexSet in
for index in indexSet {
@ -307,7 +308,7 @@ struct MySearchView: View {
} else {
Section {
ForEach(players, id: \.self) { player in
ImportedPlayerView(player: player, index: nil)
ImportedPlayerView(player: player, index: nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation)
}
} header: {
if players.isEmpty == false {
@ -326,7 +327,7 @@ struct MySearchView: View {
Button {
searchViewModel.selectedPlayers.insert(player)
} label: {
ImportedPlayerView(player: player)
ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation)
}
.buttonStyle(.plain)
}
@ -339,7 +340,7 @@ struct MySearchView: View {
} else {
Section {
ForEach(players) { player in
ImportedPlayerView(player: player)
ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation)
}
} header: {
if players.isEmpty == false {
@ -356,13 +357,13 @@ struct MySearchView: View {
Button {
searchViewModel.selectedPlayers.insert(player)
} label: {
ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil)
ImportedPlayerView(player: player, index: searchViewModel.showIndex() ? (index + 1) : nil, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation)
.contentShape(Rectangle())
}
.frame(maxWidth: .infinity)
.buttonStyle(.plain)
} else {
ImportedPlayerView(player: player)
ImportedPlayerView(player: player, showFemaleInMaleAssimilation: searchViewModel.showFemaleInMaleAssimilation)
}
}
} header: {

@ -40,6 +40,14 @@ struct FileImportView: View {
Label("beach-padel.app.fft.fr", systemImage: "tennisball")
}
Picker(selection: $fileProvider) {
ForEach(FileImportManager.FileProvider.allCases) {
Text($0.localizedLabel).tag($0)
}
} label: {
Text("Source du fichier")
}
Button {
convertingFile = false
isShowing.toggle()
@ -160,7 +168,7 @@ struct FileImportView: View {
}
}
}
.fileImporter(isPresented: $isShowing, allowedContentTypes: [.spreadsheet, .commaSeparatedText], allowsMultipleSelection: false, onCompletion: { results in
.fileImporter(isPresented: $isShowing, allowedContentTypes: [.spreadsheet, .commaSeparatedText, .text], allowsMultipleSelection: false, onCompletion: { results in
switch results {
case .success(let fileurls):
@ -267,8 +275,9 @@ struct FileImportView: View {
Section {
HStack {
VStack(alignment: .leading) {
Text(team.playerOne.playerLabel())
Text(team.playerTwo.playerLabel())
ForEach(team.players.sorted(by: \.weight)) {
Text($0.playerLabel())
}
}
Spacer()
HStack {

@ -76,7 +76,7 @@ struct InscriptionManagerView: View {
selectionSearchField = nil
}) {
NavigationStack {
SelectablePlayerListView(allowSelection: -1, filterOption: _filterOption()) { players in
SelectablePlayerListView(allowSelection: -1, filterOption: _filterOption(), showFemaleInMaleAssimilation: tournament.tournamentCategory.showFemaleInMaleAssimilation) { players in
selectionSearchField = nil
players.forEach { player in
let newPlayer = PlayerRegistration(importedPlayer: player)

@ -61,11 +61,15 @@ struct TournamentCellView: View {
let event = federalTournament.getEvent()
let newTournament = Tournament.newEmptyInstance()
newTournament.event = event.id
//todo
//newTournament.umpireMail()
//newTournament.jsonData = jsonData
newTournament.tournamentLevel = build.level
newTournament.tournamentCategory = build.category
newTournament.federalTournamentAge = build.age
newTournament.dayDuration = federalTournament.dayDuration
newTournament.startDate = federalTournament.startDate
newTournament.startDate = federalTournament.startDate.atBeginningOfDay(hourInt: 9)
newTournament.setupFederalSettings()
try? dataStore.tournaments.addOrUpdate(instance: newTournament)
}
} label: {

Loading…
Cancel
Save