You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
566 lines
23 KiB
566 lines
23 KiB
//
|
|
// EventListView.swift
|
|
// PadelClub
|
|
//
|
|
// Created by Razmig Sarkissian on 29/02/2024.
|
|
//
|
|
|
|
import SwiftUI
|
|
import LeStorage
|
|
import PadelClubData
|
|
|
|
struct EventListView: View {
|
|
@EnvironmentObject var dataStore: DataStore
|
|
@Environment(NavigationViewModel.self) var navigation: NavigationViewModel
|
|
@Environment(FederalDataViewModel.self) var federalDataViewModel: FederalDataViewModel
|
|
@Environment(\.viewStyle) var viewStyle
|
|
|
|
let tournaments: [FederalTournamentHolder]
|
|
let sortAscending: Bool
|
|
|
|
@State var showUserSearch: Bool = false
|
|
@State private var sectionImporting: Int? = nil
|
|
|
|
var lastDataSource: Date? {
|
|
guard let _lastDataSource = dataStore.appSettings.lastDataSource else { return nil }
|
|
return URL.importDateFormatter.date(from: _lastDataSource)
|
|
}
|
|
|
|
var body: some View {
|
|
let groupedTournamentsByDate = Dictionary(grouping: federalDataViewModel.filteredFederalTournaments(from: tournaments)) { $0.startDate.startOfMonth }
|
|
switch viewStyle {
|
|
case .list:
|
|
let nextMonths = groupedTournamentsByDate.keys.sorted(by: sortAscending ? { $0 < $1 } : { $0 > $1 })
|
|
|
|
ForEach(nextMonths.indices, id: \.self) { sectionIndex in
|
|
let section = nextMonths[sectionIndex]
|
|
if let _tournaments = groupedTournamentsByDate[section]?.sorted(by: sortAscending ? { $0.startDate < $1.startDate } : { $0.startDate > $1.startDate }
|
|
) {
|
|
Section {
|
|
_listView(_tournaments)
|
|
} header: {
|
|
HStack {
|
|
Text(section.monthYearFormatted)
|
|
Spacer()
|
|
let count = federalDataViewModel.countForTournamentBuilds(from: _tournaments)
|
|
Text("\(count.formatted()) tournoi" + count.pluralSuffix)
|
|
}
|
|
} footer: {
|
|
if _tournaments.isEmpty == false {
|
|
if let pcTournaments = _tournaments as? [Tournament] {
|
|
_menuOptions(pcTournaments)
|
|
} else if let federalTournaments = _tournaments as? [FederalTournament], navigation.agendaDestination == .tenup {
|
|
HStack {
|
|
FooterButtonView("Tout récupérer", role: .destructive) {
|
|
Task {
|
|
sectionImporting = sectionIndex
|
|
for federalTournament in federalTournaments {
|
|
await _importFederalTournamentBatch(federalTournament: federalTournament)
|
|
}
|
|
sectionImporting = nil
|
|
}
|
|
}
|
|
if sectionImporting == sectionIndex {
|
|
Spacer()
|
|
ProgressView()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.headerProminence(.increased)
|
|
}
|
|
}
|
|
case .calendar:
|
|
let nextMonths = _nextMonths()
|
|
ForEach(nextMonths.indices, id: \.self) { sectionIndex in
|
|
let section = nextMonths[sectionIndex]
|
|
let _tournaments = groupedTournamentsByDate[section] ?? []
|
|
Section {
|
|
CalendarView(date: section, tournaments: _tournaments).id(federalDataViewModel.id)
|
|
} header: {
|
|
HStack {
|
|
Text(section.monthYearFormatted)
|
|
Spacer()
|
|
let count = federalDataViewModel.countForTournamentBuilds(from: _tournaments)
|
|
Text("\(count.formatted()) tournoi" + count.pluralSuffix)
|
|
}
|
|
} footer: {
|
|
if _tournaments.isEmpty == false, let pcTournaments = _tournaments as? [Tournament] {
|
|
_menuOptions(pcTournaments)
|
|
}
|
|
}
|
|
.id(sectionIndex)
|
|
.headerProminence(.increased)
|
|
.task {
|
|
if navigation.agendaDestination == .tenup
|
|
&& dataStore.user.hasTenupClubs() == true
|
|
&& _tournaments.isEmpty {
|
|
await _gatherFederalTournaments(startDate: section)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func _gatherFederalTournaments(startDate: Date) async {
|
|
do {
|
|
let clubs : [Club] = dataStore.user.clubsObjects()
|
|
try await federalDataViewModel.gatherTournaments(clubs: clubs.filter { $0.code != nil }, startDate: startDate)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
|
|
private func _menuOptions(_ pcTournaments: [Tournament]) -> some View {
|
|
Menu {
|
|
_options(pcTournaments)
|
|
} label: {
|
|
Text("Options rapides pour ce mois")
|
|
.underline()
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func _options(_ pcTournaments: [Tournament]) -> some View {
|
|
if let lastDataSource, pcTournaments.anySatisfy({ $0.rankSourceShouldBeRefreshed() != nil && $0.hasEnded() == false }) {
|
|
Menu {
|
|
Button {
|
|
Task {
|
|
do {
|
|
|
|
let dataURLs = SourceFileManager.shared.allFiles.filter { $0.dateFromPath == lastDataSource }
|
|
guard !dataURLs.isEmpty else { return } // Early return if no files found
|
|
|
|
let sources = dataURLs.map { CSVParser(url: $0) }
|
|
let chunkedParsers = try await chunkAllSources(sources: sources, size: 10000)
|
|
|
|
try await pcTournaments.concurrentForEach { tournament in
|
|
if let mostRecentDate = tournament.rankSourceShouldBeRefreshed() {
|
|
try await tournament.updateRank(to: mostRecentDate, forceRefreshLockWeight: false, providedSources: chunkedParsers)
|
|
}
|
|
}
|
|
|
|
try chunkedParsers.forEach { chunk in
|
|
try FileManager.default.removeItem(at: chunk.url)
|
|
}
|
|
|
|
dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
} label: {
|
|
Text("M-à-j des classements")
|
|
}
|
|
} label: {
|
|
Text("Classement \(lastDataSource.monthYearFormatted)")
|
|
}
|
|
Divider()
|
|
}
|
|
|
|
Menu {
|
|
Picker("Choix du montant", selection: Binding<Double>(get: {
|
|
// If all tournaments share the same price, show it; otherwise default to 0
|
|
let prices = Set(pcTournaments.compactMap { $0.entryFee })
|
|
return prices.count == 1 ? prices.first ?? 0.0 : 0.0
|
|
}, set: { (newValue: Double) in
|
|
// Apply the chosen price to every tournament
|
|
pcTournaments.forEach { tournament in
|
|
tournament.entryFee = newValue
|
|
}
|
|
dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments)
|
|
})) {
|
|
ForEach([Double](stride(from: 0.0, through: 50.0, by: 5.0)), id: \.self) { (price: Double) in
|
|
Text(price.formatted(.currency(code: Locale.current.currency?.identifier ?? "EUR"))).tag(price as Double)
|
|
}
|
|
}
|
|
} label: {
|
|
Text("Montant de l'inscription")
|
|
}
|
|
|
|
Divider()
|
|
|
|
Menu {
|
|
Button {
|
|
pcTournaments.forEach { tournament in
|
|
tournament.isPrivate = false
|
|
}
|
|
dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments)
|
|
} label: {
|
|
Text("Afficher sur Padel Club")
|
|
}
|
|
|
|
Button {
|
|
pcTournaments.forEach { tournament in
|
|
tournament.isPrivate = true
|
|
}
|
|
dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments)
|
|
} label: {
|
|
Text("Masquer sur Padel Club")
|
|
}
|
|
} label: {
|
|
Text("Visibilité sur Padel Club")
|
|
}
|
|
Divider()
|
|
Menu {
|
|
// Button {
|
|
// Task {
|
|
// await pcTournaments.concurrentForEach { tournament in
|
|
// await tournament.refreshTeamList(forced: true)
|
|
// }
|
|
// }
|
|
// } label: {
|
|
// Text("M-à-j des inscriptions")
|
|
// }
|
|
|
|
Button {
|
|
pcTournaments.forEach { tournament in
|
|
if tournament.onlineRegistrationCanBeEnabled() {
|
|
tournament.enableOnlineRegistration = true
|
|
}
|
|
}
|
|
dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments)
|
|
} label: {
|
|
Text("Activer")
|
|
}
|
|
|
|
Button {
|
|
pcTournaments.forEach { tournament in
|
|
tournament.enableOnlineRegistration = false
|
|
}
|
|
dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments)
|
|
} label: {
|
|
Text("Désactiver")
|
|
}
|
|
} label: {
|
|
Text("Inscription en ligne")
|
|
}
|
|
|
|
Divider()
|
|
|
|
if dataStore.user.canEnableOnlinePayment() {
|
|
Menu {
|
|
let templateTournament = Tournament.getTemplateTournament()
|
|
|
|
if let templateTournament {
|
|
NavigationLink {
|
|
RegistrationSetupView(tournament: templateTournament)
|
|
} label: {
|
|
Text("Voir le tournoi référence")
|
|
}
|
|
} else {
|
|
Text("Aucun tournoi référence")
|
|
}
|
|
|
|
if let templateTournament {
|
|
Button {
|
|
pcTournaments.forEach { tournament in
|
|
if tournament.onlineRegistrationCanBeEnabled() {
|
|
tournament.setupRegistrationSettings(templateTournament: templateTournament)
|
|
}
|
|
}
|
|
dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments)
|
|
} label: {
|
|
Text("Utiliser ces réglages par défaut")
|
|
}
|
|
}
|
|
} label: {
|
|
Text("Inscription et paiement en ligne")
|
|
}
|
|
Divider()
|
|
}
|
|
Menu {
|
|
Button {
|
|
pcTournaments.forEach { tournament in
|
|
tournament.information = nil
|
|
}
|
|
dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments)
|
|
} label: {
|
|
Text("Effacer les descriptions")
|
|
}
|
|
|
|
Button {
|
|
let info = Set(pcTournaments.compactMap { tournament in
|
|
tournament.information?.trimmedMultiline
|
|
}).joined(separator: "\n")
|
|
|
|
pcTournaments.forEach { tournament in
|
|
tournament.information = info
|
|
}
|
|
dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments)
|
|
} label: {
|
|
Text("Mettre la même description")
|
|
}
|
|
|
|
PasteButton(payloadType: String.self) { strings in
|
|
if let pasteboard = strings.first {
|
|
pcTournaments.forEach { tournament in
|
|
tournament.information = pasteboard
|
|
}
|
|
dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments)
|
|
}
|
|
}
|
|
} label: {
|
|
Text("Description des tournois")
|
|
}
|
|
Divider()
|
|
Menu {
|
|
Button {
|
|
pcTournaments.forEach { tournament in
|
|
tournament.umpireCustomMail = nil
|
|
tournament.umpireCustomPhone = nil
|
|
tournament.umpireCustomContact = nil
|
|
tournament.hideUmpireMail = dataStore.user.hideUmpireMail
|
|
tournament.hideUmpirePhone = dataStore.user.hideUmpirePhone
|
|
}
|
|
dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments)
|
|
} label: {
|
|
Text("Retirer les informations personnalisées")
|
|
}
|
|
|
|
Button {
|
|
pcTournaments.forEach { tournament in
|
|
tournament.setupUmpireSettings()
|
|
}
|
|
dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments)
|
|
} label: {
|
|
Text("Utiliser les réglages par défaut")
|
|
}
|
|
|
|
Button {
|
|
Task {
|
|
await pcTournaments.concurrentForEach { tournament in
|
|
if let tenupId = tournament.eventObject()?.tenupId {
|
|
let umpireData = try? await NetworkFederalService.shared.getUmpireData(idTournament: tenupId)
|
|
if let email = umpireData?.email {
|
|
tournament.umpireCustomMail = email
|
|
}
|
|
if let name = umpireData?.name {
|
|
tournament.umpireCustomContact = name.lowercased().capitalized
|
|
}
|
|
if let phone = umpireData?.phone {
|
|
tournament.umpireCustomPhone = phone
|
|
}
|
|
}
|
|
}
|
|
|
|
await MainActor.run {
|
|
dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments)
|
|
}
|
|
}
|
|
} label: {
|
|
Text("Récuperer via Tenup")
|
|
}
|
|
|
|
} label: {
|
|
Text("Informations de contact Juge-Arbitre")
|
|
}
|
|
Divider()
|
|
Menu {
|
|
Button {
|
|
Task {
|
|
await pcTournaments.concurrentForEach { tournament in
|
|
if tournament.hasEnded() == false {
|
|
tournament.endDate = Date()
|
|
}
|
|
}
|
|
|
|
await MainActor.run {
|
|
dataStore.tournaments.addOrUpdate(contentOfs: pcTournaments)
|
|
}
|
|
}
|
|
} label: {
|
|
Text("Terminer les tournois encore ouverts")
|
|
}
|
|
} label: {
|
|
Text("Options avancées")
|
|
}
|
|
}
|
|
|
|
private func _nextMonths() -> [Date] {
|
|
let currentDate = Date().startOfMonth
|
|
let uniqueDates = tournaments.map { $0.startDate.startOfMonth }.uniqued().sorted()
|
|
let firstMonthOfDate = uniqueDates.first
|
|
let lastMonthOfDate = uniqueDates.last
|
|
let calendar = Calendar.current
|
|
|
|
if let firstMonthOfDate, let lastMonthOfDate {
|
|
if navigation.agendaDestination == .history {
|
|
return calendar.generateMonthRange(startDate: firstMonthOfDate, endDate: lastMonthOfDate).reversed()
|
|
} else if navigation.agendaDestination == .around || navigation.agendaDestination == .tenup {
|
|
return calendar.generateMonthRange(startDate: firstMonthOfDate, endDate: lastMonthOfDate)
|
|
} else {
|
|
let min = min(currentDate, firstMonthOfDate)
|
|
let max = max(currentDate, lastMonthOfDate)
|
|
return calendar.generateMonthRange(startDate: min, endDate: calendar.addMonths(3, to: max))
|
|
}
|
|
} else {
|
|
return calendar.generateMonthRange(startDate: currentDate, endDate: calendar.addMonths(3, to: currentDate))
|
|
}
|
|
}
|
|
|
|
private func _listView(_ tournaments: [FederalTournamentHolder]) -> some View {
|
|
ForEach(tournaments, id: \.holderId) { tournamentHolder in
|
|
if let tournament = tournamentHolder as? Tournament {
|
|
_tournamentView(tournament)
|
|
} else if let federalTournament = tournamentHolder as? FederalTournament {
|
|
_federalTournamentView(federalTournament)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func _tournamentView(_ tournament: Tournament) -> some View {
|
|
NavigationLink(value: tournament) {
|
|
TournamentCellView(tournament: tournament)
|
|
// .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name.CollectionDidLoad), perform: { notification in
|
|
//
|
|
// if let store = notification.object as? SyncedCollection<TeamRegistration> {
|
|
// if store.storeId == tournament.id {
|
|
// if tournament.store?.fileCollectionsAllLoaded() == true {
|
|
// tournament.lastTeamRefresh = nil
|
|
// }
|
|
// }
|
|
// }
|
|
// if let store = notification.object as? SyncedCollection<PlayerRegistration> {
|
|
// if store.storeId == tournament.id {
|
|
// if tournament.store?.fileCollectionsAllLoaded() == true {
|
|
// tournament.lastTeamRefresh = nil
|
|
// }
|
|
// }
|
|
// }
|
|
// })
|
|
// .id(tournament.lastTeamRefresh)
|
|
// .task(priority: .background) {
|
|
// await tournament.refreshTeamList(forced: false)
|
|
// }
|
|
}
|
|
.listRowView(isActive: tournament.enableOnlineRegistration, color: .green, hideColorVariation: true)
|
|
.onChange(of: tournament.isTemplate) {
|
|
dataStore.tournaments.addOrUpdate(instance: tournament)
|
|
}
|
|
.contextMenu {
|
|
@Bindable var bindableTournament: Tournament = tournament
|
|
Toggle(isOn: $bindableTournament.isTemplate) {
|
|
Text("Source des réglages d'inscriptions")
|
|
}
|
|
|
|
Divider()
|
|
|
|
if tournament.hasEnded() == false {
|
|
Button {
|
|
navigation.openTournamentInOrganizer(tournament)
|
|
} label: {
|
|
Label("Afficher dans le gestionnaire", systemImage: "line.diagonal.arrow")
|
|
}
|
|
|
|
Divider()
|
|
|
|
_options([tournament])
|
|
|
|
|
|
}
|
|
}
|
|
#if DEBUG
|
|
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
|
if tournament.sharing == nil {
|
|
Button(role: .destructive) {
|
|
dataStore.deleteTournament(tournament)
|
|
} label: {
|
|
LabelDelete()
|
|
}
|
|
}
|
|
Button() {
|
|
dataStore.deleteTournament(tournament, noSync: true)
|
|
} label: {
|
|
Label("Soft delete", systemImage: "trash")
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
private func _federalTournamentView(_ federalTournament: FederalTournament) -> some View {
|
|
TournamentCellView(tournament: federalTournament)
|
|
}
|
|
|
|
private func _event(of tournament: FederalTournament) -> Event? {
|
|
return dataStore.events.first(where: { $0.tenupId == tournament.id.string })
|
|
}
|
|
|
|
private func _importFederalTournamentBatch(federalTournament: FederalTournament) async {
|
|
let templateTournament = Tournament.getTemplateTournament()
|
|
let newTournaments = await withTaskGroup(of: Tournament?.self) { group in
|
|
var tournaments: [Tournament] = []
|
|
|
|
for tournament in federalTournament.tournaments {
|
|
group.addTask {
|
|
await self._create(
|
|
federalTournament: federalTournament,
|
|
existingTournament: self._event(of: federalTournament)?.existingBuild(tournament),
|
|
build: tournament,
|
|
templateTournament: templateTournament
|
|
)
|
|
}
|
|
}
|
|
|
|
for await tournament in group {
|
|
if let tournament = tournament {
|
|
tournaments.append(tournament)
|
|
}
|
|
}
|
|
|
|
return tournaments
|
|
}
|
|
|
|
dataStore.tournaments.addOrUpdate(contentOfs: newTournaments)
|
|
}
|
|
|
|
private func _create(federalTournament: FederalTournament, existingTournament: Tournament?, build: any TournamentBuildHolder, templateTournament: Tournament?) async -> Tournament? {
|
|
guard existingTournament == nil else { return nil }
|
|
|
|
let event = federalTournament.getEvent()
|
|
let newTournament = Tournament.newEmptyInstance()
|
|
newTournament.event = event.id
|
|
//todo
|
|
//newTournament.jsonData = jsonData
|
|
newTournament.tournamentLevel = build.level
|
|
newTournament.tournamentCategory = build.category
|
|
newTournament.federalTournamentAge = build.age
|
|
newTournament.dayDuration = federalTournament.dayDuration
|
|
newTournament.startDate = federalTournament.startDate.atBeginningOfDay(hourInt: 9)
|
|
newTournament.initSettings(templateTournament: templateTournament)
|
|
|
|
if federalTournament.umpireLabel().isEmpty == false {
|
|
newTournament.umpireCustomContact = federalTournament.umpireLabel()
|
|
} else {
|
|
newTournament.umpireCustomContact = DataStore.shared.user.fullName()
|
|
}
|
|
if federalTournament.mailLabel().isEmpty == false {
|
|
newTournament.umpireCustomMail = federalTournament.mailLabel()
|
|
} else {
|
|
newTournament.umpireCustomMail = DataStore.shared.user.email
|
|
}
|
|
newTournament.umpireCustomPhone = DataStore.shared.user.phone
|
|
do {
|
|
let umpireData = try await NetworkFederalService.shared.getUmpireData(idTournament: federalTournament.id)
|
|
if let email = umpireData.email {
|
|
newTournament.umpireCustomMail = email
|
|
}
|
|
if let name = umpireData.name {
|
|
newTournament.umpireCustomContact = name.lowercased().capitalized
|
|
}
|
|
if let phone = umpireData.phone {
|
|
newTournament.umpireCustomPhone = phone
|
|
}
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
|
|
return newTournament
|
|
}
|
|
}
|
|
|
|
//#Preview {
|
|
// EventListView(tournaments: [], viewStyle: .calendar, sortAscending: true)
|
|
//}
|
|
|
|
|