commit
3e01aeb7ff
@ -0,0 +1,53 @@ |
||||
// |
||||
// MonthData.swift |
||||
// PadelClub |
||||
// |
||||
// Created by Razmig Sarkissian on 18/04/2024. |
||||
// |
||||
|
||||
import Foundation |
||||
import SwiftUI |
||||
import LeStorage |
||||
|
||||
@Observable |
||||
class MonthData : ModelObject, Storable { |
||||
|
||||
static func resourceName() -> String { return "month-data" } |
||||
|
||||
private(set) var id: String = Store.randomId() |
||||
private(set) var monthKey: String |
||||
private(set) var creationDate: Date |
||||
|
||||
var maleUnrankedValue: Int? = nil |
||||
var femaleUnrankedValue: Int? = nil |
||||
|
||||
init(monthKey: String) { |
||||
self.monthKey = monthKey |
||||
self.creationDate = Date() |
||||
} |
||||
|
||||
static func calculateCurrentUnrankedValues(mostRecentDateAvailable: Date) async { |
||||
let lastDataSourceMaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: mostRecentDateAvailable, man: true) |
||||
let lastDataSourceFemaleUnranked = await FederalPlayer.lastRank(mostRecentDateAvailable: mostRecentDateAvailable, man: false) |
||||
|
||||
await MainActor.run { |
||||
if let lastDataSource = DataStore.shared.appSettings.lastDataSource { |
||||
let currentMonthData : MonthData = Store.main.filter(isIncluded: { $0.monthKey == lastDataSource }).first ?? MonthData(monthKey: lastDataSource) |
||||
currentMonthData.maleUnrankedValue = lastDataSourceMaleUnranked |
||||
currentMonthData.femaleUnrankedValue = lastDataSourceFemaleUnranked |
||||
try? DataStore.shared.monthData.addOrUpdate(instance: currentMonthData) |
||||
} |
||||
} |
||||
} |
||||
|
||||
override func deleteDependencies() throws { |
||||
} |
||||
|
||||
enum CodingKeys: String, CodingKey { |
||||
case _id = "id" |
||||
case _monthKey = "monthKey" |
||||
case _creationDate = "creationDate" |
||||
case _maleUnrankedValue = "maleUnrankedValue" |
||||
case _femaleUnrankedValue = "femaleUnrankedValue" |
||||
} |
||||
} |
||||
@ -0,0 +1,13 @@ |
||||
// |
||||
// AppScreen.swift |
||||
// PadelClub |
||||
// |
||||
// Created by Razmig Sarkissian on 18/04/2024. |
||||
// |
||||
|
||||
import Foundation |
||||
|
||||
enum AppScreen: CaseIterable, Identifiable { |
||||
var id: Self { self } |
||||
case matchFormatSettings |
||||
} |
||||
@ -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 |
||||
} |
||||
} |
||||
@ -0,0 +1,186 @@ |
||||
// |
||||
// CallMessageCustomizationView.swift |
||||
// Padel Tournament |
||||
// |
||||
// Created by Razmig Sarkissian on 02/11/2023. |
||||
// |
||||
|
||||
import SwiftUI |
||||
|
||||
struct CallMessageCustomizationView: View { |
||||
@EnvironmentObject var dataStore: DataStore |
||||
var tournament: Tournament |
||||
|
||||
@FocusState private var textEditor: Bool |
||||
@State private var customClubName: String = "" |
||||
@State private var customCallMessageBody: String = "" |
||||
@State private var customCallMessageSignature: String = "" |
||||
|
||||
init(tournament: Tournament) { |
||||
self.tournament = tournament |
||||
_customCallMessageBody = State(wrappedValue: DataStore.shared.appSettings.callMessageBody ?? "") |
||||
_customCallMessageSignature = State(wrappedValue: DataStore.shared.appSettings.callMessageSignature ?? "") |
||||
_customClubName = State(wrappedValue: tournament.clubName ?? "") |
||||
} |
||||
|
||||
var clubName: String { |
||||
customClubName |
||||
} |
||||
|
||||
var formatMessage: String? { |
||||
dataStore.appSettings.callDisplayFormat ? tournament.matchFormat.computedLongLabel + "." : nil |
||||
} |
||||
|
||||
var entryFeeMessage: String? { |
||||
dataStore.appSettings.callDisplayEntryFee ? tournament.entryFeeMessage : nil |
||||
} |
||||
|
||||
var computedMessage: String { |
||||
[formatMessage, entryFeeMessage, customCallMessageBody].compacted().map { $0.trimmed }.joined(separator: "\n") |
||||
} |
||||
|
||||
var finalMessage: String? { |
||||
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()) au \(clubName) le \(Date().formatted(Date.FormatStyle().weekday(.wide).day().month(.wide))) à \(Date().formatted(Date.FormatStyle().hour().minute())).\n\n" + computedMessage + "\n\n\(customCallMessageSignature)" |
||||
} |
||||
|
||||
var body: some View { |
||||
@Bindable var appSettings = dataStore.appSettings |
||||
List { |
||||
Section { |
||||
ZStack { |
||||
Text(customCallMessageBody).hidden() |
||||
.padding(.vertical, 20) |
||||
TextEditor(text: $customCallMessageBody) |
||||
.autocorrectionDisabled() |
||||
.focused($textEditor) |
||||
} |
||||
} header: { |
||||
Text("Personnalisation du message de convocation") |
||||
} |
||||
|
||||
Section { |
||||
ZStack { |
||||
Text(customCallMessageSignature).hidden() |
||||
TextEditor(text: $customCallMessageSignature) |
||||
.autocorrectionDisabled() |
||||
.focused($textEditor) |
||||
} |
||||
} header: { |
||||
Text("Signature du message") |
||||
} |
||||
|
||||
Section { |
||||
TextField("Nom du club", text: $customClubName) |
||||
.autocorrectionDisabled() |
||||
.onSubmit { |
||||
if let eventClub = tournament.eventObject?.clubObject { |
||||
eventClub.name = customClubName |
||||
try? dataStore.clubs.addOrUpdate(instance: eventClub) |
||||
} |
||||
} |
||||
} header: { |
||||
Text("Nom du club") |
||||
} |
||||
|
||||
Section { |
||||
if appSettings.callUseFullCustomMessage { |
||||
Text(self.computedFullCustomMessage()) |
||||
.contextMenu { |
||||
Button("Coller dans le presse-papier") { |
||||
UIPasteboard.general.string = self.computedFullCustomMessage() |
||||
} |
||||
} |
||||
} |
||||
else if let finalMessage { |
||||
Text(finalMessage) |
||||
.contextMenu { |
||||
Button("Coller dans le presse-papier") { |
||||
UIPasteboard.general.string = finalMessage |
||||
} |
||||
} |
||||
} |
||||
} header: { |
||||
Text("Exemple") |
||||
} |
||||
|
||||
Section { |
||||
LabeledContent { |
||||
Toggle(isOn: $appSettings.callUseFullCustomMessage) { |
||||
|
||||
} |
||||
} label: { |
||||
Text("contrôle complet du message") |
||||
} |
||||
} header: { |
||||
Text("Personnalisation complète") |
||||
} footer: { |
||||
Text("Utilisez ces balises dans votre texte : #titre, #jour, #horaire, #club, #signature") |
||||
} |
||||
} |
||||
.navigationTitle("Message de convocation") |
||||
.toolbar { |
||||
ToolbarItem(placement: .topBarTrailing) { |
||||
Menu { |
||||
Picker(selection: $appSettings.callDisplayFormat) { |
||||
Text("Afficher le format").tag(true) |
||||
Text("Masquer le format").tag(false) |
||||
} label: { |
||||
|
||||
} |
||||
Picker(selection: $appSettings.callDisplayEntryFee) { |
||||
Text("Afficher le prix d'inscription").tag(true) |
||||
Text("Masquer le prix d'inscription").tag(false) |
||||
} label: { |
||||
|
||||
} |
||||
} label: { |
||||
LabelOptions() |
||||
} |
||||
} |
||||
ToolbarItemGroup(placement: .keyboard) { |
||||
if textEditor { |
||||
Spacer() |
||||
Button { |
||||
textEditor = false |
||||
} label: { |
||||
Label("Fermer", systemImage: "xmark") |
||||
} |
||||
} |
||||
} |
||||
} |
||||
.onChange(of: appSettings.callUseFullCustomMessage) { |
||||
if appSettings.callUseFullCustomMessage == false { |
||||
appSettings.callMessageBody = ContactType.defaultCustomMessage |
||||
} |
||||
_save() |
||||
} |
||||
.onChange(of: customCallMessageBody) { |
||||
appSettings.callMessageBody = customCallMessageBody |
||||
_save() |
||||
} |
||||
.onChange(of: customCallMessageSignature) { |
||||
appSettings.callMessageSignature = customCallMessageSignature |
||||
_save() |
||||
} |
||||
.onChange(of: appSettings.callDisplayEntryFee) { |
||||
_save() |
||||
} |
||||
.onChange(of: appSettings.callDisplayFormat) { |
||||
_save() |
||||
} |
||||
} |
||||
|
||||
private func _save() { |
||||
dataStore.updateSettings() |
||||
} |
||||
|
||||
func computedFullCustomMessage() -> String { |
||||
var text = customCallMessageBody.replacingOccurrences(of: "#titre", with: tournament.tournamentTitle()) |
||||
text = text.replacingOccurrences(of: "#club", with: clubName) |
||||
text = text.replacingOccurrences(of: "#jour", with: "\(Date().formatted(Date.FormatStyle().weekday(.wide).day().month(.wide)))") |
||||
text = text.replacingOccurrences(of: "#horaire", with: "\(Date().formatted(Date.FormatStyle().hour().minute()))") |
||||
text = text.replacingOccurrences(of: "#signature", with: customCallMessageSignature) |
||||
return text |
||||
} |
||||
} |
||||
@ -0,0 +1,56 @@ |
||||
// |
||||
// CashierSettingsView.swift |
||||
// PadelClub |
||||
// |
||||
// Created by Razmig Sarkissian on 17/04/2024. |
||||
// |
||||
|
||||
import SwiftUI |
||||
|
||||
struct CashierSettingsView: View { |
||||
@EnvironmentObject var dataStore: DataStore |
||||
var tournaments: [Tournament] |
||||
|
||||
init(tournaments: [Tournament]) { |
||||
self.tournaments = tournaments |
||||
} |
||||
|
||||
init(tournament: Tournament) { |
||||
self.tournaments = [tournament] |
||||
} |
||||
|
||||
var body: some View { |
||||
List { |
||||
Section { |
||||
RowButtonView("Tout le monde a réglé", role: .destructive) { |
||||
let players = tournaments.flatMap({ $0.selectedPlayers() }) |
||||
players.forEach { player in |
||||
if player.hasPaid() == false { |
||||
player.registrationType = .gift |
||||
} |
||||
} |
||||
try? dataStore.playerRegistrations.addOrUpdate(contentOfs: players) |
||||
} |
||||
} footer: { |
||||
Text("Passe tous les joueurs qui n'ont pas réglé en offert") |
||||
} |
||||
|
||||
Section { |
||||
RowButtonView("Personne n'a réglé", role: .destructive) { |
||||
let players = tournaments.flatMap({ $0.selectedPlayers() }) |
||||
players.forEach { player in |
||||
player.registrationType = nil |
||||
} |
||||
try? dataStore.playerRegistrations.addOrUpdate(contentOfs: players) |
||||
} |
||||
} footer: { |
||||
Text("Remet à zéro le type d'encaissement de tous les joueurs") |
||||
} |
||||
|
||||
} |
||||
} |
||||
} |
||||
|
||||
#Preview { |
||||
CashierSettingsView(tournaments: []) |
||||
} |
||||
@ -0,0 +1,311 @@ |
||||
// |
||||
// CashierView.swift |
||||
// Padel Tournament |
||||
// |
||||
// Created by Razmig Sarkissian on 04/03/2023. |
||||
// |
||||
|
||||
import SwiftUI |
||||
import Combine |
||||
|
||||
struct CashierView: View { |
||||
@EnvironmentObject var dataStore: DataStore |
||||
var tournaments : [Tournament] |
||||
var teams: [TeamRegistration] |
||||
@State private var sortOption: SortOption = .callDate |
||||
@State private var filterOption: FilterOption = .all |
||||
@State private var sortOrder: SortOrder = .ascending |
||||
@State private var searchText = "" |
||||
@State private var isSearching: Bool = false |
||||
|
||||
init(event: Event) { |
||||
self.tournaments = event.tournaments |
||||
self.teams = [] |
||||
} |
||||
|
||||
init(tournament: Tournament, teams: [TeamRegistration]) { |
||||
self.tournaments = [tournament] |
||||
self.teams = teams |
||||
} |
||||
|
||||
private func _sharedData() -> String { |
||||
let players = teams |
||||
.flatMap({ $0.players() }) |
||||
.map { |
||||
[$0.pasteData()] |
||||
.compacted() |
||||
.joined(separator: "\n") |
||||
} |
||||
.joined(separator: "\n\n") |
||||
return players |
||||
} |
||||
|
||||
enum SortOption: Int, Identifiable, CaseIterable { |
||||
case teamRank |
||||
case alphabeticalLastName |
||||
case alphabeticalFirstName |
||||
case playerRank |
||||
case age |
||||
case callDate |
||||
|
||||
var id: Int { self.rawValue } |
||||
func localizedLabel() -> String { |
||||
switch self { |
||||
case .callDate: |
||||
return "Convocation" |
||||
case .teamRank: |
||||
return "Poids d'équipe" |
||||
case .alphabeticalLastName: |
||||
return "Nom" |
||||
case .alphabeticalFirstName: |
||||
return "Prénom" |
||||
case .playerRank: |
||||
return "Rang" |
||||
case .age: |
||||
return "Âge" |
||||
} |
||||
} |
||||
} |
||||
|
||||
enum FilterOption: Int, Identifiable, CaseIterable { |
||||
case all |
||||
case didPay |
||||
case didNotPay |
||||
|
||||
var id: Int { self.rawValue } |
||||
|
||||
func localizedLabel() -> String { |
||||
switch self { |
||||
case .all: |
||||
return "Tous" |
||||
case .didPay: |
||||
return "Réglé" |
||||
case .didNotPay: |
||||
return "Non réglé" |
||||
} |
||||
} |
||||
|
||||
func shouldDisplayPlayer(_ player: PlayerRegistration) -> Bool { |
||||
switch self { |
||||
case .all: |
||||
return true |
||||
case .didPay: |
||||
return player.hasPaid() |
||||
case .didNotPay: |
||||
return player.hasPaid() == false |
||||
|
||||
} |
||||
} |
||||
} |
||||
|
||||
var body: some View { |
||||
List { |
||||
if isSearching == false { |
||||
Section { |
||||
Picker(selection: $filterOption) { |
||||
ForEach(FilterOption.allCases) { filterOption in |
||||
Text(filterOption.localizedLabel()).tag(filterOption) |
||||
} |
||||
} label: { |
||||
Text("Statut du règlement") |
||||
} |
||||
|
||||
Picker(selection: $sortOption) { |
||||
ForEach(SortOption.allCases) { sortOption in |
||||
Text(sortOption.localizedLabel()).tag(sortOption) |
||||
} |
||||
} label: { |
||||
Text("Affichage par") |
||||
} |
||||
|
||||
Picker(selection: $sortOrder) { |
||||
Text("Croissant").tag(SortOrder.ascending) |
||||
Text("Décroissant").tag(SortOrder.descending) |
||||
} label: { |
||||
Text("Trier par ordre") |
||||
} |
||||
} header: { |
||||
Text("Options d'affichage") |
||||
} |
||||
} |
||||
|
||||
if _isContentUnavailable() { |
||||
_contentUnavailableView() |
||||
} |
||||
|
||||
switch sortOption { |
||||
case .teamRank: |
||||
_byTeamRankView() |
||||
case .alphabeticalLastName: |
||||
_byPlayerLastName() |
||||
case .alphabeticalFirstName: |
||||
_byPlayerFirstName() |
||||
case .playerRank: |
||||
_byPlayerRank() |
||||
case .age: |
||||
_byPlayerAge() |
||||
case .callDate: |
||||
_byCallDateView() |
||||
} |
||||
} |
||||
.headerProminence(.increased) |
||||
.searchable(text: $searchText, isPresented: $isSearching, prompt: Text("Chercher un joueur")) |
||||
.toolbar { |
||||
ToolbarItem(placement: .topBarTrailing) { |
||||
ShareLink(item: _sharedData()) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@ViewBuilder |
||||
func computedPlayerView(_ player: PlayerRegistration) -> some View { |
||||
EditablePlayerView(player: player, editingOptions: [.licenceId, .payment]) |
||||
} |
||||
|
||||
private func _shouldDisplayTeam(_ team: TeamRegistration) -> Bool { |
||||
team.players().allSatisfy({ |
||||
_shouldDisplayPlayer($0) |
||||
}) |
||||
} |
||||
|
||||
private func _shouldDisplayPlayer(_ player: PlayerRegistration) -> Bool { |
||||
if searchText.isEmpty == false { |
||||
filterOption.shouldDisplayPlayer(player) && player.contains(searchText) |
||||
} else { |
||||
filterOption.shouldDisplayPlayer(player) |
||||
} |
||||
} |
||||
|
||||
@ViewBuilder |
||||
private func _byPlayer(_ players: [PlayerRegistration]) -> some View { |
||||
let _players = sortOrder == .ascending ? players : players.reversed() |
||||
ForEach(_players) { player in |
||||
Section { |
||||
computedPlayerView(player) |
||||
} header: { |
||||
HStack { |
||||
if let teamCallDate = player.team()?.callDate { |
||||
Text(teamCallDate.localizedDate()) |
||||
} |
||||
Spacer() |
||||
Text(player.weight.formatted()) |
||||
} |
||||
} footer: { |
||||
if tournaments.count > 1, let tournamentTitle = player.tournament()?.tournamentTitle() { |
||||
Text(tournamentTitle) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
@ViewBuilder |
||||
private func _byPlayerRank() -> some View { |
||||
let players = teams.flatMap({ $0.players() }).sorted(using: .keyPath(\.weight)).filter({ _shouldDisplayPlayer($0) }) |
||||
_byPlayer(players) |
||||
} |
||||
|
||||
@ViewBuilder |
||||
private func _byPlayerAge() -> some View { |
||||
let players = teams.flatMap({ $0.players() }).filter({ $0.computedAge != nil }).sorted(using: .keyPath(\.computedAge!)).filter({ _shouldDisplayPlayer($0) }) |
||||
_byPlayer(players) |
||||
} |
||||
|
||||
@ViewBuilder |
||||
private func _byPlayerLastName() -> some View { |
||||
let players = teams.flatMap({ $0.players() }).sorted(using: .keyPath(\.lastName)).filter({ _shouldDisplayPlayer($0) }) |
||||
_byPlayer(players) |
||||
} |
||||
|
||||
@ViewBuilder |
||||
private func _byPlayerFirstName() -> some View { |
||||
let players = teams.flatMap({ $0.players() }).sorted(using: .keyPath(\.firstName)).filter({ _shouldDisplayPlayer($0) }) |
||||
_byPlayer(players) |
||||
} |
||||
|
||||
@ViewBuilder |
||||
private func _byTeamRankView() -> some View { |
||||
let _teams = sortOrder == .ascending ? teams : teams.reversed() |
||||
ForEach(_teams) { team in |
||||
if _shouldDisplayTeam(team) { |
||||
Section { |
||||
_cashierPlayersView(team.players()) |
||||
} header: { |
||||
HStack { |
||||
if let callDate = team.callDate { |
||||
Text(callDate.localizedDate()) |
||||
} |
||||
Spacer() |
||||
Text(team.weight.formatted()) |
||||
} |
||||
} footer: { |
||||
if tournaments.count > 1, let tournamentTitle = team.tournamentObject()?.tournamentTitle() { |
||||
Text(tournamentTitle) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
|
||||
@ViewBuilder |
||||
private func _byCallDateView() -> some View { |
||||
let groupedTeams = Dictionary(grouping: teams) { team in |
||||
team.callDate |
||||
} |
||||
let keys = sortOrder == .ascending ? groupedTeams.keys.compactMap { $0 }.sorted() : groupedTeams.keys.compactMap { $0 }.sorted().reversed() |
||||
|
||||
ForEach(keys, id: \.self) { key in |
||||
if let _teams = groupedTeams[key] { |
||||
ForEach(_teams) { team in |
||||
if _shouldDisplayTeam(team) { |
||||
Section { |
||||
_cashierPlayersView(team.players()) |
||||
} header: { |
||||
Text(key.localizedDate()) |
||||
} footer: { |
||||
if tournaments.count > 1, let tournamentTitle = team.tournamentObject()?.tournamentTitle() { |
||||
Text(tournamentTitle) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
@ViewBuilder |
||||
private func _cashierPlayersView(_ players: [PlayerRegistration]) -> some View { |
||||
ForEach(players) { player in |
||||
if _shouldDisplayPlayer(player) { |
||||
computedPlayerView(player) |
||||
} |
||||
} |
||||
} |
||||
|
||||
private func _isContentUnavailable() -> Bool { |
||||
switch sortOption { |
||||
case .teamRank, .callDate: |
||||
return teams.filter({ _shouldDisplayTeam($0) }).isEmpty |
||||
default: |
||||
return teams.flatMap({ $0.players() }).filter({ _shouldDisplayPlayer($0) }).isEmpty |
||||
} |
||||
} |
||||
|
||||
private func _unavailableIcon() -> String { |
||||
switch sortOption { |
||||
case .teamRank, .callDate: |
||||
return "person.2.slash.fill" |
||||
default: |
||||
return "person.slash.fill" |
||||
} |
||||
} |
||||
|
||||
@ViewBuilder |
||||
private func _contentUnavailableView() -> some View { |
||||
if isSearching { |
||||
ContentUnavailableView.search(text: searchText) |
||||
} else { |
||||
ContentUnavailableView("Aucun résultat", systemImage: _unavailableIcon()) |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,18 @@ |
||||
// |
||||
// PlayerListView.swift |
||||
// PadelClub |
||||
// |
||||
// Created by Razmig Sarkissian on 17/04/2024. |
||||
// |
||||
|
||||
import SwiftUI |
||||
|
||||
struct PlayerListView: View { |
||||
var body: some View { |
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) |
||||
} |
||||
} |
||||
|
||||
#Preview { |
||||
PlayerListView() |
||||
} |
||||
@ -1,23 +0,0 @@ |
||||
// |
||||
// ClubView.swift |
||||
// PadelClub |
||||
// |
||||
// Created by Laurent Morvillier on 06/02/2024. |
||||
// |
||||
|
||||
import SwiftUI |
||||
|
||||
struct ClubView: View { |
||||
|
||||
var club: Club |
||||
|
||||
var body: some View { |
||||
List(club.tournaments) { tournament in |
||||
Text(tournament.tournamentTitle()) |
||||
}.navigationTitle(club.name) |
||||
} |
||||
} |
||||
|
||||
#Preview { |
||||
ClubView(club: Club(name: "AUC", acronym: "test", address: "")) |
||||
} |
||||
@ -0,0 +1,18 @@ |
||||
// |
||||
// FooterButtonView.swift |
||||
// PadelClub |
||||
// |
||||
// Created by Razmig Sarkissian on 18/04/2024. |
||||
// |
||||
|
||||
import SwiftUI |
||||
|
||||
struct FooterButtonView: View { |
||||
var body: some View { |
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) |
||||
} |
||||
} |
||||
|
||||
#Preview { |
||||
FooterButtonView() |
||||
} |
||||
@ -1,82 +0,0 @@ |
||||
// |
||||
// ContentView.swift |
||||
// PadelClub |
||||
// |
||||
// Created by Laurent Morvillier on 02/02/2024. |
||||
// |
||||
|
||||
import SwiftUI |
||||
import LeStorage |
||||
|
||||
struct ContentView: View { |
||||
|
||||
@StateObject var dataStore = DataStore() |
||||
|
||||
var body: some View { |
||||
NavigationStack { |
||||
|
||||
VStack { |
||||
|
||||
List(self.dataStore.clubs) { club in |
||||
|
||||
NavigationLink { |
||||
ClubView(club: club) |
||||
} label: { |
||||
Text(club.name) |
||||
} |
||||
} |
||||
|
||||
Button("add") { |
||||
self._add() |
||||
} |
||||
.padding() |
||||
.buttonStyle(.bordered) |
||||
} |
||||
.toolbar(content: { |
||||
ToolbarItem { |
||||
NavigationLink { |
||||
MainUserView() |
||||
.environmentObject(self.dataStore) |
||||
} label: { |
||||
Image(systemName: "person.circle.fill") |
||||
} |
||||
} |
||||
|
||||
ToolbarItem { |
||||
NavigationLink { |
||||
SubscriptionView() |
||||
} label: { |
||||
Image(systemName: "tennisball.circle.fill") |
||||
} |
||||
} |
||||
}) |
||||
.navigationTitle("Home") |
||||
|
||||
} |
||||
} |
||||
|
||||
func _add() { |
||||
// let id = (0...1000000).randomElement()! |
||||
// let club: Club = Club(name: "test\(id)", address: "some address") |
||||
// self.dataStore.clubs.addOrUpdate(instance: club) |
||||
|
||||
// for _ in 0...20 { |
||||
// var clubs: [Club] = [] |
||||
// for _ in 0...20 { |
||||
// let id = (0...1000000).randomElement()! |
||||
// let club: Club = Club(name: "test\(id)", acronym: "test", address: "some address") |
||||
// clubs.append(club) |
||||
// } |
||||
// do { |
||||
// try self.dataStore.clubs.append(contentOfs: clubs) |
||||
// } catch { |
||||
// Logger.error(error) |
||||
// } |
||||
// } |
||||
} |
||||
|
||||
} |
||||
|
||||
#Preview { |
||||
ContentView() |
||||
} |
||||
@ -0,0 +1,45 @@ |
||||
// |
||||
// MatchTeamDetailView.swift |
||||
// PadelClub |
||||
// |
||||
// Created by Razmig Sarkissian on 18/04/2024. |
||||
// |
||||
|
||||
import SwiftUI |
||||
|
||||
struct MatchTeamDetailView: View { |
||||
let match: Match |
||||
|
||||
var body: some View { |
||||
NavigationStack { |
||||
let tournament = match.currentTournament() |
||||
List { |
||||
if let teamOne = match.team(.one) { |
||||
_teamDetailView(teamOne, inTournament: tournament) |
||||
} |
||||
if let teamTwo = match.team(.two) { |
||||
_teamDetailView(teamTwo, inTournament: tournament) |
||||
} |
||||
} |
||||
.headerProminence(.increased) |
||||
.tint(.master) |
||||
} |
||||
.presentationDetents([.fraction(0.66)]) |
||||
} |
||||
|
||||
@ViewBuilder |
||||
private func _teamDetailView(_ team: TeamRegistration, inTournament tournament: Tournament?) -> some View { |
||||
Section { |
||||
ForEach(team.players()) { player in |
||||
EditablePlayerView(player: player, editingOptions: [.licenceId, .payment]) |
||||
} |
||||
} header: { |
||||
TeamHeaderView(team: team, teamIndex: tournament?.indexOf(team: team), tournament: nil) |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
#Preview { |
||||
MatchTeamDetailView(match: Match.mock()) |
||||
} |
||||
@ -0,0 +1,25 @@ |
||||
// |
||||
// DurationSettingsView.swift |
||||
// PadelClub |
||||
// |
||||
// Created by Razmig Sarkissian on 18/04/2024. |
||||
// |
||||
|
||||
import SwiftUI |
||||
|
||||
struct DurationSettingsView: View { |
||||
var body: some View { |
||||
List { |
||||
ForEach(MatchFormat.allCases, id: \.self) { matchFormat in |
||||
MatchFormatStorageView(matchFormat: matchFormat) |
||||
} |
||||
} |
||||
.navigationTitle("Durées moyennes") |
||||
.navigationBarTitleDisplayMode(.inline) |
||||
.toolbarBackground(.visible, for: .navigationBar) |
||||
} |
||||
} |
||||
|
||||
#Preview { |
||||
DurationSettingsView() |
||||
} |
||||
@ -0,0 +1,67 @@ |
||||
// |
||||
// GlobalSettingsView.swift |
||||
// Padel Tournament |
||||
// |
||||
// Created by Razmig Sarkissian on 16/10/2023. |
||||
// |
||||
|
||||
import SwiftUI |
||||
|
||||
struct GlobalSettingsView: View { |
||||
@EnvironmentObject var dataStore : DataStore |
||||
|
||||
var body: some View { |
||||
@Bindable var appSettings = dataStore.appSettings |
||||
List { |
||||
Section { |
||||
Picker(selection: $appSettings.groupStageMatchFormatPreference) { |
||||
Text("Automatique").tag(nil as Int?) |
||||
ForEach(MatchFormat.allCases, id: \.self) { format in |
||||
Text(format.format).tag(format.rawValue as Int?) |
||||
} |
||||
} label: { |
||||
HStack { |
||||
Text("Poule") |
||||
Spacer() |
||||
} |
||||
} |
||||
Picker(selection: $appSettings.bracketMatchFormatPreference) { |
||||
Text("Automatique").tag(nil as Int?) |
||||
ForEach(MatchFormat.allCases, id: \.self) { format in |
||||
Text(format.format).tag(format.rawValue as Int?) |
||||
} |
||||
} label: { |
||||
HStack { |
||||
Text("Tableau") |
||||
Spacer() |
||||
} |
||||
} |
||||
Picker(selection: $appSettings.loserBracketMatchFormatPreference) { |
||||
Text("Automatique").tag(nil as Int?) |
||||
ForEach(MatchFormat.allCases, id: \.self) { format in |
||||
Text(format.format).tag(format.rawValue as Int?) |
||||
} |
||||
} label: { |
||||
HStack { |
||||
Text("Match de classement") |
||||
Spacer() |
||||
} |
||||
} |
||||
} header: { |
||||
Text("Vos formats préférés") |
||||
} footer: { |
||||
Text("À minima, les règles fédérales seront toujours prises en compte par défaut.") |
||||
} |
||||
} |
||||
.onChange(of: [ |
||||
appSettings.bracketMatchFormatPreference, |
||||
appSettings.groupStageMatchFormatPreference, |
||||
appSettings.loserBracketMatchFormatPreference |
||||
]) { |
||||
dataStore.updateSettings() |
||||
} |
||||
.navigationTitle("Formats par défaut") |
||||
.navigationBarTitleDisplayMode(.inline) |
||||
.toolbarBackground(.visible, for: .navigationBar) |
||||
} |
||||
} |
||||
@ -0,0 +1,50 @@ |
||||
// |
||||
// MatchFormatStorageView.swift |
||||
// PadelClub |
||||
// |
||||
// Created by Razmig Sarkissian on 18/04/2024. |
||||
// |
||||
|
||||
import SwiftUI |
||||
|
||||
struct MatchFormatStorageView: View { |
||||
@State private var estimatedDuration: Int |
||||
@EnvironmentObject var dataStore: DataStore |
||||
|
||||
let matchFormat: MatchFormat |
||||
|
||||
init(matchFormat: MatchFormat) { |
||||
self.matchFormat = matchFormat |
||||
_estimatedDuration = State(wrappedValue: matchFormat.getEstimatedDuration()) |
||||
} |
||||
|
||||
var body: some View { |
||||
Section { |
||||
LabeledContent { |
||||
StepperView(title: "minutes", count: $estimatedDuration, step: 5) |
||||
} label: { |
||||
Text("Durée \(matchFormat.format)") |
||||
Text(matchFormat.computedShortLabelWithoutPrefix) |
||||
} |
||||
} footer: { |
||||
if estimatedDuration != matchFormat.defaultEstimatedDuration { |
||||
HStack { |
||||
Spacer() |
||||
Button { |
||||
self.estimatedDuration = matchFormat.defaultEstimatedDuration |
||||
} label: { |
||||
Text("remettre la durée par défault") |
||||
.underline() |
||||
} |
||||
.buttonStyle(.borderless) |
||||
|
||||
} |
||||
} |
||||
} |
||||
.onChange(of: estimatedDuration) { |
||||
dataStore.appSettings.saveMatchFormatsDefaultDuration(matchFormat, estimatedDuration: estimatedDuration) |
||||
dataStore.updateSettings() |
||||
} |
||||
} |
||||
} |
||||
|
||||
@ -0,0 +1,54 @@ |
||||
// |
||||
// DateUpdateManagerView.swift |
||||
// PadelClub |
||||
// |
||||
// Created by Razmig Sarkissian on 17/04/2024. |
||||
// |
||||
|
||||
import SwiftUI |
||||
|
||||
enum DateUpdate { |
||||
case nextRotation |
||||
case previousRotation |
||||
case tomorrowAtNine |
||||
case inMinutes(Int) |
||||
case afterRound(Round) |
||||
case afterGroupStage(GroupStage) |
||||
} |
||||
|
||||
struct DateUpdateManagerView: View { |
||||
@Binding var startDate: Date |
||||
@State private var dateUpdated: Bool = false |
||||
|
||||
var validateAction: () -> Void |
||||
|
||||
var body: some View { |
||||
HStack { |
||||
Menu { |
||||
Text("à demain 9h") |
||||
Text("à la prochaine rotation") |
||||
Text("à la précédente rotation") |
||||
} label: { |
||||
Text("décaler") |
||||
.underline() |
||||
} |
||||
Spacer() |
||||
|
||||
if dateUpdated { |
||||
Button { |
||||
validateAction() |
||||
dateUpdated = false |
||||
} label: { |
||||
Text("valider la modification") |
||||
.underline() |
||||
} |
||||
} |
||||
} |
||||
.font(.subheadline) |
||||
.buttonStyle(.borderless) |
||||
.onChange(of: startDate) { |
||||
dateUpdated = true |
||||
} |
||||
} |
||||
} |
||||
|
||||
@ -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)") |
||||
// } |
||||
*/ |
||||
@ -0,0 +1,79 @@ |
||||
// |
||||
// LoserRoundStepScheduleEditorView.swift |
||||
// PadelClub |
||||
// |
||||
// Created by Razmig Sarkissian on 17/04/2024. |
||||
// |
||||
|
||||
import SwiftUI |
||||
|
||||
struct LoserRoundStepScheduleEditorView: View { |
||||
@EnvironmentObject var dataStore: DataStore |
||||
@Environment(Tournament.self) var tournament: Tournament |
||||
|
||||
var round: Round |
||||
var upperRound: Round |
||||
var matches: [Match] |
||||
@State private var startDate: Date |
||||
@State private var matchFormat: MatchFormat |
||||
|
||||
init(round: Round, upperRound: Round) { |
||||
self.upperRound = upperRound |
||||
self.round = round |
||||
let _matches = upperRound.loserRounds(forRoundIndex: round.index).flatMap({ $0.playedMatches() }) |
||||
self.matches = _matches |
||||
self._startDate = State(wrappedValue: round.startDate ?? _matches.first?.startDate ?? Date()) |
||||
self._matchFormat = State(wrappedValue: round.matchFormat) |
||||
} |
||||
|
||||
var body: some View { |
||||
@Bindable var round = round |
||||
Section { |
||||
MatchFormatPickerView(headerLabel: "Format", matchFormat: $round.matchFormat) |
||||
DatePicker(selection: $startDate) { |
||||
Text(startDate.formatted(.dateTime.weekday(.wide))).font(.headline) |
||||
} |
||||
NavigationLink { |
||||
List { |
||||
ForEach(matches) { match in |
||||
if match.disabled == false { |
||||
MatchScheduleEditorView(match: match) |
||||
} |
||||
} |
||||
} |
||||
.headerProminence(.increased) |
||||
.navigationTitle(round.selectionLabel()) |
||||
.environment(tournament) |
||||
} label: { |
||||
Text("Voir tous les matchs") |
||||
} |
||||
|
||||
} header: { |
||||
Text(round.selectionLabel()) |
||||
} footer: { |
||||
DateUpdateManagerView(startDate: $startDate) { |
||||
_updateSchedule() |
||||
} |
||||
} |
||||
.headerProminence(.increased) |
||||
} |
||||
|
||||
private func _updateSchedule() { |
||||
upperRound.loserRounds(forRoundIndex: round.index).forEach({ round in |
||||
round.resetRound(updateMatchFormat: round.matchFormat) |
||||
}) |
||||
|
||||
try? dataStore.matches.addOrUpdate(contentOfs: matches) |
||||
_save() |
||||
|
||||
MatchScheduler.shared.updateSchedule(tournament: tournament, fromRoundId: round.id, fromMatchId: nil, startDate: startDate) |
||||
upperRound.loserRounds(forRoundIndex: round.index).forEach({ round in |
||||
round.startDate = startDate |
||||
}) |
||||
_save() |
||||
} |
||||
|
||||
private func _save() { |
||||
try? dataStore.rounds.addOrUpdate(contentOfs: upperRound.loserRounds(forRoundIndex: round.index)) |
||||
} |
||||
} |
||||
@ -0,0 +1,104 @@ |
||||
// |
||||
// EditablePlayerView.swift |
||||
// PadelClub |
||||
// |
||||
// Created by Razmig Sarkissian on 17/04/2024. |
||||
// |
||||
|
||||
import SwiftUI |
||||
|
||||
struct EditablePlayerView: View { |
||||
|
||||
enum PlayerEditingOption { |
||||
case payment |
||||
case licenceId |
||||
} |
||||
|
||||
@EnvironmentObject var dataStore: DataStore |
||||
var player: PlayerRegistration |
||||
var editingOptions: [PlayerEditingOption] |
||||
@State private var editedLicenceId = "" |
||||
@State private var shouldPresentLicenceIdEdition: Bool = false |
||||
|
||||
var body: some View { |
||||
computedPlayerView(player) |
||||
.alert("Numéro de licence", isPresented: $shouldPresentLicenceIdEdition) { |
||||
TextField("Numéro de licence", text: $editedLicenceId) |
||||
.onSubmit { |
||||
player.licenceId = editedLicenceId |
||||
editedLicenceId = "" |
||||
try? dataStore.playerRegistrations.addOrUpdate(instance: player) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@ViewBuilder |
||||
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)") { |
||||
Link(destination: url) { |
||||
Label("Appeler", systemImage: "phone") |
||||
} |
||||
} |
||||
if let number = player.phoneNumber?.replacingOccurrences(of: " ", with: ""), let url = URL(string: "sms:\(number)") { |
||||
Link(destination: url) { |
||||
Label("SMS", systemImage: "message") |
||||
} |
||||
} |
||||
|
||||
if editingOptions.contains(.licenceId) { |
||||
Divider() |
||||
if let licenseYearValidity = player.tournament()?.licenseYearValidity(), player.isValidLicenseNumber(year: licenseYearValidity) == false, player.licenceId != nil { |
||||
Button { |
||||
player.validateLicenceId(licenseYearValidity) |
||||
} label: { |
||||
Text("Valider la licence \(licenseYearValidity)") |
||||
} |
||||
} |
||||
} |
||||
|
||||
if let license = player.licenceId?.strippedLicense { |
||||
Button { |
||||
let pasteboard = UIPasteboard.general |
||||
pasteboard.string = license |
||||
} label: { |
||||
Label("Copier la licence", systemImage: "doc.on.doc") |
||||
} |
||||
} |
||||
|
||||
Section { |
||||
Button { |
||||
editedLicenceId = player.licenceId ?? "" |
||||
shouldPresentLicenceIdEdition = true |
||||
} label: { |
||||
if player.licenceId == nil { |
||||
Text("Ajouter la licence") |
||||
} else { |
||||
Text("Modifier la licence") |
||||
} |
||||
} |
||||
PasteButton(payloadType: String.self) { strings in |
||||
guard let first = strings.first else { return } |
||||
player.licenceId = first |
||||
} |
||||
} header: { |
||||
Text("Modification de licence") |
||||
} |
||||
} label: { |
||||
Text("Options") |
||||
} |
||||
if editingOptions.contains(.payment) { |
||||
Spacer() |
||||
PlayerPayView(player: player) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,114 @@ |
||||
// |
||||
// PlayerDetailView.swift |
||||
// PadelClub |
||||
// |
||||
// Created by Razmig Sarkissian on 17/04/2024. |
||||
// |
||||
|
||||
import SwiftUI |
||||
|
||||
struct PlayerDetailView: View { |
||||
@Environment(Tournament.self) var tournament: Tournament |
||||
@EnvironmentObject var dataStore: DataStore |
||||
@Bindable var player: PlayerRegistration |
||||
@FocusState private var textFieldIsFocus: Bool |
||||
|
||||
var body: some View { |
||||
Form { |
||||
Section { |
||||
LabeledContent { |
||||
TextField("Nom", text: $player.lastName) |
||||
.keyboardType(.alphabet) |
||||
.multilineTextAlignment(.trailing) |
||||
.frame(maxWidth: .infinity) |
||||
} label: { |
||||
Text("Nom") |
||||
} |
||||
|
||||
LabeledContent { |
||||
TextField("Prénom", text: $player.firstName) |
||||
.keyboardType(.alphabet) |
||||
.multilineTextAlignment(.trailing) |
||||
.frame(maxWidth: .infinity) |
||||
} label: { |
||||
Text("Prénom") |
||||
} |
||||
|
||||
PlayerSexPickerView(player: player) |
||||
} |
||||
|
||||
Section { |
||||
LabeledContent { |
||||
TextField("Rang", value: $player.rank, format: .number) |
||||
.keyboardType(.decimalPad) |
||||
.multilineTextAlignment(.trailing) |
||||
.frame(maxWidth: .infinity) |
||||
.focused($textFieldIsFocus) |
||||
} label: { |
||||
Text("Rang") |
||||
} |
||||
} header: { |
||||
Text("Classement actuel") |
||||
} |
||||
|
||||
if player.isMalePlayer() == false && tournament.tournamentCategory == .men, let rank = player.rank { |
||||
Section { |
||||
let value = PlayerRegistration.addon(for: rank, manMax: tournament.maleUnrankedValue ?? 0, womanMax: tournament.femaleUnrankedValue ?? 0) |
||||
LabeledContent { |
||||
Text(value.formatted()) |
||||
} label: { |
||||
Text("Valeur à rajouter") |
||||
} |
||||
LabeledContent { |
||||
TextField("Rang", value: $player.weight, format: .number) |
||||
.keyboardType(.decimalPad) |
||||
.multilineTextAlignment(.trailing) |
||||
.frame(maxWidth: .infinity) |
||||
.focused($textFieldIsFocus) |
||||
} label: { |
||||
Text("Poids re-calculé") |
||||
} |
||||
} header: { |
||||
Text("Ré-assimilation") |
||||
} footer: { |
||||
Text("Calculé en fonction du sexe") |
||||
} |
||||
} |
||||
} |
||||
.scrollDismissesKeyboard(.immediately) |
||||
.onChange(of: player.sex) { |
||||
_save() |
||||
} |
||||
.onChange(of: player.weight) { |
||||
player.team()?.updateWeight() |
||||
_save() |
||||
} |
||||
.onChange(of: player.rank) { |
||||
player.setWeight(in: tournament) |
||||
player.team()?.updateWeight() |
||||
_save() |
||||
} |
||||
.headerProminence(.increased) |
||||
.navigationTitle("Édition") |
||||
.navigationBarTitleDisplayMode(.inline) |
||||
.toolbarBackground(.visible, for: .navigationBar) |
||||
.toolbar { |
||||
ToolbarItem(placement: .keyboard) { |
||||
Button("Valider") { |
||||
textFieldIsFocus = false |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
private func _save() { |
||||
try? dataStore.playerRegistrations.addOrUpdate(instance: player) |
||||
if let team = player.team() { |
||||
try? dataStore.teamRegistrations.addOrUpdate(instance: team) |
||||
} |
||||
} |
||||
} |
||||
|
||||
#Preview { |
||||
PlayerDetailView(player: PlayerRegistration.mock()) |
||||
} |
||||
@ -1,53 +0,0 @@ |
||||
// |
||||
// LoserBracketView.swift |
||||
// PadelClub |
||||
// |
||||
// Created by Razmig Sarkissian on 04/04/2024. |
||||
// |
||||
|
||||
import SwiftUI |
||||
|
||||
struct LoserBracketView: View { |
||||
@EnvironmentObject var dataStore: DataStore |
||||
let loserRounds: [Round] |
||||
|
||||
@ViewBuilder |
||||
var body: some View { |
||||
if let first = loserRounds.first { |
||||
List { |
||||
ForEach(loserRounds) { loserRound in |
||||
_loserRoundView(loserRound) |
||||
let childLoserRounds = loserRound.loserRounds() |
||||
if childLoserRounds.isEmpty == false { |
||||
let uniqueChildRound = childLoserRounds.first |
||||
if childLoserRounds.count == 1, let uniqueChildRound { |
||||
_loserRoundView(uniqueChildRound) |
||||
} else if let uniqueChildRound { |
||||
NavigationLink { |
||||
LoserBracketView(loserRounds: childLoserRounds) |
||||
} label: { |
||||
Text(uniqueChildRound.roundTitle()) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
.navigationTitle(first.roundTitle()) |
||||
} |
||||
} |
||||
|
||||
private func _loserRoundView(_ loserRound: Round) -> some View { |
||||
Section { |
||||
ForEach(loserRound.playedMatches()) { match in |
||||
MatchRowView(match: match, matchViewStyle: .standardStyle) |
||||
} |
||||
} header: { |
||||
Text(loserRound.roundTitle()) |
||||
} |
||||
} |
||||
} |
||||
|
||||
#Preview { |
||||
LoserBracketView(loserRounds: [Round.mock()]) |
||||
.environmentObject(DataStore.shared) |
||||
} |
||||
@ -0,0 +1,72 @@ |
||||
// |
||||
// LoserRoundView.swift |
||||
// PadelClub |
||||
// |
||||
// Created by Razmig Sarkissian on 04/04/2024. |
||||
// |
||||
|
||||
import SwiftUI |
||||
|
||||
struct LoserRoundView: View { |
||||
@EnvironmentObject var dataStore: DataStore |
||||
let loserRounds: [Round] |
||||
@State private var isEditingTournamentSeed: Bool = false |
||||
|
||||
private func _roundDisabled() -> Bool { |
||||
loserRounds.allSatisfy({ $0.isDisabled() }) |
||||
} |
||||
|
||||
var body: some View { |
||||
List { |
||||
if isEditingTournamentSeed == true { |
||||
_editingView() |
||||
} |
||||
|
||||
ForEach(loserRounds) { loserRound in |
||||
if isEditingTournamentSeed || loserRound.isDisabled() == false { |
||||
Section { |
||||
let matches = isEditingTournamentSeed ? loserRound.playedMatches() : loserRound.playedMatches().filter({ $0.disabled == false }) |
||||
ForEach(matches) { match in |
||||
MatchRowView(match: match, matchViewStyle: .standardStyle) |
||||
.overlay { |
||||
if match.disabled /*&& isEditingTournamentSeed*/ { |
||||
Image(systemName: "xmark") |
||||
.resizable() |
||||
.scaledToFit() |
||||
.opacity(0.8) |
||||
} |
||||
} |
||||
.disabled(match.disabled) |
||||
} |
||||
} header: { |
||||
Text(loserRound.roundTitle(.wide)) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
.headerProminence(.increased) |
||||
.toolbar { |
||||
ToolbarItem(placement: .topBarTrailing) { |
||||
Button(isEditingTournamentSeed == true ? "Valider" : "Modifier") { |
||||
isEditingTournamentSeed.toggle() |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
private func _editingView() -> some View { |
||||
if _roundDisabled() { |
||||
RowButtonView("Jouer ce tour", role: .destructive) { |
||||
loserRounds.forEach { round in |
||||
round.enableRound() |
||||
} |
||||
} |
||||
} else { |
||||
RowButtonView("Ne pas jouer ce tour", role: .destructive) { |
||||
loserRounds.forEach { round in |
||||
round.disableRound() |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,43 @@ |
||||
// |
||||
// TeamHeaderView.swift |
||||
// PadelClub |
||||
// |
||||
// Created by Razmig Sarkissian on 18/04/2024. |
||||
// |
||||
|
||||
import SwiftUI |
||||
|
||||
struct TeamHeaderView: View { |
||||
var team: TeamRegistration |
||||
var teamIndex: Int? |
||||
var tournament: Tournament? |
||||
|
||||
var body: some View { |
||||
_teamHeaderView(team, teamIndex: teamIndex) |
||||
} |
||||
|
||||
private func _teamHeaderView(_ team: TeamRegistration, teamIndex: Int?) -> some View { |
||||
HStack { |
||||
if let teamIndex { |
||||
Text("#" + (teamIndex + 1).formatted()) |
||||
} |
||||
|
||||
if team.unsortedPlayers().isEmpty == false { |
||||
Text(team.weight.formatted()) |
||||
} |
||||
if team.isWildCard() { |
||||
Text("wildcard").italic().font(.caption) |
||||
} |
||||
Spacer() |
||||
if team.walkOut { |
||||
Text("WO") |
||||
} else if let teamIndex, let tournament { |
||||
Text(tournament.cutLabel(index: teamIndex)) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
#Preview { |
||||
TeamHeaderView(team: TeamRegistration.mock(), teamIndex: 1, tournament: nil) |
||||
} |
||||
@ -0,0 +1,38 @@ |
||||
// |
||||
// TeamWeightView.swift |
||||
// PadelClub |
||||
// |
||||
// Created by Razmig Sarkissian on 18/04/2024. |
||||
// |
||||
|
||||
import SwiftUI |
||||
|
||||
struct TeamWeightView: View { |
||||
var team: TeamRegistration |
||||
var teamPosition: TeamPosition? = nil |
||||
|
||||
var body: some View { |
||||
VStack(alignment: .trailing, spacing: 0) { |
||||
if teamPosition == .one || teamPosition == nil { |
||||
Text(team.weight.formatted()) |
||||
.monospacedDigit() |
||||
.font(.caption) |
||||
} |
||||
if let teams = team.tournamentObject()?.selectedSortedTeams(), let index = team.index(in: teams) { |
||||
Text("#" + (index + 1).formatted(.number.precision(.integerLength(2...3)))) |
||||
.monospacedDigit() |
||||
.font(.title) |
||||
} |
||||
if teamPosition == .two { |
||||
Text(team.weight.formatted()) |
||||
.monospacedDigit() |
||||
.font(.caption) |
||||
|
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
#Preview { |
||||
TeamWeightView(team: TeamRegistration.mock(), teamPosition: .one) |
||||
} |
||||
@ -0,0 +1,45 @@ |
||||
// |
||||
// EditingTeamView.swift |
||||
// PadelClub |
||||
// |
||||
// Created by Razmig Sarkissian on 17/04/2024. |
||||
// |
||||
|
||||
import SwiftUI |
||||
|
||||
struct EditingTeamView: View { |
||||
@EnvironmentObject var dataStore: DataStore |
||||
var team: TeamRegistration |
||||
@State private var registrationDate : Date |
||||
|
||||
init(team: TeamRegistration) { |
||||
self.team = team |
||||
_registrationDate = State(wrappedValue: team.registrationDate ?? Date()) |
||||
} |
||||
|
||||
var body: some View { |
||||
List { |
||||
Section { |
||||
DatePicker(registrationDate.formatted(.dateTime.weekday()), selection: $registrationDate) |
||||
} header: { |
||||
Text("Date d'inscription") |
||||
} |
||||
} |
||||
.onChange(of: registrationDate) { |
||||
team.registrationDate = registrationDate |
||||
_save() |
||||
} |
||||
.headerProminence(.increased) |
||||
.navigationTitle("Édition") |
||||
.navigationBarTitleDisplayMode(.inline) |
||||
.toolbarBackground(.visible, for: .navigationBar) |
||||
} |
||||
|
||||
private func _save() { |
||||
try? dataStore.teamRegistrations.addOrUpdate(instance: team) |
||||
} |
||||
} |
||||
|
||||
#Preview { |
||||
EditingTeamView(team: TeamRegistration.mock()) |
||||
} |
||||
@ -1,41 +0,0 @@ |
||||
// |
||||
// CashierDetailView.swift |
||||
// Padel Tournament |
||||
// |
||||
// Created by Razmig Sarkissian on 31/03/2024. |
||||
// |
||||
|
||||
import SwiftUI |
||||
|
||||
struct CashierDetailView: View { |
||||
var tournaments : [Tournament] |
||||
|
||||
var body: some View { |
||||
List { |
||||
ForEach(tournaments) { tournament in |
||||
_tournamentCashierDetailView(tournament) |
||||
} |
||||
} |
||||
.headerProminence(.increased) |
||||
.navigationTitle("Résumé") |
||||
} |
||||
|
||||
private func _tournamentCashierDetailView(_ tournament: Tournament) -> some View { |
||||
Section { |
||||
ForEach(PlayerRegistration.PaymentType.allCases) { type in |
||||
let count = tournament.selectedPlayers().filter({ $0.registrationType == type }).count |
||||
LabeledContent { |
||||
if let entryFee = tournament.entryFee { |
||||
let sum = Double(count) * entryFee |
||||
Text(sum.formatted(.currency(code: "EUR"))) |
||||
} |
||||
} label: { |
||||
Text(type.localizedLabel()) |
||||
Text(count.formatted()) |
||||
} |
||||
} |
||||
} header: { |
||||
Text(tournament.tournamentTitle()) |
||||
} |
||||
} |
||||
} |
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue