multistore
Razmig Sarkissian 1 year ago
parent a7bd190313
commit 6ea9676a64
  1. 4
      PadelClub.xcodeproj/project.pbxproj
  2. 2
      PadelClub/Data/GroupStage.swift
  3. 4
      PadelClub/Data/PlayerRegistration.swift
  4. 4
      PadelClub/Data/TeamRegistration.swift
  5. 9
      PadelClub/Data/Tournament.swift
  6. 4
      PadelClub/HTML Templates/tournament-template.html
  7. 1
      PadelClub/Utils/HtmlGenerator.swift
  8. 8
      PadelClub/Utils/HtmlService.swift
  9. 33
      PadelClub/Utils/URLs.swift
  10. 50
      PadelClub/Views/Calling/SendToAllView.swift
  11. 60
      PadelClub/Views/Cashier/Event/EventLinksView.swift
  12. 9
      PadelClub/Views/Cashier/Event/EventView.swift
  13. 49
      PadelClub/Views/GroupStage/GroupStageSettingsView.swift
  14. 18
      PadelClub/Views/Tournament/Screen/BroadcastView.swift
  15. 4
      PadelClub/Views/Tournament/Screen/Components/TournamentStatusView.swift

@ -141,6 +141,7 @@
FF1F4B8A2BFA02A4000B4573 /* groupstage-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B772BFA0105000B4573 /* groupstage-template.html */; };
FF1F4B8B2BFA02A4000B4573 /* groupstageentrant-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B792BFA0105000B4573 /* groupstageentrant-template.html */; };
FF1F4B8C2BFA02A4000B4573 /* match-template.html in Resources */ = {isa = PBXBuildFile; fileRef = FF1F4B7D2BFA0105000B4573 /* match-template.html */; };
FF2B6F5E2C036A1500835EE7 /* EventLinksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2B6F5D2C036A1400835EE7 /* EventLinksView.swift */; };
FF2EFBF02BDE295E0049CE3B /* SendToAllView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2EFBEF2BDE295E0049CE3B /* SendToAllView.swift */; };
FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = FF3795602B9396D0004EA093 /* PadelClubApp.xcdatamodeld */; };
FF3795662B9399AA004EA093 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3795652B9399AA004EA093 /* Persistence.swift */; };
@ -460,6 +461,7 @@
FF1F4B7E2BFA0105000B4573 /* player-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "player-template.html"; sourceTree = "<group>"; };
FF1F4B7F2BFA0105000B4573 /* tournament-template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "tournament-template.html"; sourceTree = "<group>"; };
FF1F4B812BFA0124000B4573 /* PrintSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrintSettingsView.swift; sourceTree = "<group>"; };
FF2B6F5D2C036A1400835EE7 /* EventLinksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventLinksView.swift; sourceTree = "<group>"; };
FF2EFBEF2BDE295E0049CE3B /* SendToAllView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendToAllView.swift; sourceTree = "<group>"; };
FF3795612B9396D0004EA093 /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = "<group>"; };
FF3795652B9399AA004EA093 /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
@ -1209,6 +1211,7 @@
isa = PBXGroup;
children = (
FFBF41812BF73EB3001B24CB /* EventView.swift */,
FF2B6F5D2C036A1400835EE7 /* EventLinksView.swift */,
FFBF41832BF75ED7001B24CB /* EventTournamentsView.swift */,
FFBF41852BF75FDA001B24CB /* EventSettingsView.swift */,
FF8F263A2BAD528600650388 /* EventCreationView.swift */,
@ -1550,6 +1553,7 @@
FF8F264F2BAE0B9600650388 /* MatchTypeSelectionView.swift in Sources */,
FF967D062BAF3C4200A9A3BD /* MatchSetupView.swift in Sources */,
FF4AB6B52B9248200002987F /* NetworkManager.swift in Sources */,
FF2B6F5E2C036A1500835EE7 /* EventLinksView.swift in Sources */,
FFB9C8752BBADDF700A0EF4F /* SeedInterval.swift in Sources */,
FF025AE12BD0EB9000A86CF8 /* TournamentClubSettingsView.swift in Sources */,
FFBF065C2BBD2657009D6715 /* GroupStageTeamView.swift in Sources */,

@ -50,7 +50,7 @@ class GroupStage: ModelObject, Storable {
}
func groupStageTitle(_ displayStyle: DisplayStyle = .wide) -> String {
if let name { return "Poule " + name }
if let name { return name }
switch displayStyle {
case .wide:
return "Poule \(index + 1)"

@ -74,8 +74,8 @@ class PlayerRegistration: ModelObject, Storable {
}
internal init(federalData: [String], sex: Int, sexUnknown: Bool) {
lastName = federalData[0].trimmed.capitalized
firstName = federalData[1].trimmed.uppercased()
lastName = federalData[0].trimmed.uppercased()
firstName = federalData[1].trimmed.capitalized
birthdate = federalData[2]
licenceId = federalData[3]
clubName = federalData[4]

@ -228,8 +228,10 @@ class TeamRegistration: ModelObject, Storable {
func updatePlayers(_ players: Set<PlayerRegistration>, inTournamentCategory tournamentCategory: TournamentCategory) {
let previousPlayers = Set(unsortedPlayers())
let playersToRemove = previousPlayers.subtracting(players)
do {
try DataStore.shared.playerRegistrations.delete(contentOfs: unsortedPlayers())
try DataStore.shared.playerRegistrations.delete(contentOfs: playersToRemove)
} catch {
Logger.error(error)
}

@ -404,14 +404,9 @@ class Tournament : ModelObject, Storable {
return false
}
}
func shareURL() -> URL? {
return URLs.main.url.appending(path: "tournament/\(id)")
}
func broadcastURL() -> URL? {
return URLs.main.url.appending(path: "tournament/\(id)/broadcast")
func shareURL(_ pageLink: PageLink = .matches) -> URL? {
return URLs.main.url.appending(path: "tournament/\(id)").appending(path: pageLink.path)
}
func courtUsed() -> [Int] {

@ -80,13 +80,13 @@
}
.player {
font-size:28px;
font-size:26px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.hiddenPlayer {
font-size:28px;
font-size:26px;
white-space: pre;
overflow: hidden;
text-overflow: ellipsis;

@ -103,6 +103,7 @@ class HtmlGenerator: ObservableObject {
groupStageDone = 0
groupStageIsReady = false
pdfDocument = PDFDocument()
rects.removeAll()
try? FileManager.default.removeItem(at: pdfURL!)
print("buildPDF", width, height, zoomLevel ?? 0)
if let zoomLevel {

@ -66,7 +66,7 @@ enum HtmlService {
} else {
template = template.replacingOccurrences(of: "{{bracketStartDate}}", with: "")
}
template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: bracket.tournamentObject()!.tournamentTitle())
template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: bracket.tournamentObject()!.tournamentTitle(.short))
template = template.replacingOccurrences(of: "{{bracketTitle}}", with: bracket.groupStageTitle())
var col = ""
@ -133,7 +133,7 @@ enum HtmlService {
case .player(let entrant):
var template = html
if let playerOne = entrant.players()[safe: 0] {
template = template.replacingOccurrences(of: "{{playerOne}}", with: playerOne.playerLabel(.short))
template = template.replacingOccurrences(of: "{{playerOne}}", with: playerOne.playerLabel())
if withRank {
template = template.replacingOccurrences(of: "{{weightOne}}", with: "(\(playerOne.formattedRank()))")
} else {
@ -142,7 +142,7 @@ enum HtmlService {
}
if let playerTwo = entrant.players()[safe: 1] {
template = template.replacingOccurrences(of: "{{playerTwo}}", with: playerTwo.playerLabel(.short))
template = template.replacingOccurrences(of: "{{playerTwo}}", with: playerTwo.playerLabel())
if withRank {
template = template.replacingOccurrences(of: "{{weightTwo}}", with: "(\(playerTwo.formattedRank()))")
} else {
@ -192,7 +192,7 @@ enum HtmlService {
return bracket
case .template(let tournament):
var template = html
template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: tournament.tournamentTitle())
template = template.replacingOccurrences(of: "{{tournamentTitle}}", with: tournament.tournamentTitle(.short))
var brackets = ""
for round in tournament.rounds() {
brackets = brackets.appending(HtmlService.bracket(tournament: tournament, roundIndex: round.index).html(headName: headName, withRank: withRank, withScore: withScore))

@ -21,3 +21,36 @@ enum URLs: String, Identifiable {
}
}
enum PageLink: String, Identifiable, CaseIterable {
case teams = "Équipes"
case summons = "Convocations"
case groupStages = "Poules"
case matches = "Matchs"
case rankings = "Classement"
case broadcast = "Broadcast"
var id: String { self.rawValue }
func localizedLabel() -> String {
rawValue
}
var path: String {
switch self {
case .matches:
return ""
case .teams:
return "teams"
case .summons:
return "summons"
case .rankings:
return "rankings"
case .groupStages:
return "group-stages"
case .broadcast:
return "broadcast"
}
}
}

@ -9,6 +9,8 @@ import SwiftUI
import LeStorage
struct SendToAllView: View {
@Environment(\.dismiss) var dismiss
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
@EnvironmentObject var networkMonitor: NetworkMonitor
@ -18,7 +20,8 @@ struct SendToAllView: View {
@State private var sentError: ContactManagerError? = nil
let addLink: Bool
@State var cannotPayForTournament: Bool = false
@State private var pageLink: PageLink = .teams
var messageSentFailed: Binding<Bool> {
Binding {
sentError != nil
@ -69,12 +72,34 @@ struct SendToAllView: View {
}
}
if addLink {
Section {
let links : [PageLink] = [.teams, .summons, .groupStages, .matches, .rankings]
Picker(selection: $pageLink) {
ForEach(links) { pageLink in
Text(pageLink.localizedLabel())
}
} label: {
Text("Choisir une page du tournoi en particulier")
}
.pickerStyle(.menu)
}
}
Section {
RowButtonView("Contacter \(_totalString())") {
self._contactAndPay()
}
}
}
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("Annuler", role: .cancel) {
dismiss()
}
}
}
.environment(\.editMode, Binding.constant(EditMode.active))
.headerProminence(.increased)
.navigationTitle("Préparation")
@ -141,7 +166,10 @@ struct SendToAllView: View {
}
func _teams() -> [TeamRegistration] {
_roundTeams() + _groupStagesTeams()
if _roundTeams().isEmpty && _groupStagesTeams().isEmpty {
return tournament.selectedSortedTeams()
}
return _roundTeams() + _groupStagesTeams()
}
func _roundTeams() -> [TeamRegistration] {
@ -172,11 +200,25 @@ struct SendToAllView: View {
}
}
func finalMessage() -> String {
var message = [String?]()
message.append("\n\n")
if addLink {
message.append(tournament.shareURL(pageLink)?.absoluteString)
}
let signature = dataStore.user.summonsMessageSignature ?? dataStore.user.defaultSignature()
message.append(signature)
return message.compactMap { $0 }.joined(separator: "\n\n")
}
fileprivate func _contact() {
if contactMethod == 0 {
contactType = .message(date: nil, recipients: _teams().flatMap { $0.unsortedPlayers() }.compactMap { $0.phoneNumber }, body: addLink ? tournament.shareURL()?.absoluteString : nil, tournamentBuild: nil)
contactType = .message(date: nil, recipients: _teams().flatMap { $0.unsortedPlayers() }.compactMap { $0.phoneNumber }, body: finalMessage(), tournamentBuild: nil)
} else {
contactType = .mail(date: nil, recipients: tournament.umpireMail(), bccRecipients: _teams().flatMap { $0.unsortedPlayers() }.compactMap { $0.email }, body: addLink ? tournament.shareURL()?.absoluteString : nil, subject: tournament.tournamentTitle(), tournamentBuild: nil)
contactType = .mail(date: nil, recipients: tournament.umpireMail(), bccRecipients: _teams().flatMap { $0.unsortedPlayers() }.compactMap { $0.email }, body: finalMessage(), subject: tournament.tournamentTitle(), tournamentBuild: nil)
}
}

@ -0,0 +1,60 @@
//
// EventLinksView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 26/05/2024.
//
import SwiftUI
struct EventLinksView: View {
let event: Event
@State private var pageLink: PageLink = .teams
func eventLinksPasteData() -> String {
var link = [String]()
link.append(event.eventTitle())
event.tournaments.forEach({ tournament in
if let url = tournament.shareURL(pageLink) {
var tournamentLink = [String]()
tournamentLink.append(tournament.tournamentTitle())
tournamentLink.append(url.absoluteString)
link.append(tournamentLink.joined(separator: "\n"))
}
})
return link.joined(separator: "\n\n")
}
var body: some View {
List {
Section {
let links : [PageLink] = [.teams, .summons, .groupStages, .matches, .rankings]
Picker(selection: $pageLink) {
ForEach(links) { pageLink in
Text(pageLink.localizedLabel())
}
} label: {
Text("Choisir une page du tournoi en particulier")
}
.pickerStyle(.menu)
}
let eventLinksPasteData = eventLinksPasteData()
Section {
Text(eventLinksPasteData)
.italic()
.multilineTextAlignment(.leading)
ShareLink("Partagez ce message", item: eventLinksPasteData)
}
}
}
}
#Preview {
EventLinksView(event: Event.mock())
}

@ -9,6 +9,7 @@ import SwiftUI
import LeStorage
enum EventDestination: Identifiable, Selectable {
case links
case tournaments(Event)
case cashier
@ -18,6 +19,8 @@ enum EventDestination: Identifiable, Selectable {
func selectionLabel() -> String {
switch self {
case .links:
return "Liens"
case .tournaments:
return "Épreuves"
case .cashier:
@ -27,6 +30,8 @@ enum EventDestination: Identifiable, Selectable {
func badgeValue() -> Int? {
switch self {
case .links:
return nil
case .tournaments(let event):
return event.tournaments.count
case .cashier:
@ -49,7 +54,7 @@ struct EventView: View {
@State private var selectedDestination: EventDestination?
func allDestinations() -> [EventDestination] {
[.tournaments(event), .cashier]
[.links, .tournaments(event), .cashier]
}
var body: some View {
@ -60,6 +65,8 @@ struct EventView: View {
EventSettingsView(event: event)
case .some(let selectedEventDestination):
switch selectedEventDestination {
case .links:
EventLinksView(event: event)
case .tournaments(let event):
EventTournamentsView(event: event)
case .cashier:

@ -11,7 +11,6 @@ import LeStorage
struct GroupStageSettingsView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
@State private var nameAlphabetical: Bool = false
@State private var generationDone: Bool = false
var body: some View {
@ -60,9 +59,36 @@ struct GroupStageSettingsView: View {
Text("Redistribue les équipes par la méthode du serpentin")
}
Toggle(isOn: $nameAlphabetical) {
Text("Nommer les poules alphabétiquement")
Section {
RowButtonView("Nommer les poules alphabétiquement", role: .destructive) {
let groupStages = tournament.groupStages()
groupStages.forEach { groupStage in
if let letter = Alphabet.letterForIndex(index: groupStage.index) {
groupStage.name = "Poule " + letter
}
}
do {
try dataStore.groupStages.addOrUpdate(contentOfs: groupStages)
} catch {
Logger.error(error)
}
}
}
Section {
RowButtonView("Supprimer les noms des poules", role: .destructive) {
let groupStages = tournament.groupStages()
groupStages.forEach { groupStage in
groupStage.name = nil
}
do {
try dataStore.groupStages.addOrUpdate(contentOfs: groupStages)
} catch {
Logger.error(error)
}
}
}
}
.overlay(alignment: .bottom) {
if generationDone {
@ -71,23 +97,6 @@ struct GroupStageSettingsView: View {
.deferredRendering(for: .seconds(2))
}
}
.onChange(of: nameAlphabetical) {
let groupStages = tournament.groupStages()
if nameAlphabetical {
groupStages.forEach { groupStage in
groupStage.name = Alphabet.letterForIndex(index: groupStage.index)
}
} else {
groupStages.forEach { groupStage in
groupStage.name = nil
}
}
do {
try dataStore.groupStages.addOrUpdate(contentOfs: groupStages)
} catch {
Logger.error(error)
}
}
}

@ -21,7 +21,8 @@ struct BroadcastView: View {
let filter = CIFilter.qrCodeGenerator()
@State private var urlToShow: String?
@State private var tvMode: Bool = false
@State private var pageLink: PageLink = .teams
let tournamentPublishingTip = TournamentPublishingTip()
let tournamentTVBroadcastTip = TournamentTVBroadcastTip()
@ -180,15 +181,26 @@ struct BroadcastView: View {
}
}
if let url = tournament.shareURL() {
if let url = tournament.shareURL(pageLink) {
LabeledContent {
actionForURL(url)
} label: {
Text("Tournoi")
Text(pageLink.localizedLabel())
}
}
let links : [PageLink] = [.teams, .summons, .groupStages, .matches, .rankings]
Picker(selection: $pageLink) {
ForEach(links) { pageLink in
Text(pageLink.localizedLabel()).tag(pageLink)
}
} label: {
Text("Modifier la page du tournoi à partager")
}
.pickerStyle(.menu)
if let url = tournament.broadcastURL() {
if let url = tournament.shareURL(.broadcast) {
LabeledContent {
actionForURL(url)
} label: {

@ -81,7 +81,7 @@ struct TournamentStatusView: View {
dismiss()
}
} footer: {
Text(.init("Si votre tournoi n'a pas pu aboutir à cause de la météo ou autre, vous pouvez l'annuler et il ne sera pas comptabilisé. Toutes les données du tournoi seront conservées. Le tournoi visible sur [Padel Club](\(URLs.main)"))
Text(.init("Si votre tournoi n'a pas pu aboutir à cause de la météo ou autre, vous pouvez l'annuler et il ne sera pas comptabilisé. Toutes les données du tournoi seront conservées. Le tournoi visible sur [Padel Club](\(URLs.main.rawValue))"))
}
}
@ -90,7 +90,7 @@ struct TournamentStatusView: View {
Text("Tournoi privé")
}
} footer: {
Text(.init("Le tournoi sera masqué sur le site [Padel Club](\(URLs.main)"))
Text(.init("Le tournoi sera masqué sur le site [Padel Club](\(URLs.main.rawValue))"))
}
}
.toolbarBackground(.visible, for: .navigationBar)

Loading…
Cancel
Save