Merge branch 'main' into sync

sync2
Laurent 10 months ago
commit 9271cee545
  1. 8
      PadelClub.xcodeproj/project.pbxproj
  2. 2
      PadelClub/Data/TeamRegistration.swift
  3. 12
      PadelClub/Data/Tournament.swift
  4. 2
      PadelClub/PadelClubApp.swift
  5. 4
      PadelClub/Utils/ContactManager.swift
  6. 23
      PadelClub/Utils/Tips.swift
  7. 2
      PadelClub/Views/Calling/CallMessageCustomizationView.swift
  8. 2
      PadelClub/Views/Calling/CallView.swift
  9. 2
      PadelClub/Views/Calling/Components/PlayersWithoutContactView.swift
  10. 2
      PadelClub/Views/Calling/SendToAllView.swift
  11. 456
      PadelClub/Views/Planning/PlanningView.swift
  12. 41
      PadelClub/Views/Player/PlayerDetailView.swift
  13. 2
      PadelClub/Views/Team/EditingTeamView.swift
  14. 12
      PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift
  15. 13
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift

@ -3573,7 +3573,7 @@
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi"; INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi";
INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "En utilisant votre position, Padel Club peut trouver plus rapidement les clubs et les tournois autour de vous.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
@ -3584,7 +3584,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.39; MARKETING_VERSION = 1.0.42;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3617,7 +3617,7 @@
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi"; INFOPLIST_KEY_NSCalendarsUsageDescription = "Padel Club a besoin d'avoir accès à votre calendrier pour pouvoir y inscrire ce tournoi";
INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres"; INFOPLIST_KEY_NSCameraUsageDescription = "En autorisant l'application à utiliser la caméra, vous pourrez prendre des photos des rencontres";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "En utilisant votre position, Padel Club peut trouver plus rapidement les clubs et les tournois autour de vous.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
@ -3628,7 +3628,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.39; MARKETING_VERSION = 1.0.42;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";

@ -177,7 +177,7 @@ final class TeamRegistration: BaseTeamRegistration, SideStorable {
} }
func getPhoneNumbers() -> [String] { func getPhoneNumbers() -> [String] {
return players().compactMap { $0.phoneNumber }.filter({ $0.isMobileNumber() }) return players().compactMap { $0.phoneNumber }.filter({ $0.isEmpty == false })
} }
func getMail() -> [String] { func getMail() -> [String] {

@ -854,7 +854,7 @@ defer {
} }
} }
func registrationIssues() async -> Int { func registrationIssues() -> Int {
let players : [PlayerRegistration] = unsortedPlayers() let players : [PlayerRegistration] = unsortedPlayers()
let selectedTeams : [TeamRegistration] = selectedSortedTeams() let selectedTeams : [TeamRegistration] = selectedSortedTeams()
let callDateIssue : [TeamRegistration] = selectedTeams.filter { $0.callDate != nil && isStartDateIsDifferentThanCallDate($0) } let callDateIssue : [TeamRegistration] = selectedTeams.filter { $0.callDate != nil && isStartDateIsDifferentThanCallDate($0) }
@ -1178,7 +1178,7 @@ defer {
return unsortedTeams().first(where: { $0.includes(players: players) }) return unsortedTeams().first(where: { $0.includes(players: players) })
} }
func tournamentTitle(_ displayStyle: DisplayStyle = .wide) -> String { func tournamentTitle(_ displayStyle: DisplayStyle = .wide, hideSenior: Bool = false) -> String {
if tournamentLevel == .unlisted, displayStyle == .title { if tournamentLevel == .unlisted, displayStyle == .title {
if let name { if let name {
return name return name
@ -1186,7 +1186,13 @@ defer {
return tournamentLevel.localizedLevelLabel(.title) return tournamentLevel.localizedLevelLabel(.title)
} }
} }
let title: String = [tournamentLevel.localizedLevelLabel(displayStyle), tournamentCategory.localizedLabel(displayStyle), federalTournamentAge.localizedFederalAgeLabel(displayStyle)].filter({ $0.isEmpty == false }).joined(separator: " ") let displayStyleCategory = hideSenior ? .short : displayStyle
var levelCategory = [tournamentLevel.localizedLevelLabel(displayStyle), tournamentCategory.localizedLabel(displayStyle)]
if displayStyle == .short {
levelCategory = [tournamentLevel.localizedLevelLabel(displayStyle) + tournamentCategory.localizedLabel(displayStyle)]
}
let array = levelCategory + [federalTournamentAge.localizedFederalAgeLabel(displayStyleCategory)]
let title: String = array.filter({ $0.isEmpty == false }).joined(separator: " ")
if displayStyle == .wide, let name { if displayStyle == .wide, let name {
return [title, name].joined(separator: " - ") return [title, name].joined(separator: " - ")
} else { } else {

@ -102,7 +102,7 @@ print("Running in Release mode")
//try? Tips.resetDatastore() //try? Tips.resetDatastore()
try? Tips.configure([ try? Tips.configure([
.displayFrequency(.daily), .displayFrequency(.immediate),
.datastoreLocation(.applicationDefault) .datastoreLocation(.applicationDefault)
]) ])
} }

@ -82,7 +82,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 date = startDate ?? tournament?.startDate ?? Date()
if let tournament { if let tournament {
text = text.replacingOccurrences(of: "#titre", with: tournament.tournamentTitle(.short)) text = text.replacingOccurrences(of: "#titre", with: tournament.tournamentTitle(.title, hideSenior: true))
text = text.replacingOccurrences(of: "#prix", with: tournament.entryFeeMessage) text = text.replacingOccurrences(of: "#prix", with: tournament.entryFeeMessage)
} }
@ -132,7 +132,7 @@ Il est conseillé de vous présenter 10 minutes avant de jouer.\n\nMerci de me c
let intro = reSummon ? "Suite à des forfaits, vous êtes finalement" : "Vous êtes" let intro = reSummon ? "Suite à des forfaits, vous êtes finalement" : "Vous êtes"
if let tournament { if let tournament {
return "Bonjour,\n\n\(intro) \(localizedCalled) pour jouer en \(roundLabel.lowercased()) du \(tournament.tournamentTitle(.short)) au \(clubName) le \(date.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(date.formatted(Date.FormatStyle().hour().minute())).\n\n" + computedMessage + "\n\n\(signature)" 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)"
} else { } else {
return "Bonjour,\n\n\(intro) \(localizedCalled) \(roundLabel) au \(clubName) le \(date.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(date.formatted(Date.FormatStyle().hour().minute())).\n\nMerci de confirmer en répondant à ce message et de prévenir votre partenaire !\n\n\(signature)" return "Bonjour,\n\n\(intro) \(localizedCalled) \(roundLabel) au \(clubName) le \(date.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(date.formatted(Date.FormatStyle().hour().minute())).\n\nMerci de confirmer en répondant à ce message et de prévenir votre partenaire !\n\n\(signature)"
} }

@ -549,29 +549,36 @@ struct TeamsExportTip: Tip {
} }
} }
struct PlayerTournamentSearchTip: Tip { struct TimeSlotMoveTip: Tip {
var title: Text { var title: Text {
Text("Cherchez un tournoi autour de vous !") Text("Réorganisez vos créneaux horaires !")
} }
var message: Text? { var message: Text? {
Text("Padel Club facilite la recherche de tournois et l'inscription !") Text("Vous pouvez déplacer les créneaux horaires dans la liste en glissant-déposant.")
} }
var image: Image? { var image: Image? {
Image(systemName: "trophy.circle") Image(systemName: "arrow.up.arrow.down.circle")
}
} }
var actions: [Action] { struct TimeSlotMoveOptionTip: Tip {
Action(id: ActionKey.selectAction.rawValue, title: "Éssayer") var title: Text {
Text("Réorganisez vos créneaux horaires !")
} }
enum ActionKey: String { var message: Text? {
case selectAction = "selectAction" Text("En cliquant ici, vous pouvez déplacer les créneaux horaires dans la liste en glissant-déposant.")
} }
var image: Image? {
Image(systemName: "sparkles")
}
} }
struct TipStyleModifier: ViewModifier { struct TipStyleModifier: ViewModifier {
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
var tint: Color? var tint: Color?

@ -56,7 +56,7 @@ struct CallMessageCustomizationView: View {
var finalMessage: String? { var finalMessage: String? {
let localizedCalled = "convoqué" + (tournament.tournamentCategory == .women ? "e" : "") + "s" let localizedCalled = "convoqué" + (tournament.tournamentCategory == .women ? "e" : "") + "s"
return "Bonjour,\n\nVous êtes \(localizedCalled) pour jouer en \(RoundRule.roundName(fromRoundIndex: 2).lowercased()) du \(tournament.tournamentTitle(.short)) au \(clubName) le \(tournament.startDate.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(tournament.startDate.formatted(Date.FormatStyle().hour().minute())).\n\n" + computedMessage + "\n\n\(customCallMessageSignature)" return "Bonjour,\n\nVous êtes \(localizedCalled) pour jouer en \(RoundRule.roundName(fromRoundIndex: 2).lowercased()) du \(tournament.tournamentTitle(.title, hideSenior: true)) au \(clubName) le \(tournament.startDate.formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(tournament.startDate.formatted(Date.FormatStyle().hour().minute())).\n\n" + computedMessage + "\n\n\(customCallMessageSignature)"
} }
var body: some View { var body: some View {

@ -387,7 +387,7 @@ struct CallView: View {
recipients: tournament.umpireMail(), recipients: tournament.umpireMail(),
bccRecipients: teams.flatMap { $0.getMail() }, bccRecipients: teams.flatMap { $0.getMail() },
body: finalMessage(reSummon: reSummon, forcedEmptyMessage: forcedEmptyMessage), body: finalMessage(reSummon: reSummon, forcedEmptyMessage: forcedEmptyMessage),
subject: tournament.tournamentTitle(), subject: tournament.tournamentTitle(hideSenior: true),
tournamentBuild: nil) tournamentBuild: nil)
} }

@ -45,7 +45,7 @@ struct PlayersWithoutContactView: View {
LabeledContent { LabeledContent {
Text(withoutPhones.count.formatted()) Text(withoutPhones.count.formatted())
} label: { } label: {
Text("Joueurs sans téléphone portable") Text("Joueurs sans téléphone portable français")
} }
} }
} header: { } header: {

@ -273,7 +273,7 @@ struct SendToAllView: View {
if contactMethod == 0 { if contactMethod == 0 {
contactType = .message(date: nil, recipients: _teams().flatMap { $0.unsortedPlayers() }.compactMap { $0.phoneNumber }, body: finalMessage(), tournamentBuild: nil) contactType = .message(date: nil, recipients: _teams().flatMap { $0.unsortedPlayers() }.compactMap { $0.phoneNumber }, body: finalMessage(), tournamentBuild: nil)
} else { } else {
contactType = .mail(date: nil, recipients: tournament.umpireMail(), bccRecipients: _teams().flatMap { $0.unsortedPlayers() }.compactMap { $0.email }, body: finalMessage(), subject: tournament.tournamentTitle(), tournamentBuild: nil) contactType = .mail(date: nil, recipients: tournament.umpireMail(), bccRecipients: _teams().flatMap { $0.unsortedPlayers() }.compactMap { $0.email }, body: finalMessage(), subject: tournament.tournamentTitle(hideSenior: true), tournamentBuild: nil)
} }
} }
} }

@ -6,16 +6,21 @@
// //
import SwiftUI import SwiftUI
import LeStorage
import TipKit
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? @State private var selectedDay: Date?
@Binding var selectedScheduleDestination: ScheduleDestination? @Binding var selectedScheduleDestination: ScheduleDestination?
@State private var filterOption: PlanningFilterOption = .byDefault @State private var filterOption: PlanningFilterOption = .byDefault
@State private var showFinishedMatches: Bool = false @State private var showFinishedMatches: Bool = false
@State private var enableMove: Bool = false
let allMatches: [Match] let allMatches: [Match]
let timeSlotMoveOptionTip = TimeSlotMoveOptionTip()
init(matches: [Match], selectedScheduleDestination: Binding<ScheduleDestination?>) { init(matches: [Match], selectedScheduleDestination: Binding<ScheduleDestination?>) {
self.allMatches = matches self.allMatches = matches
@ -38,22 +43,6 @@ struct PlanningView: View {
timeSlots.keys.sorted() timeSlots.keys.sorted()
} }
enum PlanningFilterOption: Int, CaseIterable, Identifiable {
var id: Int { self.rawValue }
case byDefault
case byCourt
func localizedPlanningLabel() -> String {
switch self {
case .byCourt:
return "Par terrain"
case .byDefault:
return "Par ordre des matchs"
}
}
}
private func _computedTitle(days: [Date]) -> String { private func _computedTitle(days: [Date]) -> String {
if let selectedDay { if let selectedDay {
return selectedDay.formatted(.dateTime.day().weekday().month()) return selectedDay.formatted(.dateTime.day().weekday().month())
@ -71,8 +60,13 @@ struct PlanningView: View {
let keys = self.keys(timeSlots: timeSlots) let keys = self.keys(timeSlots: timeSlots)
let days = self.days(timeSlots: timeSlots) let days = self.days(timeSlots: timeSlots)
let matches = matches let matches = matches
BySlotView(days: days, keys: keys, timeSlots: timeSlots, matches: matches, selectedDay: selectedDay, filterOption: filterOption, showFinishedMatches: showFinishedMatches) let notSlots = matches.allSatisfy({ $0.startDate == nil })
BySlotView(days: days, keys: keys, timeSlots: timeSlots, matches: matches, selectedDay: selectedDay)
.environment(\.filterOption, filterOption)
.environment(\.showFinishedMatches, showFinishedMatches)
.environment(\.enableMove, enableMove)
.navigationTitle(Text(_computedTitle(days: days))) .navigationTitle(Text(_computedTitle(days: days)))
.navigationBarBackButtonHidden(enableMove)
.toolbar(content: { .toolbar(content: {
if days.count > 1 { if days.count > 1 {
ToolbarTitleMenu { ToolbarTitleMenu {
@ -89,11 +83,41 @@ struct PlanningView: View {
Text("Jour") Text("Jour")
} }
.pickerStyle(.automatic) .pickerStyle(.automatic)
.disabled(enableMove)
} }
} }
if enableMove {
ToolbarItem(placement: .topBarLeading) {
Button("Annuler") {
enableMove = false
}
}
ToolbarItem(placement: .topBarTrailing) {
Button("Sauver") {
do {
try self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: allMatches)
} catch {
Logger.error(error)
}
enableMove = false
}
}
} else {
ToolbarItemGroup(placement: .topBarTrailing) { ToolbarItemGroup(placement: .topBarTrailing) {
if notSlots == false {
Toggle(isOn: $enableMove) {
Label("Déplacer", systemImage: "rectangle.2.swap")
}
.popoverTip(timeSlotMoveOptionTip)
}
Menu { Menu {
Section {
Picker(selection: $showFinishedMatches) { Picker(selection: $showFinishedMatches) {
Text("Afficher tous les matchs").tag(true) Text("Afficher tous les matchs").tag(true)
Text("Masquer les matchs terminés").tag(false) Text("Masquer les matchs terminés").tag(false)
@ -102,11 +126,13 @@ struct PlanningView: View {
} }
.labelsHidden() .labelsHidden()
.pickerStyle(.inline) .pickerStyle(.inline)
} label: { } header: {
Label("Filtrer", systemImage: "clock.badge.checkmark") Text("Option de filtrage")
.symbolVariant(showFinishedMatches ? .fill : .none)
} }
Menu {
Divider()
Section {
Picker(selection: $filterOption) { Picker(selection: $filterOption) {
ForEach(PlanningFilterOption.allCases) { ForEach(PlanningFilterOption.allCases) {
Text($0.localizedPlanningLabel()).tag($0) Text($0.localizedPlanningLabel()).tag($0)
@ -116,15 +142,20 @@ struct PlanningView: View {
} }
.labelsHidden() .labelsHidden()
.pickerStyle(.inline) .pickerStyle(.inline)
} header: {
Text("Option de triage")
}
} label: { } label: {
Label("Trier", systemImage: "line.3.horizontal.decrease.circle") Label("Trier", systemImage: "line.3.horizontal.decrease.circle")
.symbolVariant(filterOption == .byCourt ? .fill : .none) .symbolVariant(filterOption == .byCourt || showFinishedMatches ? .fill : .none)
} }
} }
}
}) })
.overlay { .overlay {
if matches.allSatisfy({ $0.startDate == nil }) { if notSlots {
ContentUnavailableView { ContentUnavailableView {
Label("Aucun horaire défini", systemImage: "clock.badge.questionmark") Label("Aucun horaire défini", systemImage: "clock.badge.questionmark")
} description: { } description: {
@ -140,28 +171,146 @@ struct PlanningView: View {
struct BySlotView: View { struct BySlotView: View {
@Environment(Tournament.self) var tournament: Tournament @Environment(Tournament.self) var tournament: Tournament
@Environment(\.filterOption) private var filterOption
@Environment(\.showFinishedMatches) private var showFinishedMatches
@Environment(\.enableMove) private var enableMove
let days: [Date] let days: [Date]
let keys: [Date] let keys: [Date]
let timeSlots: [Date: [Match]] let timeSlots: [Date: [Match]]
let matches: [Match] let matches: [Match]
let selectedDay: Date? let selectedDay: Date?
let filterOption: PlanningFilterOption let timeSlotMoveTip = TimeSlotMoveTip()
let showFinishedMatches: Bool
var body: some View { var body: some View {
List { List {
if matches.allSatisfy({ $0.startDate == nil }) == false {
if enableMove {
TipView(timeSlotMoveTip)
.tipStyle(tint: .logoYellow, asSection: true)
}
if !matches.allSatisfy({ $0.startDate == nil }) {
ForEach(days.filter({ selectedDay == nil || selectedDay == $0 }), id: \.self) { day in ForEach(days.filter({ selectedDay == nil || selectedDay == $0 }), id: \.self) { day in
DaySectionView(
day: day,
keys: keys.filter({ $0.dayInt == day.dayInt }),
timeSlots: timeSlots,
selectedDay: selectedDay
)
}
}
}
}
}
struct DaySectionView: View {
@Environment(Tournament.self) var tournament: Tournament
@Environment(\.filterOption) private var filterOption
@Environment(\.showFinishedMatches) private var showFinishedMatches
@Environment(\.enableMove) private var enableMove
let day: Date
let keys: [Date]
let timeSlots: [Date: [Match]]
let selectedDay: Date?
var body: some View {
Section { Section {
ForEach(keys.filter({ $0.dayInt == day.dayInt }), id: \.self) { key in ForEach(keys, id: \.self) { key in
if let _matches = timeSlots[key]?.sorted(by: filterOption == .byDefault ? \.computedOrder : \.courtIndexForSorting) { TimeSlotSectionView(
key: key,
matches: timeSlots[key]?.sorted(by: filterOption == .byDefault ? \.computedOrder : \.courtIndexForSorting) ?? []
)
}
.onMove(perform: enableMove ? moveSection : nil)
} header: {
HeaderView(day: day, timeSlots: timeSlots)
} footer: {
if day.monthYearFormatted == Date.distantFuture.monthYearFormatted {
Text("Il s'agit des matchs qui n'ont pas réussi à être placé par Padel Club. Peut-être à cause de créneaux indisponibles, d'autres tournois ou des réglages.")
}
}
}
func moveSection(from source: IndexSet, to destination: Int) {
let daySlots = keys.filter { $0.dayInt == day.dayInt }.sorted()
guard let sourceIdx = source.first,
sourceIdx < daySlots.count,
destination <= daySlots.count else {
return
}
// Create a mutable copy of the time slots for this day
var slotsToUpdate = daySlots
let updateRange = min(sourceIdx, destination)...max(sourceIdx, destination)
// Perform the move in the array
let sourceTime = slotsToUpdate.remove(at: sourceIdx)
if sourceIdx < destination {
slotsToUpdate.insert(sourceTime, at: destination - 1)
} else {
slotsToUpdate.insert(sourceTime, at: destination)
}
// Update matches by swapping their startDates
for index in updateRange {
// Find the new time slot for these matches
let oldStartTime = slotsToUpdate[index]
let newStartTime = daySlots[index]
guard let matchesToUpdate = timeSlots[oldStartTime] else { continue }
// Update each match with the new start time
for match in matchesToUpdate {
match.startDate = newStartTime
}
}
}
}
struct TimeSlotSectionView: View {
@Environment(\.enableMove) private var enableMove
let key: Date
let matches: [Match]
var body: some View {
if !matches.isEmpty {
if enableMove {
TimeSlotHeaderView(key: key, matches: matches)
} else {
DisclosureGroup { DisclosureGroup {
ForEach(_matches) { match in MatchListView(matches: matches)
} label: {
TimeSlotHeaderView(key: key, matches: matches)
}
}
}
}
}
struct MatchListView: View {
let matches: [Match]
var body: some View {
ForEach(matches) { match in
NavigationLink { NavigationLink {
MatchDetailView(match: match) MatchDetailView(match: match)
.matchViewStyle(.sectionedStandardStyle) .matchViewStyle(.sectionedStandardStyle)
} label: { } label: {
MatchRowView(match: match)
}
}
}
}
struct MatchRowView: View {
let match: Match
var body: some View {
LabeledContent { LabeledContent {
if let courtName = match.courtName() { if let courtName = match.courtName() {
Text(courtName) Text(courtName)
@ -176,12 +325,17 @@ struct PlanningView: View {
} }
} }
} }
} label: {
_timeSlotView(key: key, matches: _matches)
} struct HeaderView: View {
} @Environment(\.filterOption) private var filterOption
} @Environment(\.showFinishedMatches) private var showFinishedMatches
} header: { @Environment(\.enableMove) private var enableMove
let day: Date
let timeSlots: [Date: [Match]]
var body: some View {
HStack { HStack {
if day.monthYearFormatted == Date.distantFuture.monthYearFormatted { if day.monthYearFormatted == Date.distantFuture.monthYearFormatted {
Text("Sans horaire") Text("Sans horaire")
@ -191,18 +345,9 @@ struct PlanningView: View {
Spacer() Spacer()
let count = _matchesCount(inDayInt: day.dayInt, timeSlots: timeSlots) let count = _matchesCount(inDayInt: day.dayInt, timeSlots: timeSlots)
if showFinishedMatches { if showFinishedMatches {
Text(self._formattedMatchCount(count)) Text(_formattedMatchCount(count))
} else { } else {
Text(self._formattedMatchCount(count) + " restant\(count.pluralSuffix)") Text("\(_formattedMatchCount(count)) restant\(count.pluralSuffix)")
}
}
} footer: {
if day.monthYearFormatted == Date.distantFuture.monthYearFormatted {
Text("Il s'agit des matchs qui n'ont pas réussi à être placé par Padel Club. Peut-être à cause de créneaux indisponibles, d'autres tournois ou des réglages.")
}
}
.headerProminence(.increased)
}
} }
} }
} }
@ -211,15 +356,28 @@ struct PlanningView: View {
timeSlots.filter { $0.key.dayInt == dayInt }.flatMap({ $0.value }).count timeSlots.filter { $0.key.dayInt == dayInt }.flatMap({ $0.value }).count
} }
private func _timeSlotView(key: Date, matches: [Match]) -> some View { private func _formattedMatchCount(_ count: Int) -> String {
return "\(count.formatted()) match\(count.pluralSuffix)"
}
}
struct TimeSlotHeaderView: View {
let key: Date
let matches: [Match]
@Environment(Tournament.self) var tournament: Tournament
var body: some View {
LabeledContent { LabeledContent {
Text(self._formattedMatchCount(matches.count)) Text("\(matches.count.formatted()) match\(matches.count.pluralSuffix)")
} label: { } label: {
if key.monthYearFormatted == Date.distantFuture.monthYearFormatted { if key.monthYearFormatted == Date.distantFuture.monthYearFormatted {
Text("Aucun horaire") Text("Aucun horaire")
} else { } else {
Text(key.formatted(date: .omitted, time: .shortened)).font(.title).fontWeight(.semibold) Text(key.formatted(date: .omitted, time: .shortened))
.font(.title)
.fontWeight(.semibold)
} }
if matches.count <= tournament.courtCount { if matches.count <= tournament.courtCount {
let names = matches.sorted(by: \.computedOrder) let names = matches.sorted(by: \.computedOrder)
.compactMap({ $0.roundTitle() }) .compactMap({ $0.roundTitle() })
@ -232,15 +390,203 @@ struct PlanningView: View {
} else { } else {
Text(matches.count.formatted().appending(" matchs")) Text(matches.count.formatted().appending(" matchs"))
} }
}
} }
} }
fileprivate func _formattedMatchCount(_ count: Int) -> String {
return "\(count.formatted()) match\(count.pluralSuffix)" // struct BySlotView: View {
// @Environment(Tournament.self) var tournament: Tournament
// let days: [Date]
// let keys: [Date]
// let timeSlots: [Date:[Match]]
// let matches: [Match]
// let selectedDay: Date?
// let filterOption: PlanningFilterOption
// let showFinishedMatches: Bool
//
// var body: some View {
// List {
// if matches.allSatisfy({ $0.startDate == nil }) == false {
// ForEach(days.filter({ selectedDay == nil || selectedDay == $0 }), id: \.self) { day in
// Section {
// ForEach(keys.filter({ $0.dayInt == day.dayInt }), id: \.self) { key in
// if let _matches = timeSlots[key]?.sorted(by: filterOption == .byDefault ? \.computedOrder : \.courtIndexForSorting) {
// DisclosureGroup {
// ForEach(_matches) { match in
// NavigationLink {
// MatchDetailView(match: match)
// .matchViewStyle(.sectionedStandardStyle)
//
// } label: {
// LabeledContent {
// if let courtName = match.courtName() {
// Text(courtName)
// }
// } label: {
// if let groupStage = match.groupStageObject {
// Text(groupStage.groupStageTitle(.title))
// } else if let round = match.roundObject {
// Text(round.roundTitle())
// }
// Text(match.matchTitle())
// }
// }
// }
// } label: {
// _timeSlotView(key: key, matches: _matches)
// }
// }
// }
// .onMove(perform: moveSection)
// } header: {
// HStack {
// if day.monthYearFormatted == Date.distantFuture.monthYearFormatted {
// Text("Sans horaire")
// } else {
// Text(day.formatted(.dateTime.day().weekday().month()))
// }
// Spacer()
// let count = _matchesCount(inDayInt: day.dayInt, timeSlots: timeSlots)
// if showFinishedMatches {
// Text(self._formattedMatchCount(count))
// } else {
// Text(self._formattedMatchCount(count) + " restant\(count.pluralSuffix)")
// }
// }
// } footer: {
// if day.monthYearFormatted == Date.distantFuture.monthYearFormatted {
// Text("Il s'agit des matchs qui n'ont pas réussi à être placé par Padel Club. Peut-être à cause de créneaux indisponibles, d'autres tournois ou des réglages.")
// }
// }
// .headerProminence(.increased)
// }
// }
// }
// }
//
// func moveSection(from source: IndexSet, to destination: Int) {
// let daySlots = keys.filter { selectedDay == nil || $0.dayInt == selectedDay?.dayInt }.sorted()
//
// guard let sourceIdx = source.first,
// sourceIdx < daySlots.count,
// destination <= daySlots.count else {
// return
// }
//
// // Create a mutable copy of the time slots for this day
// var slotsToUpdate = daySlots
//
// let updateRange = min(sourceIdx, destination)...max(sourceIdx, destination) - 1
// print(updateRange)
//
// // Perform the move in the array
// let sourceTime = slotsToUpdate.remove(at: sourceIdx)
// if sourceIdx < destination {
// slotsToUpdate.insert(sourceTime, at: destination - 1)
// } else {
// slotsToUpdate.insert(sourceTime, at: destination)
// }
//
// // Update matches by swapping their startDates
// for index in updateRange {
// // Find the new time slot for these matches
// let oldStartTime = slotsToUpdate[index]
// let newStartTime = daySlots[index]
// guard let matchesToUpdate = timeSlots[oldStartTime] else { continue }
// print("moving", oldStartTime, "to", newStartTime)
//
// // Update each match with the new start time
// for match in matchesToUpdate {
// match.startDate = newStartTime
// }
// }
//
// try? self.tournament.tournamentStore.matches.addOrUpdate(contentOfs: matches)
// }
//
//
// private func _matchesCount(inDayInt dayInt: Int, timeSlots: [Date:[Match]]) -> Int {
// timeSlots.filter { $0.key.dayInt == dayInt }.flatMap({ $0.value }).count
// }
//
// private func _timeSlotView(key: Date, matches: [Match]) -> some View {
// LabeledContent {
// Text(self._formattedMatchCount(matches.count))
// } label: {
// if key.monthYearFormatted == Date.distantFuture.monthYearFormatted {
// Text("Aucun horaire")
// } else {
// Text(key.formatted(date: .omitted, time: .shortened)).font(.title).fontWeight(.semibold)
// }
// if matches.count <= tournament.courtCount {
// let names = matches.sorted(by: \.computedOrder)
// .compactMap({ $0.roundTitle() })
// .reduce(into: [String]()) { uniqueNames, name in
// if !uniqueNames.contains(name) {
// uniqueNames.append(name)
// }
// }
// Text(names.joined(separator: ", ")).lineLimit(1).truncationMode(.tail)
// } else {
// Text(matches.count.formatted().appending(" matchs"))
// }
// }
// }
//
// fileprivate func _formattedMatchCount(_ count: Int) -> String {
// return "\(count.formatted()) match\(count.pluralSuffix)"
// }
// }
}
enum PlanningFilterOption: Int, CaseIterable, Identifiable {
var id: Int { self.rawValue }
case byDefault
case byCourt
func localizedPlanningLabel() -> String {
switch self {
case .byCourt:
return "Par terrain"
case .byDefault:
return "Par ordre des matchs"
} }
} }
} }
//#Preview {
// PlanningView(matches: [], selectedScheduleDestination: .constant(nil)) struct FilterOptionKey: EnvironmentKey {
//} static let defaultValue: PlanningFilterOption = .byDefault
}
extension EnvironmentValues {
var filterOption: PlanningFilterOption {
get { self[FilterOptionKey.self] }
set { self[FilterOptionKey.self] = newValue }
}
}
struct ShowFinishedMatchesKey: EnvironmentKey {
static let defaultValue: Bool = false
}
extension EnvironmentValues {
var showFinishedMatches: Bool {
get { self[ShowFinishedMatchesKey.self] }
set { self[ShowFinishedMatchesKey.self] = newValue }
}
}
struct EnableMoveKey: EnvironmentKey {
static let defaultValue: Bool = false
}
extension EnvironmentValues {
var enableMove: Bool {
get { self[EnableMoveKey.self] }
set { self[EnableMoveKey.self] = newValue }
}
}

@ -152,10 +152,6 @@ struct PlayerDetailView: View {
Menu { Menu {
CopyPasteButtonView(pasteValue: player.phoneNumber) CopyPasteButtonView(pasteValue: player.phoneNumber)
PasteButtonView(text: $phoneNumber) PasteButtonView(text: $phoneNumber)
.onChange(of: phoneNumber) {
player.phoneNumber = phoneNumber.prefixTrimmed(50)
_save()
}
} label: { } label: {
Text("Téléphone") Text("Téléphone")
} }
@ -177,10 +173,6 @@ struct PlayerDetailView: View {
Menu { Menu {
CopyPasteButtonView(pasteValue: player.email) CopyPasteButtonView(pasteValue: player.email)
PasteButtonView(text: $email) PasteButtonView(text: $email)
.onChange(of: email) {
player.email = email.prefixTrimmed(50)
_save()
}
} label: { } label: {
Text("Email") Text("Email")
} }
@ -216,13 +208,6 @@ struct PlayerDetailView: View {
} }
} }
} }
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
ShareLink(item: player.pasteData()) {
Label("Partager", systemImage: "square.and.arrow.up")
}
}
}
.onChange(of: player.hasArrived) { .onChange(of: player.hasArrived) {
_save() _save()
} }
@ -230,7 +215,17 @@ struct PlayerDetailView: View {
_save() _save()
} }
.navigationBarBackButtonHidden(focusedField != nil) .navigationBarBackButtonHidden(focusedField != nil)
.toolbar(content: { .headerProminence(.increased)
.navigationTitle("Édition")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
ShareLink(item: player.pasteData()) {
Label("Partager", systemImage: "square.and.arrow.up")
}
}
if focusedField != nil { if focusedField != nil {
ToolbarItem(placement: .topBarLeading) { ToolbarItem(placement: .topBarLeading) {
Button("Annuler", role: .cancel) { Button("Annuler", role: .cancel) {
@ -238,14 +233,9 @@ struct PlayerDetailView: View {
} }
} }
} }
}) if focusedField == ._rank || focusedField == ._computedRank || focusedField == ._phoneNumber {
.headerProminence(.increased) ToolbarItemGroup(placement: .keyboard) {
.navigationTitle("Édition") Spacer()
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.toolbar {
if focusedField == ._rank || focusedField == ._computedRank {
ToolbarItem(placement: .keyboard) {
Button("Valider") { Button("Valider") {
if focusedField == ._rank { if focusedField == ._rank {
player.setComputedRank(in: tournament) player.setComputedRank(in: tournament)
@ -254,6 +244,9 @@ struct PlayerDetailView: View {
} else if focusedField == ._computedRank { } else if focusedField == ._computedRank {
player.team()?.updateWeight(inTournamentCategory: tournament.tournamentCategory) player.team()?.updateWeight(inTournamentCategory: tournament.tournamentCategory)
_save() _save()
} else if focusedField == ._phoneNumber {
player.phoneNumber = phoneNumber.prefixTrimmed(50)
_save()
} }
focusedField = nil focusedField = nil
} }

@ -20,7 +20,6 @@ struct EditingTeamView: View {
@State private var sentError: ContactManagerError? = nil @State private var sentError: ContactManagerError? = nil
@State private var showSubscriptionView: Bool = false @State private var showSubscriptionView: Bool = false
@State private var registrationDate : Date @State private var registrationDate : Date
@State private var callDate : Date
@State private var name: String @State private var name: String
@FocusState private var focusedField: TeamRegistration.CodingKeys? @FocusState private var focusedField: TeamRegistration.CodingKeys?
@ -42,7 +41,6 @@ struct EditingTeamView: View {
self.team = team self.team = team
_name = .init(wrappedValue: team.name ?? "") _name = .init(wrappedValue: team.name ?? "")
_registrationDate = State(wrappedValue: team.registrationDate ?? Date()) _registrationDate = State(wrappedValue: team.registrationDate ?? Date())
_callDate = State(wrappedValue: team.callDate ?? Date())
} }
var body: some View { var body: some View {

@ -9,7 +9,7 @@ import SwiftUI
struct InscriptionInfoView: View { struct InscriptionInfoView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament let tournament: Tournament
@State private var players : [PlayerRegistration] = [] @State private var players : [PlayerRegistration] = []
@State private var selectedTeams : [TeamRegistration] = [] @State private var selectedTeams : [TeamRegistration] = []
@ -244,16 +244,17 @@ struct InscriptionInfoView: View {
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")
} }
} }
.task { .onAppear {
await _getIssues() DispatchQueue.main.async {
_getIssues()
}
} }
.navigationTitle("Synthèse") .navigationTitle("Synthèse")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
} }
private func _getIssues() async { private func _getIssues() {
Task {
players = tournament.unsortedPlayers() players = tournament.unsortedPlayers()
selectedTeams = tournament.selectedSortedTeams() selectedTeams = tournament.selectedSortedTeams()
callDateIssue = selectedTeams.filter { $0.callDate != nil && tournament.isStartDateIsDifferentThanCallDate($0) } callDateIssue = selectedTeams.filter { $0.callDate != nil && tournament.isStartDateIsDifferentThanCallDate($0) }
@ -269,7 +270,6 @@ struct InscriptionInfoView: View {
playersMissing = selectedTeams.filter({ $0.unsortedPlayers().count < 2 }) playersMissing = selectedTeams.filter({ $0.unsortedPlayers().count < 2 })
} }
} }
}
//#Preview { //#Preview {
// InscriptionInfoView() // InscriptionInfoView()

@ -173,8 +173,8 @@ struct InscriptionManagerView: View {
self.teamsHash = _simpleHash(ids: selectedSortedTeams.map { $0.id }) self.teamsHash = _simpleHash(ids: selectedSortedTeams.map { $0.id })
} }
self.registrationIssues = nil self.registrationIssues = nil
Task { DispatchQueue.main.async {
self.registrationIssues = await tournament.registrationIssues() self.registrationIssues = tournament.registrationIssues()
} }
} }
@ -718,14 +718,7 @@ struct InscriptionManagerView: View {
if tournament.isAnimation() == false { if tournament.isAnimation() == false {
NavigationLink { NavigationLink {
InscriptionInfoView() InscriptionInfoView(tournament: tournament)
.environment(tournament)
.onDisappear {
self.registrationIssues = nil
Task {
self.registrationIssues = await tournament.registrationIssues()
}
}
} label: { } label: {
LabeledContent { LabeledContent {
if let registrationIssues { if let registrationIssues {

Loading…
Cancel
Save