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/Calling/CallView.swift

408 lines
14 KiB

//
// CallView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 16/04/2024.
//
import SwiftUI
import LeStorage
struct CallView: View {
struct CallStatusView: View {
let count: Int
let total: Int
let startDate: Date?
var title: String = "convoquées au bon horaire"
var body: some View {
VStack(spacing: 0) {
HStack {
if let startDate {
Text(startDate.formattedAsHourMinute())
} else {
Text("Aucun horaire").font(.body)
}
Spacer()
Text("\(count.formatted())/\(total.formatted())")
}
.font(.largeTitle)
HStack {
if let startDate {
Text(startDate.formatted(.dateTime.weekday().day(.twoDigits).month().year()))
}
Spacer()
Text(title)
}
.font(.caption)
.foregroundColor(.secondary)
}
}
}
@EnvironmentObject var dataStore: DataStore
@EnvironmentObject var networkMonitor: NetworkMonitor
@Environment(Tournament.self) var tournament: Tournament
var teams: [TeamRegistration]
let callDate: Date
let matchFormat: MatchFormat
let roundLabel: String
let displayContext: SummoningDisplayContext
@State private var contactType: ContactType? = nil
@State private var sentError: ContactManagerError? = nil
@State var showSubscriptionView: Bool = false
@State var showUserCreationView: Bool = false
@State var summonParamByMessage: Bool = false
@State var summonParamReSummon: Bool = false
let simpleMode : Bool
init(teams: [TeamRegistration], callDate: Date, matchFormat: MatchFormat, roundLabel: String) {
self.teams = teams
self.callDate = callDate
self.matchFormat = matchFormat
self.roundLabel = roundLabel
self.simpleMode = false
self.displayContext = .footer
}
init(teams: [TeamRegistration]) {
self.teams = teams
self.callDate = Date()
self.matchFormat = MatchFormat.nineGames
self.roundLabel = ""
self.simpleMode = true
self.displayContext = .footer
}
init(team: TeamRegistration, displayContext: SummoningDisplayContext) {
self.teams = [team]
let expectedSummonDate = team.expectedSummonDate()
self.displayContext = displayContext
if let expectedSummonDate, let initialMatch = team.initialMatch() {
self.callDate = expectedSummonDate
self.matchFormat = initialMatch.matchFormat
self.roundLabel = initialMatch.roundTitle() ?? "tableau"
self.simpleMode = false
} else if let expectedSummonDate, let initialGroupStage = team.groupStageObject() {
self.callDate = expectedSummonDate
self.matchFormat = initialGroupStage.matchFormat
self.roundLabel = "poule"
self.simpleMode = false
} else {
self.callDate = Date()
self.matchFormat = MatchFormat.nineGames
self.roundLabel = ""
self.simpleMode = true
}
}
var tournamentStore: TournamentStore {
return self.tournament.tournamentStore
}
var messageSentFailed: Binding<Bool> {
Binding {
sentError != nil
} set: { newValue in
if newValue == false {
sentError = nil
}
}
}
private func _called(_ calledTeams: [TeamRegistration], _ success: Bool) {
if simpleMode {
return
}
if success {
calledTeams.forEach { team in
team.callDate = callDate
if reSummon {
team.confirmationDate = nil
}
}
do {
try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: calledTeams)
} catch {
Logger.error(error)
}
}
}
func finalMessage(reSummon: Bool) -> String {
if simpleMode {
let signature = dataStore.user.summonsMessageSignature ?? dataStore.user.defaultSignature()
return "\n\n\n\n" + signature
}
return ContactType.callingMessage(tournament: tournament, startDate: callDate, roundLabel: roundLabel, matchFormat: matchFormat, reSummon: reSummon)
}
var reSummon: Bool {
if simpleMode {
return false
}
return self.teams.allSatisfy({ $0.called() })
}
var mainWord: String {
if simpleMode {
return "Contacter"
} else {
return "Convoquer"
}
}
var body: some View {
Group {
switch displayContext {
case .footer:
_footerStyleView()
case .menu:
_menuStyleView()
}
}
.alert("Un problème est survenu", isPresented: messageSentFailed) {
Button("OK") {
}
if case .uncalledTeams(let uncalledTeams) = sentError {
NavigationLink("Voir les équipes non contactées") {
TeamsCallingView(teams: uncalledTeams)
.environment(tournament)
}
}
} 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 calledTeams = teams.filter { $0.getPhoneNumbers().isEmpty == false }
let uncalledTeams = teams.filter { $0.getPhoneNumbers().isEmpty }
if networkMonitor.connected == false {
if uncalledTeams.isEmpty == false {
self.sentError = .uncalledTeams(uncalledTeams)
} else {
self.sentError = .messageNotSent
}
} else {
if uncalledTeams.isEmpty == false {
self.sentError = .uncalledTeams(uncalledTeams)
}
self._called(calledTeams, true)
}
@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 calledTeams = teams.filter { $0.getMail().isEmpty == false }
let uncalledTeams = 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)
}
self._called(calledTeams, true)
}
@unknown default:
break
}
}
}
}
.tint(.master)
}
.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._payTournamentAndExecute {
self._summon(byMessage: self.summonParamByMessage,
reSummon: self.summonParamByMessage)
}
}
}
})
}
private func _footerStyleView() -> some View {
HStack {
let callWord : String = (reSummon ? "Reconvoquer" : mainWord)
if self.teams.count == 1 {
if simpleMode {
Text("\(callWord) cette paire par")
} else {
if let previousCallDate = teams.first?.callDate, Calendar.current.compare(previousCallDate, to: callDate, toGranularity: .minute) != .orderedSame {
Text("Reconvoquer \(self.callDate.localizedDate()) par")
} else {
Text("\(callWord) cette paire par")
}
}
} else {
Text("\(callWord) ces \(self.teams.count) paires par")
}
self._summonMenu(byMessage: true)
Text("ou")
self._summonMenu(byMessage: false)
}
.font(.subheadline)
.buttonStyle(.borderless)
}
private func _menuStyleView() -> some View {
Menu {
self._summonMenu(byMessage: true)
self._summonMenu(byMessage: false)
} label: {
let callWord : String = (reSummon ? "Reconvoquer" : mainWord)
if self.teams.count == 1 {
if simpleMode {
Text("\(callWord) cette paire")
} else {
if let previousCallDate = teams.first?.callDate, Calendar.current.compare(previousCallDate, to: callDate, toGranularity: .minute) != .orderedSame {
Text("Reconvoquer \(self.callDate.localizedDate())")
} else {
Text("\(callWord) cette paire")
}
}
} else {
Text("\(callWord) ces \(self.teams.count) paires")
}
}
}
@ViewBuilder
private func _summonMenu(byMessage: Bool) -> some View {
if self.reSummon {
Menu {
Button(mainWord) {
self._summon(byMessage: byMessage, reSummon: false)
}
Button("Re-convoquer") {
self._summon(byMessage: byMessage, reSummon: true)
}
} label: {
Text(byMessage ? "sms" : "mail")
.underline()
}
} else {
FooterButtonView(byMessage ? "sms" : "mail") {
self._summon(byMessage: byMessage, reSummon: false)
}
}
}
private func _summon(byMessage: Bool, reSummon: Bool) {
self.summonParamByMessage = byMessage
self.summonParamReSummon = reSummon
self._verifyUser {
self._payTournamentAndExecute {
if byMessage {
self._contactByMessage(reSummon: reSummon)
} else {
self._contactByMail(reSummon: reSummon)
}
}
}
}
fileprivate func _verifyUser(_ handler: () -> ()) {
if StoreCenter.main.userId != nil {
handler()
} else {
self.showUserCreationView = true
}
}
fileprivate func _payTournamentAndExecute(_ handler: () -> ()) {
do {
try self.tournament.payIfNecessary()
handler()
} catch {
self.showSubscriptionView = true
}
}
fileprivate func _contactByMessage(reSummon: Bool) {
self.contactType = .message(date: callDate,
recipients: teams.flatMap { $0.getPhoneNumbers() },
body: finalMessage(reSummon: reSummon),
tournamentBuild: nil)
}
fileprivate func _contactByMail(reSummon: Bool) {
self.contactType = .mail(date: callDate,
recipients: tournament.umpireMail(),
bccRecipients: teams.flatMap { $0.getMail() },
body: finalMessage(reSummon: reSummon),
subject: tournament.tournamentTitle(),
tournamentBuild: nil)
}
private var _networkErrorMessage: String {
ContactManagerError.getNetworkErrorMessage(sentError: sentError, networkMonitorConnected: networkMonitor.connected)
}
}
struct TeamCallView: View {
@Environment(Tournament.self) var tournament: Tournament
let team: TeamRegistration
var action: (() -> Void)?
var body: some View {
NavigationLink {
CallMenuOptionsView(team: team, action: action)
.environment(tournament)
} label: {
TeamRowView(team: team, displayCallDate: true)
}
.buttonStyle(.plain)
.listRowView(isActive: team.confirmed(), color: .green, hideColorVariation: true)
}
}