From 4a9bb32b5e2a737c4e506d3bc6d33d0044cf6442 Mon Sep 17 00:00:00 2001 From: Sven Date: Wed, 22 Apr 2026 05:34:50 +0200 Subject: [PATCH] Logbuch: Todos + alle Momente im Verlauf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LogEntryType.todoCompleted hinzugefügt (checkmark.square.fill, grün) - Abgehakte Todos erzeugen jetzt automatisch einen Logbuch-Eintrag (sowohl aus PersonDetailView als auch TodayView) - Verlauf-Vorschau in PersonDetailView zeigt jetzt eine gemischte Timeline aus Momenten + Logeinträgen statt nur LogEntries - Verlauf-Abschnitt erscheint sobald Momente oder Logeinträge vorhanden Co-Authored-By: Claude Sonnet 4.6 --- nahbar/nahbar/Localizable.xcstrings | 324 ++++++++++++--------------- nahbar/nahbar/Models.swift | 3 + nahbar/nahbar/PersonDetailView.swift | 51 ++++- nahbar/nahbar/TodayView.swift | 5 + 4 files changed, 194 insertions(+), 189 deletions(-) diff --git a/nahbar/nahbar/Localizable.xcstrings b/nahbar/nahbar/Localizable.xcstrings index 06bc181..1cecaa9 100644 --- a/nahbar/nahbar/Localizable.xcstrings +++ b/nahbar/nahbar/Localizable.xcstrings @@ -523,6 +523,9 @@ } } } + }, + "Alle" : { + }, "Alle %lld Einträge anzeigen" : { "localizations" : { @@ -780,18 +783,6 @@ } } }, - "Anstehende Termine" : { - "comment" : "TodayView – section title for upcoming reminders", - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Upcoming Events" - } - } - } - }, "Anstehende Erinnerungen" : { "comment" : "TodayView – replaced by 'Anstehende Unternehmungen'", "extractionState" : "stale", @@ -815,6 +806,18 @@ } } }, + "Anstehende Termine" : { + "comment" : "TodayView – section title for upcoming reminders", + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Upcoming Events" + } + } + } + }, "Anstehende Unternehmungen" : { "comment" : "TodayView – section title for plannable moments (Treffen, Gespräch, Vorhaben) with upcoming reminder dates", "localizations" : { @@ -1529,6 +1532,17 @@ } } }, + "Dieser Moment wird unwiderruflich gelöscht." : { + "comment" : "EditMomentView – delete confirmation message", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This moment will be permanently deleted." + } + } + } + }, "Distanzierter" : { "comment" : "RatingQuestion – negative pole for relationship closeness question", "extractionState" : "stale", @@ -2171,6 +2185,28 @@ } } }, + "Fällig am" : { + "comment" : "AddTodoView – label for due date picker", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Due on" + } + } + } + }, + "Fällige Todos" : { + "comment" : "TodayView – section header for due todos", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Due Todos" + } + } + } + }, "Falscher Code" : { "comment" : "AppLockView / AppLockSetupView – wrong PIN error", "localizations" : { @@ -3463,6 +3499,28 @@ } } }, + "Moment löschen?" : { + "comment" : "EditMomentView – delete confirmation dialog title", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delete Moment?" + } + } + } + }, + "Moment mit Kalendereintrag löschen?" : { + "comment" : "EditMomentView – calendar delete confirmation dialog title", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delete Moment with Calendar Event?" + } + } + } + }, "Moment…" : { "localizations" : { "en" : { @@ -3969,6 +4027,17 @@ } } }, + "Noch keine Todos." : { + "comment" : "PersonDetailView – empty state message when person has no todos", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No todos yet." + } + } + } + }, "Noch keine Treffen bewertet" : { "comment" : "VisitHistorySection – empty state title", "extractionState" : "stale", @@ -4456,6 +4525,17 @@ } } }, + "Todo abgeschlossen" : { + "comment" : "LogEntryType.todoCompleted raw value label", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Todo completed" + } + } + } + }, "Schritt definieren" : { "comment" : "PersonDetailView – define next step button", "extractionState" : "stale", @@ -4768,6 +4848,50 @@ } } }, + "Todo" : { + "comment" : "PersonDetailView – button label to add a new Todo", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Todo" + } + } + } + }, + "Todo anlegen" : { + "comment" : "AddTodoView – navigation title", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Add Todo" + } + } + } + }, + "Todo bearbeiten" : { + "comment" : "EditTodoView – navigation title", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Edit Todo" + } + } + } + }, + "Todos" : { + "comment" : "PersonDetailView – section header for todos", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Todos" + } + } + } + }, "Touch ID aktiviert" : { "comment" : "SettingsView – biometric label when Touch ID is active", "localizations" : { @@ -5308,6 +5432,17 @@ } } }, + "Was möchtest du erledigen?" : { + "comment" : "AddTodoView – placeholder text for todo title input", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "What do you want to do?" + } + } + } + }, "Was war der Kern des Gesprächs?\nWas möchtest du nicht vergessen?" : { "comment" : "AddMomentView – text editor placeholder", "extractionState" : "stale", @@ -5750,171 +5885,6 @@ } } } - }, - "Todo" : { - "comment" : "PersonDetailView – button label to add a new Todo", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Todo" - } - } - } - }, - "Todo anlegen" : { - "comment" : "AddTodoView – navigation title", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Add Todo" - } - } - } - }, - "Dieser Moment wird unwiderruflich gelöscht." : { - "comment" : "EditMomentView – delete confirmation message", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "This moment will be permanently deleted." - } - } - } - }, - "Moment löschen" : { - "comment" : "EditMomentView – delete button label", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Delete Moment" - } - } - } - }, - "Moment löschen?" : { - "comment" : "EditMomentView – delete confirmation dialog title", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Delete Moment?" - } - } - } - }, - "Moment + Kalendereintrag löschen" : { - "comment" : "EditMomentView – delete moment + calendar event option", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Delete Moment + Calendar Event" - } - } - } - }, - "Moment mit Kalendereintrag löschen?" : { - "comment" : "EditMomentView – calendar delete confirmation dialog title", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Delete Moment with Calendar Event?" - } - } - } - }, - "Nur Moment löschen" : { - "comment" : "EditMomentView – delete only the moment, keep calendar event", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Delete Moment Only" - } - } - } - }, - "Todo bearbeiten" : { - "comment" : "EditTodoView – navigation title", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Edit Todo" - } - } - } - }, - "Todos" : { - "comment" : "PersonDetailView – section header for todos", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Todos" - } - } - } - }, - "Was möchtest du erledigen?" : { - "comment" : "AddTodoView – placeholder text for todo title input", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "What do you want to do?" - } - } - } - }, - "Fällig am" : { - "comment" : "AddTodoView – label for due date picker", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Due on" - } - } - } - }, - "Fällige Todos" : { - "comment" : "TodayView – section header for due todos", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Due Todos" - } - } - } - }, - "Noch keine Todos." : { - "comment" : "PersonDetailView – empty state message when person has no todos", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No todos yet." - } - } - } - }, - "Speichern" : { - "comment" : "AddTodoView – save button", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Save" - } - } - } } }, "version" : "1.1" diff --git a/nahbar/nahbar/Models.swift b/nahbar/nahbar/Models.swift index de1964e..b2a8833 100644 --- a/nahbar/nahbar/Models.swift +++ b/nahbar/nahbar/Models.swift @@ -299,12 +299,14 @@ enum LogEntryType: String, Codable { case nextStep = "Schritt abgeschlossen" case calendarEvent = "Termin geplant" case call = "Anruf" + case todoCompleted = "Todo abgeschlossen" var icon: String { switch self { case .nextStep: return "checkmark.circle.fill" case .calendarEvent: return "calendar.badge.checkmark" case .call: return "phone.circle.fill" + case .todoCompleted: return "checkmark.square.fill" } } @@ -313,6 +315,7 @@ enum LogEntryType: String, Codable { case .nextStep: return "green" case .calendarEvent: return "blue" case .call: return "accent" + case .todoCompleted: return "green" } } } diff --git a/nahbar/nahbar/PersonDetailView.swift b/nahbar/nahbar/PersonDetailView.swift index c534a68..b0b77a8 100644 --- a/nahbar/nahbar/PersonDetailView.swift +++ b/nahbar/nahbar/PersonDetailView.swift @@ -39,7 +39,7 @@ struct PersonDetailView: View { personHeader momentsSection todosSection - if !person.sortedLogEntries.isEmpty { logbuchSection } + if !person.sortedMoments.isEmpty || !person.sortedLogEntries.isEmpty { logbuchSection } if hasInfoContent { infoSection } } .padding(.horizontal, 20) @@ -271,10 +271,31 @@ struct PersonDetailView: View { private let logbuchPreviewLimit = 5 + // Lokaler Hilfstyp für die gemischte Vorschau + private struct LogPreviewItem: Identifiable { + let id: String + let icon: String + let title: String + let typeLabel: String + let date: Date + } + + private var mergedLogPreview: [LogPreviewItem] { + let momentItems = person.sortedMoments.map { + LogPreviewItem(id: "m-\($0.id)", icon: $0.type.icon, title: $0.text, + typeLabel: $0.type.displayName, date: $0.createdAt) + } + let entryItems = person.sortedLogEntries.map { + LogPreviewItem(id: "e-\($0.id)", icon: $0.type.icon, title: $0.title, + typeLabel: $0.type.rawValue, date: $0.loggedAt) + } + return (momentItems + entryItems).sorted { $0.date > $1.date } + } + private var logbuchSection: some View { - let entries = person.sortedLogEntries - let preview = Array(entries.prefix(logbuchPreviewLimit)) - let hasMore = entries.count > logbuchPreviewLimit + let allItems = mergedLogPreview + let preview = Array(allItems.prefix(logbuchPreviewLimit)) + let hasMore = allItems.count > logbuchPreviewLimit return VStack(alignment: .leading, spacing: 10) { HStack { @@ -288,14 +309,14 @@ struct PersonDetailView: View { } VStack(spacing: 0) { - ForEach(Array(preview.enumerated()), id: \.element.id) { index, entry in - logEntryPreviewRow(entry) + ForEach(Array(preview.enumerated()), id: \.element.id) { index, item in + logPreviewRow(item) if index < preview.count - 1 || hasMore { RowDivider() } } if hasMore { NavigationLink(destination: LogbuchView(person: person)) { HStack { - Text("Alle \(entries.count) Einträge anzeigen") + Text("Alle \(allItems.count) Einträge anzeigen") .font(.system(size: 14)) .foregroundStyle(theme.accent) Spacer() @@ -313,27 +334,28 @@ struct PersonDetailView: View { } } - private func logEntryPreviewRow(_ entry: LogEntry) -> some View { + private func logPreviewRow(_ item: LogPreviewItem) -> some View { HStack(spacing: 12) { - Image(systemName: entry.type.icon) + Image(systemName: item.icon) .font(.system(size: 14, weight: .light)) .foregroundStyle(theme.accent) .frame(width: 20) VStack(alignment: .leading, spacing: 3) { - Text(entry.title) + Text(item.title) .font(.system(size: 15, design: theme.displayDesign)) .foregroundStyle(theme.contentPrimary) + .lineLimit(2) .fixedSize(horizontal: false, vertical: true) HStack(spacing: 6) { - Text(LocalizedStringKey(entry.type.rawValue)) + Text(LocalizedStringKey(item.typeLabel)) .font(.system(size: 12)) .foregroundStyle(theme.contentTertiary) Text("·") .font(.system(size: 12)) .foregroundStyle(theme.contentTertiary) - Text(entry.loggedAt.formatted(.dateTime.day().month(.abbreviated).year())) + Text(item.date.formatted(.dateTime.day().month(.abbreviated).year())) .font(.system(size: 12)) .foregroundStyle(theme.contentTertiary) } @@ -484,6 +506,11 @@ struct PersonDetailView: View { UNUserNotificationCenter.current() .removePendingNotificationRequests(withIdentifiers: ["todo-\(todo.id)"]) + // Logbuch-Eintrag erstellen + let entry = LogEntry(type: .todoCompleted, title: todo.title, person: person) + modelContext.insert(entry) + person.logEntries?.append(entry) + // Nach 5 Sek. sanft ausblenden DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) { withAnimation(.easeOut(duration: 0.35)) { diff --git a/nahbar/nahbar/TodayView.swift b/nahbar/nahbar/TodayView.swift index b713457..5cef1fa 100644 --- a/nahbar/nahbar/TodayView.swift +++ b/nahbar/nahbar/TodayView.swift @@ -372,6 +372,11 @@ struct TodayView: View { UNUserNotificationCenter.current() .removePendingNotificationRequests(withIdentifiers: ["todo-\(todo.id)"]) + // Logbuch-Eintrag erstellen + let entry = LogEntry(type: .todoCompleted, title: todo.title, person: todo.person) + modelContext.insert(entry) + todo.person?.logEntries?.append(entry) + do { try modelContext.save() } catch {