import SwiftUI struct ARProjectsSheet: View { let onOpen: (UUID) -> Void @State private var model: ARProjectsViewModel @State private var creating = false @State private var editing: ARProject? @Environment(\.dismiss) private var dismiss init(service: any ARProjectsProviding, onOpen: @escaping (UUID) -> Void) { self.onOpen = onOpen _model = State(initialValue: ARProjectsViewModel(service: service)) } var body: some View { NavigationStack { ARProjectListView(supplied: model.supplied, custom: model.custom, onOpen: onOpen, onDelete: model.delete, onEdit: { editing = $0 }, onCreate: { creating = true }) .overlay { if model.isLoading { ProgressView() } } .navigationTitle("AR-Projekte") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Abbrechen", systemImage: "xmark") { dismiss() } .labelStyle(.iconOnly) } ToolbarItem(placement: .primaryAction) { Button("Neues Projekt", systemImage: "plus") { creating = true } .labelStyle(.iconOnly) .buttonStyle(.borderedProminent) } } .navigationDestination(isPresented: $creating) { ARProjectCreateView { name, tank, start in guard let project = model.create(name: name, tank: tank) else { return } creating = false if start { onOpen(project.id) } } } .navigationDestination(isPresented: Binding( get: { editing != nil }, set: { if !$0 { editing = nil } } )) { if let project = editing { ARProjectCreateView(project: project) { name, tank, start in guard let updated = model.update(id: project.id, name: name, tank: tank) else { return } editing = nil if start { onOpen(updated.id) } } .id(project.id) } } } .task { model.load() } .alert("Projektaktion fehlgeschlagen", isPresented: Binding(get: { model.errorMessage != nil }, set: { if !$0 { model.errorMessage = nil } })) { Button("OK", role: .cancel) { model.errorMessage = nil } } message: { Text(model.errorMessage ?? "") } } } struct ARProjectListView: View { let supplied: [ARProject] let custom: [ARProject] let onOpen: (UUID) -> Void let onDelete: (UUID) -> Void let onEdit: (ARProject) -> Void let onCreate: () -> Void var body: some View { List { if !supplied.isEmpty { Section { ForEach(supplied) { project in HStack(spacing: 12) { ARProjectRow(project: project) Button { onOpen(project.id) } label: { Label("AR", systemImage: "arkit") } .buttonStyle(.bordered) .accessibilityLabel("AR-Modus starten: " + project.name) } } } header: { Text("Bereitgestellte Projekte") } footer: { Text("Diese Projekte werden automatisch anhand der Angebote bereitgestellt.") } } Section("Eigene Projekte") { ForEach(custom) { project in HStack(spacing: 12) { Button { onEdit(project) } label: { ARProjectRow(project: project).contentShape(Rectangle()) } .buttonStyle(.plain) .accessibilityHint("Projektkonfiguration bearbeiten") Button { onOpen(project.id) } label: { Label("AR", systemImage: "arkit") } .buttonStyle(.bordered) .accessibilityLabel("AR-Modus starten: " + project.name) Divider() .frame(height: 24) .accessibilityHidden(true) Button { onEdit(project) } label: { Image(systemName: "chevron.right") .font(.body.weight(.semibold)) .foregroundStyle(.secondary) .frame(minWidth: 44, minHeight: 44) .contentShape(Rectangle()) } .buttonStyle(.plain) .accessibilityLabel("Projekt bearbeiten: " + project.name) } .swipeActions(edge: .trailing) { Button("Löschen", systemImage: "trash", role: .destructive) { onDelete(project.id) } .tint(.red) } } Button(action: onCreate) { Label("Neues Projekt", systemImage: "plus.circle.fill") .padding(.vertical, 8) } } } .listStyle(.insetGrouped) } } struct ARProjectRow: View { let project: ARProject @Environment(\.tankCatalog) private var catalog @Environment(\.dynamicTypeSize) private var typeSize var body: some View { HStack(spacing: 12) { ARProjectTankImage(tank: project.tank) .frame(width: 76, height: 60) .accessibilityHidden(true) VStack(alignment: .leading, spacing: 5) { Text(project.name).font(.headline) if typeSize.isAccessibilitySize { configuration reference } else { HStack(alignment: .firstTextBaseline) { configuration Spacer(minLength: 4) reference } } } Spacer(minLength: 0) } .padding(.vertical, 6) .accessibilityElement(children: .combine) } private var configuration: some View { Text("\(project.tank.size.volumeTitle) · \(project.tank.placement.displayName) · \(catalog.displayName(for: project.tank.color))") .font(.caption).foregroundStyle(.secondary) } @ViewBuilder private var reference: some View { if let reference = project.reference { Text(reference).font(.caption2).foregroundStyle(.secondary) } } } #Preview("AR-Projekte") { ARProjectsSheet(service: ARProjectsPreviewService()) { _ in } .frame(maxWidth: 600, maxHeight: 700) } private struct ARProjectsPreviewService: ARProjectsProviding { func updateProject(id: UUID, name: String, tank: Tank) throws -> ARProject { ARProject(id: id, name: name, tank: tank, createdAt: .now, source: .custom, reference: nil) } func deleteProject(id: UUID) throws {} func loadProjects() throws -> [ARProject] { [ ARProject(id: UUID(), name: "Flüssiggastank 2.700 l oberirdisch", tank: Tank(), createdAt: .now, source: .supplied, reference: "A-2026-1182"), ARProject(id: UUID(), name: "Flüssiggastank 4.850 l unterirdisch", tank: Tank(size: .t2_1, color: .olive, placement: .belowGround), createdAt: .now, source: .supplied, reference: "A-2026-1197"), ARProject(id: UUID(), name: "Variante hinter dem Haus", tank: Tank(color: .green), createdAt: .now, source: .custom, reference: nil) ] } func createProject(name: String, tank: Tank) throws -> ARProject { ARProject(id: UUID(), name: name, tank: tank, createdAt: .now, source: .custom, reference: nil) } }