diff --git a/PulseHealth/Models/FinanceModels.swift b/PulseHealth/Models/FinanceModels.swift index ea2ca8f..49c22fa 100644 --- a/PulseHealth/Models/FinanceModels.swift +++ b/PulseHealth/Models/FinanceModels.swift @@ -26,14 +26,16 @@ struct FinanceCategory: Codable, Identifiable { struct FinanceSummary: Codable { var totalIncome: Double? - var totalExpenses: Double? + var totalExpense: Double? var balance: Double? + var carriedOver: Double? var month: String? enum CodingKeys: String, CodingKey { - case totalIncome = "total_income" - case totalExpenses = "total_expenses" case balance, month + case totalIncome = "total_income" + case totalExpense = "total_expense" + case carriedOver = "carried_over" } } diff --git a/PulseHealth/Models/HabitModels.swift b/PulseHealth/Models/HabitModels.swift index b474632..c11cbcf 100644 --- a/PulseHealth/Models/HabitModels.swift +++ b/PulseHealth/Models/HabitModels.swift @@ -19,7 +19,8 @@ struct Habit: Codable, Identifiable { var color: String? var frequency: HabitFrequency var reminderTime: String? - var targetDays: Int? + var targetDays: [Int]? + var targetCount: Int? var currentStreak: Int? var longestStreak: Int? var completedToday: Bool? @@ -29,6 +30,7 @@ struct Habit: Codable, Identifiable { case id, name, description, icon, color, frequency case reminderTime = "reminder_time" case targetDays = "target_days" + case targetCount = "target_count" case currentStreak = "current_streak" case longestStreak = "longest_streak" case completedToday = "completed_today" diff --git a/PulseHealth/Models/TaskModels.swift b/PulseHealth/Models/TaskModels.swift index af4a341..8bb036c 100644 --- a/PulseHealth/Models/TaskModels.swift +++ b/PulseHealth/Models/TaskModels.swift @@ -1,37 +1,38 @@ import Foundation -enum TaskPriority: String, Codable, CaseIterable { - case low, medium, high, urgent - var displayName: String { - switch self { - case .low: return "Низкий" - case .medium: return "Средний" - case .high: return "Высокий" - case .urgent: return "Срочный" - } - } - var color: String { - switch self { - case .low: return "8888aa" - case .medium: return "ffa502" - case .high: return "ff4757" - case .urgent: return "ff0000" - } - } -} - struct PulseTask: Codable, Identifiable { let id: Int var title: String var description: String? - var done: Bool - var priority: TaskPriority? + var completed: Bool + var priority: Int? + var icon: String? + var color: String? var dueDate: String? var reminderTime: String? var createdAt: String? + var priorityColor: String { + switch priority { + case 4: return "ff0000" + case 3: return "ff4757" + case 2: return "ffa502" + default: return "8888aa" + } + } + + var priorityDisplayName: String { + switch priority { + case 1: return "Низкий" + case 2: return "Средний" + case 3: return "Высокий" + case 4: return "Срочный" + default: return "Без приоритета" + } + } + enum CodingKeys: String, CodingKey { - case id, title, description, done, priority + case id, title, description, completed, priority, icon, color case dueDate = "due_date" case reminderTime = "reminder_time" case createdAt = "created_at" @@ -41,7 +42,7 @@ struct PulseTask: Codable, Identifiable { struct CreateTaskRequest: Codable { var title: String var description: String? - var priority: TaskPriority? + var priority: Int? var dueDate: String? enum CodingKeys: String, CodingKey { diff --git a/PulseHealth/PulseHealth.entitlements b/PulseHealth/PulseHealth.entitlements index b3187f1..fe2df7e 100644 --- a/PulseHealth/PulseHealth.entitlements +++ b/PulseHealth/PulseHealth.entitlements @@ -4,5 +4,9 @@ com.apple.developer.healthkit + com.apple.developer.healthkit.access + + health-records + diff --git a/PulseHealth/Views/Dashboard/DashboardView.swift b/PulseHealth/Views/Dashboard/DashboardView.swift index 3460665..ad9c98e 100644 --- a/PulseHealth/Views/Dashboard/DashboardView.swift +++ b/PulseHealth/Views/Dashboard/DashboardView.swift @@ -18,7 +18,7 @@ struct DashboardView: View { } } - var pendingTasks: [PulseTask] { tasks.filter { !$0.done } } + var pendingTasks: [PulseTask] { tasks.filter { !$0.completed } } var completedHabitsToday: Int { habits.filter { $0.completedToday == true }.count } var body: some View { @@ -67,7 +67,7 @@ struct DashboardView: View { StatCard(icon: "checkmark.circle.fill", value: "\(pendingTasks.count)", label: "Задач", color: "00d4aa") StatCard(icon: "flame.fill", value: "\(completedHabitsToday)/\(habits.count)", label: "Привычек", color: "ffa502") if let s = summary, let balance = s.balance { - StatCard(icon: "rublesign.circle.fill", value: "\(Int(balance))₽", label: "Баланс", color: "7c3aed") + StatCard(icon: "rublesign.circle.fill", value: "\(Int(balance))₽", label: "Финансы", color: "7c3aed") } } .padding(.horizontal) diff --git a/PulseHealth/Views/Finance/FinanceView.swift b/PulseHealth/Views/Finance/FinanceView.swift index 900c15f..9956b42 100644 --- a/PulseHealth/Views/Finance/FinanceView.swift +++ b/PulseHealth/Views/Finance/FinanceView.swift @@ -85,7 +85,7 @@ struct FinanceSummaryCard: View { Spacer() VStack(spacing: 4) { Text("Расходы").font(.caption).foregroundColor(Color(hex: "8888aa")) - Text("-\(Int(summary.totalExpenses ?? 0))₽").font(.headline).foregroundColor(Color(hex: "ff4757")) + Text("-\(Int(summary.totalExpense ?? 0))₽").font(.headline).foregroundColor(Color(hex: "ff4757")) } } } diff --git a/PulseHealth/Views/LoginView.swift b/PulseHealth/Views/LoginView.swift index 2ddeb14..38c849b 100644 --- a/PulseHealth/Views/LoginView.swift +++ b/PulseHealth/Views/LoginView.swift @@ -9,6 +9,7 @@ struct LoginView: View { @State private var errorMessage = "" @State private var showPassword = false @State private var isRegistering = false + @State private var showForgotSheet = false var body: some View { ZStack { @@ -83,6 +84,14 @@ struct LoginView: View { .cornerRadius(12) .disabled(isLoading || email.isEmpty || password.isEmpty) + if !isRegistering { + Button(action: { showForgotSheet = true }) { + Text("Забыли пароль?") + .font(.footnote) + .foregroundColor(Color(hex: "00d4aa")) + } + } + // Toggle login/register HStack { Text(isRegistering ? "Уже есть аккаунт?" : "Нет аккаунта?") @@ -103,6 +112,9 @@ struct LoginView: View { Spacer() } } + .sheet(isPresented: $showForgotSheet) { + ForgotPasswordView(isPresented: $showForgotSheet) + } } func login() { @@ -144,3 +156,54 @@ struct LoginView: View { } } } + +// MARK: - ForgotPasswordView + +struct ForgotPasswordView: View { + @Binding var isPresented: Bool + @State private var email = "" + @State private var isSent = false + @State private var isLoading = false + + var body: some View { + ZStack { + Color(hex: "0a0a1a").ignoresSafeArea() + VStack(spacing: 24) { + Text("Сброс пароля").font(.title2.bold()).foregroundColor(.white) + if isSent { + VStack(spacing: 12) { + Text("✅").font(.system(size: 50)) + Text("Письмо отправлено!\nПроверьте \(email)") + .foregroundColor(.white).multilineTextAlignment(.center) + } + Button("Закрыть") { isPresented = false }.foregroundColor(Color(hex: "00d4aa")) + } else { + Text("Введите email — пришлём ссылку для сброса пароля") + .foregroundColor(.white.opacity(0.6)).multilineTextAlignment(.center) + TextField("Email", text: $email) + .keyboardType(.emailAddress).autocapitalization(.none) + .padding().background(Color.white.opacity(0.1)).cornerRadius(12).foregroundColor(.white) + Button(action: send) { + if isLoading { ProgressView().tint(.black) } + else { Text("Отправить").font(.headline).foregroundColor(.black) } + } + .frame(maxWidth: .infinity).padding() + .background(Color(hex: "00d4aa")).cornerRadius(12).disabled(email.isEmpty || isLoading) + Button("Отмена") { isPresented = false }.foregroundColor(.white.opacity(0.5)) + } + }.padding(32) + } + } + + func send() { + isLoading = true + Task { + var req = URLRequest(url: URL(string: "https://api.digital-home.site/auth/forgot-password")!) + req.httpMethod = "POST" + req.setValue("application/json", forHTTPHeaderField: "Content-Type") + req.httpBody = try? JSONEncoder().encode(["email": email]) + _ = try? await URLSession.shared.data(for: req) + await MainActor.run { isSent = true; isLoading = false } + } + } +} diff --git a/PulseHealth/Views/MainTabView.swift b/PulseHealth/Views/MainTabView.swift index 1adc03e..6ceff2f 100644 --- a/PulseHealth/Views/MainTabView.swift +++ b/PulseHealth/Views/MainTabView.swift @@ -19,6 +19,9 @@ struct MainTabView: View { FinanceView() .tabItem { Label("Финансы", systemImage: "rublesign.circle.fill") } + + ProfileView() + .tabItem { Label("Профиль", systemImage: "person.fill") } } .accentColor(Color(hex: "00d4aa")) .preferredColorScheme(.dark) diff --git a/PulseHealth/Views/Profile/ProfileView.swift b/PulseHealth/Views/Profile/ProfileView.swift new file mode 100644 index 0000000..15dc3af --- /dev/null +++ b/PulseHealth/Views/Profile/ProfileView.swift @@ -0,0 +1,148 @@ +import SwiftUI + +struct ProfileView: View { + @EnvironmentObject var authManager: AuthManager + @State private var showChangePassword = false + + var body: some View { + ZStack { + Color(hex: "0a0a1a").ignoresSafeArea() + VStack(spacing: 0) { + // Header + VStack(spacing: 12) { + ZStack { + Circle().fill(Color(hex: "00d4aa").opacity(0.2)).frame(width: 80, height: 80) + Text(String(authManager.userName.prefix(1)).uppercased()) + .font(.largeTitle.bold()).foregroundColor(Color(hex: "00d4aa")) + } + Text(authManager.userName).font(.title2.bold()).foregroundColor(.white) + } + .padding(.top, 40).padding(.bottom, 32) + + // Settings list + VStack(spacing: 2) { + ProfileRow(icon: "lock.fill", title: "Сменить пароль", color: "7c3aed") { + showChangePassword = true + } + ProfileRow(icon: "heart.fill", title: "Health API ключ", subtitle: authManager.healthApiKey, color: "ff4757") {} + Divider().background(Color.white.opacity(0.1)).padding(.vertical, 8) + ProfileRow(icon: "rectangle.portrait.and.arrow.right", title: "Выйти", color: "ff4757", isDestructive: true) { + authManager.logout() + } + } + .padding(.horizontal) + + Spacer() + + Text("Pulse v1.0").font(.caption).foregroundColor(Color(hex: "8888aa")).padding(.bottom, 20) + } + } + .sheet(isPresented: $showChangePassword) { + ChangePasswordView(isPresented: $showChangePassword) + } + } +} + +struct ProfileRow: View { + let icon: String + let title: String + var subtitle: String? = nil + let color: String + var isDestructive: Bool = false + let action: () -> Void + + var body: some View { + Button(action: action) { + HStack(spacing: 14) { + ZStack { + RoundedRectangle(cornerRadius: 8).fill(Color(hex: color).opacity(0.2)).frame(width: 36, height: 36) + Image(systemName: icon).foregroundColor(Color(hex: color)).font(.subheadline) + } + VStack(alignment: .leading, spacing: 2) { + Text(title).foregroundColor(isDestructive ? Color(hex: "ff4757") : .white).font(.callout) + if let sub = subtitle { + Text(sub).font(.caption).foregroundColor(Color(hex: "8888aa")) + } + } + Spacer() + if !isDestructive { + Image(systemName: "chevron.right").foregroundColor(Color(hex: "8888aa")).font(.caption) + } + } + .padding(14) + .background(RoundedRectangle(cornerRadius: 12).fill(Color.white.opacity(0.05))) + } + } +} + +struct ChangePasswordView: View { + @Binding var isPresented: Bool + @EnvironmentObject var authManager: AuthManager + @State private var oldPassword = "" + @State private var newPassword = "" + @State private var confirm = "" + @State private var isLoading = false + @State private var errorMessage = "" + @State private var success = false + + var body: some View { + NavigationView { + ZStack { + Color(hex: "0a0a1a").ignoresSafeArea() + VStack(spacing: 16) { + if success { + VStack(spacing: 12) { + Text("✅").font(.system(size: 50)) + Text("Пароль изменён!").font(.title2.bold()).foregroundColor(.white) + }.padding(.top, 40) + Button("Закрыть") { isPresented = false }.foregroundColor(Color(hex: "00d4aa")) + } else { + SecureField("Текущий пароль", text: $oldPassword) + .padding().background(Color.white.opacity(0.08)).cornerRadius(12).foregroundColor(.white) + SecureField("Новый пароль", text: $newPassword) + .padding().background(Color.white.opacity(0.08)).cornerRadius(12).foregroundColor(.white) + SecureField("Подтвердите пароль", text: $confirm) + .padding().background(Color.white.opacity(0.08)).cornerRadius(12).foregroundColor(.white) + if !errorMessage.isEmpty { + Text(errorMessage).foregroundColor(.red).font(.caption) + } + Button(action: change) { + if isLoading { ProgressView().tint(.black) } + else { Text("Сменить").font(.headline).foregroundColor(.black) } + } + .frame(maxWidth: .infinity).padding() + .background(Color(hex: "00d4aa")).cornerRadius(12) + .disabled(oldPassword.isEmpty || newPassword.isEmpty || isLoading) + } + Spacer() + }.padding() + } + .navigationTitle("Смена пароля") + .navigationBarTitleDisplayMode(.inline) + .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Отмена") { isPresented = false } } } + .preferredColorScheme(.dark) + } + } + + func change() { + guard newPassword == confirm else { errorMessage = "Пароли не совпадают"; return } + guard newPassword.count >= 8 else { errorMessage = "Минимум 8 символов"; return } + isLoading = true + Task { + var req = URLRequest(url: URL(string: "https://api.digital-home.site/auth/password")!) + req.httpMethod = "PUT" + req.setValue("application/json", forHTTPHeaderField: "Content-Type") + req.setValue("Bearer \(authManager.token)", forHTTPHeaderField: "Authorization") + req.httpBody = try? JSONEncoder().encode(["old_password": oldPassword, "new_password": newPassword]) + let (_, response) = (try? await URLSession.shared.data(for: req)) ?? (Data(), nil) + await MainActor.run { + if (response as? HTTPURLResponse)?.statusCode == 200 { + success = true + } else { + errorMessage = "Неверный текущий пароль" + } + isLoading = false + } + } + } +} diff --git a/PulseHealth/Views/Tasks/AddTaskView.swift b/PulseHealth/Views/Tasks/AddTaskView.swift index 7792080..bba6f0b 100644 --- a/PulseHealth/Views/Tasks/AddTaskView.swift +++ b/PulseHealth/Views/Tasks/AddTaskView.swift @@ -7,9 +7,11 @@ struct AddTaskView: View { @State private var title = "" @State private var description = "" - @State private var priority: TaskPriority = .medium + @State private var priority: Int = 2 @State private var isLoading = false + private let priorities = [(1, "Низкий"), (2, "Средний"), (3, "Высокий"), (4, "Срочный")] + var body: some View { NavigationView { ZStack { @@ -20,7 +22,7 @@ struct AddTaskView: View { TextField("Описание (необязательно)", text: $description) .padding().background(Color.white.opacity(0.08)).cornerRadius(12).foregroundColor(.white) Picker("Приоритет", selection: $priority) { - ForEach(TaskPriority.allCases, id: \.self) { p in Text(p.displayName).tag(p) } + ForEach(priorities, id: \.0) { p in Text(p.1).tag(p.0) } } .pickerStyle(.segmented) Spacer() diff --git a/PulseHealth/Views/Tasks/TaskRowView.swift b/PulseHealth/Views/Tasks/TaskRowView.swift index 22b9c27..805236d 100644 --- a/PulseHealth/Views/Tasks/TaskRowView.swift +++ b/PulseHealth/Views/Tasks/TaskRowView.swift @@ -5,25 +5,20 @@ struct TaskRowView: View { let onComplete: () async -> Void var priorityColor: Color { - switch task.priority { - case .urgent: return Color(hex: "ff0000") - case .high: return Color(hex: "ff4757") - case .medium: return Color(hex: "ffa502") - default: return Color(hex: "8888aa") - } + Color(hex: task.priorityColor) } var body: some View { HStack(spacing: 12) { Button(action: { Task { await onComplete() } }) { - Image(systemName: task.done ? "checkmark.circle.fill" : "circle") + Image(systemName: task.completed ? "checkmark.circle.fill" : "circle") .font(.title3) - .foregroundColor(task.done ? Color(hex: "00d4aa") : Color(hex: "8888aa")) + .foregroundColor(task.completed ? Color(hex: "00d4aa") : Color(hex: "8888aa")) } VStack(alignment: .leading, spacing: 4) { Text(task.title) - .foregroundColor(task.done ? Color(hex: "8888aa") : .white) - .strikethrough(task.done) + .foregroundColor(task.completed ? Color(hex: "8888aa") : .white) + .strikethrough(task.completed) .font(.callout) if let desc = task.description, !desc.isEmpty { Text(desc).font(.caption).foregroundColor(Color(hex: "8888aa")).lineLimit(1) @@ -33,7 +28,7 @@ struct TaskRowView: View { } } Spacer() - if let priority = task.priority, priority != .low { + if let priority = task.priority, priority > 1 { Circle().fill(priorityColor).frame(width: 8, height: 8) } } diff --git a/PulseHealth/Views/Tasks/TasksView.swift b/PulseHealth/Views/Tasks/TasksView.swift index cd8a774..32b7a75 100644 --- a/PulseHealth/Views/Tasks/TasksView.swift +++ b/PulseHealth/Views/Tasks/TasksView.swift @@ -15,8 +15,8 @@ struct TasksView: View { var filteredTasks: [PulseTask] { switch filter { - case .pending: return tasks.filter { !$0.done } - case .completed: return tasks.filter { $0.done } + case .pending: return tasks.filter { !$0.completed } + case .completed: return tasks.filter { $0.completed } case .all: return tasks } }