Files
pulse-mobile/PulseHealth/Views/Habits/AddHabitView.swift
Cosmo e7af51af10 feat: full Pulse Mobile implementation - all modules
- Phase 0: project.yml fixes (CODE_SIGN_ENTITLEMENTS confirmed)
- Phase 1: Enhanced models (HabitModels, TaskModels, FinanceModels, SavingsModels, UserModels)
- Phase 1: Enhanced APIService with all endpoints (habits/log/stats, tasks/uncomplete, finance/analytics, savings/*)
- Phase 2: DashboardView rewrite - day progress bar, 4 stat cards, habit/task lists with Undo (3 sec)
- Phase 3: TrackerView - HabitListView (streak badge, swipe delete, archive), TaskListView (priority, overdue), StatisticsView (heatmap 84 days, line chart, bar chart via Swift Charts)
- Phase 4: FinanceView rewrite - month picker, summary card, top expenses progress bars, pie chart, line chart, transactions by day, analytics tab with bar chart + month comparison
- Phase 5: SavingsView rewrite - overview with overdue block, categories tab with type icons, operations tab with category filter + add sheet
- Phase 6: SettingsView - dark/light theme, profile edit, telegram chat id, notifications toggle + time, timezone picker, logout
- Added: AddHabitView with weekly day selector + interval days
- Added: AddTaskView with icon/color/due date picker
- Haptic feedback on all toggle actions
2026-03-25 17:14:59 +00:00

190 lines
10 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
struct AddHabitView: View {
@Binding var isPresented: Bool
@EnvironmentObject var authManager: AuthManager
let onAdded: () async -> Void
@State private var name = ""
@State private var description = ""
@State private var frequency = "daily"
@State private var selectedIcon = "🔥"
@State private var selectedColor = "#0D9488"
@State private var isLoading = false
@State private var intervalDays = "2"
@State private var selectedWeekdays: Set<Int> = [1,2,3,4,5] // Mon-Fri
let frequencies: [(String, String, String)] = [
("daily", "Каждый день", "calendar"),
("weekly", "По дням недели", "calendar.badge.clock"),
("interval", "Каждые N дней", "repeat"),
("monthly", "Каждый месяц", "calendar.badge.plus")
]
let weekdayNames = ["Вс","Пн","Вт","Ср","Чт","Пт","Сб"]
let icons = ["🔥", "💪", "🏃", "📚", "💧", "🧘", "🎯", "⭐️", "🌟", "",
"🏋️", "🚴", "🍎", "😴", "🧠", "🎨", "🎵", "💊", "🌿", "💰",
"✍️", "🧹", "🏊", "🚶", "🎮", "📝", "🌅", "🥗", "🧃", "🫁"]
let colors = ["#0D9488", "#7c3aed", "#ff4757", "#ffa502", "#6366f1",
"#ec4899", "#14b8a6", "#f59e0b", "#10b981", "#3b82f6"]
var body: some View {
ZStack {
Color(hex: "0a0a1a").ignoresSafeArea()
VStack(spacing: 0) {
RoundedRectangle(cornerRadius: 3)
.fill(Color.white.opacity(0.2)).frame(width: 40, height: 4).padding(.top, 12)
HStack {
Button("Отмена") { isPresented = false }.foregroundColor(Color(hex: "8888aa"))
Spacer()
Text("Новая привычка").font(.headline).foregroundColor(.white)
Spacer()
Button(action: save) {
if isLoading { ProgressView().tint(Color(hex: "0D9488")).scaleEffect(0.8) }
else { Text("Добавить").foregroundColor(name.isEmpty ? Color(hex: "8888aa") : Color(hex: "0D9488")).fontWeight(.semibold) }
}.disabled(name.isEmpty || isLoading)
}
.padding(.horizontal, 20).padding(.vertical, 16)
Divider().background(Color.white.opacity(0.1))
ScrollView {
VStack(spacing: 20) {
// Preview
HStack(spacing: 16) {
ZStack {
Circle()
.fill(Color(hex: String(selectedColor.dropFirst())).opacity(0.25))
.frame(width: 56, height: 56)
Text(selectedIcon).font(.title2)
}
VStack(alignment: .leading, spacing: 4) {
Text(name.isEmpty ? "Название привычки" : name)
.font(.callout.bold())
.foregroundColor(name.isEmpty ? Color(hex: "8888aa") : .white)
Text(frequencies.first { $0.0 == frequency }?.1 ?? "")
.font(.caption).foregroundColor(Color(hex: "8888aa"))
}
Spacer()
}
.padding(16)
.background(RoundedRectangle(cornerRadius: 16).fill(Color.white.opacity(0.05)))
// Name
VStack(alignment: .leading, spacing: 8) {
Label("Название", systemImage: "pencil").font(.caption).foregroundColor(Color(hex: "8888aa"))
TextField("Например: Читать 30 минут", text: $name)
.foregroundColor(.white).padding(14)
.background(RoundedRectangle(cornerRadius: 12).fill(Color.white.opacity(0.07)))
}
// Frequency
VStack(alignment: .leading, spacing: 8) {
Label("Периодичность", systemImage: "calendar").font(.caption).foregroundColor(Color(hex: "8888aa"))
VStack(spacing: 6) {
ForEach(frequencies, id: \.0) { f in
Button(action: { frequency = f.0 }) {
HStack {
Image(systemName: f.2).foregroundColor(frequency == f.0 ? Color(hex: "0D9488") : Color(hex: "8888aa"))
Text(f.1).foregroundColor(frequency == f.0 ? .white : Color(hex: "8888aa"))
Spacer()
if frequency == f.0 { Image(systemName: "checkmark").foregroundColor(Color(hex: "0D9488")) }
}
.padding(14)
.background(RoundedRectangle(cornerRadius: 12).fill(frequency == f.0 ? Color(hex: "0D9488").opacity(0.15) : Color.white.opacity(0.05)))
}
}
}
// Weekly day selector
if frequency == "weekly" {
HStack(spacing: 8) {
ForEach(0..<7) { i in
Button(action: {
if selectedWeekdays.contains(i) { if selectedWeekdays.count > 1 { selectedWeekdays.remove(i) } }
else { selectedWeekdays.insert(i) }
}) {
Text(weekdayNames[i])
.font(.caption.bold())
.foregroundColor(selectedWeekdays.contains(i) ? .black : .white)
.frame(width: 32, height: 32)
.background(Circle().fill(selectedWeekdays.contains(i) ? Color(hex: "0D9488") : Color.white.opacity(0.08)))
}
}
}
}
// Interval days
if frequency == "interval" {
HStack {
Text("Каждые").foregroundColor(Color(hex: "8888aa")).font(.callout)
TextField("2", text: $intervalDays).keyboardType(.numberPad)
.foregroundColor(.white)
.frame(width: 50)
.padding(10)
.background(RoundedRectangle(cornerRadius: 8).fill(Color.white.opacity(0.07)))
Text("дней").foregroundColor(Color(hex: "8888aa")).font(.callout)
}
}
}
// Icon picker
VStack(alignment: .leading, spacing: 8) {
Label("Иконка", systemImage: "face.smiling").font(.caption).foregroundColor(Color(hex: "8888aa"))
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 6), spacing: 8) {
ForEach(icons, id: \.self) { icon in
Button(action: { selectedIcon = icon }) {
Text(icon).font(.title3)
.frame(width: 40, height: 40)
.background(Circle().fill(selectedIcon == icon ? Color(hex: "0D9488").opacity(0.25) : Color.white.opacity(0.05)))
.overlay(Circle().stroke(selectedIcon == icon ? Color(hex: "0D9488") : Color.clear, lineWidth: 2))
}
}
}
}
// Color picker
VStack(alignment: .leading, spacing: 8) {
Label("Цвет", systemImage: "paintpalette").font(.caption).foregroundColor(Color(hex: "8888aa"))
HStack(spacing: 10) {
ForEach(colors, id: \.self) { color in
Button(action: { selectedColor = color }) {
Circle()
.fill(Color(hex: String(color.dropFirst())))
.frame(width: 32, height: 32)
.overlay(Circle().stroke(.white, lineWidth: selectedColor == color ? 2 : 0))
.scaleEffect(selectedColor == color ? 1.15 : 1.0)
.animation(.easeInOut(duration: 0.15), value: selectedColor)
}
}
}
}
}.padding(20)
}
}
}
}
func save() {
isLoading = true
Task {
var body: [String: Any] = [
"name": name,
"description": description,
"frequency": frequency,
"icon": selectedIcon,
"color": selectedColor,
"target_count": 1
]
if frequency == "weekly" {
body["target_days"] = Array(selectedWeekdays).sorted()
}
if frequency == "interval" {
body["target_count"] = Int(intervalDays) ?? 2
}
let reqBody = try? JSONSerialization.data(withJSONObject: body)
try? await APIService.shared.createHabit(token: authManager.token, body: reqBody ?? Data())
await onAdded()
await MainActor.run { isPresented = false }
}
}
}