Files
nahbar/nahbar/nahbar/AddTodoView.swift
T
sven 9a429f11a6 Resolves #10 Todos: Vollständige Implementierung
- Todo-Modell (V8-Migration): dueDate, reminderDate, isCompleted, completedAt
- AddTodoView + EditTodoView: Fälligkeit, Erinnerungspush, intelligenter Reminder-Default
- TodayView: Fällige Todos-Sektion mit Fade-out (5 s) nach Abhaken
- PersonDetailView: Todo-Sektion, tap-to-edit, Glockensymbol, Fade-out
- Momente: tap-to-edit, Favoritenstern inline, Löschen-Button in EditMomentView
- DeletableMomentRow entfernt (Swipe durch direkte Interaktion ersetzt)
- Geburtstage-Sektion zwischen Unternehmungen und Todos in TodayView
- Lokalisierung (de/en) und Tests aktualisiert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 05:18:24 +02:00

190 lines
7.6 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
import SwiftData
import UserNotifications
struct AddTodoView: View {
@Environment(\.nahbarTheme) var theme
@Environment(\.modelContext) var modelContext
@Environment(\.dismiss) var dismiss
let person: Person
@State private var title = ""
@State private var dueDate: Date = {
Calendar.current.date(byAdding: .day, value: 1, to: Date()) ?? Date()
}()
@State private var addReminder = false
@State private var reminderDate: Date = {
let due = Calendar.current.date(byAdding: .day, value: 1, to: Date()) ?? Date()
return Self.defaultReminderDate(for: due)
}()
@FocusState private var isFocused: Bool
private var isValid: Bool { !title.trimmingCharacters(in: .whitespaces).isEmpty }
/// Berechnet den Erinnerungs-Standardwert: dueDate 1 Tag (9:00),
/// falls das mehr als einen Tag in der Zukunft liegt, sonst morgen um 9:00.
static func defaultReminderDate(for dueDate: Date) -> Date {
let cal = Calendar.current
let now = Date()
if let dayBefore = cal.date(byAdding: .day, value: -1, to: dueDate),
dayBefore > now {
return cal.date(bySettingHour: 9, minute: 0, second: 0, of: dayBefore) ?? dayBefore
}
let tomorrow = cal.date(byAdding: .day, value: 1, to: now) ?? now
return cal.date(bySettingHour: 9, minute: 0, second: 0, of: tomorrow) ?? tomorrow
}
var body: some View {
NavigationStack {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
// Titel-Eingabe
ZStack(alignment: .topLeading) {
if title.isEmpty {
Text("Was möchtest du erledigen?")
.font(.system(size: 16))
.foregroundStyle(theme.contentTertiary)
.padding(.horizontal, 16)
.padding(.vertical, 14)
.allowsHitTesting(false)
}
TextEditor(text: $title)
.font(.system(size: 16, design: theme.displayDesign))
.foregroundStyle(theme.contentPrimary)
.tint(theme.accent)
.scrollContentBackground(.hidden)
.padding(.horizontal, 12)
.padding(.vertical, 10)
.focused($isFocused)
}
.frame(minHeight: 100)
.background(theme.surfaceCard)
.clipShape(RoundedRectangle(cornerRadius: theme.radiusCard))
.padding(.horizontal, 20)
// Fälligkeitsdatum + Erinnerung
VStack(spacing: 0) {
DatePicker(
"Fällig am",
selection: $dueDate,
displayedComponents: .date
)
.font(.system(size: 15))
.foregroundStyle(theme.contentPrimary)
.tint(theme.accent)
.environment(\.locale, Locale.current)
.padding(.horizontal, 16)
.padding(.vertical, 10)
RowDivider()
HStack {
Image(systemName: "bell")
.font(.system(size: 14))
.foregroundStyle(addReminder ? theme.accent : theme.contentTertiary)
Text("Erinnerung setzen")
.font(.system(size: 15))
.foregroundStyle(theme.contentPrimary)
Spacer()
Toggle("", isOn: $addReminder)
.tint(theme.accent)
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
if addReminder {
RowDivider()
DatePicker(
"Wann?",
selection: $reminderDate,
in: Date()...,
displayedComponents: [.date, .hourAndMinute]
)
.font(.system(size: 15))
.foregroundStyle(theme.contentPrimary)
.tint(theme.accent)
.environment(\.locale, Locale.current)
.padding(.horizontal, 16)
.padding(.vertical, 10)
}
}
.background(theme.surfaceCard)
.clipShape(RoundedRectangle(cornerRadius: theme.radiusCard))
.padding(.horizontal, 20)
.animation(.easeInOut(duration: 0.2), value: addReminder)
Spacer(minLength: 40)
}
.padding(.top, 16)
}
.background(theme.backgroundPrimary.ignoresSafeArea())
.navigationTitle("Todo anlegen")
.navigationBarTitleDisplayMode(.inline)
.themedNavBar()
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("Abbrechen") { dismiss() }
.foregroundStyle(theme.contentSecondary)
}
ToolbarItem(placement: .topBarTrailing) {
Button("Speichern") { save() }
.font(.system(size: 15, weight: .semibold))
.foregroundStyle(isValid ? theme.accent : theme.contentTertiary)
.disabled(!isValid)
}
}
}
.onAppear { isFocused = true }
.onChange(of: dueDate) { _, newDue in
reminderDate = Self.defaultReminderDate(for: newDue)
}
}
private func save() {
let trimmed = title.trimmingCharacters(in: .whitespaces)
guard !trimmed.isEmpty else { return }
let reminder = addReminder ? reminderDate : nil
let todo = Todo(title: trimmed, dueDate: dueDate, reminderDate: reminder, person: person)
modelContext.insert(todo)
person.touch()
if addReminder {
scheduleReminder(for: todo)
}
do {
try modelContext.save()
} catch {
AppEventLog.shared.record(
"Fehler beim Speichern des Todos: \(error.localizedDescription)",
level: .error, category: "Todo"
)
}
dismiss()
}
private func scheduleReminder(for todo: Todo) {
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert, .sound]) { granted, _ in
guard granted else { return }
let content = UNMutableNotificationContent()
content.title = person.firstName
content.body = todo.title
content.sound = .default
let components = Calendar.current.dateComponents(
[.year, .month, .day, .hour, .minute], from: reminderDate
)
let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: false)
let request = UNNotificationRequest(
identifier: "todo-\(todo.id)",
content: content,
trigger: trigger
)
center.add(request)
}
}
}