Laurent 2 years ago
commit d6be8e5305
  1. 4
      PadelClub/Data/Club.swift
  2. 12
      PadelClub/Data/Tournament.swift
  3. 1
      PadelClub/Utils/SourceFileManager.swift
  4. 2
      PadelClub/Utils/URLs.swift
  5. 2
      PadelClub/ViewModel/AgendaDestination.swift
  6. 16
      PadelClub/Views/GroupStage/GroupStageSettingsView.swift
  7. 26
      PadelClub/Views/GroupStage/GroupStagesView.swift
  8. 4
      PadelClub/Views/Navigation/Agenda/ActivityView.swift
  9. 2
      PadelClub/Views/Tournament/FileImportView.swift
  10. 118
      PadelClub/Views/Tournament/Screen/BroadcastView.swift
  11. 2
      PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift
  12. 4
      PadelClub/Views/Tournament/Screen/Components/TournamentMatchFormatsSettingsView.swift
  13. 54
      PadelClub/Views/Tournament/Screen/Components/TournamentStatusView.swift
  14. 6
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  15. 62
      PadelClub/Views/Tournament/TournamentView.swift

@ -54,6 +54,10 @@ class Club : ModelObject, Storable, Hashable {
return acronym
}
}
func shareURL() -> URL? {
return URLs.main.url.appending(path: "?club=\(id)")
}
var courts: [Court] {
Store.main.filter { $0.club == self.id }.sorted(by: \.index)

@ -98,8 +98,17 @@ class Tournament : ModelObject, Storable {
enum State {
case initial
case build
case canceled
}
func shareURL() -> URL? {
return URLs.main.url.appending(path: "tournament/\(id)")
}
func broadcastURL() -> URL? {
return URLs.main.url.appending(path: "tournament/\(id)/broadcast")
}
func courtUsed() -> [Int] {
let runningMatches : [Match] = Store.main.filter(isIncluded: { $0.isRunning() }).filter({ $0.tournamentId() == self.id })
return Set(runningMatches.compactMap { $0.courtIndex }).sorted()
@ -141,6 +150,9 @@ class Tournament : ModelObject, Storable {
}
func state() -> Tournament.State {
if currentCanceled == true {
return .canceled
}
if (groupStageCount > 0 && groupStages().isEmpty == false)
|| rounds().isEmpty == false {
return .build

@ -9,7 +9,6 @@ import Foundation
class SourceFileManager {
static let shared = SourceFileManager()
static let beachPadel = URL(string: "https://beach-padel.app.fft.fr/beachja/index/")!
var lastDataSource: String? {
DataStore.shared.appSettings.lastDataSource

@ -9,6 +9,8 @@ import Foundation
enum URLs: String, Identifiable {
case subscriptions = "https://apple.co/2Th4vqI"
case main = "https://xlr.alwaysdata.net/"
case beachPadel = "https://beach-padel.app.fft.fr/beachja/index/"
var id: String { return self.rawValue }

@ -48,7 +48,7 @@ enum AgendaDestination: CaseIterable, Identifiable, Selectable {
func badgeValue() -> Int? {
switch self {
case .activity:
DataStore.shared.tournaments.filter { $0.endDate == nil && FederalDataViewModel.shared.isTournamentValidForFilters($0) }.count
DataStore.shared.tournaments.filter { $0.endDate == nil && $0.isDeleted == false && FederalDataViewModel.shared.isTournamentValidForFilters($0) }.count
case .history:
DataStore.shared.tournaments.filter { $0.endDate != nil && FederalDataViewModel.shared.isTournamentValidForFilters($0) }.count
case .tenup:

@ -12,6 +12,7 @@ struct GroupStageSettingsView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
@State private var nameAlphabetical: Bool = false
@State private var generationDone: Bool = false
let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
@ -26,8 +27,10 @@ struct GroupStageSettingsView: View {
var body: some View {
List {
Section {
menuBuildAllGroupStages
if tournament.unsortedTeams().filter({ $0.groupStagePosition != nil }).isEmpty == false {
Section {
menuBuildAllGroupStages
}
}
Section {
@ -56,6 +59,13 @@ struct GroupStageSettingsView: View {
Text("Nommer les poules alphabétiquement")
}
}
.overlay(alignment: .bottom) {
if generationDone {
Label("Poules mises à jour", systemImage: "checkmark.circle.fill")
.toastFormatted()
.deferredRendering(for: .seconds(2))
}
}
.onChange(of: nameAlphabetical) {
let groupStages = tournament.groupStages()
if nameAlphabetical {
@ -76,6 +86,7 @@ struct GroupStageSettingsView: View {
RowButtonView("Refaire les poules", role: .destructive) {
tournament.deleteGroupStages()
tournament.buildGroupStages()
generationDone = true
_save()
}
}
@ -85,6 +96,7 @@ struct GroupStageSettingsView: View {
RowButtonView("Poule \(mode.localizedLabel().lowercased())", role: .destructive, systemImage: mode.systemImage) {
tournament.groupStageOrderingMode = mode
tournament.refreshGroupStages()
generationDone = true
_save()
}
}

@ -49,9 +49,13 @@ struct GroupStagesView: View {
init(tournament: Tournament) {
self.tournament = tournament
let gs = tournament.getActiveGroupStage()
if let gs {
_selectedDestination = State(wrappedValue: .groupStage(gs))
if tournament.unsortedTeams().filter({ $0.groupStagePosition != nil }).isEmpty {
_selectedDestination = State(wrappedValue: nil)
} else {
let gs = tournament.getActiveGroupStage()
if let gs {
_selectedDestination = State(wrappedValue: .groupStage(gs))
}
}
}
@ -67,17 +71,23 @@ struct GroupStagesView: View {
GenericDestinationPickerView(selectedDestination: $selectedDestination, destinations: allDestinations(), nilDestinationIsValid: true)
switch selectedDestination {
case .all:
let allGroupStages = tournament.groupStages()
let availableToStart = allGroupStages.flatMap({ $0.availableToStart() })
let runningMatches = allGroupStages.flatMap({ $0.runningMatches() })
let readyMatches = allGroupStages.flatMap({ $0.readyMatches() })
let finishedMatches = allGroupStages.flatMap({ $0.finishedMatches() })
List {
let allGroupStages = tournament.groupStages()
let availableToStart = allGroupStages.flatMap({ $0.availableToStart() })
let runningMatches = allGroupStages.flatMap({ $0.runningMatches() })
let readyMatches = allGroupStages.flatMap({ $0.readyMatches() })
let finishedMatches = allGroupStages.flatMap({ $0.finishedMatches() })
MatchListView(section: "disponible", matches: availableToStart, matchViewStyle: .standardStyle, isExpanded: false)
MatchListView(section: "en cours", matches: runningMatches, matchViewStyle: .standardStyle, isExpanded: false)
MatchListView(section: "à lancer", matches: readyMatches, matchViewStyle: .standardStyle, isExpanded: false)
MatchListView(section: "terminés", matches: finishedMatches, matchViewStyle: .standardStyle, isExpanded: false)
}
.overlay {
if availableToStart.isEmpty && runningMatches.isEmpty && readyMatches.isEmpty && finishedMatches.isEmpty {
ContentUnavailableView("Aucun match à afficher", systemImage: "tennisball")
}
}
.navigationTitle("Toutes les poules")
case .groupStage(let groupStage):
GroupStageView(groupStage: groupStage)

@ -22,12 +22,12 @@ struct ActivityView: View {
@State private var uuid: UUID = UUID()
var runningTournaments: [FederalTournamentHolder] {
dataStore.tournaments.filter({ $0.endDate == nil && $0.isDeleted == false })
dataStore.tournaments.filter({ $0.endDate == nil })
.filter({ federalDataViewModel.isTournamentValidForFilters($0) })
}
var endedTournaments: [Tournament] {
dataStore.tournaments.filter({ $0.endDate != nil && $0.isDeleted == false })
dataStore.tournaments.filter({ $0.endDate != nil })
.filter({ federalDataViewModel.isTournamentValidForFilters($0) })
.sorted(using: SortDescriptor(\.startDate, order: .reverse))
}

@ -37,7 +37,7 @@ struct FileImportView: View {
if teams.isEmpty {
Section {
Link(destination: SourceFileManager.beachPadel) {
Link(destination: URLs.beachPadel.url) {
Label("beach-padel.app.fft.fr", systemImage: "tennisball")
}

@ -6,15 +6,133 @@
//
import SwiftUI
import CoreImage.CIFilterBuiltins
import LeStorage
extension String : Identifiable {
public var id: String { self }
}
struct BroadcastView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
let context = CIContext()
let filter = CIFilter.qrCodeGenerator()
@State private var urlToShow: String?
@State private var tvMode: Bool = false
var body: some View {
@Bindable var tournament = tournament
List {
Section {
Toggle(isOn: $tournament.isPrivate) {
Text("Tournoi privée")
}
} footer: {
Text("Le tournoi sera masqué sur le site \(URLs.main.rawValue)")
}
Section {
LabeledContent {
actionForURL(URLs.main.url)
} label: {
Text("Lien Padel Club")
}
if let club = tournament.club(), let clubURL = club.shareURL() {
LabeledContent {
actionForURL(clubURL)
} label: {
Text("Lien du club")
}
}
if let url = tournament.shareURL() {
LabeledContent {
actionForURL(url)
} label: {
Text("Lien du tournoi")
}
}
if let url = tournament.broadcastURL() {
LabeledContent {
actionForURL(url)
} label: {
Text("Lien TV")
}
}
} header: {
Text("Liens à partager")
.textCase(nil)
}
}
.navigationTitle("Diffusion")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.sheet(item: $urlToShow) { urlToShow in
Image(uiImage: generateQRCode(from: urlToShow))
.interpolation(.none)
.resizable()
.scaledToFit()
.frame(width: 300, height: 300)
.onAppear {
UIPasteboard.general.string = urlToShow
}
}
.onChange(of: tournament.isPrivate) {
_save()
}
}
private func _save() {
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
}
private func generateQRCode(from string: String) -> UIImage {
filter.message = Data(string.utf8)
if let outputImage = filter.outputImage {
if let cgimg = context.createCGImage(outputImage, from: outputImage.extent) {
return UIImage(cgImage: cgimg)
}
}
return UIImage(systemName: "xmark.circle") ?? UIImage()
}
@ViewBuilder
func actionForURL(_ url: URL, removeSource: Bool = false) -> some View {
Menu {
Button {
UIApplication.shared.open(url)
} label: {
Label("Voir", systemImage: "safari")
}
Button {
urlToShow = url.absoluteString
} label: {
Label("QRCode", systemImage: "qrcode")
}
ShareLink(item: url) {
Label("Partager le lien", systemImage: "link")
}
} label: {
Text("lien")
.underline()
}
.buttonStyle(.borderless)
}
}
#Preview {

@ -33,7 +33,7 @@ struct InscriptionInfoView: View {
Text(entriesFromBeachPadel.count.formatted())
} label: {
Text("Paires importées")
Text(SourceFileManager.beachPadel.absoluteString)
Text(URLs.beachPadel.url.absoluteString)
}
.listRowView(color: .indigo)

@ -69,10 +69,10 @@ struct TournamentMatchFormatsSettingsView: View {
private func _confirmOrSave() {
switch tournament.state() {
case .initial:
break
case .build:
confirmUpdate = true
default:
break
}
}

@ -9,37 +9,63 @@ import SwiftUI
import LeStorage
struct TournamentStatusView: View {
@Environment(\.dismiss) private var dismiss
@Environment(Tournament.self) private var tournament: Tournament
@Environment(NavigationViewModel.self) private var navigation: NavigationViewModel
@EnvironmentObject var dataStore: DataStore
var body: some View {
@Bindable var tournament = tournament
Form {
RowButtonView("debug: Un-delete le tournoi") {
tournament.endDate = nil
tournament.isDeleted.toggle()
}
Section {
RowButtonView("Supprimer le tournoi", role: .destructive) {
tournament.isDeleted.toggle()
if tournament.endDate == nil {
RowButtonView("Terminer le tournoi", role: .destructive) {
tournament.endDate = Date()
}
} else {
RowButtonView("Ré-ouvrir le tournoi", role: .destructive) {
tournament.endDate = nil
}
}
} footer: {
Text("todo: expliquer ce que ca fait")
}
Section {
if tournament.hasEnded() == false {
if tournament.currentCanceled == false {
RowButtonView("Annuler le tournoi", role: .destructive) {
tournament.setCanceled(true)
self._save()
RowButtonView("Supprimer le tournoi", role: .destructive) {
if tournament.currentPayment == nil {
do {
try dataStore.tournaments.delete(instance: tournament)
} catch {
Logger.error(error)
}
} else {
RowButtonView("Reprendre le tournoi", role: .destructive) {
tournament.setCanceled(false)
self._save()
}
tournament.endDate = Date()
tournament.isDeleted.toggle()
tournament.navigationPath.removeAll()
}
navigation.path = NavigationPath()
}
} footer: {
Text("todo: expliquer ce que ca fait")
}
if tournament.hasEnded() == false && tournament.currentCanceled == false {
Section {
RowButtonView("Annuler le tournoi", role: .destructive) {
tournament.setCanceled(true)
dismiss()
}
} footer: {
Text("todo: expliquer ce que ca fait")
}
}
Section {
Toggle(isOn: $tournament.isPrivate) {
Text("Tournoi privée")
@ -49,9 +75,15 @@ struct TournamentStatusView: View {
}
}
.toolbarBackground(.visible, for: .navigationBar)
.onChange(of: tournament.endDate) {
_save()
}
.onChange(of: [tournament.isDeleted, tournament.isPrivate]) {
_save()
}
.onChange(of: tournament.isCanceled) {
_save()
}
}
private func _save() {

@ -171,7 +171,7 @@ struct InscriptionManagerView: View {
} label: {
Label("Importer beach-padel", systemImage: "square.and.arrow.down")
}
Link(destination: SourceFileManager.beachPadel) {
Link(destination: URLs.beachPadel.url) {
Label("beach-padel.app.fft.fr", systemImage: "safari")
}
} else {
@ -362,7 +362,7 @@ struct InscriptionManagerView: View {
TipView(fileTip) { action in
if action.id == "website" {
UIApplication.shared.open(SourceFileManager.beachPadel)
UIApplication.shared.open(URLs.beachPadel.url)
} else if action.id == "add-team-file" {
presentImportView = true
}
@ -451,7 +451,7 @@ struct InscriptionManagerView: View {
isLearningMore = true
}
if action.id == "padel-beach" {
UIApplication.shared.open(SourceFileManager.beachPadel)
UIApplication.shared.open(URLs.beachPadel.url)
}
}
.tipStyle(tint: nil)

@ -26,38 +26,50 @@ struct TournamentView: View {
VStack(spacing: 0.0) {
List {
SubscriptionInfoView()
Section {
NavigationLink(value: Screen.inscription) {
LabeledContent {
Text(tournament.unsortedTeams().count.formatted() + "/" + tournament.teamCount.formatted())
} label: {
Text("Gestion des inscriptions")
if let closedRegistrationDate = tournament.closedRegistrationDate {
Text("clôturé le " + closedRegistrationDate.formatted(date: .abbreviated, time: .shortened))
if tournament.state() != .canceled {
Section {
NavigationLink(value: Screen.inscription) {
LabeledContent {
Text(tournament.unsortedTeams().count.formatted() + "/" + tournament.teamCount.formatted())
} label: {
Text("Gestion des inscriptions")
if let closedRegistrationDate = tournament.closedRegistrationDate {
Text("clôturé le " + closedRegistrationDate.formatted(date: .abbreviated, time: .shortened))
}
}
}
}
if let endOfInscriptionDate = tournament.mandatoryRegistrationCloseDate(), tournament.inscriptionClosed() == false && tournament.hasStarted() == false {
LabeledContent {
Text(endOfInscriptionDate.formatted(date: .abbreviated, time: .shortened))
} label: {
Text("Date limite")
if let endOfInscriptionDate = tournament.mandatoryRegistrationCloseDate(), tournament.inscriptionClosed() == false && tournament.hasStarted() == false {
LabeledContent {
Text(endOfInscriptionDate.formatted(date: .abbreviated, time: .shortened))
} label: {
Text("Date limite")
}
}
}
} footer: {
if tournament.inscriptionClosed() == false && tournament.state() == .build && tournament.unsortedTeams().isEmpty == false && tournament.hasStarted() == false {
Button {
tournament.lockRegistration()
_save()
} label: {
Text("clôturer les inscriptions")
.underline()
} footer: {
if tournament.inscriptionClosed() == false && tournament.state() == .build && tournament.unsortedTeams().isEmpty == false && tournament.hasStarted() == false {
Button {
tournament.lockRegistration()
_save()
} label: {
Text("clôturer les inscriptions")
.underline()
}
.buttonStyle(.borderless)
}
.buttonStyle(.borderless)
}
}
switch tournament.state() {
case .canceled:
Section {
RowButtonView("Reprendre le tournoi", role: .destructive) {
tournament.setCanceled(false)
_save()
}
} footer: {
Text("todo expliquer cet état")
}
case .initial:
TournamentInitView()

Loading…
Cancel
Save