fix stuff scheduler

fix min more qualified team
fix min qualified
paca_championship
Raz 1 year ago
parent 996220fe6f
commit 34ad82f177
  1. 18
      PadelClub/Data/MatchScheduler.swift
  2. 2
      PadelClub/Data/PlayerRegistration.swift
  3. 16
      PadelClub/Data/Tournament.swift
  4. 2
      PadelClub/Views/Calling/TeamsCallingView.swift
  5. 3
      PadelClub/Views/Planning/PlanningSettingsView.swift
  6. 32
      PadelClub/Views/Planning/PlanningView.swift
  7. 7
      PadelClub/Views/Player/Components/EditablePlayerView.swift
  8. 21
      PadelClub/Views/Player/PlayerDetailView.swift
  9. 36
      PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift
  10. 7
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  11. 4
      PadelClub/Views/Tournament/Screen/TableStructureView.swift

@ -385,16 +385,10 @@ final class MatchScheduler : ModelObject, Storable {
print("Targeted start date is after the minimum possible end date, returning true.") print("Targeted start date is after the minimum possible end date, returning true.")
return true return true
} }
} else {
if targetedStartDate == minimumTargetedEndDate {
print("Updating minimumTargetedEndDate to minimumPossibleEndDate: \(minimumPossibleEndDate)")
minimumTargetedEndDate = minimumPossibleEndDate
} else { } else {
print("Setting minimumTargetedEndDate to the earlier of \(minimumPossibleEndDate) and \(minimumTargetedEndDate)") print("Setting minimumTargetedEndDate to the earlier of \(minimumPossibleEndDate) and \(minimumTargetedEndDate)")
minimumTargetedEndDate = min(minimumPossibleEndDate, minimumTargetedEndDate) minimumTargetedEndDate = minimumPossibleEndDate
} return true
print("Targeted start date is before the minimum possible end date, returning false.")
return false
} }
} }
@ -463,7 +457,7 @@ final class MatchScheduler : ModelObject, Storable {
var courts = initialCourts ?? Array(courtsAvailable) var courts = initialCourts ?? Array(courtsAvailable)
var shouldStartAtDispatcherDate = rotationIndex > 0 var shouldStartAtDispatcherDate = rotationIndex > 0
while !availableMatchs.isEmpty && !issueFound && rotationIndex < 100 { while !availableMatchs.isEmpty && !issueFound && rotationIndex < 50 {
freeCourtPerRotation[rotationIndex] = [] freeCourtPerRotation[rotationIndex] = []
let previousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 1 }) let previousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 1 })
var rotationStartDate: Date = getNextStartDate(fromPreviousRotationSlots: previousRotationSlots, includeBreakTime: false) ?? dispatcherStartDate var rotationStartDate: Date = getNextStartDate(fromPreviousRotationSlots: previousRotationSlots, includeBreakTime: false) ?? dispatcherStartDate
@ -604,7 +598,9 @@ final class MatchScheduler : ModelObject, Storable {
if shouldTryToFillUpCourtsAvailable == false { if shouldTryToFillUpCourtsAvailable == false {
if roundObject.parent == nil && roundObject.index > 1 && indexInRound == 0, let nextMatch = match.next() { if roundObject.parent == nil && roundObject.index > 1 && indexInRound == 0, let nextMatch = match.next() {
if courtPosition < courts.count - 1 && canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &minimumTargetedEndDate) {
var nextMinimumTargetedEndDate = minimumTargetedEndDate
if courtPosition < courts.count - 1 && canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate, minimumTargetedEndDate: &nextMinimumTargetedEndDate) {
print("Returning true: Both current \(match.index) and next match \(nextMatch.index) can be played in rotation \(rotationIndex).") print("Returning true: Both current \(match.index) and next match \(nextMatch.index) can be played in rotation \(rotationIndex).")
return true return true
} else { } else {
@ -625,7 +621,7 @@ final class MatchScheduler : ModelObject, Storable {
matchID: firstMatch.id, matchID: firstMatch.id,
rotationIndex: rotationIndex, rotationIndex: rotationIndex,
courtIndex: courtIndex, courtIndex: courtIndex,
startDate: rotationStartDate, startDate: minimumTargetedEndDate,
durationLeft: firstMatch.matchFormat.getEstimatedDuration(additionalEstimationDuration), durationLeft: firstMatch.matchFormat.getEstimatedDuration(additionalEstimationDuration),
minimumBreakTime: firstMatch.matchFormat.breakTime.breakTime minimumBreakTime: firstMatch.matchFormat.breakTime.breakTime
) )

@ -300,7 +300,7 @@ final class PlayerRegistration: ModelObject, Storable {
if let currentLicenceId = licenceId { if let currentLicenceId = licenceId {
if currentLicenceId.trimmed.hasSuffix("(\(year-1))") { if currentLicenceId.trimmed.hasSuffix("(\(year-1))") {
self.licenceId = currentLicenceId.replacingOccurrences(of: "\(year-1)", with: "\(year)") self.licenceId = currentLicenceId.replacingOccurrences(of: "\(year-1)", with: "\(year)")
} else if let computedLicense = currentLicenceId.strippedLicense { } else if let computedLicense = currentLicenceId.strippedLicense?.computedLicense {
self.licenceId = computedLicense + " (\(year))" self.licenceId = computedLicense + " (\(year))"
} }
} }

@ -1021,8 +1021,20 @@ defer {
func playersWithoutValidLicense(in players: [PlayerRegistration], isImported: Bool) -> [PlayerRegistration] { func playersWithoutValidLicense(in players: [PlayerRegistration], isImported: Bool) -> [PlayerRegistration] {
let licenseYearValidity = self.licenseYearValidity() let licenseYearValidity = self.licenseYearValidity()
return players.filter({ return players.filter({ player in
($0.isImported() && $0.isValidLicenseNumber(year: licenseYearValidity) == false) || ($0.isImported() == false && ($0.licenceId == nil || $0.formattedLicense().isLicenseNumber == false || $0.licenceId?.isEmpty == true) || ($0.isImported() == false && isImported)) if player.isImported() {
// Player is marked as imported: check if the license is valid
return !player.isValidLicenseNumber(year: licenseYearValidity)
} else {
// Player is not imported: validate license and handle `isImported` flag for non-imported players
let noLicenseId = player.licenceId == nil || player.licenceId?.isEmpty == true
let invalidFormattedLicense = player.formattedLicense().isLicenseNumber == false
// If global `isImported` is true, check license number as well
let invalidLicenseForImportedFlag = isImported && !player.isValidLicenseNumber(year: licenseYearValidity)
return noLicenseId || invalidFormattedLicense || invalidLicenseForImportedFlag
}
}) })
} }

@ -14,6 +14,8 @@ struct TeamsCallingView: View {
var body: some View { var body: some View {
List { List {
PlayersWithoutContactView(players: teams.flatMap({ $0.unsortedPlayers() }).sorted(by: \.computedRank))
Section { Section {
ForEach(teams) { team in ForEach(teams) { team in
Menu { Menu {

@ -107,7 +107,7 @@ struct PlanningSettingsView: View {
} }
} label: { } label: {
if isCreatedByUser { if isCreatedByUser {
Text("Vous avez indiqué plus de terrains dans ce tournoi que dans le club.") Text("Vous avez indiqué plus de terrains dans ce tournoi que dans le club. ")
+ Text("Mettre à jour le club ?").underline().foregroundStyle(.master) + Text("Mettre à jour le club ?").underline().foregroundStyle(.master)
} else { } else {
Label("Vous avez indiqué plus de terrains dans ce tournoi que dans le club.", systemImage: "exclamationmark.triangle.fill").foregroundStyle(.logoRed) Label("Vous avez indiqué plus de terrains dans ce tournoi que dans le club.", systemImage: "exclamationmark.triangle.fill").foregroundStyle(.logoRed)
@ -257,6 +257,7 @@ struct PlanningSettingsView: View {
_save() _save()
} }
.onChange(of: tournament.courtCount) { .onChange(of: tournament.courtCount) {
matchScheduler.courtsAvailable = Set(tournament.courtsAvailable())
_save() _save()
} }
.onChange(of: tournament.dayDuration) { .onChange(of: tournament.dayDuration) {

@ -10,6 +10,7 @@ import SwiftUI
struct PlanningView: View { struct PlanningView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament @Environment(Tournament.self) var tournament: Tournament
@State private var selectedDay: Date?
let matches: [Match] let matches: [Match]
@Binding var selectedScheduleDestination: ScheduleDestination? @Binding var selectedScheduleDestination: ScheduleDestination?
@ -39,7 +40,7 @@ struct PlanningView: View {
case .byCourt: case .byCourt:
return "Par terrain" return "Par terrain"
case .byDefault: case .byDefault:
return "Par défaut" return "Par ordre des matchs"
} }
} }
} }
@ -49,11 +50,38 @@ struct PlanningView: View {
_selectedScheduleDestination = selectedScheduleDestination _selectedScheduleDestination = selectedScheduleDestination
} }
private func _computedTitle() -> String {
if let selectedDay {
return selectedDay.formatted(.dateTime.day().weekday().month())
} else {
if days.count > 1 {
return "Tous les jours"
} else {
return "Horaires"
}
}
}
var body: some View { var body: some View {
List { List {
_bySlotView() _bySlotView()
} }
.navigationTitle(Text(_computedTitle()))
.toolbar(content: { .toolbar(content: {
if days.count > 1 {
ToolbarTitleMenu {
Picker(selection: $selectedDay) {
Text("Tous les jours").tag(nil as Date?)
ForEach(days, id: \.self) { day in
Text(day.formatted(.dateTime.day().weekday().month())).tag(day as Date?)
}
} label: {
Text("Jour")
}
.pickerStyle(.automatic)
}
}
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
Menu { Menu {
Picker(selection: $filterOption) { Picker(selection: $filterOption) {
@ -89,7 +117,7 @@ struct PlanningView: View {
@ViewBuilder @ViewBuilder
func _bySlotView() -> some View { func _bySlotView() -> some View {
if matches.allSatisfy({ $0.startDate == nil }) == false { if matches.allSatisfy({ $0.startDate == nil }) == false {
ForEach(days, id: \.self) { day in ForEach(days.filter({ selectedDay == nil || selectedDay == $0 }), id: \.self) { day in
Section { Section {
ForEach(keys.filter({ $0.dayInt == day.dayInt }), id: \.self) { key in ForEach(keys.filter({ $0.dayInt == day.dayInt }), id: \.self) { key in
if let _matches = timeSlots[key] { if let _matches = timeSlots[key] {

@ -78,6 +78,13 @@ struct EditablePlayerView: View {
Logger.error(error) Logger.error(error)
} }
} }
.onChange(of: player.licenceId) {
do {
try self.tournamentStore.playerRegistrations.addOrUpdate(instance: player)
} catch {
Logger.error(error)
}
}
.onChange(of: player.hasArrived) { .onChange(of: player.hasArrived) {
do { do {
try self.tournamentStore.playerRegistrations.addOrUpdate(instance: player) try self.tournamentStore.playerRegistrations.addOrUpdate(instance: player)

@ -186,6 +186,27 @@ struct PlayerDetailView: View {
} }
} }
} }
Section {
if let number = player.phoneNumber?.replacingOccurrences(of: " ", with: "") {
if let url = URL(string: "tel:\(number)") {
Link(destination: url) {
Label("Appeler", systemImage: "phone")
}
}
if let url = URL(string: "sms:\(number)") {
Link(destination: url) {
Label("Message", systemImage: "message")
}
}
}
if let mail = player.email, let mailURL = URL(string: "mail:\(mail)") {
Link(destination: mailURL) {
Label("Mail", systemImage: "mail")
}
}
}
} }
.toolbar { .toolbar {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {

@ -126,9 +126,7 @@ struct InscriptionInfoView: View {
} }
} }
.listRowView(color: .logoRed) .listRowView(color: .logoRed)
}
Section {
DisclosureGroup { DisclosureGroup {
ForEach(homonyms) { player in ForEach(homonyms) { player in
ImportedPlayerView(player: player) ImportedPlayerView(player: player)
@ -141,8 +139,20 @@ struct InscriptionInfoView: View {
} }
} }
.listRowView(color: .logoRed) .listRowView(color: .logoRed)
}
DisclosureGroup {
ForEach(playersMissing) {
TeamDetailView(team: $0)
}
} label: {
LabeledContent {
Text(playersMissing.count.formatted())
} label: {
Text("Paires incomplètes")
}
}
.listRowView(color: .pink)
}
Section { Section {
DisclosureGroup { DisclosureGroup {
@ -200,6 +210,11 @@ struct InscriptionInfoView: View {
ForEach(playersWithoutValidLicense) { ForEach(playersWithoutValidLicense) {
EditablePlayerView(player: $0, editingOptions: [.licenceId]) EditablePlayerView(player: $0, editingOptions: [.licenceId])
.environmentObject(tournament.tournamentStore) .environmentObject(tournament.tournamentStore)
.onChange(of: $0.licenceId) {
players = tournament.unsortedPlayers()
let isImported = players.anySatisfy({ $0.isImported() })
playersWithoutValidLicense = tournament.playersWithoutValidLicense(in: players, isImported: isImported)
}
} }
} label: { } label: {
LabeledContent { LabeledContent {
@ -212,21 +227,6 @@ struct InscriptionInfoView: View {
} footer: { } footer: {
Text("importé du fichier beach-padel sans licence valide ou créé sans licence") Text("importé du fichier beach-padel sans licence valide ou créé sans licence")
} }
Section {
DisclosureGroup {
ForEach(playersMissing) {
TeamDetailView(team: $0)
}
} label: {
LabeledContent {
Text(playersMissing.count.formatted())
} label: {
Text("Paires incomplètes")
}
}
.listRowView(color: .pink)
}
} }
.task { .task {
await _getIssues() await _getIssues()

@ -165,6 +165,7 @@ struct InscriptionManagerView: View {
if self.teamsHash == nil, selectedSortedTeams.isEmpty == false { if self.teamsHash == nil, selectedSortedTeams.isEmpty == false {
self.teamsHash = _simpleHash(ids: selectedSortedTeams.map { $0.id }) self.teamsHash = _simpleHash(ids: selectedSortedTeams.map { $0.id })
} }
self.registrationIssues = nil
Task { Task {
self.registrationIssues = await tournament.registrationIssues() self.registrationIssues = await tournament.registrationIssues()
} }
@ -708,6 +709,12 @@ struct InscriptionManagerView: View {
NavigationLink { NavigationLink {
InscriptionInfoView() InscriptionInfoView()
.environment(tournament) .environment(tournament)
.onDisappear {
self.registrationIssues = nil
Task {
self.registrationIssues = await tournament.registrationIssues()
}
}
} label: { } label: {
LabeledContent { LabeledContent {
if let registrationIssues { if let registrationIssues {

@ -106,14 +106,14 @@ struct TableStructureView: View {
if structurePreset != .doubleGroupStage { if structurePreset != .doubleGroupStage {
LabeledContent { LabeledContent {
StepperView(count: $qualifiedPerGroupStage, minimum: 0, maximum: (teamsPerGroupStage-1)) StepperView(count: $qualifiedPerGroupStage, minimum: 1, maximum: (teamsPerGroupStage-1))
} label: { } label: {
Text("Qualifié\(qualifiedPerGroupStage.pluralSuffix) par poule") Text("Qualifié\(qualifiedPerGroupStage.pluralSuffix) par poule")
} }
if qualifiedPerGroupStage < teamsPerGroupStage - 1 { if qualifiedPerGroupStage < teamsPerGroupStage - 1 {
LabeledContent { LabeledContent {
StepperView(count: $groupStageAdditionalQualified, minimum: 1, maximum: maxMoreQualified) StepperView(count: $groupStageAdditionalQualified, minimum: 0, maximum: maxMoreQualified)
} label: { } label: {
Text("Qualifié\(groupStageAdditionalQualified.pluralSuffix) supplémentaires") Text("Qualifié\(groupStageAdditionalQualified.pluralSuffix) supplémentaires")
Text(moreQualifiedLabel) Text(moreQualifiedLabel)

Loading…
Cancel
Save