diff --git a/PulseHealth/Models/SavingsModels.swift b/PulseHealth/Models/SavingsModels.swift new file mode 100644 index 0000000..fd66ce3 --- /dev/null +++ b/PulseHealth/Models/SavingsModels.swift @@ -0,0 +1,70 @@ +import Foundation + +struct SavingsCategory: Codable, Identifiable { + let id: Int + var name: String + var description: String? + var isDeposit: Bool? + var isCredit: Bool? + var isAccount: Bool? + var isRecurring: Bool? + var isMulti: Bool? + var isClosed: Bool? + var currentAmount: Double? + var depositAmount: Double? + var interestRate: Double? + var depositStartDate: String? + var depositEndDate: String? + var recurringAmount: Double? + + var icon: String { + if isDeposit == true { return "percent" } + if isAccount == true { return "building.columns.fill" } + if isRecurring == true { return "arrow.clockwise" } + return "banknote.fill" + } + + var color: String { + if isDeposit == true { return "ffa502" } + if isAccount == true { return "7c3aed" } + if isRecurring == true { return "00d4aa" } + return "8888aa" + } + + var typeLabel: String { + if isDeposit == true { return "Вклад \(Int(interestRate ?? 0))%" } + if isAccount == true { return "Счёт" } + if isRecurring == true { return "Накопления" } + return "Копилка" + } + + enum CodingKeys: String, CodingKey { + case id, name, description + case isDeposit = "is_deposit" + case isCredit = "is_credit" + case isAccount = "is_account" + case isRecurring = "is_recurring" + case isMulti = "is_multi" + case isClosed = "is_closed" + case currentAmount = "current_amount" + case depositAmount = "deposit_amount" + case interestRate = "interest_rate" + case depositStartDate = "deposit_start_date" + case depositEndDate = "deposit_end_date" + case recurringAmount = "recurring_amount" + } +} + +struct SavingsStats: Codable { + var totalBalance: Double? + var totalDeposits: Double? + var totalWithdrawals: Double? + var categoriesCount: Int? + + enum CodingKeys: String, CodingKey { + case totalBalance = "total_balance" + case totalDeposits = "total_deposits" + case totalWithdrawals = "total_withdrawals" + case categoriesCount = "categories_count" + } +} diff --git a/PulseHealth/Services/APIService.swift b/PulseHealth/Services/APIService.swift index 0fbe648..51b4cde 100644 --- a/PulseHealth/Services/APIService.swift +++ b/PulseHealth/Services/APIService.swift @@ -110,6 +110,16 @@ class APIService { return try await fetch("/finance/transactions", method: "POST", token: token, body: body) } + // MARK: - Savings + + func getSavingsCategories(token: String) async throws -> [SavingsCategory] { + return try await fetch("/savings/categories", token: token) + } + + func getSavingsStats(token: String) async throws -> SavingsStats { + return try await fetch("/savings/stats", token: token) + } + func getFinanceCategories(token: String) async throws -> [FinanceCategory] { return try await fetch("/finance/categories", token: token) } diff --git a/PulseHealth/Views/Dashboard/DashboardView.swift b/PulseHealth/Views/Dashboard/DashboardView.swift index ad9c98e..ba28b39 100644 --- a/PulseHealth/Views/Dashboard/DashboardView.swift +++ b/PulseHealth/Views/Dashboard/DashboardView.swift @@ -99,13 +99,13 @@ struct DashboardView: View { Spacer(minLength: 20) } } - .refreshable { await loadData() } + .refreshable { await loadData(refresh: true) } } .task { await loadData() } } - func loadData() async { - isLoading = true + func loadData(refresh: Bool = false) async { + if !refresh { isLoading = true } async let t = APIService.shared.getTodayTasks(token: authManager.token) async let h = APIService.shared.getHabits(token: authManager.token) async let r = HealthAPIService.shared.getReadiness(apiKey: authManager.healthApiKey) diff --git a/PulseHealth/Views/Finance/AddTransactionView.swift b/PulseHealth/Views/Finance/AddTransactionView.swift index c6cd78e..524d087 100644 --- a/PulseHealth/Views/Finance/AddTransactionView.swift +++ b/PulseHealth/Views/Finance/AddTransactionView.swift @@ -5,78 +5,131 @@ struct AddTransactionView: View { @EnvironmentObject var authManager: AuthManager let categories: [FinanceCategory] let onAdded: () async -> Void - + @State private var amount = "" @State private var description = "" @State private var type = "expense" @State private var selectedCategoryId: Int? = nil @State private var isLoading = false - + var filteredCategories: [FinanceCategory] { categories.filter { $0.type == type } } - + var isExpense: Bool { type == "expense" } + var body: some View { - NavigationView { - ZStack { - Color(hex: "0a0a1a").ignoresSafeArea() - VStack(spacing: 16) { - Picker("", selection: $type) { - Text("Расход").tag("expense") - Text("Доход").tag("income") - } - .pickerStyle(.segmented) - - TextField("Сумма", text: $amount) - .keyboardType(.decimalPad) - .padding().background(Color.white.opacity(0.08)).cornerRadius(12).foregroundColor(.white) - - TextField("Описание", text: $description) - .padding().background(Color.white.opacity(0.08)).cornerRadius(12).foregroundColor(.white) - - if !filteredCategories.isEmpty { - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 8) { - ForEach(filteredCategories) { cat in - Button(action: { selectedCategoryId = cat.id }) { - HStack(spacing: 4) { - Text(cat.icon ?? "").font(.caption) - Text(cat.name).font(.caption) + ZStack { + Color(hex: "0a0a1a").ignoresSafeArea() + + VStack(spacing: 0) { + // Handle + RoundedRectangle(cornerRadius: 3) + .fill(Color.white.opacity(0.2)) + .frame(width: 40, height: 4) + .padding(.top, 12) + + // Header + HStack { + Button("Отмена") { isPresented = false }.foregroundColor(Color(hex: "8888aa")) + Spacer() + Text("Новая операция").font(.headline).foregroundColor(.white) + Spacer() + Button(action: save) { + if isLoading { ProgressView().tint(Color(hex: "00d4aa")).scaleEffect(0.8) } + else { Text("Добавить").foregroundColor(amount.isEmpty ? Color(hex: "8888aa") : Color(hex: "00d4aa")).fontWeight(.semibold) } + }.disabled(amount.isEmpty || isLoading) + } + .padding(.horizontal, 20).padding(.vertical, 16) + + Divider().background(Color.white.opacity(0.1)) + + ScrollView { + VStack(spacing: 20) { + // Type toggle + HStack(spacing: 0) { + Button(action: { type = "expense" }) { + Text("Расход") + .font(.callout.bold()) + .foregroundColor(isExpense ? .black : Color(hex: "ff4757")) + .frame(maxWidth: .infinity).padding(.vertical, 12) + .background(isExpense ? Color(hex: "ff4757") : Color.clear) + } + Button(action: { type = "income" }) { + Text("Доход") + .font(.callout.bold()) + .foregroundColor(!isExpense ? .black : Color(hex: "00d4aa")) + .frame(maxWidth: .infinity).padding(.vertical, 12) + .background(!isExpense ? Color(hex: "00d4aa") : Color.clear) + } + } + .background(Color.white.opacity(0.07)) + .cornerRadius(12) + + // Amount + VStack(spacing: 8) { + Text(isExpense ? "Сумма расхода" : "Сумма дохода") + .font(.caption).foregroundColor(Color(hex: "8888aa")) + HStack { + Text(isExpense ? "−" : "+") + .font(.title.bold()) + .foregroundColor(isExpense ? Color(hex: "ff4757") : Color(hex: "00d4aa")) + TextField("0", text: $amount) + .keyboardType(.decimalPad) + .font(.system(size: 36, weight: .bold)) + .foregroundColor(.white) + .multilineTextAlignment(.center) + Text("₽") + .font(.title.bold()) + .foregroundColor(Color(hex: "8888aa")) + } + .padding(20) + .background(RoundedRectangle(cornerRadius: 16).fill(Color.white.opacity(0.07))) + } + + // Description + VStack(alignment: .leading, spacing: 8) { + Label("Описание", systemImage: "text.alignleft").font(.caption).foregroundColor(Color(hex: "8888aa")) + TextField("Комментарий...", text: $description) + .foregroundColor(.white).padding(14) + .background(RoundedRectangle(cornerRadius: 12).fill(Color.white.opacity(0.07))) + } + + // Categories + if !filteredCategories.isEmpty { + VStack(alignment: .leading, spacing: 8) { + Label("Категория", systemImage: "tag.fill").font(.caption).foregroundColor(Color(hex: "8888aa")) + LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))], spacing: 8) { + ForEach(filteredCategories) { cat in + Button(action: { selectedCategoryId = selectedCategoryId == cat.id ? nil : cat.id }) { + HStack(spacing: 6) { + Text(cat.icon ?? "").font(.callout) + Text(cat.name).font(.caption).lineLimit(1) + } + .foregroundColor(selectedCategoryId == cat.id ? .black : .white) + .padding(.horizontal, 10).padding(.vertical, 8) + .frame(maxWidth: .infinity) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(selectedCategoryId == cat.id ? Color(hex: "00d4aa") : Color.white.opacity(0.07)) + ) } - .padding(.horizontal, 12).padding(.vertical, 6) - .background(RoundedRectangle(cornerRadius: 20) - .fill(selectedCategoryId == cat.id ? Color(hex: "00d4aa").opacity(0.3) : Color.white.opacity(0.08))) - .foregroundColor(selectedCategoryId == cat.id ? Color(hex: "00d4aa") : .white) } } - }.padding(.horizontal) + } } } - Spacer() - }.padding() - } - .navigationTitle("Новая операция") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .cancellationAction) { Button("Отмена") { isPresented = false } } - ToolbarItem(placement: .confirmationAction) { - Button("Добавить") { - guard let a = Double(amount.replacingOccurrences(of: ",", with: ".")) else { return } - isLoading = true - Task { - let req = CreateTransactionRequest( - amount: a, - categoryId: selectedCategoryId, - description: description.isEmpty ? nil : description, - type: type - ) - try? await APIService.shared.createTransaction(token: authManager.token, request: req) - await onAdded() - await MainActor.run { isPresented = false } - } - } - .disabled(amount.isEmpty || isLoading) + .padding(20) } } - .preferredColorScheme(.dark) + } + } + + func save() { + guard let a = Double(amount.replacingOccurrences(of: ",", with: ".")) else { return } + isLoading = true + Task { + let req = CreateTransactionRequest(amount: a, categoryId: selectedCategoryId, description: description.isEmpty ? nil : description, type: type) + try? await APIService.shared.createTransaction(token: authManager.token, request: req) + await onAdded() + await MainActor.run { isPresented = false } } } } diff --git a/PulseHealth/Views/Finance/FinanceView.swift b/PulseHealth/Views/Finance/FinanceView.swift index 9956b42..9ee19a1 100644 --- a/PulseHealth/Views/Finance/FinanceView.swift +++ b/PulseHealth/Views/Finance/FinanceView.swift @@ -48,13 +48,16 @@ struct FinanceView: View { } .sheet(isPresented: $showAddTransaction) { AddTransactionView(isPresented: $showAddTransaction, categories: categories) { await loadData() } + .presentationDetents([.medium, .large]) + .presentationDragIndicator(.visible) + .presentationBackground(Color(hex: "0a0a1a")) } .task { await loadData() } - .refreshable { await loadData() } + .refreshable { await loadData(refresh: true) } } - func loadData() async { - isLoading = true + func loadData(refresh: Bool = false) async { + if !refresh { isLoading = true } async let s = APIService.shared.getFinanceSummary(token: authManager.token) async let t = APIService.shared.getTransactions(token: authManager.token) async let c = APIService.shared.getFinanceCategories(token: authManager.token) diff --git a/PulseHealth/Views/Habits/HabitsView.swift b/PulseHealth/Views/Habits/HabitsView.swift index d28f6a9..026f495 100644 --- a/PulseHealth/Views/Habits/HabitsView.swift +++ b/PulseHealth/Views/Habits/HabitsView.swift @@ -56,11 +56,11 @@ struct HabitsView: View { } } .task { await loadHabits() } - .refreshable { await loadHabits() } + .refreshable { await loadHabits(refresh: true) } } - func loadHabits() async { - isLoading = true + func loadHabits(refresh: Bool = false) async { + if !refresh { isLoading = true } habits = (try? await APIService.shared.getHabits(token: authManager.token)) ?? [] isLoading = false } diff --git a/PulseHealth/Views/Health/HealthView.swift b/PulseHealth/Views/Health/HealthView.swift index 7d767f8..be3cb9c 100644 --- a/PulseHealth/Views/Health/HealthView.swift +++ b/PulseHealth/Views/Health/HealthView.swift @@ -66,7 +66,7 @@ struct HealthView: View { } } .refreshable { - await loadData() + await loadData(refresh: true) } } .toast(isShowing: $showToast, message: toastMessage, isSuccess: toastSuccess) @@ -174,8 +174,8 @@ struct HealthView: View { // MARK: - Load Data - func loadData() async { - isLoading = true + func loadData(refresh: Bool = false) async { + if !refresh { isLoading = true } let apiKey = authManager.healthApiKey diff --git a/PulseHealth/Views/MainTabView.swift b/PulseHealth/Views/MainTabView.swift index 6ceff2f..bd7395d 100644 --- a/PulseHealth/Views/MainTabView.swift +++ b/PulseHealth/Views/MainTabView.swift @@ -17,8 +17,8 @@ struct MainTabView: View { HealthView() .tabItem { Label("Здоровье", systemImage: "heart.fill") } - FinanceView() - .tabItem { Label("Финансы", systemImage: "rublesign.circle.fill") } + SavingsView() + .tabItem { Label("Накопления", systemImage: "chart.bar.fill") } ProfileView() .tabItem { Label("Профиль", systemImage: "person.fill") } diff --git a/PulseHealth/Views/Savings/SavingsView.swift b/PulseHealth/Views/Savings/SavingsView.swift new file mode 100644 index 0000000..5922361 --- /dev/null +++ b/PulseHealth/Views/Savings/SavingsView.swift @@ -0,0 +1,182 @@ +import SwiftUI + +struct SavingsView: View { + @EnvironmentObject var authManager: AuthManager + @State private var categories: [SavingsCategory] = [] + @State private var stats: SavingsStats? + @State private var isLoading = true + + var body: some View { + ZStack { + Color(hex: "0a0a1a").ignoresSafeArea() + VStack(spacing: 0) { + HStack { + Text("Накопления").font(.title.bold()).foregroundColor(.white) + Spacer() + }.padding() + + if isLoading { + ProgressView().tint(Color(hex: "00d4aa")).padding(.top, 40) + Spacer() + } else { + ScrollView { + VStack(spacing: 16) { + if let s = stats { + SavingsTotalCard(stats: s) + } + + VStack(spacing: 10) { + ForEach(categories) { cat in + SavingsCategoryCard(category: cat) + } + } + .padding(.horizontal) + } + } + } + } + } + .task { await loadData() } + .refreshable { await loadData(refresh: true) } + } + + func loadData(refresh: Bool = false) async { + if !refresh { isLoading = true } + async let cats = APIService.shared.getSavingsCategories(token: authManager.token) + async let st = APIService.shared.getSavingsStats(token: authManager.token) + categories = (try? await cats) ?? [] + stats = try? await st + isLoading = false + } +} + +struct SavingsTotalCard: View { + let stats: SavingsStats + + var body: some View { + VStack(spacing: 20) { + VStack(spacing: 6) { + Text("Общий баланс").font(.subheadline).foregroundColor(Color(hex: "8888aa")) + Text(formatAmount(stats.totalBalance ?? 0)) + .font(.system(size: 36, weight: .bold)) + .foregroundColor(.white) + } + + HStack(spacing: 0) { + VStack(spacing: 4) { + Text("Пополнения").font(.caption).foregroundColor(Color(hex: "8888aa")) + Text("+\(formatAmount(stats.totalDeposits ?? 0))").font(.callout.bold()).foregroundColor(Color(hex: "00d4aa")) + } + Spacer() + VStack(spacing: 4) { + Text("Снятия").font(.caption).foregroundColor(Color(hex: "8888aa")) + Text("-\(formatAmount(stats.totalWithdrawals ?? 0))").font(.callout.bold()).foregroundColor(Color(hex: "ff4757")) + } + Spacer() + VStack(spacing: 4) { + Text("Категорий").font(.caption).foregroundColor(Color(hex: "8888aa")) + Text("\(stats.categoriesCount ?? 0)").font(.callout.bold()).foregroundColor(.white) + } + } + } + .padding(20) + .background( + LinearGradient(colors: [Color(hex: "1a1a3e"), Color(hex: "12122a")], startPoint: .topLeading, endPoint: .bottomTrailing) + ) + .cornerRadius(20) + .overlay(RoundedRectangle(cornerRadius: 20).stroke(Color(hex: "00d4aa").opacity(0.3), lineWidth: 1)) + .padding(.horizontal) + } + + func formatAmount(_ v: Double) -> String { + if v >= 1_000_000 { + return String(format: "%.2f млн ₽", v / 1_000_000) + } else if v >= 1000 { + return String(format: "%.0f ₽", v) + } + return String(format: "%.0f ₽", v) + } +} + +struct SavingsCategoryCard: View { + let category: SavingsCategory + @State private var appeared = false + + var body: some View { + VStack(spacing: 12) { + HStack(spacing: 12) { + ZStack { + Circle() + .fill(Color(hex: category.color).opacity(0.2)) + .frame(width: 46, height: 46) + Image(systemName: category.icon) + .foregroundColor(Color(hex: category.color)) + .font(.title3) + } + + VStack(alignment: .leading, spacing: 4) { + Text(category.name).font(.callout.bold()).foregroundColor(.white) + Text(category.typeLabel).font(.caption).foregroundColor(Color(hex: "8888aa")) + } + + Spacer() + + VStack(alignment: .trailing, spacing: 2) { + Text(formatAmount(category.currentAmount ?? 0)) + .font(.callout.bold()) + .foregroundColor(Color(hex: category.color)) + if let end = category.depositEndDate { + Text("до \(formatDate(end))") + .font(.caption2) + .foregroundColor(Color(hex: "8888aa")) + } + } + } + + if category.isDeposit == true, let target = category.depositAmount, target > 0 { + let progress = min((category.currentAmount ?? 0) / target, 1.0) + VStack(spacing: 4) { + GeometryReader { geo in + ZStack(alignment: .leading) { + RoundedRectangle(cornerRadius: 4).fill(Color.white.opacity(0.08)) + RoundedRectangle(cornerRadius: 4) + .fill(LinearGradient(colors: [Color(hex: "ffa502"), Color(hex: "ff6b35")], startPoint: .leading, endPoint: .trailing)) + .frame(width: geo.size.width * CGFloat(appeared ? progress : 0)) + .animation(.easeInOut(duration: 0.8), value: appeared) + } + } + .frame(height: 6) + HStack { + Text("\(Int(progress * 100))%") + .font(.caption2).foregroundColor(Color(hex: "ffa502")) + Spacer() + Text("цель: \(formatAmount(target))") + .font(.caption2).foregroundColor(Color(hex: "8888aa")) + } + } + } + } + .padding(16) + .background( + RoundedRectangle(cornerRadius: 16) + .fill(Color.white.opacity(0.04)) + .overlay(RoundedRectangle(cornerRadius: 16).stroke(Color(hex: category.color).opacity(0.15), lineWidth: 1)) + ) + .opacity(appeared ? 1 : 0) + .offset(y: appeared ? 0 : 20) + .onAppear { + withAnimation(.easeOut(duration: 0.4)) { appeared = true } + } + } + + func formatAmount(_ v: Double) -> String { + if v >= 1_000_000 { return String(format: "%.2f млн ₽", v / 1_000_000) } + return String(format: "%.0f ₽", v) + } + + func formatDate(_ s: String) -> String { + let parts = s.prefix(10).split(separator: "-") + guard parts.count == 3 else { return s } + return "\(parts[2]).\(parts[1]).\(parts[0])" + } +} diff --git a/PulseHealth/Views/Tasks/AddTaskView.swift b/PulseHealth/Views/Tasks/AddTaskView.swift index bba6f0b..28d41a4 100644 --- a/PulseHealth/Views/Tasks/AddTaskView.swift +++ b/PulseHealth/Views/Tasks/AddTaskView.swift @@ -4,54 +4,107 @@ struct AddTaskView: View { @Binding var isPresented: Bool @EnvironmentObject var authManager: AuthManager let onAdded: () async -> Void - + @State private var title = "" @State private var description = "" @State private var priority: Int = 2 @State private var isLoading = false - - private let priorities = [(1, "Низкий"), (2, "Средний"), (3, "Высокий"), (4, "Срочный")] - + + let priorities: [(Int, String, String)] = [ + (1, "Низкий", "8888aa"), + (2, "Средний", "ffa502"), + (3, "Высокий", "ff4757"), + (4, "Срочный", "ff0000") + ] + var body: some View { - NavigationView { - ZStack { - Color(hex: "0a0a1a").ignoresSafeArea() - VStack(spacing: 20) { - TextField("Название задачи", text: $title) - .padding().background(Color.white.opacity(0.08)).cornerRadius(12).foregroundColor(.white) - TextField("Описание (необязательно)", text: $description) - .padding().background(Color.white.opacity(0.08)).cornerRadius(12).foregroundColor(.white) - Picker("Приоритет", selection: $priority) { - ForEach(priorities, id: \.0) { p in Text(p.1).tag(p.0) } - } - .pickerStyle(.segmented) - Spacer() - }.padding() - } - .navigationTitle("Новая задача") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .cancellationAction) { + ZStack { + Color(hex: "0a0a1a").ignoresSafeArea() + + VStack(spacing: 0) { + // Handle + RoundedRectangle(cornerRadius: 3) + .fill(Color.white.opacity(0.2)) + .frame(width: 40, height: 4) + .padding(.top, 12) + + // Header + HStack { Button("Отмена") { isPresented = false } - } - ToolbarItem(placement: .confirmationAction) { - Button("Добавить") { - isLoading = true - Task { - let req = CreateTaskRequest( - title: title, - description: description.isEmpty ? nil : description, - priority: priority - ) - try? await APIService.shared.createTask(token: authManager.token, request: req) - await onAdded() - await MainActor.run { isPresented = false } - } + .foregroundColor(Color(hex: "8888aa")) + Spacer() + Text("Новая задача").font(.headline).foregroundColor(.white) + Spacer() + Button(action: save) { + if isLoading { ProgressView().tint(Color(hex: "00d4aa")).scaleEffect(0.8) } + else { Text("Добавить").foregroundColor(title.isEmpty ? Color(hex: "8888aa") : Color(hex: "00d4aa")).fontWeight(.semibold) } } .disabled(title.isEmpty || isLoading) } + .padding(.horizontal, 20) + .padding(.vertical, 16) + + Divider().background(Color.white.opacity(0.1)) + + ScrollView { + VStack(spacing: 16) { + // Title field + VStack(alignment: .leading, spacing: 8) { + Label("Название", systemImage: "pencil").font(.caption).foregroundColor(Color(hex: "8888aa")) + TextField("Что нужно сделать?", text: $title, axis: .vertical) + .lineLimit(1...3) + .foregroundColor(.white) + .padding(14) + .background(RoundedRectangle(cornerRadius: 12).fill(Color.white.opacity(0.07))) + } + + // Description field + VStack(alignment: .leading, spacing: 8) { + Label("Описание", systemImage: "text.alignleft").font(.caption).foregroundColor(Color(hex: "8888aa")) + TextField("Дополнительные детали...", text: $description, axis: .vertical) + .lineLimit(2...5) + .foregroundColor(.white) + .padding(14) + .background(RoundedRectangle(cornerRadius: 12).fill(Color.white.opacity(0.07))) + } + + // Priority selector + VStack(alignment: .leading, spacing: 8) { + Label("Приоритет", systemImage: "flag.fill").font(.caption).foregroundColor(Color(hex: "8888aa")) + HStack(spacing: 8) { + ForEach(priorities, id: \.0) { p in + Button(action: { priority = p.0 }) { + Text(p.1) + .font(.caption.bold()) + .foregroundColor(priority == p.0 ? .black : Color(hex: p.2)) + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background( + RoundedRectangle(cornerRadius: 20) + .fill(priority == p.0 ? Color(hex: p.2) : Color(hex: p.2).opacity(0.15)) + ) + } + } + } + } + } + .padding(20) + } } - .preferredColorScheme(.dark) + } + } + + func save() { + isLoading = true + Task { + let req = CreateTaskRequest( + title: title, + description: description.isEmpty ? nil : description, + priority: priority + ) + try? await APIService.shared.createTask(token: authManager.token, request: req) + await onAdded() + await MainActor.run { isPresented = false } } } } diff --git a/PulseHealth/Views/Tasks/TasksView.swift b/PulseHealth/Views/Tasks/TasksView.swift index 32b7a75..47f3919 100644 --- a/PulseHealth/Views/Tasks/TasksView.swift +++ b/PulseHealth/Views/Tasks/TasksView.swift @@ -76,13 +76,16 @@ struct TasksView: View { } .sheet(isPresented: $showAddTask) { AddTaskView(isPresented: $showAddTask) { await loadTasks() } + .presentationDetents([.medium, .large]) + .presentationDragIndicator(.visible) + .presentationBackground(Color(hex: "0a0a1a")) } .task { await loadTasks() } - .refreshable { await loadTasks() } + .refreshable { await loadTasks(refresh: true) } } - func loadTasks() async { - isLoading = true + func loadTasks(refresh: Bool = false) async { + if !refresh { isLoading = true } tasks = (try? await APIService.shared.getTasks(token: authManager.token)) ?? [] isLoading = false } diff --git a/project.yml b/project.yml index f15f833..67f4bb7 100644 --- a/project.yml +++ b/project.yml @@ -14,7 +14,6 @@ targets: SWIFT_VERSION: 5.9 INFOPLIST_FILE: PulseHealth/Info.plist CODE_SIGN_STYLE: Automatic - DEVELOPMENT_TEAM: "" entitlements: path: PulseHealth/PulseHealth.entitlements capabilities: