Add lead workflows, PDF signing, AR projects and photo gallery integration

This commit is contained in:
Dennis Nemec
2026-11-08 17:49:39 +01:00
parent a343bde741
commit 8d18873412
82 changed files with 6401 additions and 34 deletions

View File

@ -0,0 +1,111 @@
import Foundation
import UIKit
@MainActor
final class StubLeadDetailRemoteService: LeadDetailRemoteService {
enum Scenario { case success, offline, timeout, http(Int), invalidFile }
var detailScenario: Scenario = .success
var fileScenario: Scenario = .success
var delay: Duration
private(set) var downloadCount = 0
private let leads: any LeadRemoteService
init(leads: (any LeadRemoteService)? = nil,
delay: Duration = .milliseconds(400)) {
self.leads = leads ?? StubLeadRemoteService(delay: .zero)
self.delay = delay
}
func fetchDetail(id: String, salesCode: String) async throws -> LeadDetailDTO {
try await simulate(detailScenario)
let lead = try await leads.fetchLead(id: id, salesCode: salesCode).toDomain()
return LeadDetailDTO(detail: LeadDetailSampleData.make(for: lead))
}
func download(_ reference: LeadFileReference, leadID: String, salesCode: String) async throws -> Data {
downloadCount += 1
try await simulate(fileScenario)
if case .invalidFile = fileScenario { return Data("not a valid file".utf8) }
let lead = try await leads.fetchLead(id: leadID, salesCode: salesCode).toDomain()
let detail = LeadDetailSampleData.make(for: lead)
switch reference.kind {
case .pdf:
guard let document = detail.offers.flatMap(\.documents).first(where: { $0.file == reference }) else {
throw LeadRemoteError.http(statusCode: 404)
}
return makePDF(document: document, customer: lead.displayName)
case .image:
guard let photo = detail.photos.first(where: { $0.thumbnail == reference || $0.original == reference }) else {
throw LeadRemoteError.http(statusCode: 404)
}
return makeImage(photo: photo, thumbnail: reference == photo.thumbnail)
}
}
private func simulate(_ scenario: Scenario) async throws {
try await Task.sleep(for: delay)
try Task.checkCancellation()
switch scenario {
case .success, .invalidFile: break
case .offline: throw URLError(.notConnectedToInternet)
case .timeout: throw URLError(.timedOut)
case .http(let code): throw LeadRemoteError.http(statusCode: code)
}
}
private func makePDF(document: LeadDocument, customer: String) -> Data {
let renderer = UIGraphicsPDFRenderer(bounds: CGRect(x: 0, y: 0, width: 595, height: 842))
return renderer.pdfData { context in
for page in 1...document.pageCount {
context.beginPage()
let title = document.title.replacingOccurrences(of: ".pdf", with: "")
(title as NSString).draw(in: CGRect(x: 48, y: 56, width: 499, height: 100),
withAttributes: [.font: UIFont.boldSystemFont(ofSize: 24)])
let body = """
DrachenGas – Beispieldokument
\(customer)
Seite \(page) von \(document.pageCount)
Diese Datei dient ausschließlich zur Vorschau der Download- und PDF-Anzeige.
Sie enthält keine verbindlichen Vertragsbedingungen.
Bitte prüfen Sie alle Seiten vor der Unterschrift.
"""
(body as NSString).draw(in: CGRect(x: 48, y: 180, width: 499, height: 540),
withAttributes: [.font: UIFont.systemFont(ofSize: 15),
.foregroundColor: UIColor.darkGray])
if let placement = document.signaturePlacement, placement.pageIndex == page - 1 {
let bounds = placement.bounds
let field = CGRect(x: bounds.minX, y: 842 - bounds.maxY,
width: bounds.width, height: bounds.height)
UIColor.lightGray.setStroke()
UIBezierPath(rect: field).stroke()
("Unterschrift Kunde" as NSString).draw(
at: CGPoint(x: field.minX, y: field.maxY + 8),
withAttributes: [.font: UIFont.systemFont(ofSize: 11),
.foregroundColor: UIColor.darkGray]
)
}
}
}
}
private func makeImage(photo: LeadPhoto, thumbnail: Bool) -> Data {
let size = thumbnail ? CGSize(width: 360, height: 220) : CGSize(width: 1200, height: 800)
let renderer = UIGraphicsImageRenderer(size: size)
return renderer.pngData { context in
UIColor.systemTeal.withAlphaComponent(0.14).setFill()
context.fill(CGRect(origin: .zero, size: size))
let symbol = UIImage(systemName: photo.isAR ? "arkit" : "photo")?
.withTintColor(.systemBlue, renderingMode: .alwaysOriginal)
symbol?.draw(in: CGRect(x: size.width * 0.4, y: size.height * 0.2,
width: size.width * 0.2, height: size.height * 0.35))
("Beispiel · \(photo.title)" as NSString).draw(
in: CGRect(x: 20, y: size.height * 0.72, width: size.width - 40, height: 60),
withAttributes: [.font: UIFont.systemFont(ofSize: thumbnail ? 16 : 32),
.foregroundColor: UIColor.darkGray]
)
}
}
}