import SwiftUI struct LeadOffersSection: View { let offers: [LeadOffer] @Environment(\.accessibilityReduceMotion) private var reduceMotion let model: LeadDetailViewModel var body: some View { VStack(alignment: .leading, spacing: 8) { HStack { LeadSectionHeading(title: "ANGEBOTE") Spacer() Button(model.isAllExpanded ? "Alle zuklappen" : "Alle aufklappen") { withAnimation(reduceMotion ? nil : .smooth(duration: 0.32)) { model.toggleAll() } } .font(.subheadline) .disabled(offers.isEmpty) .padding(.trailing, 16) } if offers.isEmpty { ContentUnavailableView("Noch keine Angebote", systemImage: "doc.text", description: Text("Für diesen Interessenten liegen noch keine Angebote vor.")) .frame(maxWidth: .infinity) .background(LeadDetailStyle.card, in: .rect(cornerRadius: 12)) } else { VStack(spacing: 0) { ForEach(offers) { offer in LeadOfferRow(offer: offer, model: model) if offer.id != offers.last?.id { Divider().padding(.leading, 44) } } } .background(LeadDetailStyle.card, in: .rect(cornerRadius: 12)) } } } } struct LeadOfferRow: View { let offer: LeadOffer let model: LeadDetailViewModel var body: some View { VStack(spacing: 0) { LeadTreeHeading(title: offer.title, subtitle: offer.subtitle, icon: "doc.on.doc.fill", expanded: model.expandedOffers.contains(offer.id), signed: offer.signedCount, total: offer.documents.count, prominent: true, onSign: { model.startSigning(offer: offer) }) { model.toggleOffer(offer.id) } LeadTreeDisclosure(expanded: model.expandedOffers.contains(offer.id)) { ForEach(offer.groups) { group in Divider().padding(.leading, 44) LeadDocumentGroupRow(group: group, offer: offer, model: model).padding(.leading, 24) } } } } } struct LeadDocumentGroupRow: View { let group: LeadDocumentGroup let offer: LeadOffer let model: LeadDetailViewModel var body: some View { VStack(spacing: 0) { LeadTreeHeading(title: group.title, subtitle: "\(group.documents.count) \(group.documents.count == 1 ? "PDF" : "PDFs")", icon: "folder.fill", expanded: model.expandedGroups.contains(group.id), signed: group.signedCount, total: group.documents.count, prominent: false, onSign: { model.startSigning(offer: offer, group: group) }) { model.toggleGroup(group.id) } LeadTreeDisclosure(expanded: model.expandedGroups.contains(group.id)) { ForEach(group.documents) { document in Divider().padding(.leading, 70) LeadDocumentRow(document: document) { model.selectedDocument = document } .padding(.leading, 50) } } } } } private struct LeadTreeDisclosure: View { let expanded: Bool @ViewBuilder let content: Content var body: some View { // Keep the natural content height while animating the clipped container. VStack(spacing: 0) { content } .fixedSize(horizontal: false, vertical: true) .frame(height: expanded ? nil : 0, alignment: .top) .clipped() .allowsHitTesting(expanded) .accessibilityHidden(!expanded) } } private struct LeadTreeHeading: View { let title: String let subtitle: String let icon: String let expanded: Bool let signed: Int let total: Int let prominent: Bool let onSign: () -> Void let onToggle: () -> Void @Environment(\.accessibilityReduceMotion) private var reduceMotion @Environment(\.dynamicTypeSize) private var dynamicTypeSize var body: some View { let layout = dynamicTypeSize.isAccessibilitySize ? AnyLayout(VStackLayout(alignment: .leading, spacing: 10)) : AnyLayout(HStackLayout(spacing: 12)) layout { Button { withAnimation(reduceMotion ? nil : .smooth(duration: 0.32)) { onToggle() } } label: { HStack(spacing: 14) { Image(systemName: "chevron.right") .rotationEffect(.degrees(expanded ? 90 : 0)) .font(.caption.weight(.semibold)).frame(width: 12) .foregroundStyle(LeadDetailStyle.blue) Image(systemName: icon).font(.title3).frame(width: 22) .foregroundStyle(LeadDetailStyle.blue) VStack(alignment: .leading, spacing: 4) { Text(title).font(prominent ? .headline : .body) .foregroundStyle(.primary).fixedSize(horizontal: false, vertical: true) Text(subtitle).font(.caption).foregroundStyle(.secondary) .lineLimit(2) } .frame(maxWidth: .infinity, alignment: .leading) } .contentShape(.rect) .frame(minHeight: 44) } .buttonStyle(.plain) .accessibilityLabel(title) .accessibilityValue(expanded ? "Aufgeklappt" : "Zugeklappt") .accessibilityHint("Zum Auf- oder Zuklappen doppeltippen") LeadSignatureProgress(signed: signed, total: total) .fixedSize(horizontal: !dynamicTypeSize.isAccessibilitySize, vertical: true) if signed < total { LeadSignButton(action: onSign) } } .padding(.horizontal, 16).padding(.vertical, 10) } } struct LeadDocumentRow: View { let document: LeadDocument let onOpen: () -> Void @Environment(\.dynamicTypeSize) private var dynamicTypeSize var body: some View { Button(action: onOpen) { HStack(spacing: 14) { Image(systemName: "doc.fill").foregroundStyle(.red) .font(.title3).frame(width: 22) VStack(alignment: .leading, spacing: 4) { Text(document.title).foregroundStyle(.primary) .fixedSize(horizontal: false, vertical: true) Text("PDF · \(document.pageCount) \(document.pageCount == 1 ? "Seite" : "Seiten")") .font(.caption).foregroundStyle(.secondary) if dynamicTypeSize.isAccessibilitySize { status } } Spacer(minLength: 4) if !dynamicTypeSize.isAccessibilitySize { status } Image(systemName: "chevron.right").font(.caption).foregroundStyle(.tertiary) } .padding(.horizontal, 16).padding(.vertical, 11) .frame(minHeight: 48) .contentShape(.rect) } .buttonStyle(.plain) .accessibilityHint("PDF öffnen") } private var status: some View { Label(document.status == .signed ? "Unterschrieben" : "Offen", systemImage: document.status == .signed ? "checkmark.circle.fill" : "circle.fill") .font(.caption).foregroundStyle(document.status == .signed ? .green : .orange) .fixedSize() } }