import SwiftUI import SwiftData struct ScheduleCalendarView: View { @Query(sort: \SyncJob.name) private var jobs: [SyncJob] @State private var displayedMonth = Calendar.current.date(from: Calendar.current.dateComponents([.year, .month], from: Date())) ?? Date() @State private var selectedDay = Date() @State private var editingJob: SyncJob? private let calendar = Calendar.current private let columns = Array(repeating: GridItem(.flexible(), spacing: 6), count: 7) var body: some View { HStack(spacing: 0) { VStack(spacing: 0) { monthHeader Divider() calendarGrid } .frame(maxWidth: .infinity, maxHeight: .infinity) Divider() agenda .frame(width: 320) .background(.bar) } .sheet(item: $editingJob) { job in JobEditorView(job: job) } } private var monthHeader: some View { HStack { Button { moveMonth(-1) } label: { Image(systemName: "chevron.left") } .buttonStyle(.bordered) VStack(alignment: .leading, spacing: 2) { Text(displayedMonth.formatted(.dateTime.month(.wide).year())) .font(.title2.bold()) Text("\(monthOccurrences.count) scheduled runs") .font(.caption) .foregroundStyle(.secondary) } Spacer() Button("Today") { displayedMonth = calendar.date(from: calendar.dateComponents([.year, .month], from: Date())) ?? Date() selectedDay = Date() } .buttonStyle(.borderedProminent) Button { moveMonth(1) } label: { Image(systemName: "chevron.right") } .buttonStyle(.bordered) } .padding(18) } private var calendarGrid: some View { VStack(spacing: 8) { LazyVGrid(columns: columns, spacing: 6) { ForEach(calendar.shortWeekdaySymbols, id: \.self) { symbol in Text(symbol.uppercased()) .font(.caption2.weight(.bold)) .foregroundStyle(.secondary) .frame(maxWidth: .infinity) } ForEach(monthCells.indices, id: \.self) { index in if let date = monthCells[index] { dayCell(date) } else { Color.clear.frame(minHeight: 86) } } } } .padding(16) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) } private func dayCell(_ date: Date) -> some View { let occurrences = occurrences(on: date) let selected = calendar.isDate(date, inSameDayAs: selectedDay) let today = calendar.isDateInToday(date) return Button { selectedDay = date } label: { VStack(alignment: .leading, spacing: 6) { HStack { Text("\(calendar.component(.day, from: date))") .font(.body.weight(today ? .bold : .medium)) .foregroundStyle(today ? Color.accentColor : .primary) Spacer() if !occurrences.isEmpty { Text("\(occurrences.count)") .font(.caption2.bold()) .padding(.horizontal, 6) .padding(.vertical, 2) .background(Color.accentColor.opacity(0.14), in: Capsule()) } } ForEach(Array(occurrences.prefix(2)), id: \.id) { item in HStack(spacing: 4) { Circle().fill(Color.accentColor).frame(width: 5, height: 5) Text(item.job.name) .font(.caption2) .lineLimit(1) Spacer() Text(item.date.formatted(date: .omitted, time: .shortened)) .font(.caption2) .foregroundStyle(.secondary) } } Spacer(minLength: 0) } .padding(9) .frame(maxWidth: .infinity, minHeight: 86, alignment: .topLeading) .background(selected ? Color.accentColor.opacity(0.10) : Color(nsColor: .controlBackgroundColor), in: RoundedRectangle(cornerRadius: 10, style: .continuous)) .overlay(RoundedRectangle(cornerRadius: 10, style: .continuous).stroke(selected ? Color.accentColor.opacity(0.6) : Color.secondary.opacity(0.12))) } .buttonStyle(.plain) } private var agenda: some View { VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 3) { Text("Agenda").font(.title3.bold()) Text(selectedDay.formatted(date: .complete, time: .omitted)) .font(.caption) .foregroundStyle(.secondary) } let items = occurrences(on: selectedDay) if items.isEmpty { ContentUnavailableView( "No Scheduled Runs", systemImage: "calendar", description: Text("Select another day or add a schedule to a sync job.") ) } else { ScrollView { LazyVStack(spacing: 10) { ForEach(items, id: \.id) { item in VStack(alignment: .leading, spacing: 6) { HStack { Image(systemName: item.job.isRemote ? "server.rack" : "folder.fill") .foregroundStyle(Color.accentColor) Text(item.job.name).fontWeight(.semibold) Spacer() } Label(item.date.formatted(date: .omitted, time: .shortened), systemImage: "clock") .font(.caption) .foregroundStyle(.secondary) Text(item.job.destinationDisplay) .font(.caption2) .foregroundStyle(.secondary) .lineLimit(2) HStack { Spacer() Button("Edit Schedule") { editingJob = item.job } .buttonStyle(.bordered) .controlSize(.small) } } .padding(12) .frame(maxWidth: .infinity, alignment: .leading) .background(.background, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) } } } } Spacer() } .padding(18) } private var monthInterval: DateInterval { calendar.dateInterval(of: .month, for: displayedMonth) ?? DateInterval(start: displayedMonth, duration: 2_592_000) } private var monthCells: [Date?] { let interval = monthInterval let days = calendar.range(of: .day, in: .month, for: interval.start) ?? 1..<2 let weekday = calendar.component(.weekday, from: interval.start) let leading = (weekday - calendar.firstWeekday + 7) % 7 var cells = Array(repeating: nil, count: leading) for day in days { if let date = calendar.date(byAdding: .day, value: day - 1, to: interval.start) { cells.append(date) } } while cells.count % 7 != 0 { cells.append(nil) } return cells } private var monthOccurrences: [CalendarOccurrence] { scheduledJobs.flatMap { job in job.occurrences(from: monthInterval.start, through: monthInterval.end).map { CalendarOccurrence(job: job, date: $0) } }.sorted { $0.date < $1.date } } private var scheduledJobs: [SyncJob] { jobs.filter { $0.isEnabled && $0.scheduleEnabled && $0.scheduleKind != .manual } } private func occurrences(on day: Date) -> [CalendarOccurrence] { monthOccurrences.filter { calendar.isDate($0.date, inSameDayAs: day) } } private func moveMonth(_ offset: Int) { if let next = calendar.date(byAdding: .month, value: offset, to: displayedMonth) { displayedMonth = next selectedDay = next } } } private struct CalendarOccurrence: Identifiable { let job: SyncJob let date: Date var id: String { "\(job.id.uuidString)-\(date.timeIntervalSince1970)" } }