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:
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user