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.
 
 
PadelClub/PadelClub/Views/Navigation/Agenda/EventListView.swift

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)
//}