ios-rebuild #3
@@ -3,12 +3,26 @@ import SwiftUI
|
|||||||
struct TimeEntriesView: View {
|
struct TimeEntriesView: View {
|
||||||
@StateObject private var viewModel = TimeEntriesViewModel()
|
@StateObject private var viewModel = TimeEntriesViewModel()
|
||||||
@State private var selectedDay: Date? = Calendar.current.startOfDay(for: Date())
|
@State private var selectedDay: Date? = Calendar.current.startOfDay(for: Date())
|
||||||
|
@State private var visibleWeekStart: Date = Self.mondayOfWeek(containing: Date())
|
||||||
@State private var showFilterSheet = false
|
@State private var showFilterSheet = false
|
||||||
@State private var showAddEntry = false
|
@State private var showAddEntry = false
|
||||||
@State private var entryToEdit: TimeEntry?
|
@State private var entryToEdit: TimeEntry?
|
||||||
@State private var entryToDelete: TimeEntry?
|
@State private var entryToDelete: TimeEntry?
|
||||||
@State private var showDeleteConfirmation = false
|
@State private var showDeleteConfirmation = false
|
||||||
|
|
||||||
|
private static func mondayOfWeek(containing date: Date) -> Date {
|
||||||
|
var cal = Calendar.current
|
||||||
|
cal.firstWeekday = 2 // Monday
|
||||||
|
let comps = cal.dateComponents([.yearForWeekOfYear, .weekOfYear], from: date)
|
||||||
|
return cal.date(from: comps) ?? Calendar.current.startOfDay(for: date)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var visibleWeekDays: [Date] {
|
||||||
|
(0..<7).compactMap {
|
||||||
|
Calendar.current.date(byAdding: .day, value: $0, to: visibleWeekStart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
Group {
|
Group {
|
||||||
@@ -72,19 +86,22 @@ struct TimeEntriesView: View {
|
|||||||
// MARK: - Main content
|
// MARK: - Main content
|
||||||
|
|
||||||
private var mainContent: some View {
|
private var mainContent: some View {
|
||||||
ScrollView {
|
VStack(spacing: 0) {
|
||||||
VStack(spacing: 0) {
|
WeekStripView(
|
||||||
// Month calendar
|
weekDays: visibleWeekDays,
|
||||||
CalendarGridView(
|
selectedDay: $selectedDay,
|
||||||
daysWithEntries: viewModel.daysWithEntries,
|
daysWithEntries: viewModel.daysWithEntries,
|
||||||
selectedDay: $selectedDay
|
onSwipeLeft: {
|
||||||
)
|
visibleWeekStart = Calendar.current.date(byAdding: .weekOfYear, value: 1, to: visibleWeekStart) ?? visibleWeekStart
|
||||||
.padding(.horizontal)
|
},
|
||||||
.padding(.top, 8)
|
onSwipeRight: {
|
||||||
|
visibleWeekStart = Calendar.current.date(byAdding: .weekOfYear, value: -1, to: visibleWeekStart) ?? visibleWeekStart
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
Divider().padding(.top, 8)
|
Divider()
|
||||||
|
|
||||||
// Day detail — entries for the selected day
|
ScrollView {
|
||||||
if let day = selectedDay {
|
if let day = selectedDay {
|
||||||
dayEntriesSection(for: day)
|
dayEntriesSection(for: day)
|
||||||
} else {
|
} else {
|
||||||
@@ -242,83 +259,137 @@ struct EntryRow: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Calendar Grid View (UICalendarView wrapper)
|
// MARK: - Week Strip View
|
||||||
|
|
||||||
struct CalendarGridView: UIViewRepresentable {
|
struct WeekStripView: View {
|
||||||
let daysWithEntries: Set<Date>
|
let weekDays: [Date]
|
||||||
@Binding var selectedDay: Date?
|
@Binding var selectedDay: Date?
|
||||||
|
let daysWithEntries: Set<Date>
|
||||||
|
let onSwipeLeft: () -> Void
|
||||||
|
let onSwipeRight: () -> Void
|
||||||
|
|
||||||
func makeUIView(context: Context) -> UICalendarView {
|
@GestureState private var dragOffset: CGFloat = 0
|
||||||
let view = UICalendarView()
|
|
||||||
view.calendar = .current
|
|
||||||
view.locale = .current
|
|
||||||
view.fontDesign = .rounded
|
|
||||||
view.delegate = context.coordinator
|
|
||||||
|
|
||||||
let selection = UICalendarSelectionSingleDate(delegate: context.coordinator)
|
private let cal = Calendar.current
|
||||||
if let day = selectedDay {
|
|
||||||
selection.selectedDate = Calendar.current.dateComponents([.year, .month, .day], from: day)
|
|
||||||
}
|
|
||||||
view.selectionBehavior = selection
|
|
||||||
|
|
||||||
// Show current month
|
private var monthYearLabel: String {
|
||||||
let today = Date()
|
// Show the month/year of the majority of days in the strip
|
||||||
let comps = Calendar.current.dateComponents([.year, .month], from: today)
|
let formatter = DateFormatter()
|
||||||
if let startOfMonth = Calendar.current.date(from: comps) {
|
formatter.dateFormat = "MMMM yyyy"
|
||||||
view.visibleDateComponents = Calendar.current.dateComponents(
|
let midWeek = weekDays.count >= 4 ? weekDays[3] : (weekDays.first ?? Date())
|
||||||
[.year, .month, .day], from: startOfMonth
|
return formatter.string(from: midWeek)
|
||||||
)
|
|
||||||
}
|
|
||||||
return view
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUIView(_ uiView: UICalendarView, context: Context) {
|
var body: some View {
|
||||||
// Reload all decorations when daysWithEntries changes
|
VStack(spacing: 4) {
|
||||||
uiView.reloadDecorations(forDateComponents: Array(daysWithEntries.map {
|
// Month / year header with navigation arrows
|
||||||
Calendar.current.dateComponents([.year, .month, .day], from: $0)
|
HStack {
|
||||||
}), animated: false)
|
Button { onSwipeRight() } label: {
|
||||||
}
|
Image(systemName: "chevron.left")
|
||||||
|
.font(.footnote.weight(.semibold))
|
||||||
func makeCoordinator() -> Coordinator {
|
.foregroundStyle(.secondary)
|
||||||
Coordinator(parent: self)
|
.frame(width: 32, height: 32)
|
||||||
}
|
}
|
||||||
|
Spacer()
|
||||||
final class Coordinator: NSObject, UICalendarViewDelegate, UICalendarSelectionSingleDateDelegate {
|
Text(monthYearLabel)
|
||||||
var parent: CalendarGridView
|
.font(.subheadline.weight(.semibold))
|
||||||
|
.foregroundStyle(.primary)
|
||||||
init(parent: CalendarGridView) {
|
Spacer()
|
||||||
self.parent = parent
|
Button { onSwipeLeft() } label: {
|
||||||
}
|
Image(systemName: "chevron.right")
|
||||||
|
.font(.footnote.weight(.semibold))
|
||||||
// Dot decorations for days that have entries
|
.foregroundStyle(.secondary)
|
||||||
func calendarView(_ calendarView: UICalendarView,
|
.frame(width: 32, height: 32)
|
||||||
decorationFor dateComponents: DateComponents) -> UICalendarView.Decoration? {
|
}
|
||||||
guard let date = Calendar.current.date(from: dateComponents) else { return nil }
|
|
||||||
let normalized = Calendar.current.startOfDay(for: date)
|
|
||||||
guard parent.daysWithEntries.contains(normalized) else { return nil }
|
|
||||||
return .default(color: .systemBlue, size: .small)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Date selection
|
|
||||||
func dateSelection(_ selection: UICalendarSelectionSingleDate,
|
|
||||||
didSelectDate dateComponents: DateComponents?) {
|
|
||||||
guard let comps = dateComponents,
|
|
||||||
let date = Calendar.current.date(from: comps) else {
|
|
||||||
parent.selectedDay = nil
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
let normalized = Calendar.current.startOfDay(for: date)
|
.padding(.horizontal, 8)
|
||||||
// Tap same day again to deselect
|
.padding(.top, 6)
|
||||||
if let current = parent.selectedDay, Calendar.current.isDate(current, inSameDayAs: normalized) {
|
|
||||||
parent.selectedDay = nil
|
|
||||||
selection.selectedDate = nil
|
|
||||||
} else {
|
|
||||||
parent.selectedDay = normalized
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func dateSelection(_ selection: UICalendarSelectionSingleDate,
|
// Day cells
|
||||||
canSelectDate dateComponents: DateComponents?) -> Bool { true }
|
HStack(spacing: 0) {
|
||||||
|
ForEach(weekDays, id: \.self) { day in
|
||||||
|
DayCell(
|
||||||
|
day: day,
|
||||||
|
isSelected: selectedDay.map { cal.isDate($0, inSameDayAs: day) } ?? false,
|
||||||
|
isToday: cal.isDateInToday(day),
|
||||||
|
hasDot: daysWithEntries.contains(cal.startOfDay(for: day))
|
||||||
|
)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onTapGesture {
|
||||||
|
let normalized = cal.startOfDay(for: day)
|
||||||
|
if let current = selectedDay, cal.isDate(current, inSameDayAs: normalized) {
|
||||||
|
selectedDay = nil
|
||||||
|
} else {
|
||||||
|
selectedDay = normalized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.bottom, 6)
|
||||||
|
}
|
||||||
|
.gesture(
|
||||||
|
DragGesture(minimumDistance: 40, coordinateSpace: .local)
|
||||||
|
.onEnded { value in
|
||||||
|
if value.translation.width < -40 {
|
||||||
|
onSwipeLeft()
|
||||||
|
} else if value.translation.width > 40 {
|
||||||
|
onSwipeRight()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Day Cell
|
||||||
|
|
||||||
|
private struct DayCell: View {
|
||||||
|
let day: Date
|
||||||
|
let isSelected: Bool
|
||||||
|
let isToday: Bool
|
||||||
|
let hasDot: Bool
|
||||||
|
|
||||||
|
private static let weekdayFormatter: DateFormatter = {
|
||||||
|
let f = DateFormatter()
|
||||||
|
f.dateFormat = "EEEEE" // Single letter: M T W T F S S
|
||||||
|
return f
|
||||||
|
}()
|
||||||
|
|
||||||
|
private static let dayFormatter: DateFormatter = {
|
||||||
|
let f = DateFormatter()
|
||||||
|
f.dateFormat = "d"
|
||||||
|
return f
|
||||||
|
}()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 3) {
|
||||||
|
Text(Self.weekdayFormatter.string(from: day))
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
|
ZStack {
|
||||||
|
if isSelected {
|
||||||
|
Circle()
|
||||||
|
.fill(Color.accentColor)
|
||||||
|
.frame(width: 32, height: 32)
|
||||||
|
} else if isToday {
|
||||||
|
Circle()
|
||||||
|
.strokeBorder(Color.accentColor, lineWidth: 1.5)
|
||||||
|
.frame(width: 32, height: 32)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(Self.dayFormatter.string(from: day))
|
||||||
|
.font(.callout.weight(isToday || isSelected ? .semibold : .regular))
|
||||||
|
.foregroundStyle(isSelected ? .white : (isToday ? Color.accentColor : .primary))
|
||||||
|
}
|
||||||
|
.frame(width: 32, height: 32)
|
||||||
|
|
||||||
|
// Dot indicator
|
||||||
|
Circle()
|
||||||
|
.fill(hasDot ? Color.accentColor.opacity(isSelected ? 0 : 0.7) : Color.clear)
|
||||||
|
.frame(width: 4, height: 4)
|
||||||
|
}
|
||||||
|
.padding(.vertical, 4)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user