Add lead workflows, PDF signing, AR projects and photo gallery integration
This commit is contained in:
32
GasTankAR/Domain/AR/ARProject.swift
Normal file
32
GasTankAR/Domain/AR/ARProject.swift
Normal file
@ -0,0 +1,32 @@
|
||||
import Foundation
|
||||
|
||||
/// Neutral projection for project selection. Persistence and customer associations
|
||||
/// belong to the host application's service.
|
||||
struct ARProject: Identifiable, Equatable {
|
||||
let id: UUID
|
||||
let name: String
|
||||
let tank: Tank
|
||||
let createdAt: Date
|
||||
let source: Source
|
||||
let reference: String?
|
||||
|
||||
enum Source { case supplied, custom }
|
||||
}
|
||||
|
||||
@MainActor
|
||||
protocol ARProjectsProviding {
|
||||
func updateProject(id: UUID, name: String, tank: Tank) throws -> ARProject
|
||||
func deleteProject(id: UUID) throws
|
||||
func loadProjects() throws -> [ARProject]
|
||||
func createProject(name: String, tank: Tank) throws -> ARProject
|
||||
}
|
||||
|
||||
nonisolated extension TankSize {
|
||||
var volumeTitle: String {
|
||||
switch self {
|
||||
case .t1_2: "2.700 l"
|
||||
case .t2_1: "4.850 l"
|
||||
case .t2_9: "6.400 l"
|
||||
}
|
||||
}
|
||||
}
|
||||
36
GasTankAR/Domain/Leads/Lead.swift
Normal file
36
GasTankAR/Domain/Leads/Lead.swift
Normal file
@ -0,0 +1,36 @@
|
||||
import Foundation
|
||||
|
||||
struct Lead: Identifiable, Equatable, Sendable {
|
||||
let id: String
|
||||
let companyName: String?
|
||||
let contact: LeadContact?
|
||||
let address: LeadAddress
|
||||
let phoneNumber: String?
|
||||
let createdAt: Date
|
||||
let offerCount: Int
|
||||
let unsignedContractCount: Int
|
||||
var isBookmarked: Bool
|
||||
}
|
||||
|
||||
struct LeadContact: Equatable, Sendable {
|
||||
let title: String?
|
||||
let firstName: String
|
||||
let lastName: String
|
||||
}
|
||||
|
||||
struct LeadAddress: Equatable, Sendable {
|
||||
let street: String?
|
||||
let postalCode: String
|
||||
let city: String
|
||||
}
|
||||
|
||||
enum LeadError: Error, Equatable, Sendable {
|
||||
case offline
|
||||
case timeout
|
||||
case serviceUnavailable
|
||||
case unauthorized
|
||||
case forbidden
|
||||
case notFound(leadID: String)
|
||||
case invalidResponse
|
||||
case unexpected
|
||||
}
|
||||
77
GasTankAR/Domain/Leads/LeadDetail.swift
Normal file
77
GasTankAR/Domain/Leads/LeadDetail.swift
Normal file
@ -0,0 +1,77 @@
|
||||
import Foundation
|
||||
|
||||
struct LeadDetail: Equatable, Sendable {
|
||||
let lead: Lead
|
||||
let customerType: String
|
||||
let contactName: String?
|
||||
let contactRole: String?
|
||||
let mobile: String?
|
||||
let email: String?
|
||||
let tankOptions: [LeadTankOption]
|
||||
var offers: [LeadOffer]
|
||||
let photos: [LeadPhoto]
|
||||
}
|
||||
|
||||
struct LeadTankOption: Identifiable, Equatable, Sendable {
|
||||
let id: String
|
||||
let title: String
|
||||
let tank: Tank
|
||||
}
|
||||
|
||||
struct LeadOffer: Identifiable, Equatable, Sendable {
|
||||
let id: String
|
||||
let title: String
|
||||
let kind: LeadOfferKind
|
||||
let amount: Decimal
|
||||
let billing: LeadBilling
|
||||
let validUntil: Date?
|
||||
let acceptedAt: Date?
|
||||
var groups: [LeadDocumentGroup]
|
||||
var tank: Tank? = nil
|
||||
|
||||
var documents: [LeadDocument] { groups.flatMap(\.documents) }
|
||||
var signedCount: Int { documents.filter { $0.status == .signed }.count }
|
||||
var isComplete: Bool { !documents.isEmpty && signedCount == documents.count }
|
||||
}
|
||||
|
||||
enum LeadOfferKind: String, Sendable { case purchase, rental, service }
|
||||
enum LeadBilling: String, Sendable { case once, monthly, annually }
|
||||
|
||||
struct LeadDocumentGroup: Identifiable, Equatable, Sendable {
|
||||
let id: String
|
||||
let title: String
|
||||
var documents: [LeadDocument]
|
||||
var signedCount: Int { documents.filter { $0.status == .signed }.count }
|
||||
var isComplete: Bool { !documents.isEmpty && signedCount == documents.count }
|
||||
}
|
||||
|
||||
struct LeadDocument: Identifiable, Equatable, Sendable {
|
||||
let id: String
|
||||
let title: String
|
||||
let pageCount: Int
|
||||
var status: LeadSignatureStatus
|
||||
let file: LeadFileReference
|
||||
var signaturePlacement: PDFSignaturePlacement? = nil
|
||||
}
|
||||
|
||||
enum LeadSignatureStatus: String, Sendable { case pending, signed }
|
||||
|
||||
struct LeadPhoto: Identifiable, Equatable, Sendable {
|
||||
let id: String
|
||||
let title: String
|
||||
let isAR: Bool
|
||||
let thumbnail: LeadFileReference
|
||||
let original: LeadFileReference
|
||||
}
|
||||
|
||||
struct LeadFileReference: Hashable, Sendable {
|
||||
let id: String
|
||||
let version: String
|
||||
let kind: LeadFileKind
|
||||
}
|
||||
|
||||
enum LeadFileKind: String, Sendable { case pdf, image }
|
||||
|
||||
enum LeadFileError: Error, Equatable {
|
||||
case notFound, invalidFile, storageUnavailable
|
||||
}
|
||||
34
GasTankAR/Domain/Leads/LeadDetailRepository.swift
Normal file
34
GasTankAR/Domain/Leads/LeadDetailRepository.swift
Normal file
@ -0,0 +1,34 @@
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
protocol LeadDetailRepository {
|
||||
func detail(id: String, salesCode: String) async throws -> LeadDetail
|
||||
func file(_ reference: LeadFileReference, leadID: String, salesCode: String) async throws -> URL
|
||||
}
|
||||
|
||||
struct GetLeadDetailUseCase {
|
||||
let repository: any LeadDetailRepository
|
||||
|
||||
func execute(id: String, salesCode: String) async throws -> LeadDetail {
|
||||
try await repository.detail(id: id, salesCode: salesCode)
|
||||
}
|
||||
}
|
||||
|
||||
struct DownloadLeadDocumentUseCase {
|
||||
let repository: any LeadDetailRepository
|
||||
|
||||
func execute(_ document: LeadDocument, leadID: String, salesCode: String) async throws -> URL {
|
||||
guard document.file.kind == .pdf else { throw LeadFileError.invalidFile }
|
||||
return try await repository.file(document.file, leadID: leadID, salesCode: salesCode)
|
||||
}
|
||||
}
|
||||
|
||||
struct LoadLeadPhotoUseCase {
|
||||
let repository: any LeadDetailRepository
|
||||
|
||||
func execute(_ photo: LeadPhoto, original: Bool = false,
|
||||
leadID: String, salesCode: String) async throws -> URL {
|
||||
try await repository.file(original ? photo.original : photo.thumbnail,
|
||||
leadID: leadID, salesCode: salesCode)
|
||||
}
|
||||
}
|
||||
23
GasTankAR/Domain/Leads/LeadQuery.swift
Normal file
23
GasTankAR/Domain/Leads/LeadQuery.swift
Normal file
@ -0,0 +1,23 @@
|
||||
import Foundation
|
||||
|
||||
struct LeadQuery: Equatable, Sendable {
|
||||
var searchText = ""
|
||||
var sort: LeadSort = .newest
|
||||
var filter: LeadFilter = .all
|
||||
|
||||
var normalized: LeadQuery {
|
||||
var query = self
|
||||
query.searchText = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return query
|
||||
}
|
||||
}
|
||||
|
||||
enum LeadSort: Sendable {
|
||||
case newest
|
||||
case nameAscending
|
||||
}
|
||||
|
||||
enum LeadFilter: Sendable {
|
||||
case all
|
||||
case unsignedContracts
|
||||
}
|
||||
8
GasTankAR/Domain/Leads/LeadRepository.swift
Normal file
8
GasTankAR/Domain/Leads/LeadRepository.swift
Normal file
@ -0,0 +1,8 @@
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
protocol LeadRepository {
|
||||
func leads(for salesCode: String, query: LeadQuery) async throws -> [Lead]
|
||||
func lead(id: String, salesCode: String) async throws -> Lead
|
||||
func setBookmark(_ bookmarked: Bool, leadID: String, salesCode: String) async throws -> Lead
|
||||
}
|
||||
33
GasTankAR/Domain/Leads/LeadUseCases.swift
Normal file
33
GasTankAR/Domain/Leads/LeadUseCases.swift
Normal file
@ -0,0 +1,33 @@
|
||||
import Foundation
|
||||
|
||||
struct LoadLeadsUseCase {
|
||||
let repository: any LeadRepository
|
||||
|
||||
func execute(salesCode: String) async throws -> [Lead] {
|
||||
try await repository.leads(for: salesCode, query: LeadQuery())
|
||||
}
|
||||
}
|
||||
|
||||
struct SearchLeadsUseCase {
|
||||
let repository: any LeadRepository
|
||||
|
||||
func execute(salesCode: String, query: LeadQuery) async throws -> [Lead] {
|
||||
try await repository.leads(for: salesCode, query: query.normalized)
|
||||
}
|
||||
}
|
||||
|
||||
struct SetLeadBookmarkUseCase {
|
||||
let repository: any LeadRepository
|
||||
|
||||
func execute(_ bookmarked: Bool, leadID: String, salesCode: String) async throws -> Lead {
|
||||
try await repository.setBookmark(bookmarked, leadID: leadID, salesCode: salesCode)
|
||||
}
|
||||
}
|
||||
|
||||
struct GetLeadUseCase {
|
||||
let repository: any LeadRepository
|
||||
|
||||
func execute(id: String, salesCode: String) async throws -> Lead {
|
||||
try await repository.lead(id: id, salesCode: salesCode)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user