diff --git a/ios/Sources/APIClient.swift b/ios/Sources/APIClient.swift index fda29cb..d75d26d 100644 --- a/ios/Sources/APIClient.swift +++ b/ios/Sources/APIClient.swift @@ -259,6 +259,22 @@ actor APIClient { try await send(try makeRequest("/shops"), as: [ShopItem].self) } + func createShop(_ payload: NewShopRequest) async throws -> ShopItem { + var request = try makeRequest("/shops", method: "POST") + try jsonBody(&request, payload) + return try await send(request, as: ShopItem.self) + } + + func updateShop(id: Int, _ payload: ShopUpdateRequest) async throws -> ShopItem { + var request = try makeRequest("/shops/\(id)", method: "PATCH") + try jsonBody(&request, payload) + return try await send(request, as: ShopItem.self) + } + + func deleteShop(id: Int) async throws { + try await sendNoContent(try makeRequest("/shops/\(id)", method: "DELETE")) + } + /// Effektive (vererbte) Felder einer Kategorie – fuer das Artikelformular. func categoryFields(categoryId: Int) async throws -> [FieldDefinition] { try await send(try makeRequest("/categories/\(categoryId)/fields"), as: [FieldDefinition].self) diff --git a/ios/Sources/MasterDataViews.swift b/ios/Sources/MasterDataViews.swift index 3a7919f..dd74f59 100644 --- a/ios/Sources/MasterDataViews.swift +++ b/ios/Sources/MasterDataViews.swift @@ -24,6 +24,9 @@ struct AdminTabView: View { NavigationLink { GroupsView() } label: { Label("Gruppen", systemImage: "square.stack.3d.up") } + NavigationLink { ShopsView() } label: { + Label("Shops", systemImage: "cart") + } } Section("App") { @@ -550,3 +553,100 @@ struct GroupsView: View { } } } + +/// Bezugsquellen für „gekauft bei" (nur Gegenstände). Wie im Web pflegbar – +/// Name plus optionale Website. +struct ShopsView: View { + var body: some View { + MasterDataListView( + title: "Shops", + singular: "Der Shop", + load: { + try await APIClient.shared.shops().map { + // Die Website steht im Untertitel und wird beim Bearbeiten + // wieder daraus gelesen (wie bei den Gebinden die Mehrzahl). + MasterDataItem(id: $0.id, title: $0.name, + subtitle: $0.website ?? "", isBuiltin: false) + } + }, + delete: { try await APIClient.shared.deleteShop(id: $0) } + ) { item, done in + ShopEditor(id: item?.id, + name: item?.title ?? "", + website: item?.subtitle ?? "", + done: done) + } + } +} + +/// Shops haben Name und optionale Website. +struct ShopEditor: View { + @Environment(\.dismiss) private var dismiss + + let id: Int? + let done: () -> Void + + @State private var name: String + @State private var website: String + @State private var error: String? + @State private var busy = false + + init(id: Int?, name: String, website: String, done: @escaping () -> Void) { + self.id = id + self.done = done + _name = State(initialValue: name) + _website = State(initialValue: website) + } + + var body: some View { + NavigationStack { + Form { + Section("Name") { + TextField("z. B. Digitec", text: $name) + } + Section { + TextField("https://…", text: $website) + .keyboardType(.URL) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + } header: { + Text("Website (optional)") + } + if let error { + Section { Text(error).foregroundStyle(.red).font(.callout) } + } + } + .navigationTitle(id == nil ? "Shop anlegen" : "Shop bearbeiten") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button("Abbrechen") { dismiss() } + } + ToolbarItem(placement: .topBarTrailing) { + Button(busy ? "Sichern…" : "Sichern") { Task { await submit() } } + .disabled(busy || name.trimmingCharacters(in: .whitespaces).isEmpty) + } + } + } + } + + private func submit() async { + busy = true + defer { busy = false } + let n = name.trimmingCharacters(in: .whitespaces) + // Leeres Feld bewusst als "" senden (nicht nil): der Server setzt die + // Website damit auf leer – so lässt sie sich auch wieder entfernen. + let w = website.trimmingCharacters(in: .whitespaces) + do { + if let id { + _ = try await APIClient.shared.updateShop(id: id, ShopUpdateRequest(name: n, website: w)) + } else { + _ = try await APIClient.shared.createShop(NewShopRequest(name: n, website: w)) + } + done() + dismiss() + } catch { + self.error = error.localizedDescription + } + } +} diff --git a/ios/Sources/Models.swift b/ios/Sources/Models.swift index bc09d0a..69818be 100644 --- a/ios/Sources/Models.swift +++ b/ios/Sources/Models.swift @@ -863,6 +863,16 @@ struct NewCategoryRequest: Codable { } } +struct NewShopRequest: Codable { + let name: String + let website: String? +} + +struct ShopUpdateRequest: Codable { + let name: String? + let website: String? +} + /// Umbenennen von Stammdaten. Ein einzelnes Feld reicht, weil das Backend nur /// mitgeschickte Schluessel auswertet. struct RenameRequest: Codable {