iOS: Shops (Bezugsquellen) verwalten

Die iOS-Stammdaten hatten keinen Punkt fuer Shops – die "gekauft bei"-Liste war
nur im Web pflegbar. Jetzt gibt es unter Verwaltung > Stammdaten "Shops" mit
Anlegen, Bearbeiten (Name + optionale Website) und Loeschen, analog zum Web.
Client um createShop/updateShop/deleteShop samt Request-Models ergaenzt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-26 12:32:17 +02:00
parent 2a507851d9
commit 6b597313d5
3 changed files with 126 additions and 0 deletions

View File

@@ -259,6 +259,22 @@ actor APIClient {
try await send(try makeRequest("/shops"), as: [ShopItem].self) 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. /// Effektive (vererbte) Felder einer Kategorie fuer das Artikelformular.
func categoryFields(categoryId: Int) async throws -> [FieldDefinition] { func categoryFields(categoryId: Int) async throws -> [FieldDefinition] {
try await send(try makeRequest("/categories/\(categoryId)/fields"), as: [FieldDefinition].self) try await send(try makeRequest("/categories/\(categoryId)/fields"), as: [FieldDefinition].self)

View File

@@ -24,6 +24,9 @@ struct AdminTabView: View {
NavigationLink { GroupsView() } label: { NavigationLink { GroupsView() } label: {
Label("Gruppen", systemImage: "square.stack.3d.up") Label("Gruppen", systemImage: "square.stack.3d.up")
} }
NavigationLink { ShopsView() } label: {
Label("Shops", systemImage: "cart")
}
} }
Section("App") { 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
}
}
}

View File

@@ -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 /// Umbenennen von Stammdaten. Ein einzelnes Feld reicht, weil das Backend nur
/// mitgeschickte Schluessel auswertet. /// mitgeschickte Schluessel auswertet.
struct RenameRequest: Codable { struct RenameRequest: Codable {