Compare commits

..

5 Commits

Author SHA1 Message Date
sven d541640c74 Logbuch-Button aus Toolbar entfernt (Zugang über Verlauf-Sektion)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 05:40:36 +02:00
sven 30b150a286 Umbenennung: Verlauf → Verlauf & KI-Analyse (sparkles-Icon)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 05:38:21 +02:00
sven 4a9bb32b5e Logbuch: Todos + alle Momente im Verlauf
- 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 <noreply@anthropic.com>
2026-04-22 05:34:50 +02:00
sven 2b9346c78b Fix #16 Logbuch-Button in Toolbar ergänzt
Buchsymbol in der NavBar öffnet LogbuchView unabhängig davon, ob
bereits Logeinträge vorhanden sind (vorheriger Fix griff nur wenn der
Verlauf-Abschnitt sichtbar war).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 05:29:19 +02:00
sven 66a7b23f5a Resolves #16 Logbuch-Button immer sichtbar in PersonDetailView
"Alle"-Link im Verlauf-Header zeigt LogbuchView unabhängig von der
Eintragsanzahl (vorher nur bei >5 Einträgen sichtbar).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 05:27:24 +02:00
4 changed files with 212 additions and 190 deletions
+159 -177
View File
@@ -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",
@@ -4768,6 +4837,62 @@
}
}
},
"Todo" : {
"comment" : "PersonDetailView button label to add a new Todo",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Todo"
}
}
}
},
"Todo abgeschlossen" : {
"comment" : "LogEntryType.todoCompleted raw value label",
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Todo completed"
}
}
}
},
"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" : {
@@ -5032,6 +5157,17 @@
}
}
},
"Verlauf & KI-Analyse" : {
"comment" : "PersonDetailView logbuch section header",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "History & AI Analysis"
}
}
}
},
"Version" : {
"comment" : "SettingsView version info row label",
"extractionState" : "stale",
@@ -5308,6 +5444,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 +5897,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"
+3
View File
@@ -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"
}
}
}
+45 -13
View File
@@ -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)
@@ -264,26 +264,52 @@ 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 {
SectionHeader(title: "Verlauf", icon: "book.closed")
SectionHeader(title: "Verlauf & KI-Analyse", icon: "sparkles")
Spacer()
NavigationLink(destination: LogbuchView(person: person)) {
Text("Alle")
.font(.system(size: 13))
.foregroundStyle(theme.accent)
}
}
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()
@@ -301,27 +327,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)
}
@@ -472,6 +499,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)) {
+5
View File
@@ -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 {