Fix #30: Verlaufsansicht – neue Momente 5 s in Momente, dann in Verlauf

Neue Logbuch-Momente (vergangene Treffen, Notizen) erscheinen nach dem
Speichern 5 Sekunden mit 45 % Deckkraft in der Momente-Sektion und wandern
dann animiert in den Verlauf. Aktive Momente (offene Vorhaben, Zukunfts-
treffen) bleiben dauerhaft in der Momente-Sektion. Der Verlauf zeigt nur
noch abgeschlossene/vergangene Einträge.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-23 12:26:56 +02:00
parent 801095d9b9
commit e365400537
+48 -5
View File
@@ -39,6 +39,10 @@ struct PersonDetailView: View {
@State private var todoForEdit: Todo? = nil @State private var todoForEdit: Todo? = nil
@State private var fadingOutTodos: [Todo] = [] @State private var fadingOutTodos: [Todo] = []
// Neu hinzugefügte Logbuch-Momente 5 s in Momente sichtbar, dann in Verlauf
@State private var fadingOutMoments: [Moment] = []
@State private var seenMomentIDs: Set<UUID> = []
// Kalender-Lösch-Bestätigung // Kalender-Lösch-Bestätigung
@State private var momentPendingDelete: Moment? = nil @State private var momentPendingDelete: Moment? = nil
@State private var showCalendarDeleteDialog = false @State private var showCalendarDeleteDialog = false
@@ -70,7 +74,7 @@ struct PersonDetailView: View {
} }
momentsSection momentsSection
todosSection todosSection
if !person.sortedMoments.isEmpty || !person.sortedLogEntries.isEmpty { logbuchSection } if !mergedLogPreview.isEmpty { logbuchSection }
if hasInfoContent { infoSection } if hasInfoContent { infoSection }
} }
.padding(.horizontal, 20) .padding(.horizontal, 20)
@@ -100,6 +104,26 @@ struct PersonDetailView: View {
} }
} }
} }
.onChange(of: showingAddMoment) { _, isShowing in
if isShowing {
seenMomentIDs = Set(person.sortedMoments.map(\.id))
} else {
// Neu gespeicherte Logbuch-Momente (keine Vorhaben, kein Zukunftstreffen) kurz anzeigen
let newLogbuchMoments = person.sortedMoments.filter { moment in
guard !seenMomentIDs.contains(moment.id) else { return false }
let isActive = moment.isOpen || (moment.isMeeting && moment.createdAt > Date())
return !isActive
}
for moment in newLogbuchMoments {
withAnimation { fadingOutMoments.append(moment) }
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
withAnimation(.easeOut(duration: 0.35)) {
fadingOutMoments.removeAll { $0.id == moment.id }
}
}
}
}
}
.sheet(isPresented: $showingEditPerson) { .sheet(isPresented: $showingEditPerson) {
AddPersonView(existingPerson: person) AddPersonView(existingPerson: person)
} }
@@ -342,6 +366,18 @@ struct PersonDetailView: View {
// MARK: - Momente // MARK: - Momente
/// Aktive Momente: offene Vorhaben + noch ausstehende Treffen in der Zukunft.
private var activeMoments: [Moment] {
person.sortedMoments.filter { $0.isOpen || ($0.isMeeting && $0.createdAt > Date()) }
}
/// Was in der Momente-Sektion angezeigt wird: aktive + kurzzeitig sichtbare neue Logbuch-Momente.
private var visibleMoments: [Moment] {
let fadingIDs = Set(fadingOutMoments.map(\.id))
let active = activeMoments.filter { !fadingIDs.contains($0.id) }
return active + fadingOutMoments
}
private var momentsSection: some View { private var momentsSection: some View {
VStack(alignment: .leading, spacing: 10) { VStack(alignment: .leading, spacing: 10) {
HStack { HStack {
@@ -376,9 +412,9 @@ struct PersonDetailView: View {
.font(.system(size: 14)) .font(.system(size: 14))
.foregroundStyle(theme.contentTertiary) .foregroundStyle(theme.contentTertiary)
.padding(.vertical, 4) .padding(.vertical, 4)
} else { } else if !visibleMoments.isEmpty {
VStack(spacing: 0) { VStack(spacing: 0) {
ForEach(Array(person.sortedMoments.enumerated()), id: \.element.id) { index, moment in ForEach(Array(visibleMoments.enumerated()), id: \.element.id) { index, moment in
VStack(spacing: 0) { VStack(spacing: 0) {
MomentRowView( MomentRowView(
moment: moment, moment: moment,
@@ -390,7 +426,8 @@ struct PersonDetailView: View {
onEdit: { momentForTextEdit = moment }, onEdit: { momentForTextEdit = moment },
onToggleImportant: { toggleImportant(moment) } onToggleImportant: { toggleImportant(moment) }
) )
if index < person.sortedMoments.count - 1 { RowDivider() } .opacity(fadingOutMoments.contains(where: { $0.id == moment.id }) ? 0.45 : 1.0)
if index < visibleMoments.count - 1 { RowDivider() }
} }
} }
} }
@@ -460,7 +497,13 @@ struct PersonDetailView: View {
} }
private var mergedLogPreview: [LogPreviewItem] { private var mergedLogPreview: [LogPreviewItem] {
let momentItems = person.sortedMoments.map { // Nur Momente die weder aktiv (offene Vorhaben / Zukunftstreffen) noch gerade sichtbar ausklingend sind
let activeIDs = Set(activeMoments.map(\.id))
let fadingIDs = Set(fadingOutMoments.map(\.id))
let logbuchMoments = person.sortedMoments.filter {
!activeIDs.contains($0.id) && !fadingIDs.contains($0.id)
}
let momentItems = logbuchMoments.map {
LogPreviewItem(id: "m-\($0.id)", icon: $0.type.icon, title: $0.text, LogPreviewItem(id: "m-\($0.id)", icon: $0.type.icon, title: $0.text,
typeLabel: $0.type.displayName, date: $0.createdAt) typeLabel: $0.type.displayName, date: $0.createdAt)
} }