multistore
Razmig Sarkissian 2 years ago
parent 348aa591f3
commit c0eb74bb43
  1. 6
      PadelClub.xcodeproj/project.pbxproj
  2. 4
      PadelClub/Data/DataStore.swift
  3. 56
      PadelClub/Data/DateInterval.swift
  4. 6
      PadelClub/Data/Event.swift
  5. 1
      PadelClub/Data/GroupStage.swift
  6. 6
      PadelClub/Data/MockData.swift
  7. 2
      PadelClub/Data/Tournament.swift
  8. 32
      PadelClub/ViewModel/DateInterval.swift
  9. 7
      PadelClub/ViewModel/MatchScheduler.swift
  10. 4
      PadelClub/Views/Club/CreateClubView.swift
  11. 22
      PadelClub/Views/Components/ButtonValidateView.swift
  12. 29
      PadelClub/Views/GroupStage/GroupStageSettingsView.swift
  13. 35
      PadelClub/Views/GroupStage/GroupStageView.swift
  14. 1
      PadelClub/Views/GroupStage/GroupStagesView.swift
  15. 48
      PadelClub/Views/Planning/CourtAvailabilitySettingsView.swift
  16. 14
      PadelClub/Views/Planning/PlanningSettingsView.swift
  17. 4
      PadelClub/Views/Player/Components/PlayerPopoverView.swift
  18. 6
      PadelClub/Views/Tournament/FileImportView.swift
  19. 9
      PadelClub/Views/Tournament/Screen/TableStructureView.swift
  20. 2
      PadelClub/Views/Tournament/TournamentView.swift

@ -238,6 +238,7 @@
FFDB1C6D2BB2A02000F1E467 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */; };
FFDB1C732BB2CFE900F1E467 /* MySortDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDB1C722BB2CFE900F1E467 /* MySortDescriptor.swift */; };
FFDDD40C2B93B2BB00C91A49 /* DeferredViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */; };
FFF03C942BD91D0C00B516FC /* ButtonValidateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF03C932BD91D0C00B516FC /* ButtonValidateView.swift */; };
FFF116E12BD2A9B600A33B06 /* DateInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF116E02BD2A9B600A33B06 /* DateInterval.swift */; };
FFF116E32BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF116E22BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift */; };
FFF527D62BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */; };
@ -534,6 +535,7 @@
FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
FFDB1C722BB2CFE900F1E467 /* MySortDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MySortDescriptor.swift; sourceTree = "<group>"; };
FFDDD40B2B93B2BB00C91A49 /* DeferredViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeferredViewModifier.swift; sourceTree = "<group>"; };
FFF03C932BD91D0C00B516FC /* ButtonValidateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonValidateView.swift; sourceTree = "<group>"; };
FFF116E02BD2A9B600A33B06 /* DateInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateInterval.swift; sourceTree = "<group>"; };
FFF116E22BD2AF4800A33B06 /* CourtAvailabilitySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourtAvailabilitySettingsView.swift; sourceTree = "<group>"; };
FFF527D52BC6DDD000FF4EF2 /* MatchScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchScheduleEditorView.swift; sourceTree = "<group>"; };
@ -678,6 +680,7 @@
FF1DC5522BAB354A00FD8220 /* MockData.swift */,
FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */,
FFC91B002BD85C2F00B29808 /* Court.swift */,
FFF116E02BD2A9B600A33B06 /* DateInterval.swift */,
FF6EC9012B94799200EA7F5A /* Coredata */,
FF6EC9022B9479B900EA7F5A /* Federal */,
);
@ -756,6 +759,7 @@
FF967CF72BAEDF0000A9A3BD /* Labels.swift */,
FFC91AF82BD6A09100B29808 /* FortuneWheelView.swift */,
FF1DF49A2BD8D23900822FA0 /* BarButtonView.swift */,
FFF03C932BD91D0C00B516FC /* ButtonValidateView.swift */,
);
path = Components;
sourceTree = "<group>";
@ -979,7 +983,6 @@
FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */,
FFBF065F2BBD9F6D009D6715 /* NavigationViewModel.swift */,
FF3B60A22BC49BBC008C2E66 /* MatchScheduler.swift */,
FFF116E02BD2A9B600A33B06 /* DateInterval.swift */,
);
path = ViewModel;
sourceTree = "<group>";
@ -1503,6 +1506,7 @@
FF3F74FF2B91A2D4004CFE0E /* AgendaDestination.swift in Sources */,
FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */,
FFCFC0162BBC5A4C00B82851 /* SetInputView.swift in Sources */,
FFF03C942BD91D0C00B516FC /* ButtonValidateView.swift in Sources */,
FF5D0D892BB4935C005CB568 /* ClubRowView.swift in Sources */,
FF1DC5512BAB351300FD8220 /* ClubDetailView.swift in Sources */,
FF9268032BCE94A30080F940 /* GroupStageCallingView.swift in Sources */,

@ -26,7 +26,8 @@ class DataStore: ObservableObject {
fileprivate(set) var rounds: StoredCollection<Round>
fileprivate(set) var teamScores: StoredCollection<TeamScore>
fileprivate(set) var monthData: StoredCollection<MonthData>
fileprivate(set) var dateIntervals: StoredCollection<DateInterval>
fileprivate var _userStorage: OptionalStorage<User> = OptionalStorage<User>(fileName: "user.json")
fileprivate var _appSettingsStorage: MicroStorage<AppSettings> = MicroStorage()
@ -78,6 +79,7 @@ class DataStore: ObservableObject {
self.rounds = store.registerCollection(synchronized: false, indexed: indexed)
self.matches = store.registerCollection(synchronized: false, indexed: indexed)
self.monthData = store.registerCollection(synchronized: false, indexed: indexed)
self.dateIntervals = store.registerCollection(synchronized: false, indexed: indexed)
NotificationCenter.default.addObserver(self, selector: #selector(collectionWasUpdated), name: NSNotification.Name.CollectionDidLoad, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(collectionWasUpdated), name: NSNotification.Name.CollectionDidChange, object: nil)

@ -0,0 +1,56 @@
//
// DateInterval.swift
// PadelClub
//
// Created by Razmig Sarkissian on 19/04/2024.
//
import Foundation
import SwiftUI
import LeStorage
@Observable
class DateInterval: ModelObject, Storable {
static func resourceName() -> String { return "date-intervals" }
var id: String = Store.randomId()
var event: String
var courtIndex: Int
var startDate: Date
var endDate: Date
internal init(event: String, courtIndex: Int, startDate: Date, endDate: Date) {
self.event = event
self.courtIndex = courtIndex
self.startDate = startDate
self.endDate = endDate
}
var range: Range<Date> {
startDate..<endDate
}
func isSingleDay() -> Bool {
Calendar.current.isDate(startDate, inSameDayAs: endDate)
}
func isDateInside(_ date: Date) -> Bool {
date >= startDate && date <= endDate
}
func isDateOutside(_ date: Date) -> Bool {
date <= startDate && date <= endDate && date >= startDate && date >= endDate
}
override func deleteDependencies() throws {
}
enum CodingKeys: String, CodingKey {
case _id = "id"
case _event = "event"
case _courtIndex = "courtIndex"
case _startDate = "startDate"
case _endDate = "endDate"
}
}

@ -47,8 +47,14 @@ class Event: ModelObject, Storable {
tournaments.first(where: { $0.isSameBuild(build) })
}
var courtsUnavailability: [DateInterval] {
Store.main.filter(isIncluded: { $0.event == id })
}
override func deleteDependencies() throws {
try Store.main.deleteDependencies(items: self.tournaments)
try Store.main.deleteDependencies(items: self.courtsUnavailability)
}
}

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

@ -13,6 +13,12 @@ extension Court {
}
}
extension Event {
static func mock() -> Event {
Event()
}
}
extension Club {
static func mock() -> Club {
Club(name: "AUC", acronym: "AUC")

@ -43,8 +43,6 @@ class Tournament : ModelObject, Storable {
var entryFee: Double?
var payment: TournamentPayment? = nil
var additionalEstimationDuration: Int = 0
var courtsUnavailability: [Int: [DateInterval]]? = nil
@ObservationIgnored
var navigationPath: [Screen] = []

@ -1,32 +0,0 @@
//
// DateInterval.swift
// PadelClub
//
// Created by Razmig Sarkissian on 19/04/2024.
//
import Foundation
import LeStorage
struct DateInterval: Identifiable, Codable {
var id: String = Store.randomId()
let startDate: Date
let endDate: Date
var range: Range<Date> {
startDate..<endDate
}
func isSingleDay() -> Bool {
Calendar.current.isDate(startDate, inSameDayAs: endDate)
}
func isDateInside(_ date: Date) -> Bool {
date >= startDate && date <= endDate
}
func isDateOutside(_ date: Date) -> Bool {
date <= startDate && date <= endDate && date >= startDate && date >= endDate
}
}

@ -67,7 +67,7 @@ class MatchScheduler {
var timeDifferenceLimit: Double = 300.0
var loserBracketRotationDifference: Int = 0
var upperBracketRotationDifference: Int = 1
var courtsUnavailability: [Int: [DateInterval]]? = nil
var courtsUnavailability: [DateInterval]? = nil
func shouldHandleUpperRoundSlice() -> Bool {
options.contains(.shouldHandleUpperRoundSlice)
@ -522,14 +522,15 @@ class MatchScheduler {
func courtsUnavailable(startDate: Date, duration: Int) -> Int {
let endDate = startDate.addingTimeInterval(Double(duration) * 60)
guard let courtsUnavailability else { return 0 }
let courts = courtsUnavailability.keys
let groupedBy = Dictionary(grouping: courtsUnavailability, by: { $0.courtIndex })
let courts = groupedBy.keys
return courts.filter {
courtUnavailable(courtIndex: $0, from: startDate, to: endDate)
}.count
}
func courtUnavailable(courtIndex: Int, from startDate: Date, to endDate: Date) -> Bool {
guard let courtLockedSchedule = courtsUnavailability?[courtIndex] else { return true }
guard let courtLockedSchedule = courtsUnavailability?.filter({ $0.courtIndex == courtIndex }) else { return true }
return courtLockedSchedule.anySatisfy({ dateInterval in
dateInterval.isDateInside(startDate) || dateInterval.isDateInside(endDate)
})

@ -27,12 +27,10 @@ struct CreateClubView: View {
}
}
ToolbarItem(placement: .confirmationAction) {
Button("Valider") {
ButtonValidateView {
try? dataStore.clubs.addOrUpdate(instance: club)
dismiss()
}
.clipShape(Capsule())
.buttonStyle(.bordered)
.disabled(club.isValid == false)
}
}

@ -0,0 +1,22 @@
//
// ButtonValidateView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 24/04/2024.
//
import SwiftUI
struct ButtonValidateView: View {
var role: ButtonRole?
let action: () -> ()
var body: some View {
Button("Valider", role: role) {
action()
}
.clipShape(Capsule())
.buttonStyle(.bordered)
}
}

@ -8,10 +8,26 @@
import SwiftUI
struct GroupStageSettingsView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
@State private var nameAlphabetical: Bool = false
let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
private func _letterForIndex(index: Int) -> String? {
let uppercaseAlphabet = Array(alphabet)
if let letter = uppercaseAlphabet[safe: index] {
return String(letter)
} else {
return nil
}
}
var body: some View {
List {
Toggle(isOn: $nameAlphabetical) {
Text("Nommer les poules alphabétiquement")
}
Menu {
//menuAddGroupStage
menuBuildAllGroupStages
@ -146,6 +162,19 @@ struct GroupStageSettingsView: View {
}
.onChange(of: nameAlphabetical) {
let groupStages = tournament.groupStages()
if nameAlphabetical {
groupStages.forEach { groupStage in
groupStage.name = _letterForIndex(index: groupStage.index)
}
} else {
groupStages.forEach { groupStage in
groupStage.name = nil
}
}
try? dataStore.groupStages.addOrUpdate(contentOfs: groupStages)
}
}

@ -14,7 +14,8 @@ struct GroupStageView: View {
@State private var sortingMode: GroupStageSortingMode = .auto
@State private var confirmRemoveAll: Bool = false
@State private var confirmResetMatch: Bool = false
@State private var groupStageName: String = ""
private enum GroupStageSortingMode {
case auto
case score
@ -29,6 +30,11 @@ struct GroupStageView: View {
sortByScore ? groupStage.teams(sortByScore)[safe: index] : groupStage.teamAt(groupStagePosition: index)
}
init(groupStage: GroupStage) {
self.groupStage = groupStage
_groupStageName = State(wrappedValue: groupStage.groupStageTitle())
}
var body: some View {
List {
Section {
@ -66,11 +72,16 @@ struct GroupStageView: View {
MatchListView(section: "à lancer", matches: groupStage.readyMatches()).id(UUID())
MatchListView(section: "terminés", matches: groupStage.finishedMatches()).id(UUID())
}
.onChange(of: groupStageName) {
groupStage.name = groupStageName
try? dataStore.groupStages.addOrUpdate(instance: groupStage)
}
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
_groupStageMenuView()
}
}
.navigationTitle($groupStageName)
}
private func _groupStageView() -> some View {
@ -129,15 +140,23 @@ struct GroupStageView: View {
private func _groupStageMenuView() -> some View {
Menu {
if groupStage.playedMatches().isEmpty {
Button {
//groupStage.startGroupStage()
//save()
} label: {
Text("Créer les matchs")
if groupStage.name != nil {
Button("Retirer le nom") {
groupStage.name = nil
groupStageName = groupStage.groupStageTitle()
try? dataStore.groupStages.addOrUpdate(instance: groupStage)
}
.buttonStyle(.borderless)
}
// if groupStage.playedMatches().isEmpty {
// Button {
// //groupStage.startGroupStage()
// //save()
// } label: {
// Text("Créer les matchs")
// }
// .buttonStyle(.borderless)
// }
Button("Retirer tout le monde", role: .destructive) {
confirmRemoveAll = true

@ -81,7 +81,6 @@ struct GroupStagesView: View {
.navigationTitle("Toutes les poules")
case .groupStage(let groupStage):
GroupStageView(groupStage: groupStage)
.navigationTitle(groupStage.groupStageTitle())
case nil:
GroupStageSettingsView()
.navigationTitle("Réglages")

@ -9,15 +9,25 @@ import SwiftUI
struct CourtAvailabilitySettingsView: View {
@Environment(Tournament.self) var tournament: Tournament
@State private var courtsUnavailability: [Int: [DateInterval]] = [Int:[DateInterval]]()
@EnvironmentObject var dataStore: DataStore
let event: Event
@State private var showingPopover: Bool = false
@State private var courtIndex: Int = 0
@State private var startDate: Date = Date()
@State private var endDate: Date = Date()
var courtsUnavailability: [Int: [DateInterval]] {
let groupedBy = Dictionary(grouping: event.courtsUnavailability, by: { dateInterval in
return dateInterval.courtIndex
})
return groupedBy
}
var body: some View {
List {
let keys = courtsUnavailability.keys.sorted(by: \.self)
let keys = courtsUnavailability.keys.sorted()
ForEach(keys, id: \.self) { key in
if let dates = courtsUnavailability[key] {
Section {
@ -38,18 +48,22 @@ struct CourtAvailabilitySettingsView: View {
}
.contextMenu(menuItems: {
Button("dupliquer") {
let duplicatedDateInterval = DateInterval(event: event.id, courtIndex: (courtIndex+1)%tournament.courtCount, startDate: dateInterval.startDate, endDate: dateInterval.endDate)
try? dataStore.dateIntervals.addOrUpdate(instance: duplicatedDateInterval)
}
Button("éditer") {
courtIndex = dateInterval.courtIndex
startDate = dateInterval.startDate
endDate = dateInterval.endDate
showingPopover = true
}
Button("effacer") {
Button("effacer", role: .destructive) {
try? dataStore.dateIntervals.delete(instance: dateInterval)
}
})
.swipeActions {
Button(role: .destructive) {
courtsUnavailability[key]?.removeAll(where: { $0.id == dateInterval.id })
try? dataStore.dateIntervals.delete(instance: dateInterval)
} label: {
LabelDelete()
}
@ -74,9 +88,6 @@ struct CourtAvailabilitySettingsView: View {
}
}
}
.onDisappear {
tournament.courtsUnavailability = courtsUnavailability
}
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Créneaux")
@ -91,26 +102,23 @@ struct CourtAvailabilitySettingsView: View {
DatePicker("Début", selection: $startDate)
DatePicker("Fin", selection: $endDate)
} footer: {
Button("jour entier") {
FooterButtonView("jour entier") {
startDate = startDate.startOfDay
endDate = endDate.endOfDay()
}
.buttonStyle(.borderless)
.underline()
}
}
.toolbar {
Button("Valider") {
let dateInterval = DateInterval(startDate: startDate, endDate: endDate)
var courtUnavailability = courtsUnavailability[courtIndex] ?? [DateInterval]()
courtUnavailability.append(dateInterval)
courtsUnavailability[courtIndex] = courtUnavailability
ButtonValidateView {
let dateInterval = DateInterval(event: event.id, courtIndex: courtIndex, startDate: startDate, endDate: endDate)
try? dataStore.dateIntervals.addOrUpdate(instance: dateInterval)
showingPopover = false
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.navigationTitle("Nouveau créneau")
.tint(.master)
}
.onAppear {
UIDatePicker.appearance().minuteInterval = 5
@ -139,5 +147,5 @@ struct CourtPicker: View {
}
#Preview {
CourtAvailabilitySettingsView()
CourtAvailabilitySettingsView(event: Event.mock())
}

@ -62,11 +62,13 @@ struct PlanningSettingsView: View {
TournamentFieldsManagerView(localizedStringKey: "Terrains par poule", count: $groupStageCourtCount, max: tournament.maximumCourtsPerGroupSage())
}
NavigationLink {
CourtAvailabilitySettingsView()
.environment(tournament)
} label: {
Text("Préciser la disponibilité des terrains")
if let event = tournament.eventObject {
NavigationLink {
CourtAvailabilitySettingsView(event: event)
.environment(tournament)
} label: {
Text("Préciser la disponibilité des terrains")
}
}
}
@ -176,7 +178,7 @@ struct PlanningSettingsView: View {
let groupStages = tournament.groupStages()
let numberOfCourtsAvailablePerRotation: Int = tournament.courtCount
let matchScheduler = MatchScheduler.shared
matchScheduler.courtsUnavailability = tournament.courtsUnavailability
matchScheduler.courtsUnavailability = tournament.eventObject?.courtsUnavailability
matchScheduler.options.removeAll()
if randomCourtDistribution {

@ -186,12 +186,10 @@ struct PlayerPopoverView: View {
.toolbarBackground(.visible, for: .navigationBar)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("Valider") {
ButtonValidateView {
createManualPlayer()
dismiss()
}
.clipShape(Capsule())
.buttonStyle(.bordered)
.disabled(isPlayerValid() == false)
}

@ -223,7 +223,7 @@ struct FileImportView: View {
}
ToolbarItem(placement: .topBarTrailing) {
Button {
ButtonValidateView {
if false { //selectedOptions.contains(.deleteBeforeImport)
try? dataStore.teamRegistrations.delete(contentOfs: tournament.unsortedTeams())
}
@ -245,11 +245,7 @@ struct FileImportView: View {
tournament.importTeams(filteredTeams)
dismiss()
} label: {
Text("Valider")
}
.buttonStyle(.bordered)
.clipShape(Capsule())
.disabled(teams.isEmpty)
}
}

@ -191,23 +191,18 @@ struct TableStructureView: View {
}
ToolbarItem(placement: .confirmationAction) {
if tournament.state() == .initial {
Button("Valider") {
ButtonValidateView {
_save(rebuildEverything: true)
}
.clipShape(Capsule())
.buttonStyle(.bordered)
} else {
let requirements = Set(updatedElements.compactMap { $0.requiresRebuilding })
Button("Valider", role: .destructive) {
ButtonValidateView(role: .destructive) {
if requirements.isEmpty {
_save(rebuildEverything: false)
} else {
presentRefreshStructureWarning = true
}
}
.clipShape(Capsule())
.buttonStyle(.bordered)
.disabled(updatedElements.isEmpty)
.confirmationDialog("Mise à jour de la structure", isPresented: $presentRefreshStructureWarning, actions: {

@ -29,7 +29,6 @@ struct TournamentView: View {
NavigationLink(value: Screen.inscription) {
LabeledContent {
Text(tournament.unsortedTeams().count.formatted() + "/" + tournament.teamCount.formatted())
.foregroundStyle(.master)
} label: {
Text("Gestion des inscriptions")
if let closedRegistrationDate = tournament.closedRegistrationDate {
@ -40,7 +39,6 @@ struct TournamentView: View {
if let endOfInscriptionDate = tournament.mandatoryRegistrationCloseDate(), tournament.inscriptionClosed() == false && tournament.hasStarted() == false {
LabeledContent {
Text(endOfInscriptionDate.formatted(date: .abbreviated, time: .shortened))
.foregroundStyle(.master)
} label: {
Text("Date limite")
}

Loading…
Cancel
Save