18112cb52c
Der Speichern-Button am Ende des Flows ersetzt den Abbrechen-Button in der Toolbar für beide Fragebögen (Meeting + Nachwirkung). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
150 lines
5.0 KiB
Swift
150 lines
5.0 KiB
Swift
import SwiftUI
|
|
import SwiftData
|
|
|
|
// MARK: - MeetingRatingFlowView
|
|
// Scrollbarer Bewertungs-Flow für die Sofort-Bewertung eines Treffens.
|
|
// Fragen blenden nacheinander von unten ein, sobald die vorherige beantwortet wurde.
|
|
// Nach der letzten Frage erscheint ein "Speichern"-Button.
|
|
|
|
struct MeetingRatingFlowView: View {
|
|
@Environment(\.modelContext) private var modelContext
|
|
@Environment(\.nahbarTheme) var theme
|
|
@Environment(\.dismiss) private var dismiss
|
|
|
|
let moment: Moment
|
|
var aftermathDelay: TimeInterval = 36 * 3600
|
|
|
|
private let questions = RatingQuestion.immediate // 5 Fragen
|
|
@State private var values: [Int?]
|
|
@State private var revealedCount: Int = 1
|
|
@State private var showSummary: Bool = false
|
|
|
|
init(moment: Moment, aftermathDelay: TimeInterval = 36 * 3600) {
|
|
self.moment = moment
|
|
self.aftermathDelay = aftermathDelay
|
|
_values = State(initialValue: Array(repeating: nil, count: RatingQuestion.immediate.count))
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Group {
|
|
if showSummary {
|
|
MeetingSummaryView(moment: moment, onDismiss: { dismiss() })
|
|
} else {
|
|
questionFlow
|
|
}
|
|
}
|
|
.navigationTitle("Treffen bewerten")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
|
|
}
|
|
}
|
|
|
|
// MARK: - Scrollbarer Fragen-Flow
|
|
|
|
private var questionFlow: some View {
|
|
ScrollViewReader { proxy in
|
|
ScrollView {
|
|
VStack(spacing: 14) {
|
|
ForEach(0..<revealedCount, id: \.self) { i in
|
|
QuestionCard(
|
|
question: questions[i],
|
|
index: i,
|
|
total: questions.count,
|
|
isActive: i == revealedCount - 1,
|
|
value: $values[i],
|
|
onAnswer: { revealNext(after: i) },
|
|
onSkip: { revealNext(after: i) }
|
|
)
|
|
.id(i)
|
|
.transition(.asymmetric(
|
|
insertion: .opacity.combined(with: .move(edge: .bottom)),
|
|
removal: .identity
|
|
))
|
|
}
|
|
|
|
if revealedCount == questions.count {
|
|
saveButton
|
|
.id("save")
|
|
.transition(.asymmetric(
|
|
insertion: .opacity.combined(with: .move(edge: .bottom)),
|
|
removal: .identity
|
|
))
|
|
}
|
|
}
|
|
.padding(16)
|
|
.padding(.bottom, 32)
|
|
}
|
|
.background(theme.backgroundPrimary.ignoresSafeArea())
|
|
.onChange(of: revealedCount) { _, newCount in
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
|
|
withAnimation(.easeOut(duration: 0.4)) {
|
|
proxy.scrollTo(newCount - 1, anchor: .top)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private var saveButton: some View {
|
|
Button { saveRatings() } label: {
|
|
Text("Speichern")
|
|
.font(.system(size: 16, weight: .semibold))
|
|
.foregroundStyle(.white)
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, 15)
|
|
.background(theme.accent)
|
|
.clipShape(RoundedRectangle(cornerRadius: theme.radiusCard))
|
|
}
|
|
}
|
|
|
|
// MARK: - Navigation
|
|
|
|
private func revealNext(after index: Int) {
|
|
guard index == revealedCount - 1 else { return }
|
|
guard revealedCount < questions.count else { return }
|
|
withAnimation(.easeOut(duration: 0.35)) {
|
|
revealedCount += 1
|
|
}
|
|
}
|
|
|
|
// MARK: - Speichern
|
|
|
|
private func saveRatings() {
|
|
for (i, q) in questions.enumerated() {
|
|
let rating = Rating(
|
|
category: q.category,
|
|
questionIndex: i,
|
|
value: values[i],
|
|
isAftermath: false,
|
|
moment: moment
|
|
)
|
|
modelContext.insert(rating)
|
|
}
|
|
|
|
moment.meetingStatus = .awaitingAftermath
|
|
|
|
do {
|
|
try modelContext.save()
|
|
AppEventLog.shared.record(
|
|
"Treffen bewertet: \(moment.person?.firstName ?? "?") (\(questions.count) Fragen)",
|
|
level: .info, category: "Meeting"
|
|
)
|
|
} catch {
|
|
AppEventLog.shared.record(
|
|
"Fehler beim Speichern der Treffen-Bewertung: \(error.localizedDescription)",
|
|
level: .error, category: "Meeting"
|
|
)
|
|
}
|
|
|
|
AftermathNotificationManager.shared.scheduleAftermath(
|
|
momentID: moment.id,
|
|
personName: moment.person?.firstName ?? "",
|
|
delay: aftermathDelay
|
|
)
|
|
moment.aftermathNotificationScheduled = true
|
|
|
|
withAnimation { showSummary = true }
|
|
}
|
|
}
|