import SwiftUI import SwiftData struct GoalsTabView: View { @Query(sort: \GoalEntity.createdAt) private var goals: [GoalEntity] @State private var showAddSheet = false var body: some View { NavigationStack { Group { if goals.isEmpty { EmptyStateView( icon: "target", message: String(localized: "goals.empty") ) } else { List(goals) { goal in GoalRowView(goal: goal) } .scrollContentBackground(.hidden) } } .background(Color.tfBackground) .navigationTitle(String(localized: "tab.goals")) .toolbar { ToolbarItem(placement: .primaryAction) { Button { showAddSheet = true } label: { Image(systemName: "plus") } .tint(Color.tfPrimary) } } .sheet(isPresented: $showAddSheet) { AddGoalSheet() } } } } // MARK: – Goal row private struct GoalRowView: View { let goal: GoalEntity var body: some View { HStack { VStack(alignment: .leading, spacing: 2) { Text(goal.title) .foregroundStyle(Color.tfOnBackground) HStack(spacing: 4) { Text(goal.period.localizedName) Text("·") Text(targetDescription) } .font(.caption) .foregroundStyle(Color.tfMuted) } Spacer() Text(goal.isActive ? String(localized: "goal.active") : String(localized: "goal.inactive")) .font(.caption) .padding(.horizontal, 8) .padding(.vertical, 3) .background( goal.isActive ? Color.tfPrimary.opacity(0.15) : Color.tfMuted.opacity(0.15) ) .foregroundStyle(goal.isActive ? Color.tfPrimary : Color.tfMuted) .clipShape(Capsule()) } .padding(.vertical, 4) } private var targetDescription: String { switch goal.targetType { case .duration(let min, let max): if let max { return "\(min.hhmmss) – \(max.hhmmss)" } return min.hhmmss case .count(let min, let max): if let max { return "\(min) – \(max)" } return "\(min)" } } } // MARK: – Add goal sheet private struct AddGoalSheet: View { @Environment(\.modelContext) private var context @Environment(\.dismiss) private var dismiss @Query(sort: \TaskItem.sortOrder) private var allTasks: [TaskItem] @Query(sort: \TagEntity.sortOrder) private var allTags: [TagEntity] @State private var title = "" @State private var selectedTaskID: UUID? = nil @State private var selectedTagID: UUID? = nil @State private var period: GoalPeriod = .daily @State private var unit: GoalUnit = .seconds @State private var targetMin: Double = 1800 @State private var hasMax = false @State private var targetMaxValue: Double = 3600 private var activeTasks: [TaskItem] { allTasks.filter { !$0.isArchived } } private var canSave: Bool { !title.trimmingCharacters(in: .whitespaces).isEmpty } var body: some View { NavigationStack { Form { Section(String(localized: "goal.form.section.title")) { TextField(String(localized: "goal.form.title.placeholder"), text: $title) } Section(String(localized: "goal.form.section.scope")) { Picker(String(localized: "goal.form.task"), selection: $selectedTaskID) { Text(String(localized: "goal.form.none")).tag(Optional.none) ForEach(activeTasks) { task in Label(task.name, systemImage: task.icon).tag(Optional(task.id)) } } Picker(String(localized: "goal.form.tag"), selection: $selectedTagID) { Text(String(localized: "goal.form.none")).tag(Optional.none) ForEach(allTags) { tag in Text(tag.name).tag(Optional(tag.id)) } } } Section(String(localized: "goal.form.section.period")) { Picker("", selection: $period) { ForEach(GoalPeriod.allCases, id: \.self) { p in Text(p.localizedName).tag(p) } } .pickerStyle(.segmented) .labelsHidden() } Section(String(localized: "goal.form.section.target")) { Picker("", selection: $unit) { Text(String(localized: "goal.unit.duration")).tag(GoalUnit.seconds) Text(String(localized: "goal.unit.count")).tag(GoalUnit.count) } .pickerStyle(.segmented) .labelsHidden() .onChange(of: unit) { _, newUnit in targetMin = newUnit == .seconds ? 1800 : 1 targetMaxValue = newUnit == .seconds ? 3600 : 5 } minValueStepper Toggle(String(localized: "goal.form.has_max"), isOn: $hasMax) if hasMax { maxValueStepper } } Section { Toggle(String(localized: "goal.active"), isOn: .constant(true)) } } .navigationTitle(String(localized: "goal.add.title")) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button(String(localized: "action.cancel")) { dismiss() } } ToolbarItem(placement: .confirmationAction) { Button(String(localized: "action.save")) { save() } .disabled(!canSave) .tint(Color.tfPrimary) } } } } @ViewBuilder private var minValueStepper: some View { if unit == .seconds { Stepper( value: $targetMin, in: 60...86400, step: 300 ) { Text("\(String(localized: "goal.form.min")): \(targetMin.hhmmss)") .font(.subheadline.monospacedDigit()) } } else { Stepper( value: $targetMin, in: 1...9999, step: 1 ) { Text("\(String(localized: "goal.form.min")): \(Int(targetMin))") .font(.subheadline) } } } @ViewBuilder private var maxValueStepper: some View { if unit == .seconds { Stepper( value: $targetMaxValue, in: targetMin...86400 * 7, step: 300 ) { Text("\(String(localized: "goal.form.max")): \(targetMaxValue.hhmmss)") .font(.subheadline.monospacedDigit()) } } else { Stepper( value: $targetMaxValue, in: targetMin...9999, step: 1 ) { Text("\(String(localized: "goal.form.max")): \(Int(targetMaxValue))") .font(.subheadline) } } } private func save() { let goal = GoalEntity( title: title.trimmingCharacters(in: .whitespaces), targetValue: targetMin, targetMax: hasMax ? targetMaxValue : nil, unit: unit, period: period ) goal.task = activeTasks.first { $0.id == selectedTaskID } goal.tag = allTags.first { $0.id == selectedTagID } context.insert(goal) dismiss() } } #Preview { GoalsTabView() .modelContainer(for: [GoalEntity.self, TaskItem.self, TagEntity.self], inMemory: true) .environment(AppPreferences()) }