9a429f11a6
- 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>
190 lines
7.6 KiB
Swift
190 lines
7.6 KiB
Swift
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)
|
||
}
|
||
}
|
||
}
|