multistore
Razmig Sarkissian 1 year ago
parent 689d7e6d8d
commit 3cfd6cdf81
  1. 4
      PadelClub.xcodeproj/project.pbxproj
  2. 4
      PadelClub/Utils/HtmlGenerator.swift
  3. 2
      PadelClub/Utils/HtmlService.swift
  4. 28
      PadelClub/Utils/Tips.swift
  5. 5
      PadelClub/ViewModel/SearchViewModel.swift
  6. 7
      PadelClub/ViewModel/Selectable.swift
  7. 224
      PadelClub/Views/Cashier/CashierView.swift
  8. 5
      PadelClub/Views/Club/ClubSearchView.swift
  9. 23
      PadelClub/Views/Components/GenericDestinationPickerView.swift
  10. 22
      PadelClub/Views/Tournament/Screen/BroadcastView.swift
  11. 189
      PadelClub/Views/Tournament/Screen/PrintSettingsView.swift
  12. 20
      PadelClub/Views/Tournament/Screen/TournamentCashierView.swift
  13. 18
      PadelClub/Views/Tournament/TournamentBuildView.swift
  14. 9
      PadelClub/Views/Tournament/TournamentInitView.swift
  15. 1
      PadelClub/Views/Tournament/TournamentView.swift

@ -1939,7 +1939,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 33; CURRENT_PROJECT_VERSION = 34;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -1977,7 +1977,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 33; CURRENT_PROJECT_VERSION = 34;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;

@ -58,6 +58,10 @@ class HtmlGenerator: ObservableObject {
self.width = width as! CGFloat self.width = width as! CGFloat
}) })
} }
if self.completionHandler != nil {
self.buildPDF()
}
}) })
} }

@ -183,7 +183,7 @@ enum HtmlService {
var template = "" var template = ""
var bracket = "" var bracket = ""
if let round = tournament.rounds().first(where: { $0.index == roundIndex }) { if let round = tournament.rounds().first(where: { $0.index == roundIndex }) {
for (_, match) in round.playedMatches().enumerated() { for (_, match) in round._matches().enumerated() {
template = template.appending(HtmlService.match(match: match).html(headName: headName, withRank: withRank, withScore: withScore)) template = template.appending(HtmlService.match(match: match).html(headName: headName, withRank: withRank, withScore: withScore))
} }
bracket = html.replacingOccurrences(of: "{{match-template}}", with: template) bracket = html.replacingOccurrences(of: "{{match-template}}", with: template)

@ -412,6 +412,34 @@ struct TournamentRunningTip: Tip {
} }
} }
struct CreateAccountTip: Tip {
var title: Text {
Text("Créer votre compte Padel Club")
}
var message: Text? {
let message = "Un compte est nécessaire pour publier le tournoi sur [Padel Club](\(URLs.main.rawValue)) et profiter de toutes du site, comme le mode TV pour transformer l'expérience de vos tournois !"
return Text(.init(message))
}
var image: Image? {
Image(systemName: "person.crop.circle")
}
var actions: [Action] {
Action(id: ActionKey.createAccount.rawValue, title: "Créer votre compte")
//todo
//Action(id: ActionKey.learnMore.rawValue, title: "En savoir plus")
Action(id: ActionKey.accessPadelClubWebPage.rawValue, title: "Jeter un oeil au site Padel Club")
}
enum ActionKey: String {
case createAccount = "createAccount"
case learnMore = "learnMore"
case accessPadelClubWebPage = "accessPadelClubWebPage"
}
}
struct TipStyleModifier: ViewModifier { struct TipStyleModifier: ViewModifier {
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
var tint: Color? var tint: Color?

@ -7,6 +7,11 @@
import SwiftUI import SwiftUI
class DebouncableViewModel: ObservableObject {
@Published var debouncableText: String = ""
var debounceTrigger: Double = 0.15
}
class SearchViewModel: ObservableObject, Identifiable { class SearchViewModel: ObservableObject, Identifiable {
let id: UUID = UUID() let id: UUID = UUID()
var allowSelection : Int = 0 var allowSelection : Int = 0

@ -13,6 +13,13 @@ protocol Selectable {
func badgeValue() -> Int? func badgeValue() -> Int?
func badgeImage() -> Badge? func badgeImage() -> Badge?
func badgeValueColor() -> Color? func badgeValueColor() -> Color?
func displayImageIfValueZero() -> Bool
}
extension Selectable {
func displayImageIfValueZero() -> Bool {
return false
}
} }
enum Badge { enum Badge {

@ -8,39 +8,62 @@
import SwiftUI import SwiftUI
import Combine import Combine
struct CashierView: View { struct ShareableObject {
@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(tournament: Tournament, teams: [TeamRegistration]) { let cashierViewModel: CashierViewModel
self.tournaments = [tournament] let teams: [TeamRegistration]
self.teams = teams let fileName: String
if tournament.hasEnded(), tournament.players().anySatisfy({ $0.hasPaid() == false }) {
_filterOption = .init(wrappedValue: .didNotPay)
}
if teams.filter({ $0.callDate != nil }).isEmpty {
_sortOption = .init(wrappedValue: .teamRank)
} else {
_sortOption = .init(wrappedValue: .callDate)
}
}
private func _sharedData() -> String { func sharedData() async -> Data? {
let players = teams.filter({ _shouldDisplayTeam($0) }) let players = teams.filter({ cashierViewModel._shouldDisplayTeam($0) })
.flatMap({ $0.players().filter({ _shouldDisplayPlayer($0) }) }) .flatMap({ $0.players().filter({ cashierViewModel._shouldDisplayPlayer($0) }) })
.map { .map {
[$0.pasteData()] [$0.pasteData()]
.compacted() .compacted()
.joined(separator: "\n") .joined(separator: "\n")
} }
.joined(separator: "\n\n") .joined(separator: "\n\n")
return players return players.data(using: .utf8)
}
}
extension ShareableObject: Transferable {
enum ShareError: Error {
case failed
}
static var transferRepresentation: some TransferRepresentation {
let rep = DataRepresentation<ShareableObject>(exportedContentType: .plainText) { object in
guard let data = await object.sharedData() else {
throw ShareError.failed
}
return data
}
return rep.suggestedFileName { object in object.fileName }
}
}
class CashierViewModel: ObservableObject {
let id: UUID = UUID()
@Published var sortOption: SortOption = .callDate
@Published var filterOption: FilterOption = .all
@Published var sortOrder: SortOrder = .ascending
@Published var searchText: String = ""
@Published var isSearching: Bool = false
func _shouldDisplayTeam(_ team: TeamRegistration) -> Bool {
team.unsortedPlayers().anySatisfy({
_shouldDisplayPlayer($0)
})
}
func _shouldDisplayPlayer(_ player: PlayerRegistration) -> Bool {
if searchText.isEmpty == false {
filterOption.shouldDisplayPlayer(player) && player.contains(searchText)
} else {
filterOption.shouldDisplayPlayer(player)
}
} }
enum SortOption: Int, Identifiable, CaseIterable { enum SortOption: Int, Identifiable, CaseIterable {
@ -101,27 +124,42 @@ struct CashierView: View {
} }
} }
}
struct CashierView: View {
@EnvironmentObject var dataStore: DataStore
@EnvironmentObject var cashierViewModel: CashierViewModel
var tournaments : [Tournament]
var teams: [TeamRegistration]
@State private var shareableObject: ShareableObject?
init(tournament: Tournament, teams: [TeamRegistration]) {
self.tournaments = [tournament]
self.teams = teams
}
var body: some View { var body: some View {
List { List {
if isSearching == false { if cashierViewModel.isSearching == false {
Section { Section {
Picker(selection: $filterOption) { Picker(selection: $cashierViewModel.filterOption) {
ForEach(FilterOption.allCases) { filterOption in ForEach(CashierViewModel.FilterOption.allCases) { filterOption in
Text(filterOption.localizedLabel()).tag(filterOption) Text(filterOption.localizedLabel()).tag(filterOption)
} }
} label: { } label: {
Text("Statut du règlement") Text("Statut du règlement")
} }
Picker(selection: $sortOption) { Picker(selection: $cashierViewModel.sortOption) {
ForEach(SortOption.allCases) { sortOption in ForEach(CashierViewModel.SortOption.allCases) { sortOption in
Text(sortOption.localizedLabel()).tag(sortOption) Text(sortOption.localizedLabel()).tag(sortOption)
} }
} label: { } label: {
Text("Affichage par") Text("Affichage par")
} }
Picker(selection: $sortOrder) { Picker(selection: $cashierViewModel.sortOrder) {
Text("Croissant").tag(SortOrder.ascending) Text("Croissant").tag(SortOrder.ascending)
Text("Décroissant").tag(SortOrder.descending) Text("Décroissant").tag(SortOrder.descending)
} label: { } label: {
@ -132,11 +170,7 @@ struct CashierView: View {
} }
} }
if _isContentUnavailable() { switch cashierViewModel.sortOption {
_contentUnavailableView()
}
switch sortOption {
case .teamRank: case .teamRank:
_byTeamRankView() _byTeamRankView()
case .alphabeticalLastName: case .alphabeticalLastName:
@ -151,47 +185,48 @@ struct CashierView: View {
_byCallDateView() _byCallDateView()
} }
} }
.searchable(text: $cashierViewModel.searchText, isPresented: $cashierViewModel.isSearching, placement: .navigationBarDrawer(displayMode: .always), prompt: Text("Chercher un joueur"))
.onAppear {
cashierViewModel.searchText = ""
// if tournaments.count == 1 {
// if tournaments.first!.hasEnded() == true, tournaments.first!.players().anySatisfy({ $0.hasPaid() == false }) {
// filterOption = .didNotPay
// }
// }
if cashierViewModel.sortOption == .callDate && teams.first(where: { $0.callDate != nil }) == nil {
cashierViewModel.sortOption = .teamRank
}
self.shareableObject = ShareableObject(cashierViewModel: cashierViewModel, teams: teams, fileName: "Encaissement.txt")
}
.headerProminence(.increased) .headerProminence(.increased)
.searchable(text: $searchText, isPresented: $isSearching, prompt: Text("Chercher un joueur"))
.toolbar { .toolbar {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
ShareLink(item: _sharedData().createTxtFile("bilan")) if let shareableObject {
} ShareLink(
} item: shareableObject,
} preview: SharePreview(shareableObject.fileName)
)
@ViewBuilder
func computedPlayerView(_ player: PlayerRegistration) -> some View {
EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment])
} }
private func _shouldDisplayTeam(_ team: TeamRegistration) -> Bool {
team.players().anySatisfy({
_shouldDisplayPlayer($0)
})
} }
private func _shouldDisplayPlayer(_ player: PlayerRegistration) -> Bool {
if searchText.isEmpty == false {
filterOption.shouldDisplayPlayer(player) && player.contains(searchText)
} else {
filterOption.shouldDisplayPlayer(player)
} }
} }
@ViewBuilder @ViewBuilder
private func _byPlayer(_ players: [PlayerRegistration]) -> some View { private func _byPlayer(_ players: [PlayerRegistration]) -> some View {
let _players = sortOrder == .ascending ? players : players.reversed() let _players = cashierViewModel.sortOrder == .ascending ? players : players.reversed()
if _players.isEmpty {
_contentUnavailableView()
} else {
ForEach(_players) { player in ForEach(_players) { player in
Section { Section {
computedPlayerView(player) EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment])
} header: { } header: {
HStack { HStack {
if let teamCallDate = player.team()?.callDate { if let teamCallDate = player.team()?.callDate {
Text(teamCallDate.localizedDate()) Text(teamCallDate.localizedDate())
} }
Spacer() Spacer()
Text(player.computedRank.formatted()) Text(player.formattedRank())
} }
} footer: { } footer: {
if tournaments.count > 1, let tournamentTitle = player.tournament()?.tournamentTitle() { if tournaments.count > 1, let tournamentTitle = player.tournament()?.tournamentTitle() {
@ -200,46 +235,57 @@ struct CashierView: View {
} }
} }
} }
}
@ViewBuilder @ViewBuilder
private func _byPlayerRank() -> some View { private func _byPlayerRank() -> some View {
let players = teams.flatMap({ $0.players() }).sorted(using: .keyPath(\.computedRank)).filter({ _shouldDisplayPlayer($0) }) let players = teams.flatMap({ $0.unsortedPlayers() }).sorted(using: .keyPath(\.computedRank)).filter({ cashierViewModel._shouldDisplayPlayer($0) })
_byPlayer(players) _byPlayer(players)
} }
@ViewBuilder @ViewBuilder
private func _byPlayerAge() -> some View { private func _byPlayerAge() -> some View {
let players = teams.flatMap({ $0.players() }).filter({ $0.computedAge != nil }).sorted(using: .keyPath(\.computedAge!)).filter({ _shouldDisplayPlayer($0) }) let players = teams.flatMap({ $0.unsortedPlayers() }).filter({ $0.computedAge != nil }).sorted(using: .keyPath(\.computedAge!)).filter({ cashierViewModel._shouldDisplayPlayer($0) })
_byPlayer(players) _byPlayer(players)
} }
@ViewBuilder @ViewBuilder
private func _byPlayerLastName() -> some View { private func _byPlayerLastName() -> some View {
let players = teams.flatMap({ $0.players() }).sorted(using: .keyPath(\.lastName)).filter({ _shouldDisplayPlayer($0) }) let players = teams.flatMap({ $0.unsortedPlayers() }).sorted(using: .keyPath(\.lastName)).filter({ cashierViewModel._shouldDisplayPlayer($0) })
_byPlayer(players) _byPlayer(players)
} }
@ViewBuilder @ViewBuilder
private func _byPlayerFirstName() -> some View { private func _byPlayerFirstName() -> some View {
let players = teams.flatMap({ $0.players() }).sorted(using: .keyPath(\.firstName)).filter({ _shouldDisplayPlayer($0) }) let players = teams.flatMap({ $0.unsortedPlayers() }).sorted(using: .keyPath(\.firstName)).filter({ cashierViewModel._shouldDisplayPlayer($0) })
_byPlayer(players) _byPlayer(players)
} }
@ViewBuilder @ViewBuilder
private func _byTeamRankView() -> some View { private func _byTeamRankView() -> some View {
let _teams = sortOrder == .ascending ? teams : teams.reversed() let _teams = cashierViewModel.sortOrder == .ascending ? teams : teams.reversed()
ForEach(_teams) { team in let _filteredTeams = _teams.filter({ cashierViewModel._shouldDisplayTeam($0) })
if _shouldDisplayTeam(team) { if _filteredTeams.isEmpty {
_contentUnavailableView()
} else {
ForEach(_filteredTeams) { team in
Section { Section {
_cashierPlayersView(team.players()) ForEach(team.players()) { player in
if cashierViewModel._shouldDisplayPlayer(player) {
EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment])
}
}
} header: { } header: {
HStack { HStack {
if let callDate = team.callDate { if let callDate = team.callDate {
Text(callDate.localizedDate()) Text(callDate.localizedDate())
} }
Spacer() Spacer()
VStack(alignment: .trailing, spacing: 0) {
Text("Poids").font(.caption)
Text(team.weight.formatted()) Text(team.weight.formatted())
} }
}
} footer: { } footer: {
if tournaments.count > 1, let tournamentTitle = team.tournamentObject()?.tournamentTitle() { if tournaments.count > 1, let tournamentTitle = team.tournamentObject()?.tournamentTitle() {
Text(tournamentTitle) Text(tournamentTitle)
@ -252,17 +298,26 @@ struct CashierView: View {
@ViewBuilder @ViewBuilder
private func _byCallDateView() -> some View { private func _byCallDateView() -> some View {
let groupedTeams = Dictionary(grouping: teams) { team in let _teams = teams.filter({ $0.callDate != nil && cashierViewModel._shouldDisplayTeam($0) })
if _teams.isEmpty {
_contentUnavailableView()
}
let groupedTeams = Dictionary(grouping: _teams) { team in
team.callDate team.callDate
} }
let keys = sortOrder == .ascending ? groupedTeams.keys.compactMap { $0 }.sorted() : groupedTeams.keys.compactMap { $0 }.sorted().reversed() let keys = cashierViewModel.sortOrder == .ascending ? groupedTeams.keys.compactMap { $0 }.sorted() : groupedTeams.keys.compactMap { $0 }.sorted().reversed()
ForEach(keys, id: \.self) { key in ForEach(keys, id: \.self) { key in
if let _teams = groupedTeams[key] { if let _teams = groupedTeams[key] {
ForEach(_teams) { team in ForEach(_teams) { team in
if _shouldDisplayTeam(team) {
Section { Section {
_cashierPlayersView(team.players()) ForEach(team.players()) { player in
if cashierViewModel._shouldDisplayPlayer(player) {
EditablePlayerView(player: player, editingOptions: [.licenceId, .name, .payment])
}
}
} header: { } header: {
Text(key.localizedDate()) Text(key.localizedDate())
} footer: { } footer: {
@ -274,30 +329,9 @@ struct CashierView: View {
} }
} }
} }
}
@ViewBuilder
private func _cashierPlayersView(_ players: [PlayerRegistration]) -> some View {
ForEach(players) { player in
if _shouldDisplayPlayer(player) {
computedPlayerView(player)
}
}
}
private func _isContentUnavailable() -> Bool {
switch sortOption {
case .callDate:
return teams.filter({ $0.callDate != nil && _shouldDisplayTeam($0) }).isEmpty
case .teamRank:
return teams.filter({ _shouldDisplayTeam($0) }).isEmpty
default:
return teams.flatMap({ $0.players() }).filter({ _shouldDisplayPlayer($0) }).isEmpty
}
}
private func _unavailableIcon() -> String { private func _unavailableIcon() -> String {
switch sortOption { switch cashierViewModel.sortOption {
case .teamRank, .callDate: case .teamRank, .callDate:
return "person.2.slash.fill" return "person.2.slash.fill"
default: default:
@ -307,8 +341,8 @@ struct CashierView: View {
@ViewBuilder @ViewBuilder
private func _contentUnavailableView() -> some View { private func _contentUnavailableView() -> some View {
if isSearching { if cashierViewModel.isSearching {
ContentUnavailableView.search(text: searchText) ContentUnavailableView.search(text: cashierViewModel.searchText)
} else { } else {
ContentUnavailableView("Aucun résultat", systemImage: _unavailableIcon()) ContentUnavailableView("Aucun résultat", systemImage: _unavailableIcon())
} }

@ -41,11 +41,6 @@ struct ClubSearchView: View {
var club: Club? var club: Club?
var selection: ((Club) -> ())? = nil var selection: ((Club) -> ())? = nil
fileprivate class DebouncableViewModel: ObservableObject {
@Published var debouncableText: String = ""
var debounceTrigger: Double = 0.15
}
private var distanceLimit: Measurement<UnitLength> { private var distanceLimit: Measurement<UnitLength> {
Measurement(value: radius, unit: .kilometers) Measurement(value: radius, unit: .kilometers)
} }

@ -48,6 +48,28 @@ struct GenericDestinationPickerView<T: Identifiable & Selectable & Equatable >:
.id(destination.id) .id(destination.id)
.buttonStyle(.plain) .buttonStyle(.plain)
.overlay(alignment: .bottomTrailing) { .overlay(alignment: .bottomTrailing) {
if destination.displayImageIfValueZero() {
let count = destination.badgeValue()
if let count, count == 0, let badge = destination.badgeImage() {
Image(systemName: badge.systemName())
.foregroundColor(badge.color())
.imageScale(.medium)
.background (
Color(.systemBackground)
.clipShape(.circle)
)
.offset(x: 3, y: 3)
} else if let count, count > 0 {
Image(systemName: count <= 50 ? "\(count).circle.fill" : "plus.circle.fill")
.foregroundColor(destination.badgeValueColor() ?? .red)
.imageScale(.medium)
.background (
Color(.systemBackground)
.clipShape(.circle)
)
.offset(x: 3, y: 3)
}
} else {
if let badge = destination.badgeImage() { if let badge = destination.badgeImage() {
Image(systemName: badge.systemName()) Image(systemName: badge.systemName())
.foregroundColor(badge.color()) .foregroundColor(badge.color())
@ -70,6 +92,7 @@ struct GenericDestinationPickerView<T: Identifiable & Selectable & Equatable >:
} }
} }
} }
}
.fixedSize() .fixedSize()
.padding(8) .padding(8)
} }

@ -17,18 +17,40 @@ extension String : Identifiable {
struct BroadcastView: View { struct BroadcastView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament @Environment(Tournament.self) var tournament: Tournament
@Environment(NavigationViewModel.self) var navigation: NavigationViewModel
let context = CIContext() let context = CIContext()
let filter = CIFilter.qrCodeGenerator() let filter = CIFilter.qrCodeGenerator()
@State private var urlToShow: String? @State private var urlToShow: String?
@State private var tvMode: Bool = false @State private var tvMode: Bool = false
@State private var pageLink: PageLink = .teams @State private var pageLink: PageLink = .teams
let createAccountTip = CreateAccountTip()
let tournamentPublishingTip = TournamentPublishingTip() let tournamentPublishingTip = TournamentPublishingTip()
let tournamentTVBroadcastTip = TournamentTVBroadcastTip() let tournamentTVBroadcastTip = TournamentTVBroadcastTip()
var body: some View { var body: some View {
@Bindable var tournament = tournament @Bindable var tournament = tournament
List { List {
if Store.main.userId == nil {
Section {
TipView(createAccountTip) { action in
switch action.id {
case CreateAccountTip.ActionKey.accessPadelClubWebPage.rawValue:
UIApplication.shared.open(URLs.main.url)
case CreateAccountTip.ActionKey.createAccount.rawValue:
navigation.selectedTab = .umpire
default:
break
//todo
// case CreateAccountTip.ActionKey.learnMore.rawValue:
// UIApplication.shared.open(URLs.padelClubLandingPage.url)
}
}
.tipStyle(tint: .master)
}
}
Section { Section {
TipView(tournamentPublishingTip) { action in TipView(tournamentPublishingTip) { action in
UIApplication.shared.open(URLs.main.url) UIApplication.shared.open(URLs.main.url)

@ -13,6 +13,9 @@ struct PrintSettingsView: View {
@StateObject var generator: HtmlGenerator @StateObject var generator: HtmlGenerator
@State private var presentShareView: Bool = false @State private var presentShareView: Bool = false
@State private var prepareGroupStage: Bool = false @State private var prepareGroupStage: Bool = false
@State private var generationId: UUID = UUID()
@State private var generationGroupStageId: UUID = UUID()
@State private var generating: Bool = false
init(tournament: Tournament) { init(tournament: Tournament) {
self.tournament = tournament self.tournament = tournament
@ -63,26 +66,55 @@ struct PrintSettingsView: View {
} header: { } header: {
Text("Tableau principal") Text("Tableau principal")
} }
if generating == false {
RowButtonView("Générer le PDF", systemImage: "printer") {
await MainActor.run() {
self.generating = true
}
generator.preparePDF { result in
switch result {
case .success(true):
if generator.includeGroupStage && generator.groupStageIsReady == false && tournament.groupStages().isEmpty == false {
self.prepareGroupStage = true
self.generationGroupStageId = UUID()
} else {
self.presentShareView = true
self.generating = false
}
case .success(false):
print("didn't save pdf")
break
case .failure(let error):
print(error)
break
}
}
self.prepareGroupStage = false
self.generationId = UUID()
}
.disabled(generator.includeBracket == false && generator.includeGroupStage == false && generator.includeLoserBracket == false)
} else {
LabeledContent {
ProgressView()
} label: {
Text("Préparation du PDF")
}
.id(generationId)
}
} }
Section { Section {
NavigationLink { NavigationLink {
WebView(htmlRawData: generator.generateHtml(), loadStatusChanged: { loaded, error, webView in WebViewPreview(bracket: true)
}) .environmentObject(generator)
} label: { } label: {
Text("Aperçu du tableau") Text("Aperçu du tableau")
} }
ForEach(tournament.groupStages()) { groupStage in ForEach(tournament.groupStages()) { groupStage in
NavigationLink { NavigationLink {
WebView(htmlRawData: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false), loadStatusChanged: { loaded, error, webView in WebViewPreview(groupStage: groupStage)
if let error { .environmentObject(generator)
print("preparePDF", error)
} else if loaded == false {
generator.generateGroupStage(webView: webView)
} else {
print("preparePDF", "is loading")
}
})
} label: { } label: {
Text("Aperçu de la \(groupStage.groupStageTitle())") Text("Aperçu de la \(groupStage.groupStageTitle())")
} }
@ -90,6 +122,44 @@ struct PrintSettingsView: View {
} }
} }
.background { .background {
if generating {
_backgroundGenerationWebView()
_backgroundGroupStageWebView()
}
}
.navigationTitle("Imprimer")
.toolbarBackground(.visible, for: .navigationBar)
.navigationBarTitleDisplayMode(.inline)
// .toolbar {
// ToolbarItem(placement: .topBarTrailing) {
// Menu {
// Section {
// ShareLink(item: generator.generateHtml()) {
// Text("Tableau")
// }
//
// if let groupStage = tournament.groupStages().first {
// ShareLink(item: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false)) {
// Text("Poule")
// }
// }
// } header: {
// Text("Partager le code source HTML")
// }
// } label: {
// Label("Options", systemImage: "ellipsis.circle")
// }
// }
// }
.sheet(isPresented: $presentShareView) {
if let pdfURL = generator.pdfURL {
ShareSheet(urls: [pdfURL])
}
}
}
@ViewBuilder
private func _backgroundGenerationWebView() -> some View {
WebView(htmlRawData: generator.generateHtml(), loadStatusChanged: { loaded, error, webView in WebView(htmlRawData: generator.generateHtml(), loadStatusChanged: { loaded, error, webView in
if let error { if let error {
print("preparePDF", error) print("preparePDF", error)
@ -98,8 +168,13 @@ struct PrintSettingsView: View {
} else { } else {
print("preparePDF", "is loading") print("preparePDF", "is loading")
} }
}).opacity(0) })
.opacity(0)
.id(generationId)
}
private func _backgroundGroupStageWebView() -> some View {
Group {
if prepareGroupStage { if prepareGroupStage {
ForEach(tournament.groupStages()) { groupStage in ForEach(tournament.groupStages()) { groupStage in
WebView(htmlRawData: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false), loadStatusChanged: { loaded, error, webView in WebView(htmlRawData: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false), loadStatusChanged: { loaded, error, webView in
@ -114,64 +189,7 @@ struct PrintSettingsView: View {
} }
} }
} }
.navigationTitle("Imprimer") .id(generationGroupStageId)
.toolbarBackground(.visible, for: .navigationBar)
.toolbarBackground(.visible, for: .bottomBar)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .bottomBar) {
Button {
generator.preparePDF { result in
switch result {
case .success(true):
if generator.includeGroupStage && generator.groupStageIsReady == false {
self.prepareGroupStage = true
} else {
self.presentShareView = true
}
case .success(false):
print("didn't save pdf")
break
case .failure(let error):
print(error)
break
}
}
self.prepareGroupStage = false
self.generator.buildPDF()
} label: {
Text("Obtenir le PDF")
}
.disabled(generator.includeBracket == false && generator.includeGroupStage == false && generator.includeLoserBracket == false)
.buttonStyle(.borderedProminent)
}
ToolbarItem(placement: .topBarTrailing) {
Menu {
Section {
ShareLink(item: generator.generateHtml()) {
Text("Tableau")
}
if let groupStage = tournament.groupStages().first {
ShareLink(item: HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false)) {
Text("Poule")
}
}
} header: {
Text("Partager le code source HTML")
}
} label: {
Label("Options", systemImage: "ellipsis.circle")
}
}
}
.sheet(isPresented: $presentShareView) {
if let pdfURL = generator.pdfURL {
ShareSheet(urls: [pdfURL])
}
}
} }
} }
@ -239,3 +257,34 @@ struct WebView: UIViewRepresentable {
} }
} }
struct WebViewPreview: View {
@EnvironmentObject var generator: HtmlGenerator
let bracket: Bool
let groupStage: GroupStage?
@State private var html: String?
init(bracket: Bool = false, groupStage: GroupStage? = nil) {
self.bracket = bracket
self.groupStage = groupStage
}
var body: some View {
Group {
if let html {
WebView(htmlRawData: html, loadStatusChanged: { loaded, error, webView in
})
} else {
ProgressView()
.onAppear {
if let groupStage {
html = HtmlService.groupstage(groupStage: groupStage).html(headName: generator.displayHeads, withRank: generator.displayRank, withScore: false)
} else {
html = generator.generateHtml()
}
}
}
}
}
}

@ -42,6 +42,10 @@ enum CashierDestination: Identifiable, Selectable, Equatable {
} }
} }
func displayImageIfValueZero() -> Bool {
return true
}
func badgeValue() -> Int? { func badgeValue() -> Int? {
switch self { switch self {
case .summary: case .summary:
@ -51,7 +55,7 @@ enum CashierDestination: Identifiable, Selectable, Equatable {
case .bracket(let round): case .bracket(let round):
return round.seeds().flatMap { $0.unsortedPlayers() }.filter({ $0.hasPaid() == false }).count return round.seeds().flatMap { $0.unsortedPlayers() }.filter({ $0.hasPaid() == false }).count
case .all(let tournament): case .all(let tournament):
return tournament.selectedPlayers().filter({ $0.hasPaid() == false }).count return nil
} }
} }
@ -63,12 +67,10 @@ enum CashierDestination: Identifiable, Selectable, Equatable {
switch self { switch self {
case .summary: case .summary:
return nil return nil
case .groupStage(let groupStage): case .all:
return groupStage.unsortedPlayers().allSatisfy({ $0.hasPaid() }) ? .checkmark : nil return nil
case .bracket(let round): default:
return round.seeds().flatMap { $0.unsortedPlayers() }.allSatisfy({ $0.hasPaid() }) ? .checkmark : nil return .checkmark
case .all(let tournament):
return tournament.selectedPlayers().allSatisfy({ $0.hasPaid() }) ? .checkmark : nil
} }
} }
@ -77,6 +79,7 @@ enum CashierDestination: Identifiable, Selectable, Equatable {
struct TournamentCashierView: View { struct TournamentCashierView: View {
var tournament: Tournament var tournament: Tournament
@State private var selectedDestination: CashierDestination? @State private var selectedDestination: CashierDestination?
@StateObject private var cashierViewModel: CashierViewModel = CashierViewModel()
func allDestinations() -> [CashierDestination] { func allDestinations() -> [CashierDestination] {
var allDestinations : [CashierDestination] = [] var allDestinations : [CashierDestination] = []
@ -132,10 +135,13 @@ struct TournamentCashierView: View {
CashierDetailView(tournament: tournament) CashierDetailView(tournament: tournament)
case .groupStage(let groupStage): case .groupStage(let groupStage):
CashierView(tournament: tournament, teams: groupStage.teams()) CashierView(tournament: tournament, teams: groupStage.teams())
.environmentObject(cashierViewModel)
case .bracket(let round): case .bracket(let round):
CashierView(tournament: tournament, teams: round.seeds()) CashierView(tournament: tournament, teams: round.seeds())
.environmentObject(cashierViewModel)
case .all(let tournament): case .all(let tournament):
CashierView(tournament: tournament, teams: tournament.selectedSortedTeams()) CashierView(tournament: tournament, teams: tournament.selectedSortedTeams())
.environmentObject(cashierViewModel)
} }
} }
} }

@ -20,7 +20,7 @@ struct TournamentBuildView: View {
if tournament.hasEnded() { if tournament.hasEnded() {
Section { Section {
NavigationLink(value: Screen.rankings) { NavigationLink(value: Screen.rankings) {
Text("Classement") Text("Classement final des équipes")
} }
} }
} }
@ -30,7 +30,7 @@ struct TournamentBuildView: View {
NavigationLink(value: Screen.groupStage) { NavigationLink(value: Screen.groupStage) {
LabeledContent { LabeledContent {
if let groupStageStatus { if let groupStageStatus {
Text(groupStageStatus) Text(groupStageStatus).lineLimit(1)
.multilineTextAlignment(.trailing) .multilineTextAlignment(.trailing)
} else { } else {
ProgressView() ProgressView()
@ -51,7 +51,7 @@ struct TournamentBuildView: View {
NavigationLink(value: Screen.round) { NavigationLink(value: Screen.round) {
LabeledContent { LabeledContent {
if let bracketStatus { if let bracketStatus {
Text(bracketStatus) Text(bracketStatus).lineLimit(1)
.multilineTextAlignment(.trailing) .multilineTextAlignment(.trailing)
} else { } else {
ProgressView() ProgressView()
@ -82,7 +82,9 @@ struct TournamentBuildView: View {
} label: { } label: {
Text("Horaires") Text("Horaires")
if let tournamentStatus { if let tournamentStatus {
Text(tournamentStatus.label) Text(tournamentStatus.label).lineLimit(1)
} else {
Text(" ")
} }
} }
} }
@ -101,7 +103,9 @@ struct TournamentBuildView: View {
} label: { } label: {
Text("Convocations") Text("Convocations")
if let tournamentStatus { if let tournamentStatus {
Text(tournamentStatus.label) Text(tournamentStatus.label).lineLimit(1)
} else {
Text(" ")
} }
} }
} }
@ -122,7 +126,9 @@ struct TournamentBuildView: View {
} label: { } label: {
Text("Encaissement") Text("Encaissement")
if let tournamentStatus { if let tournamentStatus {
Text(tournamentStatus.label) Text(tournamentStatus.label).lineLimit(1)
} else {
Text(" ")
} }
} }
} }

@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import LeStorage
struct TournamentInitView: View { struct TournamentInitView: View {
var tournament: Tournament var tournament: Tournament
@ -43,13 +44,21 @@ struct TournamentInitView: View {
NavigationLink(value: Screen.broadcast) { NavigationLink(value: Screen.broadcast) {
LabeledContent { LabeledContent {
if Store.main.userId == nil {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.logoRed)
} else {
if tournament.isPrivate { if tournament.isPrivate {
Text("tournoi privé").foregroundStyle(.logoRed) Text("tournoi privé").foregroundStyle(.logoRed)
} else { } else {
Text("Automatique") Text("Automatique")
} }
}
} label: { } label: {
Text("Publication") Text("Publication")
if Store.main.userId == nil {
Text("Un compte Padel Club est nécessaire")
}
} }
} }

@ -93,6 +93,7 @@ struct TournamentView: View {
TournamentRankView() TournamentRankView()
case .broadcast: case .broadcast:
BroadcastView() BroadcastView()
.environment(navigation)
case .event: case .event:
if let event = tournament.eventObject() { if let event = tournament.eventObject() {
EventView(event: event) EventView(event: event)

Loading…
Cancel
Save