Add lead workflows, PDF signing, AR projects and photo gallery integration
This commit is contained in:
192
GasTankAR/Views/Leads/Detail/LeadOffersSection.swift
Normal file
192
GasTankAR/Views/Leads/Detail/LeadOffersSection.swift
Normal file
@ -0,0 +1,192 @@
|
||||
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<Content: View>: 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user