161 lines
5.0 KiB
Swift
161 lines
5.0 KiB
Swift
import SwiftUI
|
|
import SwiftData
|
|
|
|
struct DashboardView: View {
|
|
@Environment(\.modelContext) private var modelContext
|
|
@State private var viewModel = DashboardViewModel()
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
Theme.background.ignoresSafeArea()
|
|
ScrollView {
|
|
VStack(spacing: 20) {
|
|
progressRing
|
|
if !viewModel.topGoalProgress.isEmpty {
|
|
topGoals
|
|
}
|
|
remainingTasksSection
|
|
}
|
|
.padding()
|
|
}
|
|
}
|
|
.navigationTitle(String(localized: "tab.dashboard"))
|
|
.onAppear { viewModel.setup(context: modelContext) }
|
|
}
|
|
|
|
// MARK: - Progress Ring
|
|
|
|
private var progressRing: some View {
|
|
VStack(spacing: 12) {
|
|
ZStack {
|
|
Circle()
|
|
.stroke(Theme.surface, lineWidth: 16)
|
|
Circle()
|
|
.trim(from: 0, to: viewModel.overallProgress)
|
|
.stroke(Theme.green, style: StrokeStyle(lineWidth: 16, lineCap: .round))
|
|
.rotationEffect(.degrees(-90))
|
|
.animation(.easeInOut(duration: 0.4), value: viewModel.overallProgress)
|
|
VStack(spacing: 4) {
|
|
Text("\(Int(viewModel.overallProgress * 100))%")
|
|
.font(.system(size: 34, weight: .bold, design: .rounded))
|
|
.foregroundStyle(Theme.primaryText)
|
|
Text(String(localized: "dashboard.progress.title"))
|
|
.font(.caption)
|
|
.foregroundStyle(Theme.primaryText.opacity(0.6))
|
|
}
|
|
}
|
|
.frame(width: 180, height: 180)
|
|
.padding(.top, 8)
|
|
}
|
|
}
|
|
|
|
// MARK: - Top Goals
|
|
|
|
private var topGoals: some View {
|
|
VStack(alignment: .leading, spacing: 10) {
|
|
Text(String(localized: "dashboard.goals.title"))
|
|
.font(.headline)
|
|
.foregroundStyle(Theme.primaryText)
|
|
|
|
VStack(spacing: 10) {
|
|
ForEach(viewModel.topGoalProgress) { progress in
|
|
TopGoalCard(progress: progress)
|
|
}
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
|
|
// MARK: - Remaining Tasks
|
|
|
|
private var remainingTasksSection: some View {
|
|
VStack(alignment: .leading, spacing: 10) {
|
|
Text(String(localized: "dashboard.tasks.title"))
|
|
.font(.headline)
|
|
.foregroundStyle(Theme.primaryText)
|
|
|
|
if viewModel.remainingTasks.isEmpty {
|
|
Text(String(localized: "dashboard.tasks.empty"))
|
|
.font(.subheadline)
|
|
.foregroundStyle(Theme.primaryText.opacity(0.5))
|
|
.frame(maxWidth: .infinity, alignment: .center)
|
|
.padding(.vertical, 24)
|
|
} else {
|
|
VStack(spacing: 8) {
|
|
ForEach(viewModel.remainingTasks) { task in
|
|
RemainingTaskRow(task: task) {
|
|
viewModel.addCount(to: task)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
}
|
|
|
|
// MARK: - Top Goal Card
|
|
|
|
private struct TopGoalCard: View {
|
|
let progress: GoalProgress
|
|
|
|
var body: some View {
|
|
HStack(spacing: 12) {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(progress.displayName)
|
|
.font(.body)
|
|
.fontWeight(.semibold)
|
|
.foregroundStyle(Theme.primaryText)
|
|
ProgressView(value: progress.ratio)
|
|
.tint(progress.isCompleted ? Theme.green : Theme.yellow)
|
|
}
|
|
Text(progress.percentText)
|
|
.font(.caption)
|
|
.fontWeight(.bold)
|
|
.foregroundStyle(progress.isCompleted ? Theme.green : Theme.yellow)
|
|
}
|
|
.padding()
|
|
.background(Theme.surface, in: RoundedRectangle(cornerRadius: 12))
|
|
}
|
|
}
|
|
|
|
// MARK: - Remaining Task Row
|
|
|
|
private struct RemainingTaskRow: View {
|
|
let task: TaskItem
|
|
let onAddCount: () -> Void
|
|
|
|
var body: some View {
|
|
HStack(spacing: 12) {
|
|
Text(task.icon)
|
|
.font(.title3)
|
|
.frame(width: 30)
|
|
|
|
Text(task.name)
|
|
.font(.body)
|
|
.foregroundStyle(Theme.primaryText)
|
|
|
|
Spacer()
|
|
|
|
if task.type == .count {
|
|
Button(action: onAddCount) {
|
|
Image(systemName: "plus.circle.fill")
|
|
.foregroundStyle(Theme.green)
|
|
}
|
|
.buttonStyle(.plain)
|
|
} else {
|
|
Image(systemName: "clock")
|
|
.foregroundStyle(Theme.primaryText.opacity(0.4))
|
|
}
|
|
}
|
|
.padding(12)
|
|
.background(Theme.surface, in: RoundedRectangle(cornerRadius: 10))
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
NavigationStack {
|
|
DashboardView()
|
|
}
|
|
}
|