feat: полноценное Pulse приложение с TabBar

- Auth: переключено на Pulse API (api.digital-home.site) вместо health
- TabBar: Главная, Задачи, Привычки, Здоровье, Финансы
- Models: TaskModels, HabitModels, FinanceModels, обновлённые AuthModels
- Services: APIService (Pulse API), HealthAPIService (health отдельно)
- Dashboard: обзор дня с задачами, привычками, readiness, балансом
- Tasks: список, фильтр, создание, выполнение, удаление
- Habits: список с прогресс-баром, отметка выполнения, стрики
- Health: бывший DashboardView, HealthKit sync через health API key
- Finance: баланс, список транзакций, добавление расхода/дохода
- Health данные через x-api-key вместо JWT токена health сервиса
This commit is contained in:
Cosmo
2026-03-25 11:49:52 +00:00
parent cf0e535639
commit c015824b36
23 changed files with 1090 additions and 202 deletions

View File

@@ -9,9 +9,6 @@ struct LoginView: View {
@State private var errorMessage = ""
@State private var showPassword = false
@State private var isRegistering = false
@State private var forgotEmail = ""
@State private var showForgotSheet = false
@State private var forgotSent = false
var body: some View {
ZStack {
@@ -20,9 +17,9 @@ struct LoginView: View {
VStack(spacing: 32) {
VStack(spacing: 8) {
Text("🫀").font(.system(size: 60))
Text("Pulse Health").font(.largeTitle.bold()).foregroundColor(.white)
Text(isRegistering ? "Создать аккаунт" : "Персональный дашборд здоровья")
Text("").font(.system(size: 60))
Text("Pulse").font(.largeTitle.bold()).foregroundColor(.white)
Text(isRegistering ? "Создать аккаунт" : "Управление жизнью")
.font(.subheadline).foregroundColor(.white.opacity(0.6))
}.padding(.top, 60)
@@ -86,15 +83,6 @@ struct LoginView: View {
.cornerRadius(12)
.disabled(isLoading || email.isEmpty || password.isEmpty)
// Forgot password (only on login)
if !isRegistering {
Button(action: { showForgotSheet = true }) {
Text("Забыли пароль?")
.font(.footnote)
.foregroundColor(Color(hex: "00d4aa"))
}
}
// Toggle login/register
HStack {
Text(isRegistering ? "Уже есть аккаунт?" : "Нет аккаунта?")
@@ -114,9 +102,6 @@ struct LoginView: View {
Spacer()
}
.sheet(isPresented: $showForgotSheet) {
ForgotPasswordView(isPresented: $showForgotSheet)
}
}
}
@@ -124,10 +109,12 @@ struct LoginView: View {
isLoading = true; errorMessage = ""
Task {
do {
let response = try await APIService.shared.login(email: email.trimmingCharacters(in: .whitespaces), password: password)
let profile = try? await APIService.shared.getProfile(token: response.token)
let response = try await APIService.shared.login(
email: email.trimmingCharacters(in: .whitespaces),
password: password
)
await MainActor.run {
authManager.login(token: response.token, name: response.user.name, apiKey: profile?.apiKey ?? "")
authManager.login(token: response.token, user: response.user)
}
} catch let error as APIError {
await MainActor.run { errorMessage = error.errorDescription ?? "Ошибка"; isLoading = false }
@@ -141,10 +128,13 @@ struct LoginView: View {
isLoading = true; errorMessage = ""
Task {
do {
let response = try await APIService.shared.register(email: email.trimmingCharacters(in: .whitespaces), password: password, name: name)
let profile = try? await APIService.shared.getProfile(token: response.token)
let response = try await APIService.shared.register(
email: email.trimmingCharacters(in: .whitespaces),
password: password,
name: name
)
await MainActor.run {
authManager.login(token: response.token, name: response.user.name, apiKey: profile?.apiKey ?? "")
authManager.login(token: response.token, user: response.user)
}
} catch let error as APIError {
await MainActor.run { errorMessage = error.errorDescription ?? "Ошибка"; isLoading = false }
@@ -154,49 +144,3 @@ struct LoginView: View {
}
}
}
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 }
.padding().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 {
try? await APIService.shared.forgotPassword(email: email)
await MainActor.run { isSent = true; isLoading = false }
}
}
}