119 lines
3.6 KiB
Swift
119 lines
3.6 KiB
Swift
//
|
||
// DocuframeViewModel.swift
|
||
// GasTankAR
|
||
//
|
||
// Created by Dennis Nemec on 28.08.26.
|
||
//
|
||
|
||
import Foundation
|
||
import SwiftData
|
||
|
||
/// Präsentationslogik für den DOCUframe-Tab: verwaltet die Fotoauswahl und stößt Upload bzw.
|
||
/// Mailversand über den injizierten ``DocuframeService`` an. Kennt keine UI-Details.
|
||
@MainActor
|
||
@Observable
|
||
final class DocuframeViewModel {
|
||
/// Auswahl anhand der stabilen SwiftData-Identität der Fotos.
|
||
var selectedIDs: Set<PersistentIdentifier> = []
|
||
/// Läuft gerade ein Upload/Mailversand?
|
||
private(set) var isProcessing = false
|
||
/// Ergebnis des letzten Vorgangs für die Anzeige (nil = kein Hinweis).
|
||
var result: DocuframeActionResult?
|
||
|
||
@ObservationIgnored private let service: DocuframeService
|
||
|
||
/// Der Service wird injiziert (Default: Stub) – austauschbar für echte Anbindung oder Tests.
|
||
init(service: DocuframeService = StubDocuframeService()) {
|
||
self.service = service
|
||
}
|
||
|
||
var hasSelection: Bool { !selectedIDs.isEmpty }
|
||
var selectionCount: Int { selectedIDs.count }
|
||
|
||
func isSelected(_ photo: ProjectPhoto) -> Bool {
|
||
selectedIDs.contains(photo.persistentModelID)
|
||
}
|
||
|
||
func toggleSelection(_ photo: ProjectPhoto) {
|
||
let id = photo.persistentModelID
|
||
if selectedIDs.contains(id) {
|
||
selectedIDs.remove(id)
|
||
} else {
|
||
selectedIDs.insert(id)
|
||
}
|
||
}
|
||
|
||
func upload(from photos: [ProjectPhoto], leadID: String) async {
|
||
await perform(from: photos, leadID: leadID, successText: "in DOCUframe hochgeladen") {
|
||
try await service.uploadPhotos($0, projectLeadID: $1)
|
||
}
|
||
}
|
||
|
||
func sendEmail(from photos: [ProjectPhoto], leadID: String) async {
|
||
await perform(from: photos, leadID: leadID, successText: "per E-Mail versendet") {
|
||
try await service.sendEmail($0, projectLeadID: $1)
|
||
}
|
||
}
|
||
|
||
// MARK: - Gemeinsame Ausführung
|
||
|
||
private func perform(
|
||
from photos: [ProjectPhoto],
|
||
leadID: String,
|
||
successText: String,
|
||
action: ([DocuframePhoto], String) async throws -> Void
|
||
) async {
|
||
let selected = photos.filter { selectedIDs.contains($0.persistentModelID) }
|
||
guard !selected.isEmpty else {
|
||
result = .failure(message: DocuframeError.noPhotosSelected.localizedDescription)
|
||
return
|
||
}
|
||
|
||
isProcessing = true
|
||
defer { isProcessing = false }
|
||
|
||
let payload = selected.map(Self.makeDTO)
|
||
do {
|
||
try await action(payload, leadID)
|
||
result = .success(message: "\(selected.count) Foto(s) \(successText).")
|
||
} catch {
|
||
// Bei HTTP 4xx/5xx (und anderen Fehlern) den Nutzer informieren.
|
||
result = .failure(message: error.localizedDescription)
|
||
}
|
||
}
|
||
|
||
/// Bildet das Persistenz-Model auf ein persistenzunabhängiges Domänen-DTO ab.
|
||
private static func makeDTO(_ photo: ProjectPhoto) -> DocuframePhoto {
|
||
DocuframePhoto(
|
||
imageData: photo.imageData,
|
||
creationDate: photo.creationDate,
|
||
latitude: photo.latitude,
|
||
longitude: photo.longitude
|
||
)
|
||
}
|
||
}
|
||
|
||
/// Ergebnis eines DOCUframe-Vorgangs für die Anzeige (Erfolg oder Fehler).
|
||
enum DocuframeActionResult {
|
||
case success(message: String)
|
||
case failure(message: String)
|
||
|
||
var title: String {
|
||
switch self {
|
||
case .success: "Erledigt"
|
||
case .failure: "Fehler"
|
||
}
|
||
}
|
||
|
||
var message: String {
|
||
switch self {
|
||
case .success(let message), .failure(let message): message
|
||
}
|
||
}
|
||
|
||
var isError: Bool {
|
||
if case .failure = self { return true }
|
||
return false
|
||
}
|
||
}
|