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/Team/EditingTeamView.swift

555 lines
22 KiB

//
// EditingTeamView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 17/04/2024.
//
import SwiftUI
import LeStorage
import PadelClubData
struct EditingTeamView: View {
@EnvironmentObject var dataStore: DataStore
@EnvironmentObject var networkMonitor: NetworkMonitor
@Environment(Tournament.self) var tournament: Tournament
@Environment(\.dismiss) private var dismiss
var team: TeamRegistration
@State private var editedTeam: TeamRegistration?
@State private var contactType: ContactType? = nil
@State private var sentError: ContactManagerError? = nil
@State private var showSubscriptionView: Bool = false
@State private var registrationDate : Date
@State private var walkOut : Bool
@State private var wildCardBracket : Bool
@State private var wildCardGroupStage : Bool
@State private var name: String
@FocusState private var focusedField: TeamRegistration.CodingKeys?
@State private var isProcessingRefund = false
@State private var refundMessage: String?
@State private var registrationDateModified: Date
@State private var uniqueRandomIndex: Int
var messageSentFailed: Binding<Bool> {
Binding {
sentError != nil
} set: { newValue in
if newValue == false {
sentError = nil
}
}
}
var hasChanged: Binding<Bool> {
Binding {
if canSaveWithoutWarning() {
return false
}
return
registrationDate != team.registrationDate
|| uniqueRandomIndex != team.uniqueRandomIndex
|| walkOut != team.walkOut
|| wildCardBracket != team.wildCardBracket
|| wildCardGroupStage != team.wildCardGroupStage
} set: { _ in
}
}
var tournamentStore: TournamentStore? {
return self.tournament.tournamentStore
}
init(team: TeamRegistration) {
self.team = team
_name = .init(wrappedValue: team.name ?? "")
let date = Date()
_registrationDate = State(wrappedValue: team.registrationDate ?? date)
_registrationDateModified = .init(wrappedValue: team.registrationDate ?? date)
_walkOut = State(wrappedValue: team.walkOut)
_wildCardBracket = State(wrappedValue: team.wildCardBracket)
_wildCardGroupStage = State(wrappedValue: team.wildCardGroupStage)
_uniqueRandomIndex = .init(wrappedValue: team.uniqueRandomIndex)
}
private func _resetTeam() {
team.resetPositions()
team.qualified = false
team.wildCardGroupStage = false
team.walkOut = false
team.wildCardBracket = false
team.uniqueRandomIndex = 0
}
var body: some View {
List {
Section {
RowButtonView("Modifier la composition de l'équipe", role: (team.hasRegisteredOnline() || team.hasPaidOnline()) ? .destructive : .none, confirmationMessage: "Vous êtes sur le point de modifier une équipe qui s'est inscrite en ligne.") {
editedTeam = team
}
TeamDetailView(team: team)
} header: {
if team.hasRegisteredOnline() {
Text("Inscription en ligne")
} else {
Text("Inscription par vous-même")
}
} footer: {
HStack {
CopyPasteButtonView(pasteValue: team.playersPasteData())
Spacer()
if team.isWildCard(), team.unsortedPlayers().isEmpty {
TeamPickerView(pickTypeContext: .wildcard) { teamregistration in
teamregistration.wildCardBracket = team.wildCardBracket
teamregistration.wildCardGroupStage = team.wildCardGroupStage
tournament.tournamentStore?.teamRegistrations.addOrUpdate(instance: teamregistration)
tournament.tournamentStore?.teamRegistrations.delete(instance: team)
dismiss()
}
} else {
NavigationLink {
GroupStageTeamReplacementView(team: team)
.environment(tournament)
} label: {
Text("Chercher à remplacer")
.underline()
}
}
}
}
.headerProminence(.increased)
if team.hasRegisteredOnline() || team.hasPaidOnline() {
Section {
LabeledContent {
Text(team.hasRegisteredOnline() ? "Oui" : "Non")
} label: {
Text("Inscrits en ligne")
}
if tournament.enableOnlinePayment || team.hasPaidOnline() {
LabeledContent {
Text(team.hasPaidOnline() ? "Oui" : "Non")
} label: {
Text("Payé en ligne")
}
}
if let refundMessage, refundMessage.isEmpty == false {
Text(refundMessage).foregroundStyle(.logoRed)
}
if team.hasPaidOnline() {
RowButtonView("Rembourser l'équipe", role: .destructive) {
await _processRefund()
}
} else if team.hasConfirmed() == false, tournament.enableTimeToConfirm {
RowButtonView("Confirmer l'inscription de l'équipe", role: .destructive) {
team.confirmRegistration()
}
.disabled(team.walkOut)
}
} header: {
Text("Statut de l'inscription en ligne")
} footer: {
if team.hasPaidOnline() {
Text("Le remboursement passe part le service de Stripe qui re-crédite le moyen de paiement utilisé du montant payé.")
}
}
}
Section {
if let callDate = team.callDate {
LabeledContent() {
Text(callDate.localizedDate())
} label: {
Text("Convocation")
}
Toggle(isOn: confirmationReceived) {
Text("Confirmation reçue")
Text("L'équipe vous a confirmé votre convocation")
}
} else {
Text("Cette équipe n'a pas été convoquée")
}
if team.unsortedPlayers().isEmpty == false {
Toggle(isOn: hasArrived) {
Text("Équipe sur place")
}
}
}
Section {
DatePicker(selection: $registrationDateModified) {
if registrationDate != registrationDateModified {
HStack {
Button("Valider", systemImage: "checkmark.circle") {
registrationDate = registrationDateModified
}
.tint(.green)
Divider()
Button("Annuler", systemImage: "xmark.circle", role: .cancel) {
registrationDateModified = registrationDate
}
.tint(.logoRed)
}
.labelStyle(.iconOnly)
.buttonStyle(.borderedProminent)
} else {
Text("Inscription")
Text(registrationDateModified.localizedWeekDay().capitalized)
}
}
#if DEBUG
.disabled(false)
#else
.disabled(team.hasPaidOnline() || team.hasRegisteredOnline())
#endif
Toggle(isOn: $wildCardBracket) {
Text("Wildcard Tableau")
}
Toggle(isOn: $wildCardGroupStage) {
Text("Wildcard Poule")
}.disabled(tournament.groupStageCount == 0)
Toggle(isOn: $walkOut) {
Text("Forfait")
}
}
Section {
LabeledContent {
StepperView(count: $uniqueRandomIndex, minimum: 0)
} label: {
Text("Ordre à poids de paire égal")
}
} footer: {
Text("Si plusieurs équipes ont le même poids et que leur position est tiré au sort, ce champ permet de les positionner correctement dans l'ordre croissant.")
}
Section {
HStack {
TextField("Nom de l'équipe", text: $name)
.autocorrectionDisabled()
.focused($focusedField, equals: ._name)
.keyboardType(.alphabet)
.frame(maxWidth: .infinity)
.submitLabel(.done)
.onSubmit(of: .text) {
let trimmed = name.prefixTrimmed(200)
if trimmed.isEmpty {
team.name = nil
} else {
team.name = trimmed
}
_save()
}
if name.isEmpty == false {
FooterButtonView("effacer", role: .destructive) {
name = ""
team.name = nil
_save()
}
}
}
} header: {
Text("Nom de l'équipe")
}
if tournament.tournamentLevel.coachingIsAuthorized {
CoachListView(team: team)
}
Section {
RowButtonView("Retirer des poules", role: .destructive) {
team.resetGroupeStagePosition()
_save()
}
.disabled(team.inGroupStage() == false)
}
Section {
RowButtonView("Retirer du tableau", role: .destructive) {
team.resetBracketPosition()
_save()
}
.disabled(team.inRound() == false)
}
Section {
RowButtonView("Effacer l'équipe", role: .destructive, systemImage: "trash") {
_resetTeam()
team.deleteTeamScores()
do {
try tournamentStore?.teamRegistrations.delete(instance: team)
} catch {
Logger.error(error)
}
dismiss()
}
.disabled(team.hasPaidOnline())
} footer: {
if team.hasPaidOnline() {
Text("Il n'est pas possible de supprimer cette équipe, leur inscription a été payée en ligne. Vous pouvez les mettre forfait et/ou les rembourser.").foregroundStyle(.logoRed)
} else if team.hasRegisteredOnline() {
Text("Attention, supprimer cette équipe notifiera par email que leur inscription a été annulée.").foregroundStyle(.logoRed)
}
}
}
.alert("Attention", isPresented: hasChanged, actions: {
Button("Confirmer") {
if walkOut == false && team.walkOut == true {
registrationDateModified = Date()
registrationDate = registrationDateModified
}
_resetTeam()
team.registrationDate = registrationDate
team.wildCardBracket = wildCardBracket
team.wildCardGroupStage = wildCardGroupStage
team.walkOut = walkOut
team.uniqueRandomIndex = uniqueRandomIndex
_save()
}
Button("Annuler", role: .cancel) {
registrationDate = team.registrationDate ?? Date()
walkOut = team.walkOut
wildCardBracket = team.wildCardBracket
wildCardGroupStage = team.wildCardGroupStage
uniqueRandomIndex = team.uniqueRandomIndex
}
}, message: {
Text("Ce changement peut entraîner l'entrée ou la sortie d'une équipe de votre sélection. Padel Club préviendra automatiquement une équipe inscrite en ligne de son nouveau statut.")
})
.navigationBarBackButtonHidden(focusedField != nil)
.toolbar(content: {
if focusedField != nil {
ToolbarItem(placement: .topBarLeading) {
Button("Annuler", role: .cancel) {
focusedField = nil
}
}
}
})
.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, _):
if Guard.main.paymentForNewTournament() != nil {
MessageComposeView(recipients: recipients, body: body) { result in
switch result {
case .cancelled:
break
case .failed:
self.sentError = .messageFailed
case .sent:
if networkMonitor.connected == false {
self.contactType = nil
if team.getPhoneNumbers().isEmpty == false {
self.sentError = .uncalledTeams([team])
} else {
self.sentError = .messageNotSent
}
}
@unknown default:
break
}
}
} else {
SubscriptionView(isPresented: self.$showSubscriptionView, showLackOfPlanMessage: true)
.environment(\.colorScheme, .light)
}
case .mail(_, let recipients, let bccRecipients, let body, let subject, _):
if Guard.main.paymentForNewTournament() != nil {
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:
if networkMonitor.connected == false {
self.contactType = nil
if team.getMail().isEmpty == false {
self.sentError = .uncalledTeams([team])
} else {
self.sentError = .mailNotSent
}
}
@unknown default:
break
}
}
} else {
SubscriptionView(isPresented: self.$showSubscriptionView, showLackOfPlanMessage: true)
.environment(\.colorScheme, .light)
}
}
}
.tint(.master)
}
.fullScreenCover(item: $editedTeam) { editedTeam in
NavigationStack {
AddTeamView(tournament: tournament, editedTeam: editedTeam)
}
.tint(.master)
}
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
MenuWarningView(tournament: tournament, teams: [team], contactType: $contactType)
}
}
.onChange(of: registrationDate) {
if canSaveWithoutWarning() {
if let tr = team.registrationDate, tr == registrationDate {
} else {
team.registrationDate = registrationDate
_save()
}
}
}
.onChange(of: uniqueRandomIndex) {
if canSaveWithoutWarning() {
team.uniqueRandomIndex = uniqueRandomIndex
_save()
}
}
.onChange(of: [walkOut, wildCardBracket, wildCardGroupStage]) {
if canSaveWithoutWarning() {
if walkOut == false && team.walkOut == true {
registrationDateModified = Date()
team.registrationDate = registrationDateModified
registrationDate = registrationDateModified
}
_resetTeam()
team.walkOut = walkOut
team.wildCardBracket = wildCardBracket
team.wildCardGroupStage = wildCardGroupStage
_save()
}
}
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Édition de l'équipe")
.navigationBarTitleDisplayMode(.inline)
}
func canSaveWithoutWarning() -> Bool {
(tournament.shouldWarnOnlineRegistrationUpdates() && tournament.teamCount <= tournament.unsortedTeamsCount()) == false
}
private var confirmationReceived: Binding<Bool> {
Binding {
team.confirmed()
} set: { confirmed in
team.confirmationDate = confirmed ? Date() : nil
do {
try tournamentStore?.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
}
}
}
private var hasArrived: Binding<Bool> {
Binding {
team.isHere()
} set: { hasArrived in
team.unsortedPlayers().forEach {
$0.hasArrived = hasArrived
}
do {
try tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: team.unsortedPlayers())
} catch {
Logger.error(error)
}
}
}
private func _save() {
do {
try tournamentStore?.teamRegistrations.addOrUpdate(instance: team)
} catch {
Logger.error(error)
}
}
private var _networkErrorMessage: String {
ContactManagerError.getNetworkErrorMessage(sentError: sentError, networkMonitorConnected: networkMonitor.connected)
}
private func _processRefund() async {
isProcessingRefund = true
do {
let response = try await RefundService.processRefund(teamRegistrationId: team.id)
await MainActor.run {
isProcessingRefund = false
refundMessage = response.message
if response.success {
if let players = response.players {
tournamentStore?.playerRegistrations.addOrUpdate(contentOfs: players)
} else {
refundMessage = response.message + "\nLa mise à jour des équipes n'a pas été reçue pour le moment."
}
}
}
} catch {
await MainActor.run {
isProcessingRefund = false
refundMessage = "Erreur lors du remboursement. Veuillez réessayer." + "\n" + error.localizedDescription
}
}
}
}
struct RegistrationStatusPicker: View {
var player: PlayerRegistration
@State var selectedStatus: PlayerRegistration.RegistrationStatus
init(player: PlayerRegistration) {
self.player = player
_selectedStatus = .init(wrappedValue: player.registrationStatus)
}
var body: some View {
Picker(selection: $selectedStatus) {
ForEach(PlayerRegistration.RegistrationStatus.allCases) { status in
Text(status.localizedRegistrationStatus())
.tag(status)
}
} label: {
if player.registrationStatus != selectedStatus {
HStack {
FooterButtonView("Valider") {
player.registrationStatus = selectedStatus
}
Divider()
FooterButtonView("Annuler", role: .cancel) {
selectedStatus = player.registrationStatus
}
.foregroundStyle(.blue)
}
} else {
Text("État de la confirmation")
}
}
}
}