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/Match/MatchDetailView.swift

673 lines
25 KiB

//
// MatchDetailView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 23/03/2024.
//
import SwiftUI
import LeStorage
import PadelClubData
struct MatchDetailView: View {
@EnvironmentObject var dataStore: DataStore
@EnvironmentObject var networkMonitor: NetworkMonitor
@Environment(\.dismiss) var dismiss
@Environment(\.matchViewStyle) private var matchViewStyle
@State private var showLiveScore: Bool = false
@State private var editScore: Bool = false
@State private var scoreType: ScoreType?
@State private var shareStat: Bool = false
@State private var startDateSetup: MatchDateSetup = .now
@State private var fieldSetup: MatchFieldSetup = .random
@State private var broadcasted: Bool = false
@State private var startDate: Date = Date()
@State private var endDate: Date = Date()
@State private var isEditing: Bool = false
@State private var showDetails: Bool = false
@State private var contactType: ContactType? = nil
@State private var sentError: ContactManagerError? = nil
// @State private var showSubscriptionView: Bool = false
@State var showSubscriptionView: Bool = false
@State var showUserCreationView: Bool = false
@State private var presentFollowUpMatch: Bool = false
@State private var dismissWhenPresentFollowUpMatchIsDismissed: Bool = false
@State private var presentRanking: Bool = false
@State private var confirmScoreEdition: Bool = false
var tournamentStore: TournamentStore? {
return match.tournamentStore
}
var messageSentFailed: Binding<Bool> {
Binding {
sentError != nil
} set: { newValue in
if newValue == false {
sentError = nil
}
}
}
var match: Match
init(match: Match, updatedField: Int? = nil) {
self.match = match
if match.hasStarted() == false && (match.startDate == nil || match.courtIndex == nil) {
_isEditing = State(wrappedValue: true)
}
if let startDate = match.startDate {
_startDateSetup = State(wrappedValue: .customDate)
_startDate = State(wrappedValue: startDate)
} else if match.isReady() == false {
_startDateSetup = State(wrappedValue: .customDate)
}
if let endDate = match.endDate {
_endDate = State(wrappedValue: endDate)
}
if let courtIndex = updatedField ?? match.courtIndex {
_fieldSetup = State(wrappedValue: .field(courtIndex))
}
}
var body: some View {
List {
if match.hasWalkoutTeam() == false {
if match.hasStarted() {
quickLookHeader
} else {
startingOptionView
}
}
Section {
MatchSummaryView(match: match)
.matchViewStyle(.plainStyle)
} footer: {
if match.isEmpty() == false {
HStack {
FooterButtonView("Détails des joueurs") {
showDetails = true
}
Spacer()
if let tournament = match.currentTournament() {
MenuWarningView(tournament: tournament, teams: match.teams(), message: match.matchWarningMessage(), subject: match.matchWarningSubject(), contactType: $contactType)
.buttonStyle(.borderless)
}
}
}
}
Section {
RowButtonView("Saisir les résultats", systemImage: "list.clipboard") {
self._editScores()
}
.disabled(match.teams().count < 2)
}
if self.match.currentTournament()?.isFree() == false {
let players = self.match.teams().flatMap { $0.players() }
let unpaid = players.filter({ $0.hasPaid() == false })
if unpaid.isEmpty == false {
Section {
DisclosureGroup {
ForEach(unpaid) { player in
LabeledContent {
if let store = self.tournamentStore {
PlayerPayView(player: player)
.environmentObject(store)
}
} label: {
Text(player.playerLabel())
}
}
} label: {
LabeledContent {
Text(unpaid.count.formatted() + " / " + players.count.formatted())
} label: {
Text("Encaissement manquant")
}
}
}
}
}
menuView
}
.sheet(isPresented: $showDetails) {
if let store = self.tournamentStore {
MatchTeamDetailView(match: match).tint(.master)
.environmentObject(store)
} else {
Text("no store")
}
}
.sheet(isPresented: self.$showSubscriptionView, content: {
NavigationStack {
SubscriptionView(isPresented: self.$showSubscriptionView, showLackOfPlanMessage: true)
.environment(\.colorScheme, .light)
}
})
.sheet(isPresented: self.$showUserCreationView, content: {
NavigationStack {
LoginView(reason: LoginReason.loginRequiredForFeature) { _ in
self.showUserCreationView = false
self._editScores()
}
}
})
.sheet(isPresented: $presentFollowUpMatch, onDismiss: {
if dismissWhenPresentFollowUpMatchIsDismissed {
dismiss()
}
}) {
NavigationStack {
FollowUpMatchView(match: match, dismissWhenPresentFollowUpMatchIsDismissed: $dismissWhenPresentFollowUpMatchIsDismissed)
}
.tint(.master)
}
.sheet(isPresented: $presentRanking, content: {
if let currentTournament = match.currentTournament() {
NavigationStack {
TournamentRankView()
.environment(currentTournament)
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("Retour", role: .cancel) {
presentRanking = false
dismiss()
}
}
}
}
.tint(.master)
}
})
.sheet(item: $scoreType, onDismiss: {
if match.hasEnded(), confirmScoreEdition {
confirmScoreEdition = false
if match.index == 0, match.isGroupStage() == false, match.roundObject?.parent == nil {
presentRanking = true
} else if match.isGroupStage(), match.currentTournament()?.hasEnded() == true {
presentRanking = true
} else {
presentFollowUpMatch = true
}
}
}) { scoreType in
EditScoreView(match: match, confirmScoreEdition: $confirmScoreEdition)
.tint(.master)
// switch scoreType {
// case .edition:
// let matchDescriptor = MatchDescriptor(match: match)
// EditScoreView(matchDescriptor: matchDescriptor)
// case .live:
// if let score = match.score {
// if score.sets.isEmpty {
// SplashView(score: score)
// } else {
// NewLiveScoringView(score: score)
// }
// }
// case .prepare:
// if match.freeMatchTeams.isEmpty == false {
// EditFreeMatchView(match: match)
// } else {
// PrepareMatchView(match: match)
// }
// case .stat:
// if let score = match.score {
// MatchStatView()
// .environmentObject(score)
// }
// case .health:
// HealthKitView(match: match)
// .presentationDetents([.medium])
// case .feeling:
// if let feedbackData = match.feedbackData {
// FeedbackView(feedbackData: feedbackData)
// }
// }
}
.alert("Un problème est survenu", isPresented: messageSentFailed) {
Button("OK") {
}
} message: {
Text(_networkErrorMessage)
}
.sheet(item: $contactType) { contactType in
Group {
switch contactType {
case .message(_, let recipients, let body, _):
MessageComposeView(recipients: recipients, body: body) { result in
switch result {
case .cancelled:
break
case .failed:
self.sentError = .messageFailed
case .sent:
let uncalledTeams = match.teams().filter { $0.getPhoneNumbers().isEmpty }
if networkMonitor.connected == false {
self.contactType = nil
if uncalledTeams.isEmpty == false {
self.sentError = .uncalledTeams(uncalledTeams)
} else {
self.sentError = .messageNotSent
}
} else {
if uncalledTeams.isEmpty == false {
self.sentError = .uncalledTeams(uncalledTeams)
}
}
@unknown default:
break
}
}
case .mail(_, let recipients, let bccRecipients, let body, let subject, _):
MailComposeView(recipients: recipients, bccRecipients: bccRecipients, body: body, subject: subject) { result in
switch result {
case .cancelled, .saved:
self.contactType = nil
case .failed:
self.contactType = nil
self.sentError = .mailFailed
case .sent:
let uncalledTeams = match.teams().filter { $0.getMail().isEmpty }
if networkMonitor.connected == false {
self.contactType = nil
if uncalledTeams.isEmpty == false {
self.sentError = .uncalledTeams(uncalledTeams)
} else {
self.sentError = .mailNotSent
}
} else {
if uncalledTeams.isEmpty == false {
self.sentError = .uncalledTeams(uncalledTeams)
}
}
@unknown default:
break
}
}
}
}
.tint(.master)
}
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Menu {
NavigationLink {
ShareModelView(instance: self.match)
} label: {
Label("Partager", systemImage: "square.and.arrow.up")
}
Toggle(isOn: .init(get: {
return match.confirmed
}, set: { value in
match.confirmed = value
save()
})) {
Text(match.confirmed ? "Confirmé" : "Non confirmé")
}
Divider()
if match.courtIndex != nil {
Button(role: .destructive) {
match.removeCourt()
save()
} label: {
Text("Supprimer la piste")
}
}
Button(role: .destructive) {
match.updateStartDate(nil, keepPlannedStartDate: true)
} label: {
Text("Supprimer l'horaire")
}
Button(role: .destructive) {
match.resetScores()
match.resetMatch()
match.confirmed = false
match.updateFollowingMatchTeamScore()
save()
} label: {
Text("Supprimer les scores")
}
Divider()
Button(role: .destructive) {
match.updateStartDate(nil, keepPlannedStartDate: false)
match.resetTeamScores(outsideOf: [])
match.resetMatch()
match.confirmed = false
save()
} label: {
Text("Remise-à-zéro")
}
Menu {
Button("Effacer le nom") {
match.name = nil
tournamentStore?.matches.addOrUpdate(instance: match)
}
if let tournament = match.currentTournament() {
Menu {
ForEach(tournament.generateSeedGroups(base: 16, teamCount: 8), id: \.self) { seedGroup in
Button {
match.name = seedGroup.localizedInterval()
tournamentStore?.matches.addOrUpdate(instance: match)
} label: {
Text(seedGroup.localizedInterval())
}
}
} label: {
Text("Choisir un nom")
}
}
} label: {
Text("[Site] Nom du match")
if let name = match.name {
Text("(\(name))")
}
}
if match.teamScores.isEmpty == false {
Divider()
Menu {
ForEach(match.teamScores) { teamScore in
Button(role: .destructive) {
do {
try tournamentStore?.teamScores.delete(instance: teamScore)
} catch {
Logger.error(error)
}
match.confirmed = false
_saveMatch()
} label: {
Text(teamScore.team?.teamLabel() ?? "Aucun nom")
}
}
} label: {
Text("Supprimer une équipe")
}
}
} label: {
LabelOptions()
}
}
}
.navigationTitle(match.matchTitle())
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
}
var quickLookHeader: some View {
Section {
HStack {
Menu {
Button("Non défini") {
match.removeCourt()
save()
}
if let tournament = match.currentTournament() {
ForEach(0..<tournament.courtCount, id: \.self) { courtIndex in
Button(tournament.courtName(atIndex: courtIndex)) {
match.setCourt(courtIndex)
save()
}
}
}
} label: {
VStack(alignment: .leading) {
Text("piste").font(.footnote).foregroundStyle(.secondary)
if let courtName = match.courtName() {
Text(courtName)
.foregroundStyle(Color.master)
.underline()
} else {
Text("Choisir")
.foregroundStyle(Color.master)
.underline()
}
}
}
Spacer()
MatchDateView(match: match, showPrefix: true)
}
.font(.title)
.buttonStyle(.plain)
} footer: {
// if match.hasWalkoutTeam() == false {
// if let weatherData = match.weatherData {
// HStack {
// WeatherView(weatherData: weatherData)
// }
// }
// }
}
}
enum ScoreType: Int, Identifiable, Hashable {
var id: Int {
self.rawValue
}
case edition = 0
case live = 1
case prepare = 2
case stat = 3
case feeling = 4
case health = 5
}
@ViewBuilder
var menuView: some View {
if match.hasStarted() {
Section {
editionView
}
}
NavigationLink {
EditSharingView(match: match)
} label: {
Text("Partage sur les réseaux sociaux")
}
if match.currentTournament()?.hasEnded() == false {
Section {
RowButtonView("Match à suivre") {
presentFollowUpMatch = true
}
}
}
}
var editionView: some View {
DisclosureGroup(isExpanded: $isEditing) {
startingOptionView
} label: {
Text("Modifier l'horaire et la piste")
}
}
@ViewBuilder
var startingOptionView: some View {
if match.hasEnded() == false {
let rotationDuration = match.getDuration()
Picker(selection: $startDateSetup) {
if match.isReady() {
Text("Tout de suite").tag(MatchDateSetup.now)
Text("Dans 5 minutes").tag(MatchDateSetup.inMinutes(5))
Text("Dans 15 minutes").tag(MatchDateSetup.inMinutes(15))
}
Text("Précédente rotation").tag(MatchDateSetup.previousRotation)
Text("Prochaine rotation").tag(MatchDateSetup.nextRotation)
Text("À").tag(MatchDateSetup.customDate)
} label: {
Text("Horaire")
}
.onChange(of: startDateSetup) {
let date = Date()
switch startDateSetup {
case .customDate:
break
case .now:
startDate = date
case .nextRotation:
let baseDate = match.startDate ?? date
startDate = baseDate.addingTimeInterval(Double(rotationDuration) * 60)
case .previousRotation:
let baseDate = match.startDate ?? date
startDate = baseDate.addingTimeInterval(Double(-rotationDuration) * 60)
case .inMinutes(let minutes):
startDate = date.addingTimeInterval(Double(minutes) * 60)
}
}
}
if match.startDate != nil || startDateSetup == .customDate {
DatePicker("Début", selection: $startDate)
.datePickerStyle(.compact)
}
if match.endDate != nil {
DatePicker("Fin", selection: $endDate)
.datePickerStyle(.compact)
}
Picker(selection: $fieldSetup) {
Text("Au hasard parmi les libres").tag(MatchFieldSetup.random)
Text("Au hasard").tag(MatchFieldSetup.fullRandom)
//Text("Premier disponible").tag(MatchFieldSetup.firstAvailable)
if let tournament = match.currentTournament() {
ForEach(0..<tournament.courtCount, id: \.self) { courtIndex in
Text(tournament.courtName(atIndex: courtIndex)) .tag(MatchFieldSetup.field(courtIndex))
}
} else {
ForEach(0..<20, id: \.self) { courtIndex in
Text(Court.courtIndexedTitle(atIndex: courtIndex)) .tag(MatchFieldSetup.field(courtIndex))
}
}
} label: {
Text("Piste")
}
.onChange(of: fieldSetup) {
if let courtIndex = fieldSetup.courtIndex {
match.setCourt(courtIndex)
} else {
match.removeCourt()
}
_saveMatch()
}
@Bindable var bindableMatch = match
Picker(selection: $bindableMatch.matchFormat) {
ForEach(MatchFormat.allCases) { format in
MatchFormatRowView(matchFormat: format, additionalEstimationDuration: match.currentTournament()?.additionalEstimationDuration).tag(format)
}
} label: {
Text("Format")
}
.onChange(of: match.matchFormat) {
_saveMatch()
}
RowButtonView("Valider") {
match.validateMatch(fromStartDate: startDateSetup == .now ? Date().withoutSeconds() : startDate, toEndDate: endDate, fieldSetup: fieldSetup)
save()
isEditing.toggle()
if match.hasStarted() == false {
dismiss()
}
}
}
fileprivate func _editScores() {
if match.isReady() == false && match.teams().count == 2 {
let teamsScores = match.getOrCreateTeamScores()
do {
try tournamentStore?.teamScores.addOrUpdate(contentOfs: teamsScores)
} catch {
Logger.error(error)
}
}
self._verifyUser {
self._payTournamentAndExecute {
self.scoreType = .edition
}
}
}
fileprivate func _verifyUser(_ handler: () -> ()) {
if StoreCenter.main.userId != nil {
handler()
} else {
self.showUserCreationView = true
}
}
fileprivate func _payTournamentAndExecute(_ handler: () -> ()) {
guard let tournament = match.currentTournament() else { fatalError("missing tournament") }
do {
try tournament.payIfNecessary()
handler()
} catch {
self.showSubscriptionView = true
}
}
private func save() {
if let startDate = match.startDate, let tournament = match.currentTournament() {
if startDate < tournament.startDate {
tournament.startDate = startDate
}
do {
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
Logger.error(error)
}
}
_saveMatch()
}
private func _saveMatch() {
do {
try tournamentStore?.matches.addOrUpdate(instance: match)
} catch {
Logger.error(error)
}
}
private var _networkErrorMessage: String {
ContactManagerError.getNetworkErrorMessage(sentError: sentError, networkMonitorConnected: networkMonitor.connected)
}
}
//#Preview {
// MatchDetailView(match: Match.mock(), matchViewStyle: .standardStyle)
//}