Add lead workflows, PDF signing, AR projects and photo gallery integration
This commit is contained in:
156
GasTankAR/Views/Leads/Signing/LeadSigningSheet.swift
Normal file
156
GasTankAR/Views/Leads/Signing/LeadSigningSheet.swift
Normal file
@ -0,0 +1,156 @@
|
||||
import SwiftUI
|
||||
import PencilKit
|
||||
|
||||
struct LeadSigningSheet: View {
|
||||
@State private var model: LeadSigningViewModel
|
||||
@State private var confirmDiscard = false
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
|
||||
let onSigned: (LeadDocument, URL) -> Void
|
||||
|
||||
init(request: LeadSigningRequest, leadID: String, salesCode: String,
|
||||
repository: any LeadDetailRepository, onSigned: @escaping (LeadDocument, URL) -> Void) {
|
||||
_model = State(initialValue: LeadSigningViewModel(
|
||||
request: request, leadID: leadID, salesCode: salesCode, repository: repository
|
||||
))
|
||||
self.onSigned = onSigned
|
||||
}
|
||||
|
||||
init(model: LeadSigningViewModel, onSigned: @escaping (LeadDocument, URL) -> Void) {
|
||||
_model = State(initialValue: model)
|
||||
self.onSigned = onSigned
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
GeometryReader { geometry in
|
||||
if geometry.size.width >= 700 && !dynamicTypeSize.isAccessibilitySize {
|
||||
HStack(alignment: .top, spacing: 0) {
|
||||
ScrollView {
|
||||
LeadSigningDocumentListView(
|
||||
documents: model.request.documents, currentID: model.currentDocument?.id,
|
||||
signedIDs: Set(model.signedURLs.keys)
|
||||
)
|
||||
}
|
||||
.frame(width: 240)
|
||||
Divider()
|
||||
ScrollView {
|
||||
LeadSigningContentView(model: model, onSigned: onSigned)
|
||||
.padding(16)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
LeadSigningDocumentListView(
|
||||
documents: model.request.documents, currentID: model.currentDocument?.id,
|
||||
signedIDs: Set(model.signedURLs.keys)
|
||||
)
|
||||
LeadSigningContentView(model: model, onSigned: onSigned)
|
||||
.padding(16)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(Color(uiColor: .systemGroupedBackground))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
LeadSigningHeaderView(
|
||||
offerID: model.request.offerID, current: model.currentDocumentNumber,
|
||||
total: model.request.documents.count, isComplete: model.isComplete,
|
||||
isSigning: model.isBusy, canConfirm: model.canFinish
|
||||
) {
|
||||
if model.drawing.strokes.isEmpty { dismiss() } else { confirmDiscard = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
.tint(LeadDetailStyle.blue)
|
||||
.frame(idealWidth: 1000, idealHeight: 790)
|
||||
.presentationSizing(.fitted)
|
||||
.presentationCornerRadius(20)
|
||||
.interactiveDismissDisabled(model.isBusy || !model.drawing.strokes.isEmpty)
|
||||
.confirmationDialog("Unterschrift verwerfen?", isPresented: $confirmDiscard, titleVisibility: .visible) {
|
||||
Button("Unterschrift verwerfen", role: .destructive) { dismiss() }
|
||||
Button("Weiter unterschreiben", role: .cancel) {}
|
||||
} message: {
|
||||
Text("Bereits gespeicherte PDFs bleiben erhalten. Nur die aktuelle Eingabe wird verworfen.")
|
||||
}
|
||||
.task {
|
||||
if model.sourceURL == nil { await model.loadCurrentDocument() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("Unterschreiben", traits: .fixedLayout(width: 1000, height: 790), .modifier(LeadSigningPreviewData())) {
|
||||
LeadSigningPreviewHost()
|
||||
}
|
||||
|
||||
private struct LeadSigningPreviewHost: View {
|
||||
@Environment(LeadSigningViewModel.self) private var model
|
||||
|
||||
var body: some View {
|
||||
LeadSigningSheet(model: model, onSigned: { _, _ in })
|
||||
}
|
||||
}
|
||||
|
||||
private struct LeadSigningPreviewData: PreviewModifier {
|
||||
static func makeSharedContext() async throws -> LeadSigningViewModel {
|
||||
let repository = DefaultLeadDetailRepository(
|
||||
remoteService: StubLeadDetailRemoteService(delay: .zero)
|
||||
)
|
||||
let detail = try await repository.detail(id: "L-240318", salesCode: "MKR")
|
||||
guard let offer = detail.offers.first else { throw PDFSigningError.invalidDocument }
|
||||
let request = LeadSigningRequest(offerID: offer.id, signerName: "Klaus Hoffmann",
|
||||
documents: offer.documents)
|
||||
let model = LeadSigningViewModel(request: request, leadID: detail.lead.id,
|
||||
salesCode: "MKR", repository: repository)
|
||||
await model.loadCurrentDocument()
|
||||
return model
|
||||
}
|
||||
|
||||
func body(content: Content, context: LeadSigningViewModel) -> some View {
|
||||
content.environment(context)
|
||||
}
|
||||
}
|
||||
|
||||
private struct LeadSigningContentView: View {
|
||||
@Bindable var model: LeadSigningViewModel
|
||||
let onSigned: (LeadDocument, URL) -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 20) {
|
||||
if let url = model.sourceURL {
|
||||
LeadSigningPreviewView(
|
||||
offerID: model.request.offerID, url: url,
|
||||
placement: model.isComplete ? nil : model.currentDocument?.signaturePlacement
|
||||
)
|
||||
} else if model.isLoading {
|
||||
ProgressView("PDF wird geladen …")
|
||||
.frame(maxWidth: .infinity, minHeight: 310)
|
||||
}
|
||||
if model.showsCompletion {
|
||||
LeadSigningCompletionView(model: model)
|
||||
} else {
|
||||
LeadSigningSignatureView(
|
||||
signerName: model.request.signerName, drawing: $model.drawing,
|
||||
isEnabled: model.sourceURL != nil && !model.isSigning
|
||||
)
|
||||
.padding(.top, 16)
|
||||
if let message = model.message {
|
||||
Text(message).font(.callout).foregroundStyle(.red)
|
||||
if model.sourceURL == nil {
|
||||
Button("Erneut versuchen") { Task { await model.loadCurrentDocument() } }
|
||||
.disabled(model.isLoading)
|
||||
}
|
||||
}
|
||||
LeadSigningFooterView(
|
||||
title: model.currentDocument?.title.replacingOccurrences(of: ".pdf", with: "") ?? "",
|
||||
canSign: model.canSign, isSigning: model.isSigning,
|
||||
isLast: model.isLastPendingDocument
|
||||
) {
|
||||
Task { await model.signAndAdvance(onSigned: onSigned) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user