diff --git a/ios/TimeTracker/TimeTracker/Features/TimeEntries/TimeEntriesView.swift b/ios/TimeTracker/TimeTracker/Features/TimeEntries/TimeEntriesView.swift index fdf252f..7b17d49 100644 --- a/ios/TimeTracker/TimeTracker/Features/TimeEntries/TimeEntriesView.swift +++ b/ios/TimeTracker/TimeTracker/Features/TimeEntries/TimeEntriesView.swift @@ -2,27 +2,17 @@ import SwiftUI struct TimeEntriesView: View { @StateObject private var viewModel = TimeEntriesViewModel() - @State private var selectedDay: Date? = Calendar.current.startOfDay(for: Date()) - @State private var visibleWeekStart: Date = Self.mondayOfWeek(containing: Date()) + @State private var selectedDate: Date = Calendar.current.startOfDay(for: Date()) + + // For infinite paging, we use an offset from 'today' + @State private var dayOffset: Int = 0 + @State private var showFilterSheet = false @State private var showAddEntry = false @State private var entryToEdit: TimeEntry? @State private var entryToDelete: TimeEntry? @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 { NavigationStack { Group { @@ -37,6 +27,7 @@ struct TimeEntriesView: View { } } .navigationTitle("Entries") + .navigationBarTitleDisplayMode(.inline) .toolbar { toolbarContent } .task { await viewModel.loadEntries() } .refreshable { await viewModel.loadEntries() } @@ -81,34 +72,49 @@ struct TimeEntriesView: View { Image(systemName: viewModel.activeFilters.isEmpty ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill") } } + + // Place the DatePicker in the principal placement (center of nav bar) + ToolbarItem(placement: .principal) { + DatePicker( + "", + selection: Binding( + get: { + Calendar.current.date(byAdding: .day, value: dayOffset, to: Calendar.current.startOfDay(for: Date())) ?? Date() + }, + set: { newDate in + let today = Calendar.current.startOfDay(for: Date()) + let normalizedNewDate = Calendar.current.startOfDay(for: newDate) + let components = Calendar.current.dateComponents([.day], from: today, to: normalizedNewDate) + if let dayDifference = components.day { + dayOffset = dayDifference + } + } + ), + displayedComponents: .date + ) + .datePickerStyle(.compact) + .labelsHidden() + .environment(\.locale, Locale.current) // Ensure correct start of week + } } // MARK: - Main content private var mainContent: some View { - VStack(spacing: 0) { - WeekStripView( - weekDays: visibleWeekDays, - selectedDay: $selectedDay, - daysWithEntries: viewModel.daysWithEntries, - onSwipeLeft: { - visibleWeekStart = Calendar.current.date(byAdding: .weekOfYear, value: 1, to: visibleWeekStart) ?? visibleWeekStart - }, - onSwipeRight: { - visibleWeekStart = Calendar.current.date(byAdding: .weekOfYear, value: -1, to: visibleWeekStart) ?? visibleWeekStart - } - ) - - Divider() - - ScrollView { - if let day = selectedDay { - dayEntriesSection(for: day) - } else { - allEntriesSection + // We use a wide range of offsets to simulate infinite paging. + // -1000 days is roughly 2.7 years in the past. + // 1000 days is roughly 2.7 years in the future. + TabView(selection: $dayOffset) { + ForEach(-1000...1000, id: \.self) { offset in + let dayForPage = Calendar.current.date(byAdding: .day, value: offset, to: Calendar.current.startOfDay(for: Date())) ?? Date() + + ScrollView { + dayEntriesSection(for: dayForPage) } + .tag(offset) // Important: tag must match the selection type (Int) } } + .tabViewStyle(.page(indexDisplayMode: .never)) // Native swipe gesture } // MARK: - Day entries section @@ -116,7 +122,8 @@ struct TimeEntriesView: View { private func dayEntriesSection(for day: Date) -> some View { let dayEntries = viewModel.entries(for: day) return VStack(alignment: .leading, spacing: 0) { - // Section header + + // Optional: A small summary header for the day HStack { Text(dayTitle(day)) .font(.subheadline) @@ -155,41 +162,7 @@ struct TimeEntriesView: View { } } } - } - - // MARK: - All entries (no day selected) — grouped by day - - private var allEntriesSection: some View { - LazyVStack(alignment: .leading, pinnedViews: .sectionHeaders) { - ForEach(viewModel.entriesByDay, id: \.date) { group in - Section { - ForEach(Array(group.entries.enumerated()), id: \.element.id) { index, entry in - EntryRow(entry: entry) - .contentShape(Rectangle()) - .onTapGesture { entryToEdit = entry } - .swipeActions(edge: .trailing, allowsFullSwipe: false) { - Button(role: .destructive) { - entryToDelete = entry - showDeleteConfirmation = true - } label: { - Label("Delete", systemImage: "trash") - } - } - if index < group.entries.count - 1 { - Divider().padding(.leading, 56) - } - } - } header: { - Text(dayTitle(group.date)) - .font(.subheadline) - .fontWeight(.semibold) - .padding(.horizontal) - .padding(.vertical, 8) - .frame(maxWidth: .infinity, alignment: .leading) - .background(.regularMaterial) - } - } - } + .padding(.bottom, 40) // Give some breathing room at the bottom of the scroll } // MARK: - Helpers @@ -198,6 +171,7 @@ struct TimeEntriesView: View { let cal = Calendar.current if cal.isDateInToday(date) { return "Today" } if cal.isDateInYesterday(date) { return "Yesterday" } + if cal.isDateInTomorrow(date) { return "Tomorrow" } let formatter = DateFormatter() formatter.dateFormat = "EEEE, MMM d" return formatter.string(from: date) @@ -259,139 +233,6 @@ struct EntryRow: View { } } -// MARK: - Week Strip View - -struct WeekStripView: View { - let weekDays: [Date] - @Binding var selectedDay: Date? - let daysWithEntries: Set - let onSwipeLeft: () -> Void - let onSwipeRight: () -> Void - - @GestureState private var dragOffset: CGFloat = 0 - - private let cal = Calendar.current - - private var monthYearLabel: String { - // Show the month/year of the majority of days in the strip - let formatter = DateFormatter() - formatter.dateFormat = "MMMM yyyy" - let midWeek = weekDays.count >= 4 ? weekDays[3] : (weekDays.first ?? Date()) - return formatter.string(from: midWeek) - } - - var body: some View { - VStack(spacing: 4) { - // Month / year header with navigation arrows - HStack { - Button { onSwipeRight() } label: { - Image(systemName: "chevron.left") - .font(.footnote.weight(.semibold)) - .foregroundStyle(.secondary) - .frame(width: 32, height: 32) - } - Spacer() - Text(monthYearLabel) - .font(.subheadline.weight(.semibold)) - .foregroundStyle(.primary) - Spacer() - Button { onSwipeLeft() } label: { - Image(systemName: "chevron.right") - .font(.footnote.weight(.semibold)) - .foregroundStyle(.secondary) - .frame(width: 32, height: 32) - } - } - .padding(.horizontal, 8) - .padding(.top, 6) - - // Day cells - 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) - } -} // MARK: - Filter Sheet