import SwiftUI import SwiftData import Charts struct GoalView: View { @Environment(\.modelContext) private var modelContext @State private var viewModel = GoalViewModel() @State private var showAddSheet = false var body: some View { ZStack { Theme.background.ignoresSafeArea() Group { if viewModel.goals.isEmpty { emptyState } else { goalContent } } } .navigationTitle(String(localized: "tab.goals")) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button { showAddSheet = true } label: { Image(systemName: "plus") .foregroundStyle(Theme.green) } } } .sheet(isPresented: $showAddSheet) { AddGoalSheet { targetType, conditions, frequency, task, category in viewModel.addGoal(targetType: targetType, conditions: conditions, frequency: frequency, task: task, category: category) } } .onAppear { viewModel.setup(context: modelContext) } } private var emptyState: some View { VStack(spacing: 16) { Image(systemName: "flag.badge.ellipsis") .font(.system(size: 56)) .foregroundStyle(Theme.green.opacity(0.6)) Text(String(localized: "goal.empty")) .foregroundStyle(Theme.primaryText.opacity(0.6)) } } private var goalContent: some View { ScrollView { VStack(spacing: 16) { progressChart .padding(.horizontal) goalCards .padding(.horizontal) } .padding(.vertical) } } // MARK: - Chart private var progressChart: some View { VStack(alignment: .leading, spacing: 10) { Text(String(localized: "goal.chart.title")) .font(.headline) .foregroundStyle(Theme.primaryText) Chart(viewModel.goalProgress) { progress in BarMark( x: .value(String(localized: "goal.progress"), progress.ratio * 100), y: .value("", progress.displayName) ) .foregroundStyle(progress.isCompleted ? Theme.green : Theme.yellow) .cornerRadius(4) } .chartXScale(domain: 0.0...100.0) .chartXAxis { AxisMarks(values: [0.0, 25.0, 50.0, 75.0, 100.0]) { value in AxisGridLine() AxisValueLabel { if let v = value.as(Double.self) { Text("\(Int(v))%") .font(.caption2) .foregroundStyle(Theme.primaryText.opacity(0.5)) } } } } .chartYAxis { AxisMarks { _ in AxisValueLabel() } } .frame(height: CGFloat(viewModel.goalProgress.count) * 48 + 20) } .padding() .background(Theme.surface, in: RoundedRectangle(cornerRadius: 12)) } // MARK: - Goal Cards private var goalCards: some View { VStack(spacing: 12) { ForEach(viewModel.goalProgress) { progress in GoalCard(progress: progress) { viewModel.deleteGoal(progress.goal) } } } } } // MARK: - Goal Card private struct GoalCard: View { let progress: GoalProgress let onDelete: () -> Void var body: some View { VStack(alignment: .leading, spacing: 10) { HStack(alignment: .top) { VStack(alignment: .leading, spacing: 2) { Text(progress.displayName) .font(.body) .fontWeight(.semibold) .foregroundStyle(Theme.primaryText) Text(subtitleText) .font(.caption) .foregroundStyle(Theme.primaryText.opacity(0.5)) } Spacer() Button(role: .destructive, action: onDelete) { Image(systemName: "trash") .font(.caption) .foregroundStyle(.red.opacity(0.7)) } .buttonStyle(.plain) } ProgressView(value: progress.ratio) .tint(progress.isCompleted ? Theme.green : Theme.yellow) HStack { Text(currentText) .font(.caption) .foregroundStyle(Theme.primaryText.opacity(0.6)) Spacer() Text(progress.percentText) .font(.caption) .fontWeight(.bold) .foregroundStyle(progress.isCompleted ? Theme.green : Theme.yellow) } } .padding() .background(Theme.surface, in: RoundedRectangle(cornerRadius: 12)) } private var subtitleText: String { let cond = progress.goal.targetType == "count" ? String(localized: "goal.condition.count") : String(localized: "goal.condition.time") let freq: String switch progress.goal.frequency { case .daily: freq = String(localized: "goal.frequency.daily") case .weekly: freq = String(localized: "goal.frequency.weekly") case .monthly: freq = String(localized: "goal.frequency.monthly") } return "\(cond) ยท \(freq)" } private var currentText: String { if progress.goal.targetType == "count" { return "\(Int(progress.current)) / \(Int(progress.target)) \(String(localized: "goal.condition.count"))" } else { let currentMins = Int(progress.current) / 60 let targetMins = Int(progress.target) / 60 return "\(currentMins)m / \(targetMins)m" } } } // MARK: - Add Goal Sheet private struct AddGoalSheet: View { @Environment(\.dismiss) private var dismiss @Query(sort: \TaskItem.name) private var tasks: [TaskItem] @Query(sort: \Category.name) private var categories: [Category] @State private var targetType: GoalTargetType = .task @State private var selectedTask: TaskItem? @State private var selectedCategory: Category? @State private var conditionType: String = "count" @State private var threshold: Double = 5 @State private var frequency: GoalFrequency = .daily let onAdd: (String, Double, GoalFrequency, TaskItem?, Category?) -> Void var body: some View { NavigationStack { Form { Section(header: Text(String(localized: "goal.target"))) { Picker(String(localized: "goal.target"), selection: $targetType) { Text(String(localized: "goal.target.task")).tag(GoalTargetType.task) Text(String(localized: "goal.target.category")).tag(GoalTargetType.category) } .pickerStyle(.segmented) .onChange(of: targetType) { _, _ in selectedTask = nil selectedCategory = nil } if targetType == .task { Picker(String(localized: "goal.target.task"), selection: $selectedTask) { Text(String(localized: "goal.no.target")).tag(Optional.none) ForEach(tasks) { task in Text("\(task.icon) \(task.name)").tag(Optional(task)) } } } else { Picker(String(localized: "goal.target.category"), selection: $selectedCategory) { Text(String(localized: "goal.no.target")).tag(Optional.none) ForEach(categories) { cat in Text(cat.name).tag(Optional(cat)) } } } } Section(header: Text(String(localized: "goal.condition"))) { Picker(String(localized: "goal.condition"), selection: $conditionType) { Text(String(localized: "goal.condition.count")).tag("count") Text(String(localized: "goal.condition.time")).tag("duration") } .pickerStyle(.segmented) .onChange(of: conditionType) { _, _ in threshold = 5 } } Section(header: Text(String(localized: "goal.threshold"))) { HStack { Slider( value: $threshold, in: conditionType == "count" ? 1.0...100.0 : 5.0...240.0, step: conditionType == "count" ? 1.0 : 5.0 ) .tint(Theme.green) Text(thresholdText) .font(.body) .monospacedDigit() .foregroundStyle(Theme.green) .frame(width: 52, alignment: .trailing) } } Section(header: Text(String(localized: "goal.frequency"))) { Picker(String(localized: "goal.frequency"), selection: $frequency) { Text(String(localized: "goal.frequency.daily")).tag(GoalFrequency.daily) Text(String(localized: "goal.frequency.weekly")).tag(GoalFrequency.weekly) Text(String(localized: "goal.frequency.monthly")).tag(GoalFrequency.monthly) } .pickerStyle(.segmented) } } .navigationTitle(String(localized: "goal.add")) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button(String(localized: "button.cancel")) { dismiss() } } ToolbarItem(placement: .confirmationAction) { Button(String(localized: "button.add")) { let storedConditions = conditionType == "duration" ? threshold * 60.0 : threshold onAdd( conditionType, storedConditions, frequency, targetType == .task ? selectedTask : nil, targetType == .category ? selectedCategory : nil ) dismiss() } .disabled(!canAdd) } } } } private var thresholdText: String { if conditionType == "count" { return "\(Int(threshold))" } else { let h = Int(threshold) / 60 let m = Int(threshold) % 60 return h > 0 ? "\(h)h\(m)m" : "\(Int(threshold))m" } } private var canAdd: Bool { targetType == .task ? selectedTask != nil : selectedCategory != nil } } #Preview { NavigationStack { GoalView() } }