App: Obergruppen zuordnen, Mindestbestaende je Ort bearbeiten
Die App konnte bei Gruppen bisher nur umbenennen und Mindestbestaende lesen.
Jetzt gibt es einen vollen Gruppen-Editor (Sources/GroupViews.swift): Name,
Obergruppen als Mehrfachauswahl, Einheit und Mindestbestaende je Ort. Bewusst
eine flache Liste mit "unter: ..." statt TreeMasterView - das Protokoll TreeItem
kennt genau einen Elternteil und wuerde eine Gruppe mit zwei Obergruppen doppelt
zeigen. Die eigene Gruppe und ihre Untergruppen sind in der Auswahl
ausgeschlossen, damit kein Ring entsteht.
Mindestbestaende-Uebersicht ist nach Lagerort gegliedert ("Ueberall" zuerst) und
nicht mehr nur lesend - Gruppenzeilen oeffnen den neuen Editor. Am Artikel
entfaellt das separate Feld "Gesamt": der Bedarf haengt immer an einem Ort, und
"Ueberall" ist einer davon. Das Stammdaten-Speichern schickt min_stock nicht
mehr mit, sonst wuerde es die dort gesetzte Ueberall-Zeile ueberschreiben.
APIClient kann jetzt updateGroup und setGroupLocationMinStock. GroupUpdateRequest
bekommt bewusst KEINEN eigenen Encoder: der synthetisierte laesst nil-Felder weg,
ein handgeschriebener wuerde "name": null senden und den Namen loeschen.
Neue Datei -> xcodegen generate; Projektdatei ist mit committet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -527,6 +527,20 @@ actor APIClient {
|
|||||||
try await sendNoContent(try makeRequest("/categories/\(id)", method: "DELETE"))
|
try await sendNoContent(try makeRequest("/categories/\(id)", method: "DELETE"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gruppe aendern (Name, Mindestbestand, Einheit, Obergruppen).
|
||||||
|
func updateGroup(id: Int, _ payload: GroupUpdateRequest) async throws -> GroupItem {
|
||||||
|
var request = try makeRequest("/groups/\(id)", method: "PATCH")
|
||||||
|
try jsonBody(&request, payload)
|
||||||
|
return try await send(request, as: GroupItem.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mindestbestaende einer Gruppe je Lagerort ersetzen (Basiseinheiten).
|
||||||
|
func setGroupLocationMinStock(id: Int, _ list: [LocationMinStockIn]) async throws -> GroupItem {
|
||||||
|
var request = try makeRequest("/groups/\(id)/location-min-stock", method: "PUT")
|
||||||
|
try jsonBody(&request, list)
|
||||||
|
return try await send(request, as: GroupItem.self)
|
||||||
|
}
|
||||||
|
|
||||||
func renameGroup(id: Int, name: String) async throws -> GroupItem {
|
func renameGroup(id: Int, name: String) async throws -> GroupItem {
|
||||||
var request = try makeRequest("/groups/\(id)", method: "PATCH")
|
var request = try makeRequest("/groups/\(id)", method: "PATCH")
|
||||||
try jsonBody(&request, RenameRequest(name: name))
|
try jsonBody(&request, RenameRequest(name: name))
|
||||||
|
|||||||
327
ios/Sources/GroupViews.swift
Normal file
327
ios/Sources/GroupViews.swift
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
// Gruppen: Stammdaten, Obergruppen und Mindestbestände.
|
||||||
|
//
|
||||||
|
// Gruppen bilden einen Graphen, keinen Baum: eine Gruppe darf unter MEHREREN
|
||||||
|
// Obergruppen hängen („Grillwurst" unter „Wurst" UND unter „Grillgut").
|
||||||
|
// `TreeMasterView` (MasterDataViews.swift) kann nur einen Elternteil und würde
|
||||||
|
// dieselbe Gruppe doppelt anzeigen – deshalb hier eine eigene, flache Ansicht
|
||||||
|
// statt der Baumdarstellung der Lagerorte.
|
||||||
|
|
||||||
|
/// Gruppenliste mit „unter: …" als Untertitel.
|
||||||
|
struct GroupsView: View {
|
||||||
|
@State private var groups: [GroupItem] = []
|
||||||
|
@State private var busy = true
|
||||||
|
@State private var error: String?
|
||||||
|
@State private var editing: GroupItem?
|
||||||
|
@State private var anlegen = false
|
||||||
|
@State private var loeschen: GroupItem?
|
||||||
|
|
||||||
|
private var sortiert: [GroupItem] {
|
||||||
|
groups.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
if !busy && groups.isEmpty {
|
||||||
|
Text("Noch keine Gruppen.").foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
ForEach(sortiert) { g in
|
||||||
|
Button { editing = g } label: { zeile(g) }
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.swipeActions {
|
||||||
|
Button(role: .destructive) { loeschen = g } label: {
|
||||||
|
Label("Löschen", systemImage: "trash")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Gruppen")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .topBarTrailing) {
|
||||||
|
Button { anlegen = true } label: { Image(systemName: "plus") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.overlay { if busy && groups.isEmpty { ProgressView() } }
|
||||||
|
.refreshable { await load() }
|
||||||
|
.task { await load() }
|
||||||
|
.sheet(item: $editing) { g in
|
||||||
|
NavigationStack { GroupEditor(item: g, all: groups) { Task { await load() } } }
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $anlegen) {
|
||||||
|
NavigationStack { GroupEditor(item: nil, all: groups) { Task { await load() } } }
|
||||||
|
}
|
||||||
|
.confirmationDialog("Gruppe löschen?", isPresented: Binding(
|
||||||
|
get: { loeschen != nil }, set: { if !$0 { loeschen = nil } }
|
||||||
|
), titleVisibility: .visible) {
|
||||||
|
Button("Löschen", role: .destructive) {
|
||||||
|
if let g = loeschen { Task { await entfernen(g) } }
|
||||||
|
}
|
||||||
|
Button("Abbrechen", role: .cancel) { loeschen = nil }
|
||||||
|
} message: {
|
||||||
|
Text("Die Artikel bleiben erhalten und verlieren nur ihre Zuordnung. "
|
||||||
|
+ "Untergruppen bleiben ebenfalls bestehen – sie rücken nicht nach oben.")
|
||||||
|
}
|
||||||
|
.alert("Fehler", isPresented: Binding(get: { error != nil }, set: { if !$0 { error = nil } })) {
|
||||||
|
Button("OK", role: .cancel) {}
|
||||||
|
} message: { Text(error ?? "") }
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func zeile(_ g: GroupItem) -> some View {
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(g.name).foregroundStyle(.primary)
|
||||||
|
Text(untertitel(g)).font(.caption).foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
|
||||||
|
private func untertitel(_ g: GroupItem) -> String {
|
||||||
|
let map = Dictionary(groups.map { ($0.id, $0.name) }, uniquingKeysWith: { a, _ in a })
|
||||||
|
let eltern = (g.parentIds ?? []).compactMap { map[$0] }
|
||||||
|
var teile = [eltern.isEmpty ? "oberste Ebene" : "unter: " + eltern.joined(separator: ", ")]
|
||||||
|
teile.append("\(g.productCount ?? 0) Artikel")
|
||||||
|
if let n = g.childIds?.count, n > 0 { teile.append("\(n) Untergruppen") }
|
||||||
|
return teile.joined(separator: " · ")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func load() async {
|
||||||
|
busy = true
|
||||||
|
defer { busy = false }
|
||||||
|
do { groups = try await APIClient.shared.groups() }
|
||||||
|
catch { self.error = error.localizedDescription }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func entfernen(_ g: GroupItem) async {
|
||||||
|
loeschen = nil
|
||||||
|
do {
|
||||||
|
try await APIClient.shared.deleteGroup(id: g.id)
|
||||||
|
await load()
|
||||||
|
} catch { self.error = error.localizedDescription }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gruppe anlegen/ändern: Name, Obergruppen, Einheit und Mindestbestände je Ort.
|
||||||
|
struct GroupEditor: View {
|
||||||
|
let item: GroupItem?
|
||||||
|
let all: [GroupItem]
|
||||||
|
var done: () -> Void
|
||||||
|
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
private struct MinRow: Identifiable {
|
||||||
|
let id = UUID()
|
||||||
|
var locationId: String?
|
||||||
|
var amount: String
|
||||||
|
}
|
||||||
|
|
||||||
|
@State private var name = ""
|
||||||
|
@State private var parentIds: Set<Int> = []
|
||||||
|
@State private var rows: [MinRow] = []
|
||||||
|
@State private var units: [Unit] = []
|
||||||
|
@State private var unitId: Int?
|
||||||
|
@State private var locations: [StorageLocation] = []
|
||||||
|
@State private var busy = false
|
||||||
|
@State private var error: String?
|
||||||
|
|
||||||
|
/// Basiseinheiten je Erfassungseinheit – gespeichert wird in Basiseinheiten.
|
||||||
|
private var faktor: Double {
|
||||||
|
let f = item?.minFaktor ?? 1
|
||||||
|
return f == 0 ? 1 : f
|
||||||
|
}
|
||||||
|
private var einheit: String { item?.minEinheit ?? "" }
|
||||||
|
|
||||||
|
/// Auswahlmenge für Obergruppen: alles außer der Gruppe selbst und ihren
|
||||||
|
/// Untergruppen – sonst entstünde ein Ring.
|
||||||
|
private var moeglicheEltern: [GroupItem] {
|
||||||
|
guard let item else { return all }
|
||||||
|
let verboten = Self.nachfahren(of: item.id, in: all).union([item.id])
|
||||||
|
return all.filter { !verboten.contains($0.id) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private var elternText: String {
|
||||||
|
let map = Dictionary(all.map { ($0.id, $0.name) }, uniquingKeysWith: { a, _ in a })
|
||||||
|
let liste = parentIds.compactMap { map[$0] }.sorted()
|
||||||
|
return liste.isEmpty ? "– keine –" : liste.joined(separator: ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
private var mengenTitel: String {
|
||||||
|
einheit.isEmpty ? "Mindestbestand" : "Mindestbestand (in \(einheit))"
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
Section("Name") {
|
||||||
|
TextField("z.B. Wurst", text: $name)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
NavigationLink {
|
||||||
|
GroupParentPicker(auswahl: $parentIds, moeglich: moeglicheEltern)
|
||||||
|
} label: {
|
||||||
|
LabeledContent("Obergruppen", value: elternText)
|
||||||
|
}
|
||||||
|
} footer: {
|
||||||
|
Text("Eine Gruppe darf unter mehreren Obergruppen hängen – „Grillwurst“ "
|
||||||
|
+ "etwa unter „Wurst“ und unter „Grillgut“. Bestand und Mindestbestand "
|
||||||
|
+ "der Obergruppe zählen sie dann mit.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if item != nil {
|
||||||
|
Section("Einheit") {
|
||||||
|
Picker("Einheit", selection: $unitId) {
|
||||||
|
Text("– Basiseinheit –").tag(Int?.none)
|
||||||
|
ForEach(units) { u in Text(u.name).tag(Int?.some(u.id)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
ForEach($rows) { $row in
|
||||||
|
mengenZeile($row)
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
rows.append(MinRow(locationId: UEBERALL_ID, amount: ""))
|
||||||
|
} label: {
|
||||||
|
Label("Mindestbestand hinzufügen", systemImage: "plus")
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text(mengenTitel)
|
||||||
|
} footer: {
|
||||||
|
Text("„Überall“ heißt: egal wo, Hauptsache im Haus – Käufe für einen "
|
||||||
|
+ "Lagerort decken das mit ab.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let error {
|
||||||
|
Section { Text(error).foregroundStyle(.red).font(.callout) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle(item == nil ? "Gruppe anlegen" : "Gruppe ändern")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .topBarLeading) { Button("Abbrechen") { dismiss() } }
|
||||||
|
ToolbarItem(placement: .topBarTrailing) {
|
||||||
|
Button(busy ? "Speichern…" : "Speichern") { Task { await save() } }
|
||||||
|
.disabled(busy || name.trimmingCharacters(in: .whitespaces).isEmpty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.task { await load() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func mengenZeile(_ row: Binding<MinRow>) -> some View {
|
||||||
|
HStack {
|
||||||
|
Picker("Ort", selection: row.locationId) {
|
||||||
|
Text(UEBERALL_NAME).tag(String?.some(UEBERALL_ID))
|
||||||
|
ForEach(locations) { l in
|
||||||
|
Text(l.path(in: locations)).tag(String?.some(l.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TextField("Menge", text: row.amount)
|
||||||
|
.keyboardType(.decimalPad)
|
||||||
|
.multilineTextAlignment(.trailing)
|
||||||
|
.frame(width: 70)
|
||||||
|
Button(role: .destructive) {
|
||||||
|
let id = row.wrappedValue.id
|
||||||
|
rows.removeAll { $0.id == id }
|
||||||
|
} label: { Image(systemName: "trash") }
|
||||||
|
.buttonStyle(.borderless)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func load() async {
|
||||||
|
name = item?.name ?? ""
|
||||||
|
parentIds = Set(item?.parentIds ?? [])
|
||||||
|
unitId = item?.minStockUnitId
|
||||||
|
units = (try? await APIClient.shared.units()) ?? []
|
||||||
|
locations = (try? await APIClient.shared.locations()) ?? []
|
||||||
|
rows = (item?.locationMinStocks ?? []).map {
|
||||||
|
MinRow(locationId: $0.locationId ?? UEBERALL_ID,
|
||||||
|
amount: formatAmount($0.minStock / faktor))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func save() async {
|
||||||
|
busy = true
|
||||||
|
defer { busy = false }
|
||||||
|
let sauber = name.trimmingCharacters(in: .whitespaces)
|
||||||
|
do {
|
||||||
|
guard let item else {
|
||||||
|
_ = try await APIClient.shared.createGroup(NewGroupRequest(
|
||||||
|
name: sauber, minStock: nil, minStockUnitId: unitId,
|
||||||
|
parentIds: Array(parentIds)))
|
||||||
|
done()
|
||||||
|
dismiss()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = try await APIClient.shared.updateGroup(id: item.id, GroupUpdateRequest(
|
||||||
|
name: sauber, minStockUnitId: unitId, parentIds: Array(parentIds)))
|
||||||
|
var liste: [LocationMinStockIn] = []
|
||||||
|
var gesehen: Set<String> = []
|
||||||
|
for row in rows {
|
||||||
|
guard let loc = row.locationId, !gesehen.contains(loc) else { continue }
|
||||||
|
let wert = Double(row.amount.replacingOccurrences(of: ",", with: ".")) ?? 0
|
||||||
|
if wert > 0 {
|
||||||
|
gesehen.insert(loc)
|
||||||
|
// Eingabe in der Erfassungseinheit → Basiseinheiten.
|
||||||
|
liste.append(LocationMinStockIn(
|
||||||
|
locationId: loc == UEBERALL_ID ? nil : loc,
|
||||||
|
minStock: wert * faktor))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = try await APIClient.shared.setGroupLocationMinStock(id: item.id, liste)
|
||||||
|
done()
|
||||||
|
dismiss()
|
||||||
|
} catch {
|
||||||
|
self.error = error.localizedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Alle Untergruppen (transitiv). Menge statt Baum-Walk: eine Gruppe ist
|
||||||
|
/// über mehrere Wege erreichbar und würde sonst mehrfach besucht.
|
||||||
|
private static func nachfahren(of id: Int, in all: [GroupItem]) -> Set<Int> {
|
||||||
|
var ergebnis: Set<Int> = []
|
||||||
|
var offen = [id]
|
||||||
|
while let cur = offen.popLast() {
|
||||||
|
for kind in all.first(where: { $0.id == cur })?.childIds ?? [] {
|
||||||
|
if ergebnis.insert(kind).inserted { offen.append(kind) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ergebnis
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mehrfachauswahl der Obergruppen. Ein Picker kann das nicht – deshalb eine
|
||||||
|
/// Liste mit Häkchen, das SwiftUI-Idiom dafür.
|
||||||
|
struct GroupParentPicker: View {
|
||||||
|
@Binding var auswahl: Set<Int>
|
||||||
|
let moeglich: [GroupItem]
|
||||||
|
|
||||||
|
private var sortiert: [GroupItem] {
|
||||||
|
moeglich.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
ForEach(sortiert) { g in
|
||||||
|
Button {
|
||||||
|
if auswahl.contains(g.id) { auswahl.remove(g.id) } else { auswahl.insert(g.id) }
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Text(g.name).foregroundStyle(.primary)
|
||||||
|
Spacer()
|
||||||
|
if auswahl.contains(g.id) {
|
||||||
|
Image(systemName: "checkmark").foregroundStyle(Color.marke)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Obergruppen")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -76,7 +76,10 @@ struct ShoppingListView: View {
|
|||||||
ListRow(
|
ListRow(
|
||||||
systemImage: "square.stack.3d.up",
|
systemImage: "square.stack.3d.up",
|
||||||
title: group.name,
|
title: group.name,
|
||||||
subtitle: "fehlt \(formatAmount(group.deficit)) \(group.unitName) · \(group.productCount) Artikel"
|
subtitle: "fehlt \(formatAmount(group.deficit)) \(group.unitName)"
|
||||||
|
+ " · \(group.productCount) Artikel"
|
||||||
|
+ ((group.subgroupCount ?? 0) > 0
|
||||||
|
? " · inkl. \(group.subgroupCount ?? 0) Untergruppen" : "")
|
||||||
) { GroupBadge() }
|
) { GroupBadge() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,10 +188,15 @@ struct ExpiringView: View {
|
|||||||
|
|
||||||
// MARK: - Mindestbestände
|
// MARK: - Mindestbestände
|
||||||
|
|
||||||
/// Übersicht aller Mindestbestände: Produkte und Gruppen, jeweils Gesamt UND je
|
/// Übersicht aller Mindestbestände – nach LAGERORT gegliedert.
|
||||||
/// Lagerort (Soll in der Anzeige-/Gruppeneinheit). Produktzeilen öffnen den
|
///
|
||||||
/// Artikel zum Bearbeiten; das Setzen von Gruppen-/Lagerort-Bedarfen läuft übers
|
/// Ein Mindestbestand ist immer ein Bedarf an einem Ort. „Überall" (egal wo,
|
||||||
/// Web.
|
/// Hauptsache im Haus) ist dabei der oberste Ort; Käufe für einen einzelnen
|
||||||
|
/// Lagerort decken ihn mit ab. Mengen kommen in Basiseinheiten und werden hier
|
||||||
|
/// in die Erfassungseinheit des Ziels umgerechnet.
|
||||||
|
///
|
||||||
|
/// Artikelzeilen öffnen den Artikel, Gruppenzeilen die Gruppe – beide lassen
|
||||||
|
/// sich dort bearbeiten.
|
||||||
struct MinStockView: View {
|
struct MinStockView: View {
|
||||||
@State private var products: [Product] = []
|
@State private var products: [Product] = []
|
||||||
@State private var groups: [GroupItem] = []
|
@State private var groups: [GroupItem] = []
|
||||||
@@ -198,71 +206,92 @@ struct MinStockView: View {
|
|||||||
private struct MinRow: Identifiable {
|
private struct MinRow: Identifiable {
|
||||||
let id: String
|
let id: String
|
||||||
let title: String
|
let title: String
|
||||||
let ort: String // "Gesamt" oder Lagerort-Name
|
|
||||||
let soll: String
|
let soll: String
|
||||||
let bestand: String?
|
let bestand: String?
|
||||||
let productId: Int? // Tippziel (nur Produkte)
|
let productId: Int? // Tippziel (Artikel)
|
||||||
let isGroup: Bool
|
let groupId: Int? // Tippziel (Gruppe)
|
||||||
let unter: Bool
|
let unter: Bool
|
||||||
|
let untergruppen: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct OrtBlock: Identifiable {
|
||||||
|
let id: String
|
||||||
|
let name: String
|
||||||
|
let zeilen: [MinRow]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Alle Zeilen, gebündelt je Ort. „Überall" (Ort nil) steht zuerst.
|
||||||
|
private var bloecke: [OrtBlock] {
|
||||||
|
var jeOrt: [String: [MinRow]] = [:]
|
||||||
|
var namen: [String: String] = [UEBERALL_ID: UEBERALL_NAME]
|
||||||
|
|
||||||
|
func merken(_ locationId: String?, _ locationName: String?, _ row: MinRow) {
|
||||||
|
let key = locationId ?? UEBERALL_ID
|
||||||
|
if let n = locationName, locationId != nil { namen[key] = n }
|
||||||
|
jeOrt[key, default: []].append(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var rows: [MinRow] {
|
|
||||||
var out: [MinRow] = []
|
|
||||||
for p in products.sorted(by: { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }) {
|
for p in products.sorted(by: { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }) {
|
||||||
let disp = p.unitFactor == 0 ? 1 : p.unitFactor // Basis -> Anzeigeeinheit
|
let disp = p.unitFactor == 0 ? 1 : p.unitFactor // Basis -> Anzeigeeinheit
|
||||||
let art = p.articleUnitFactor == 0 ? 1 : p.articleUnitFactor
|
|
||||||
if let ms = p.minStock, ms > 0 {
|
|
||||||
out.append(MinRow(
|
|
||||||
id: "p\(p.id)g", title: p.name, ort: "Gesamt",
|
|
||||||
soll: "\(formatAmount(ms / disp)) \(p.unitName)",
|
|
||||||
bestand: "\(formatAmount(p.stock / disp)) \(p.unitName)",
|
|
||||||
productId: p.id, isGroup: false, unter: p.stock < ms))
|
|
||||||
}
|
|
||||||
for l in (p.locationMinStocks ?? []) {
|
for l in (p.locationMinStocks ?? []) {
|
||||||
// je Lagerort in Artikeleinheiten gespeichert -> Anzeigeeinheit.
|
merken(l.locationId, l.locationName, MinRow(
|
||||||
out.append(MinRow(
|
id: "p\(p.id)l\(l.id)", title: p.name,
|
||||||
id: "p\(p.id)l\(l.locationId)", title: p.name,
|
soll: "\(formatAmount(l.minStock / disp)) \(p.unitName)",
|
||||||
ort: l.locationName ?? "Lagerort",
|
bestand: l.stock.map { "\(formatAmount($0 / disp)) \(p.unitName)" },
|
||||||
soll: "\(formatAmount(l.minStock * art / disp)) \(p.unitName)",
|
productId: p.id, groupId: nil,
|
||||||
bestand: nil, productId: p.id, isGroup: false, unter: false))
|
unter: (l.stock ?? 0) < l.minStock, untergruppen: 0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for g in groups.sorted(by: { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }) {
|
for g in groups.sorted(by: { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }) {
|
||||||
let unit = g.minStockUnitName ?? ""
|
let faktor = g.minFaktor == 0 ? 1 : g.minFaktor
|
||||||
if let ms = g.minStock, ms > 0 {
|
let einheit = g.minEinheit
|
||||||
out.append(MinRow(
|
|
||||||
id: "g\(g.id)g", title: g.name, ort: "Gesamt",
|
|
||||||
soll: "\(formatAmount(ms)) \(unit)",
|
|
||||||
bestand: g.stock.map { "\(formatAmount($0)) \(unit)" },
|
|
||||||
productId: nil, isGroup: true, unter: (g.stock ?? 0) < ms))
|
|
||||||
}
|
|
||||||
for l in (g.locationMinStocks ?? []) {
|
for l in (g.locationMinStocks ?? []) {
|
||||||
out.append(MinRow(
|
merken(l.locationId, l.locationName, MinRow(
|
||||||
id: "g\(g.id)l\(l.locationId)", title: g.name,
|
id: "g\(g.id)l\(l.id)", title: g.name,
|
||||||
ort: l.locationName ?? "Lagerort",
|
soll: "\(formatAmount(l.minStock / faktor)) \(einheit)",
|
||||||
soll: "\(formatAmount(l.minStock)) \(unit)",
|
bestand: l.stock.map { "\(formatAmount($0 / faktor)) \(einheit)" },
|
||||||
bestand: nil, productId: nil, isGroup: true, unter: false))
|
productId: nil, groupId: g.id,
|
||||||
|
unter: (l.stock ?? 0) < l.minStock,
|
||||||
|
untergruppen: g.childIds?.count ?? 0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out
|
|
||||||
|
// „Überall" zuerst, danach die Lagerorte alphabetisch.
|
||||||
|
let schluessel = jeOrt.keys.sorted { a, b in
|
||||||
|
if a == UEBERALL_ID { return true }
|
||||||
|
if b == UEBERALL_ID { return false }
|
||||||
|
return (namen[a] ?? "").localizedCaseInsensitiveCompare(namen[b] ?? "") == .orderedAscending
|
||||||
|
}
|
||||||
|
return schluessel.map {
|
||||||
|
OrtBlock(id: $0, name: namen[$0] ?? "Lagerort",
|
||||||
|
zeilen: jeOrt[$0] ?? [])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
if !busy && rows.isEmpty {
|
if !busy && bloecke.isEmpty {
|
||||||
Text("Noch keine Mindestbestände gesetzt.").foregroundStyle(.secondary)
|
Text("Noch keine Mindestbestände gesetzt.").foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
ForEach(rows) { r in
|
ForEach(bloecke) { block in
|
||||||
|
Section(block.id == UEBERALL_ID ? "Überall (egal wo)" : block.name) {
|
||||||
|
ForEach(block.zeilen) { r in
|
||||||
if let pid = r.productId, let p = products.first(where: { $0.id == pid }) {
|
if let pid = r.productId, let p = products.first(where: { $0.id == pid }) {
|
||||||
NavigationLink { ProductDetailView(product: p) } label: { zeile(r) }
|
NavigationLink { ProductDetailView(product: p) } label: { zeile(r) }
|
||||||
|
} else if let gid = r.groupId, let g = groups.first(where: { $0.id == gid }) {
|
||||||
|
NavigationLink {
|
||||||
|
GroupEditor(item: g, all: groups) { Task { await load() } }
|
||||||
|
} label: { zeile(r) }
|
||||||
} else {
|
} else {
|
||||||
zeile(r)
|
zeile(r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.navigationTitle("Mindestbestände")
|
.navigationTitle("Mindestbestände")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.overlay { if busy && rows.isEmpty { ProgressView() } }
|
.overlay { if busy && bloecke.isEmpty { ProgressView() } }
|
||||||
.refreshable { await load() }
|
.refreshable { await load() }
|
||||||
.onAppear { Task { await load() } }
|
.onAppear { Task { await load() } }
|
||||||
.alert("Fehler", isPresented: Binding(get: { error != nil }, set: { if !$0 { error = nil } })) {
|
.alert("Fehler", isPresented: Binding(get: { error != nil }, set: { if !$0 { error = nil } })) {
|
||||||
@@ -276,13 +305,18 @@ struct MinStockView: View {
|
|||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
Text(r.title)
|
Text(r.title)
|
||||||
if r.isGroup {
|
if r.groupId != nil {
|
||||||
Text("Gruppe").font(.caption2)
|
Text("Gruppe").font(.caption2)
|
||||||
.padding(.horizontal, 6).padding(.vertical, 1)
|
.padding(.horizontal, 6).padding(.vertical, 1)
|
||||||
.background(Color.marke.opacity(0.2), in: Capsule())
|
.background(Color.marke.opacity(0.2), in: Capsule())
|
||||||
}
|
}
|
||||||
|
if r.untergruppen > 0 {
|
||||||
|
Text("inkl. \(r.untergruppen)").font(.caption2)
|
||||||
|
.padding(.horizontal, 6).padding(.vertical, 1)
|
||||||
|
.background(Color.secondary.opacity(0.15), in: Capsule())
|
||||||
}
|
}
|
||||||
Text("\(r.ort) · Soll \(r.soll)" + (r.bestand.map { " · Bestand \($0)" } ?? ""))
|
}
|
||||||
|
Text("Soll \(r.soll)" + (r.bestand.map { " · Bestand \($0)" } ?? ""))
|
||||||
.font(.caption).foregroundStyle(.secondary)
|
.font(.caption).foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|||||||
@@ -1126,34 +1126,6 @@ struct FieldEditor: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct GroupsView: View {
|
|
||||||
var body: some View {
|
|
||||||
MasterDataListView(
|
|
||||||
title: "Gruppen",
|
|
||||||
singular: "Die Gruppe",
|
|
||||||
load: {
|
|
||||||
try await APIClient.shared.groups().map {
|
|
||||||
MasterDataItem(id: $0.id, title: $0.name, subtitle: "", isBuiltin: false)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
delete: { try await APIClient.shared.deleteGroup(id: $0) }
|
|
||||||
) { item, done in
|
|
||||||
NameEditor(
|
|
||||||
title: item == nil ? "Gruppe anlegen" : "Gruppe umbenennen",
|
|
||||||
initial: item?.title ?? "",
|
|
||||||
save: { name in
|
|
||||||
if let item {
|
|
||||||
_ = try await APIClient.shared.renameGroup(id: item.id, name: name)
|
|
||||||
} else {
|
|
||||||
_ = try await APIClient.shared.createGroup(
|
|
||||||
NewGroupRequest(name: name, minStock: nil, minStockUnitId: nil))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
done: done
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Bezugsquellen für „gekauft bei" (nur Gegenstände). Wie im Web pflegbar –
|
/// Bezugsquellen für „gekauft bei" (nur Gegenstände). Wie im Web pflegbar –
|
||||||
/// Name plus optionale Website.
|
/// Name plus optionale Website.
|
||||||
|
|||||||
@@ -146,29 +146,49 @@ struct Product: Codable, Identifiable, Hashable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Mindestbestand eines Produkts/einer Gruppe an einem Lagerort (Anzeige).
|
/// Mindestbestand eines Produkts/einer Gruppe an einem Lagerort (Anzeige).
|
||||||
|
///
|
||||||
|
/// ``locationId`` nil heisst „Ueberall": egal wo, Hauptsache die Menge ist im
|
||||||
|
/// Haus. Das ersetzt den frueheren separaten Gesamt-Mindestbestand. Mengen in
|
||||||
|
/// Basiseinheiten (g/ml/Stueck).
|
||||||
struct LocationMinStock: Codable, Hashable, Identifiable {
|
struct LocationMinStock: Codable, Hashable, Identifiable {
|
||||||
let locationId: String
|
let locationId: String?
|
||||||
let locationName: String?
|
let locationName: String?
|
||||||
let minStock: Double
|
let minStock: Double
|
||||||
var id: String { locationId }
|
/// Bestand an diesem Ort (inkl. Unterorte), ebenfalls in Basiseinheiten.
|
||||||
|
var stock: Double? = nil
|
||||||
|
var id: String { locationId ?? UEBERALL_ID }
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case stock
|
||||||
case minStock = "min_stock"
|
case minStock = "min_stock"
|
||||||
case locationId = "location_id"
|
case locationId = "location_id"
|
||||||
case locationName = "location_name"
|
case locationName = "location_name"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ein Mindestbestand-Eintrag zum Speichern (Menge in Artikeleinheiten).
|
/// Ein Mindestbestand-Eintrag zum Speichern (Menge in Basiseinheiten).
|
||||||
|
/// ``locationId`` nil = „Ueberall".
|
||||||
struct LocationMinStockIn: Codable {
|
struct LocationMinStockIn: Codable {
|
||||||
let locationId: String
|
let locationId: String?
|
||||||
let minStock: Double
|
let minStock: Double
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case minStock = "min_stock"
|
case minStock = "min_stock"
|
||||||
case locationId = "location_id"
|
case locationId = "location_id"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Den Ort immer senden – auch null, sonst laesst sich „Ueberall" gar nicht
|
||||||
|
// ausdruecken (Swift laesst nil-Optionals sonst weg).
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var c = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try c.encode(locationId, forKey: .locationId)
|
||||||
|
try c.encode(minStock, forKey: .minStock)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Platzhalter-ID fuer „Ueberall" in Pickern – im JSON ist es schlicht null.
|
||||||
|
let UEBERALL_ID = "__ueberall__"
|
||||||
|
let UEBERALL_NAME = "Überall"
|
||||||
|
|
||||||
/// Auswahleintrag fuer Einheiten.
|
/// Auswahleintrag fuer Einheiten.
|
||||||
///
|
///
|
||||||
@@ -390,6 +410,8 @@ struct GroupShoppingItem: Codable, Identifiable {
|
|||||||
let deficit: Double
|
let deficit: Double
|
||||||
let unitName: String
|
let unitName: String
|
||||||
let productCount: Int
|
let productCount: Int
|
||||||
|
/// Wie viele Untergruppen mitgezaehlt werden (0 = keine).
|
||||||
|
var subgroupCount: Int? = nil
|
||||||
|
|
||||||
var id: Int { groupId }
|
var id: Int { groupId }
|
||||||
|
|
||||||
@@ -399,6 +421,7 @@ struct GroupShoppingItem: Codable, Identifiable {
|
|||||||
case minStock = "min_stock"
|
case minStock = "min_stock"
|
||||||
case unitName = "unit_name"
|
case unitName = "unit_name"
|
||||||
case productCount = "product_count"
|
case productCount = "product_count"
|
||||||
|
case subgroupCount = "subgroup_count"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -637,20 +660,86 @@ struct SettingEntry: Codable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Gruppe: fasst Bestaende mehrerer Marken zusammen (z.B. "Mehl").
|
/// Gruppe: fasst Bestaende mehrerer Marken zusammen (z.B. "Mehl").
|
||||||
|
///
|
||||||
|
/// Gruppen bilden einen Graphen, keinen Baum: eine Gruppe darf unter MEHREREN
|
||||||
|
/// Obergruppen haengen („Grillwurst" unter „Wurst" UND unter „Grillgut").
|
||||||
|
/// Deshalb ``parentIds`` als Liste – und deshalb konformiert GroupItem NICHT zu
|
||||||
|
/// ``TreeItem``, das genau einen Elternteil kennt.
|
||||||
struct GroupItem: Codable, Identifiable, Hashable {
|
struct GroupItem: Codable, Identifiable, Hashable {
|
||||||
let id: Int
|
let id: Int
|
||||||
let name: String
|
let name: String
|
||||||
// Mindestbestand-Infos (nur aus /groups befuellt; Picker brauchen sie nicht).
|
// Mindestbestand-Infos (nur aus /groups befuellt; Picker brauchen sie nicht).
|
||||||
var minStock: Double? = nil
|
var minStock: Double? = nil
|
||||||
|
var minStockUnitId: Int? = nil
|
||||||
var minStockUnitName: String? = nil
|
var minStockUnitName: String? = nil
|
||||||
|
var minStockUnitFactor: Double? = nil
|
||||||
|
var kind: String? = nil
|
||||||
|
var packageSize: Double? = nil
|
||||||
|
var packageLabel: String? = nil
|
||||||
|
var minStockInPackages: Bool? = nil
|
||||||
var stock: Double? = nil
|
var stock: Double? = nil
|
||||||
var locationMinStocks: [LocationMinStock]? = nil
|
var locationMinStocks: [LocationMinStock]? = nil
|
||||||
|
var parentIds: [Int]? = nil
|
||||||
|
var childIds: [Int]? = nil
|
||||||
|
/// Artikel inkl. Untergruppen; ``directProductCount`` nur die eigenen.
|
||||||
|
var productCount: Int? = nil
|
||||||
|
var directProductCount: Int? = nil
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case id, name, stock
|
case id, name, stock, kind
|
||||||
case minStock = "min_stock"
|
case minStock = "min_stock"
|
||||||
|
case minStockUnitId = "min_stock_unit_id"
|
||||||
case minStockUnitName = "min_stock_unit_name"
|
case minStockUnitName = "min_stock_unit_name"
|
||||||
|
case minStockUnitFactor = "min_stock_unit_factor"
|
||||||
|
case packageSize = "package_size"
|
||||||
|
case packageLabel = "package_label"
|
||||||
|
case minStockInPackages = "min_stock_in_packages"
|
||||||
case locationMinStocks = "location_min_stocks"
|
case locationMinStocks = "location_min_stocks"
|
||||||
|
case parentIds = "parent_ids"
|
||||||
|
case childIds = "child_ids"
|
||||||
|
case productCount = "product_count"
|
||||||
|
case directProductCount = "direct_product_count"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Basiseinheiten je Erfassungseinheit – zum Umrechnen der Mindestbestaende,
|
||||||
|
/// die in Basiseinheiten gespeichert sind.
|
||||||
|
var minFaktor: Double {
|
||||||
|
if minStockInPackages == true, let s = packageSize, s > 0 { return s }
|
||||||
|
return minStockUnitFactor ?? 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Beschriftung der Erfassungseinheit („Glas", „Gramm", …).
|
||||||
|
var minEinheit: String {
|
||||||
|
if minStockInPackages == true, (packageSize ?? 0) > 0 {
|
||||||
|
return packageLabel ?? "Packung"
|
||||||
|
}
|
||||||
|
if let n = minStockUnitName { return n }
|
||||||
|
switch kind {
|
||||||
|
case "weight": return "g"
|
||||||
|
case "volume": return "ml"
|
||||||
|
case "count": return "Stück"
|
||||||
|
default: return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gruppe aendern. Nur mitgeschickte Felder wertet das Backend aus – deshalb
|
||||||
|
/// KEIN eigener Encoder: der synthetisierte laesst nil-Optionals weg
|
||||||
|
/// (encodeIfPresent). Ein handgeschriebener wuerde ``"name": null`` senden und
|
||||||
|
/// damit den Namen loeschen.
|
||||||
|
struct GroupUpdateRequest: Codable {
|
||||||
|
var name: String? = nil
|
||||||
|
var minStock: Double? = nil
|
||||||
|
var minStockUnitId: Int? = nil
|
||||||
|
var minStockInPackages: Bool? = nil
|
||||||
|
var parentIds: [Int]? = nil
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case name
|
||||||
|
case minStock = "min_stock"
|
||||||
|
case minStockUnitId = "min_stock_unit_id"
|
||||||
|
case minStockInPackages = "min_stock_in_packages"
|
||||||
|
case parentIds = "parent_ids"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1283,10 +1372,13 @@ struct NewGroupRequest: Codable {
|
|||||||
let name: String
|
let name: String
|
||||||
let minStock: Double?
|
let minStock: Double?
|
||||||
let minStockUnitId: Int?
|
let minStockUnitId: Int?
|
||||||
|
/// Obergruppen (mehrere moeglich) – optional, Standard: keine.
|
||||||
|
var parentIds: [Int]? = nil
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case name
|
case name
|
||||||
case minStock = "min_stock"
|
case minStock = "min_stock"
|
||||||
case minStockUnitId = "min_stock_unit_id"
|
case minStockUnitId = "min_stock_unit_id"
|
||||||
|
case parentIds = "parent_ids"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ struct ProductDetailView: View {
|
|||||||
@State private var shopId: Int?
|
@State private var shopId: Int?
|
||||||
@State private var productUrl = ""
|
@State private var productUrl = ""
|
||||||
// Gesamt-Mindestbestand (nur Gegenstände; Menge in Stück/Artikeleinheit).
|
// Gesamt-Mindestbestand (nur Gegenstände; Menge in Stück/Artikeleinheit).
|
||||||
@State private var minStock = ""
|
|
||||||
@State private var shops: [ShopItem] = []
|
@State private var shops: [ShopItem] = []
|
||||||
@State private var locations: [StorageLocation] = []
|
@State private var locations: [StorageLocation] = []
|
||||||
@State private var fieldDefs: [FieldDefinition] = []
|
@State private var fieldDefs: [FieldDefinition] = []
|
||||||
@@ -63,8 +62,6 @@ struct ProductDetailView: View {
|
|||||||
if packageSize != (current.packageSize.map { formatAmount($0) } ?? "") { return true }
|
if packageSize != (current.packageSize.map { formatAmount($0) } ?? "") { return true }
|
||||||
if packageLabel != (current.packageLabel ?? "") { return true }
|
if packageLabel != (current.packageLabel ?? "") { return true }
|
||||||
if datePrecision != (current.datePrecision == "month" ? "month" : "day") { return true }
|
if datePrecision != (current.datePrecision == "month" ? "month" : "day") { return true }
|
||||||
// Mindestbestand gibt es bei Lebensmitteln und Verbrauchsgegenständen.
|
|
||||||
if minStock != minStockFeld { return true }
|
|
||||||
}
|
}
|
||||||
if current.isObject {
|
if current.isObject {
|
||||||
if objMode != currentObjMode { return true }
|
if objMode != currentObjMode { return true }
|
||||||
@@ -197,26 +194,31 @@ struct ProductDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mindestbestand bei Lebensmitteln UND Verbrauchsgegenständen (Charge) –
|
// Mindestbestand bei Lebensmitteln UND Verbrauchsgegenständen (Charge).
|
||||||
// in der Anzeigeeinheit (z.B. Gramm/Milliliter), nicht in Packungen.
|
|
||||||
// Menge je Lagerort und Einzelstücke haben keinen Mindestbestand.
|
// Menge je Lagerort und Einzelstücke haben keinen Mindestbestand.
|
||||||
if current.foodLike {
|
//
|
||||||
Section("Mindestbestand") {
|
// Es gibt kein separates „Gesamt"-Feld mehr: der Bedarf hängt immer
|
||||||
QuantityField(label: "Gesamt (\(current.unitName))", text: $minStock)
|
// an einem Ort, und „Überall" ist einer davon (der oberste).
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if current.foodLike {
|
if current.foodLike {
|
||||||
Section {
|
Section {
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
ProductLocationMinView(product: current) { await reload() }
|
ProductLocationMinView(product: current) { await reload() }
|
||||||
} label: {
|
} label: {
|
||||||
let n = (current.locationMinStocks ?? []).count
|
let zeilen = current.locationMinStocks ?? []
|
||||||
Label(n > 0 ? "Mindestbestand je Lagerort (\(n))" : "Mindestbestand je Lagerort",
|
let ueberall = zeilen.first { $0.locationId == nil }
|
||||||
systemImage: "mappin.and.ellipse")
|
Label {
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text("Mindestbestand")
|
||||||
|
Text(minStockUntertitel(zeilen: zeilen, ueberall: ueberall))
|
||||||
|
.font(.caption).foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
} icon: {
|
||||||
|
Image(systemName: "mappin.and.ellipse")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} footer: {
|
} footer: {
|
||||||
Text("Bedarf je Lagerort (z.B. Ferienhaus, Zuhause), zusätzlich zum Gesamt-Mindestbestand.")
|
Text("„Überall“ heißt: egal wo, Hauptsache im Haus – Käufe für einen "
|
||||||
|
+ "Lagerort decken das mit ab.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,10 +331,17 @@ struct ProductDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gesamt-Mindestbestand in der Anzeigeeinheit (Gramm/Milliliter/Stück) – aus
|
/// Kurzfassung der hinterlegten Mindestbestände für die Übersichtszeile.
|
||||||
/// den Basiseinheiten umgerechnet, unabhängig davon, wie er erfasst wurde.
|
private func minStockUntertitel(zeilen: [LocationMinStock], ueberall: LocationMinStock?) -> String {
|
||||||
private var minStockFeld: String {
|
if zeilen.isEmpty { return "keiner hinterlegt" }
|
||||||
current.minStock.map { formatAmount($0 / max(current.unitFactor, 1)) } ?? ""
|
let faktor = max(current.unitFactor, 1)
|
||||||
|
var teile: [String] = []
|
||||||
|
if let u = ueberall {
|
||||||
|
teile.append("Überall \(formatAmount(u.minStock / faktor)) \(current.unitName)")
|
||||||
|
}
|
||||||
|
let orte = zeilen.count - (ueberall == nil ? 0 : 1)
|
||||||
|
if orte > 0 { teile.append("\(orte) \(orte == 1 ? "Lagerort" : "Lagerorte")") }
|
||||||
|
return teile.joined(separator: " · ")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Aktuelle Verwaltungsart des geladenen Artikels.
|
/// Aktuelle Verwaltungsart des geladenen Artikels.
|
||||||
@@ -350,7 +359,6 @@ struct ProductDetailView: View {
|
|||||||
categoryId = current.categoryId
|
categoryId = current.categoryId
|
||||||
shopId = current.shopId
|
shopId = current.shopId
|
||||||
productUrl = current.productUrl ?? ""
|
productUrl = current.productUrl ?? ""
|
||||||
minStock = minStockFeld
|
|
||||||
objMode = currentObjMode
|
objMode = currentObjMode
|
||||||
fieldValues = Self.stringValues(current.fieldValues)
|
fieldValues = Self.stringValues(current.fieldValues)
|
||||||
}
|
}
|
||||||
@@ -411,9 +419,6 @@ struct ProductDetailView: View {
|
|||||||
busy = true
|
busy = true
|
||||||
defer { busy = false }
|
defer { busy = false }
|
||||||
do {
|
do {
|
||||||
// Mindestbestand in der Anzeigeeinheit erfasst → Basiseinheiten.
|
|
||||||
let minBase = Double(minStock.replacingOccurrences(of: ",", with: "."))
|
|
||||||
.map { $0 * current.unitFactor }
|
|
||||||
if current.foodLike {
|
if current.foodLike {
|
||||||
// Lebensmittel ODER Verbrauchsgegenstand: als Charge mit Einheit/
|
// Lebensmittel ODER Verbrauchsgegenstand: als Charge mit Einheit/
|
||||||
// Packung/MHD/Gruppe. Verbrauchsgegenstände (isObject) tragen
|
// Packung/MHD/Gruppe. Verbrauchsgegenstände (isObject) tragen
|
||||||
@@ -427,11 +432,9 @@ struct ProductDetailView: View {
|
|||||||
groupId: groupId,
|
groupId: groupId,
|
||||||
categoryId: categoryId
|
categoryId: categoryId
|
||||||
)
|
)
|
||||||
// Mindestbestand für Lebensmittel UND Verbrauchsgegenstände.
|
// Der Mindestbestand wird in ProductLocationMinView gepflegt und
|
||||||
req.minStock = minBase
|
// hier bewusst NICHT mitgeschickt – sonst überschriebe das
|
||||||
req.minStockInPackages = false
|
// Stammdaten-Speichern die dort gesetzte „Überall"-Zeile.
|
||||||
req.minStockUnitId = nil
|
|
||||||
req.sendMinStock = true
|
|
||||||
if current.isObject {
|
if current.isObject {
|
||||||
var fv: [String: String?] = [:]
|
var fv: [String: String?] = [:]
|
||||||
for def in fieldDefs { fv[String(def.id)] = fieldValues[String(def.id)] ?? "" }
|
for def in fieldDefs { fv[String(def.id)] = fieldValues[String(def.id)] ?? "" }
|
||||||
@@ -660,22 +663,21 @@ struct ProductLocationMinView: View {
|
|||||||
@State private var busy = false
|
@State private var busy = false
|
||||||
@State private var error: String?
|
@State private var error: String?
|
||||||
|
|
||||||
// Speicherung in Artikeleinheiten, Anzeige in der Anzeigeeinheit (Gramm/ml/Stück).
|
// Gespeichert wird in Basiseinheiten, erfasst in der Anzeigeeinheit (g/ml/Stück).
|
||||||
private var artToDisp: Double {
|
private var dispFaktor: Double { product.unitFactor == 0 ? 1 : product.unitFactor }
|
||||||
let disp = product.unitFactor == 0 ? 1 : product.unitFactor
|
|
||||||
return product.articleUnitFactor / disp
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
Section {
|
Section {
|
||||||
Text("Bedarf je Lagerort (z.B. Ferienhaus, Zuhause). Menge in \(product.unitName).")
|
Text("Was soll wo immer da sein? „Überall“ heißt: egal wo, Hauptsache "
|
||||||
|
+ "im Haus – Käufe für einen Lagerort decken das mit ab. "
|
||||||
|
+ "Menge in \(product.unitName).")
|
||||||
.font(.caption).foregroundStyle(.secondary)
|
.font(.caption).foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
ForEach($rows) { $row in
|
ForEach($rows) { $row in
|
||||||
HStack {
|
HStack {
|
||||||
Picker("Lagerort", selection: $row.locationId) {
|
Picker("Ort", selection: $row.locationId) {
|
||||||
Text("– wählen –").tag(String?.none)
|
Text(UEBERALL_NAME).tag(String?.some(UEBERALL_ID))
|
||||||
ForEach(locations) { l in Text(l.path(in: locations)).tag(String?.some(l.id)) }
|
ForEach(locations) { l in Text(l.path(in: locations)).tag(String?.some(l.id)) }
|
||||||
}
|
}
|
||||||
TextField("Menge", text: $row.amount)
|
TextField("Menge", text: $row.amount)
|
||||||
@@ -691,15 +693,15 @@ struct ProductLocationMinView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Button {
|
Button {
|
||||||
rows.append(MinRow(locationId: nil, amount: ""))
|
rows.append(MinRow(locationId: UEBERALL_ID, amount: ""))
|
||||||
} label: {
|
} label: {
|
||||||
Label("Lagerort hinzufügen", systemImage: "plus")
|
Label("Mindestbestand hinzufügen", systemImage: "plus")
|
||||||
}
|
}
|
||||||
if let error {
|
if let error {
|
||||||
Section { Text(error).foregroundStyle(.red).font(.callout) }
|
Section { Text(error).foregroundStyle(.red).font(.callout) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Bedarf je Lagerort")
|
.navigationTitle("Mindestbestand")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .topBarTrailing) {
|
ToolbarItem(placement: .topBarTrailing) {
|
||||||
@@ -712,8 +714,10 @@ struct ProductLocationMinView: View {
|
|||||||
|
|
||||||
private func load() async {
|
private func load() async {
|
||||||
locations = (try? await APIClient.shared.locations()) ?? []
|
locations = (try? await APIClient.shared.locations()) ?? []
|
||||||
|
// „Überall" ist der Ort nil – im Picker braucht es einen Platzhalter.
|
||||||
rows = (product.locationMinStocks ?? []).map {
|
rows = (product.locationMinStocks ?? []).map {
|
||||||
MinRow(locationId: $0.locationId, amount: formatAmount($0.minStock * artToDisp))
|
MinRow(locationId: $0.locationId ?? UEBERALL_ID,
|
||||||
|
amount: formatAmount($0.minStock / dispFaktor))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -727,8 +731,10 @@ struct ProductLocationMinView: View {
|
|||||||
let wert = Double(row.amount.replacingOccurrences(of: ",", with: ".")) ?? 0
|
let wert = Double(row.amount.replacingOccurrences(of: ",", with: ".")) ?? 0
|
||||||
if wert > 0 {
|
if wert > 0 {
|
||||||
gesehen.insert(loc)
|
gesehen.insert(loc)
|
||||||
// Eingabe in Anzeigeeinheit → Artikeleinheiten (so gespeichert).
|
// Eingabe in Anzeigeeinheit → Basiseinheiten (so gespeichert).
|
||||||
list.append(LocationMinStockIn(locationId: loc, minStock: wert / artToDisp))
|
list.append(LocationMinStockIn(
|
||||||
|
locationId: loc == UEBERALL_ID ? nil : loc,
|
||||||
|
minStock: wert * dispFaktor))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
|
|||||||
@@ -2,5 +2,9 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>com.apple.developer.associated-domains</key>
|
||||||
|
<array>
|
||||||
|
<string>applinks:vorrania-ch.scarriffle.com</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
0E0AE6CC6FBA82B190BBC36F /* CheckInFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99E56353CB483EFB1CCD09D8 /* CheckInFormView.swift */; };
|
0E0AE6CC6FBA82B190BBC36F /* CheckInFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99E56353CB483EFB1CCD09D8 /* CheckInFormView.swift */; };
|
||||||
17032C3F3B89BAD6CB249443 /* NotificationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 939C7B7C2F2F58927ED5F2F1 /* NotificationSettingsView.swift */; };
|
17032C3F3B89BAD6CB249443 /* NotificationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 939C7B7C2F2F58927ED5F2F1 /* NotificationSettingsView.swift */; };
|
||||||
25FC3913EFFA37D013328FDE /* CategoryPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDD672F65352DA0D122EC9AE /* CategoryPicker.swift */; };
|
25FC3913EFFA37D013328FDE /* CategoryPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDD672F65352DA0D122EC9AE /* CategoryPicker.swift */; };
|
||||||
|
34787DA16A1AB035BF129BB7 /* GroupViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED41226394E7F31F37EC3B8 /* GroupViews.swift */; };
|
||||||
38133EE3EC920B462BDAA29F /* NotificationScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 163F5DCB00387215F0EA1F21 /* NotificationScheduler.swift */; };
|
38133EE3EC920B462BDAA29F /* NotificationScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 163F5DCB00387215F0EA1F21 /* NotificationScheduler.swift */; };
|
||||||
39F1F17DCB4E491652D08DEE /* APIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378F1B2DE567B62169E84426 /* APIClient.swift */; };
|
39F1F17DCB4E491652D08DEE /* APIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378F1B2DE567B62169E84426 /* APIClient.swift */; };
|
||||||
44F08407AF69F5B7FD15F932 /* ScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF19A735AC17451B57BF5BB /* ScannerView.swift */; };
|
44F08407AF69F5B7FD15F932 /* ScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AF19A735AC17451B57BF5BB /* ScannerView.swift */; };
|
||||||
@@ -51,6 +52,7 @@
|
|||||||
0365A16FEAE3F2BEC321E68A /* CheckInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckInView.swift; sourceTree = "<group>"; };
|
0365A16FEAE3F2BEC321E68A /* CheckInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckInView.swift; sourceTree = "<group>"; };
|
||||||
090485CC54558EB4433376A9 /* DashboardCards.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardCards.swift; sourceTree = "<group>"; };
|
090485CC54558EB4433376A9 /* DashboardCards.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardCards.swift; sourceTree = "<group>"; };
|
||||||
099CCD94907BE0179B7D5742 /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = wrapper.icon; path = AppIcon.icon; sourceTree = "<group>"; };
|
099CCD94907BE0179B7D5742 /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = wrapper.icon; path = AppIcon.icon; sourceTree = "<group>"; };
|
||||||
|
0ED41226394E7F31F37EC3B8 /* GroupViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupViews.swift; sourceTree = "<group>"; };
|
||||||
121D44B9990DA931A9CD5847 /* ServerProfiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerProfiles.swift; sourceTree = "<group>"; };
|
121D44B9990DA931A9CD5847 /* ServerProfiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerProfiles.swift; sourceTree = "<group>"; };
|
||||||
163F5DCB00387215F0EA1F21 /* NotificationScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationScheduler.swift; sourceTree = "<group>"; };
|
163F5DCB00387215F0EA1F21 /* NotificationScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationScheduler.swift; sourceTree = "<group>"; };
|
||||||
1847E65D41ACEDD01CD108BC /* ServerFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerFetch.swift; sourceTree = "<group>"; };
|
1847E65D41ACEDD01CD108BC /* ServerFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerFetch.swift; sourceTree = "<group>"; };
|
||||||
@@ -61,7 +63,7 @@
|
|||||||
3731608B8D48960DC98912BC /* NotificationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettings.swift; sourceTree = "<group>"; };
|
3731608B8D48960DC98912BC /* NotificationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettings.swift; sourceTree = "<group>"; };
|
||||||
378F1B2DE567B62169E84426 /* APIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClient.swift; sourceTree = "<group>"; };
|
378F1B2DE567B62169E84426 /* APIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClient.swift; sourceTree = "<group>"; };
|
||||||
4741D0E95875919C921945CF /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = "<group>"; };
|
4741D0E95875919C921945CF /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = "<group>"; };
|
||||||
4BF4E4F5524B15728CEEE116 /* Vorrania.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Vorrania.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
4BF4E4F5524B15728CEEE116 /* Vorrania.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = Vorrania.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
5312AFE88B7284401AA7B0A3 /* ProductPhotoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductPhotoView.swift; sourceTree = "<group>"; };
|
5312AFE88B7284401AA7B0A3 /* ProductPhotoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductPhotoView.swift; sourceTree = "<group>"; };
|
||||||
660ACEFA22BC77D52FBE736A /* AppDashboards.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDashboards.swift; sourceTree = "<group>"; };
|
660ACEFA22BC77D52FBE736A /* AppDashboards.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDashboards.swift; sourceTree = "<group>"; };
|
||||||
69B59B9F69BC30451D45A3C7 /* ItemViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViews.swift; sourceTree = "<group>"; };
|
69B59B9F69BC30451D45A3C7 /* ItemViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViews.swift; sourceTree = "<group>"; };
|
||||||
@@ -105,6 +107,7 @@
|
|||||||
E6E6139CCD0B6C51B3919EE9 /* DashboardView.swift */,
|
E6E6139CCD0B6C51B3919EE9 /* DashboardView.swift */,
|
||||||
717C8EB336170526F5F3E695 /* DateScanView.swift */,
|
717C8EB336170526F5F3E695 /* DateScanView.swift */,
|
||||||
AAC97A555856C6C5DD353E25 /* DisplaySettings.swift */,
|
AAC97A555856C6C5DD353E25 /* DisplaySettings.swift */,
|
||||||
|
0ED41226394E7F31F37EC3B8 /* GroupViews.swift */,
|
||||||
7DACFBB69CB536BA7CBBD484 /* HistoryView.swift */,
|
7DACFBB69CB536BA7CBBD484 /* HistoryView.swift */,
|
||||||
00288DFA409BAD753AA4CA5D /* ItemListView.swift */,
|
00288DFA409BAD753AA4CA5D /* ItemListView.swift */,
|
||||||
69B59B9F69BC30451D45A3C7 /* ItemViews.swift */,
|
69B59B9F69BC30451D45A3C7 /* ItemViews.swift */,
|
||||||
@@ -234,6 +237,7 @@
|
|||||||
926451E72FD13708C5DD657B /* DashboardView.swift in Sources */,
|
926451E72FD13708C5DD657B /* DashboardView.swift in Sources */,
|
||||||
ED61DAE4824165D6D87F8C1D /* DateScanView.swift in Sources */,
|
ED61DAE4824165D6D87F8C1D /* DateScanView.swift in Sources */,
|
||||||
D70CB183C868FF0143A6A5E7 /* DisplaySettings.swift in Sources */,
|
D70CB183C868FF0143A6A5E7 /* DisplaySettings.swift in Sources */,
|
||||||
|
34787DA16A1AB035BF129BB7 /* GroupViews.swift in Sources */,
|
||||||
4C486D190D1AB338925D29B5 /* HistoryView.swift in Sources */,
|
4C486D190D1AB338925D29B5 /* HistoryView.swift in Sources */,
|
||||||
749139BEFF35F9DAFFE87C3B /* ItemListView.swift in Sources */,
|
749139BEFF35F9DAFFE87C3B /* ItemListView.swift in Sources */,
|
||||||
56A5DFAC86DF972B1481A691 /* ItemViews.swift in Sources */,
|
56A5DFAC86DF972B1481A691 /* ItemViews.swift in Sources */,
|
||||||
@@ -329,7 +333,6 @@
|
|||||||
DEVELOPMENT_TEAM = PP34X97WS3;
|
DEVELOPMENT_TEAM = PP34X97WS3;
|
||||||
GENERATE_INFOPLIST_FILE = NO;
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
INFOPLIST_FILE = Sources/Info.plist;
|
INFOPLIST_FILE = Sources/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@@ -337,10 +340,6 @@
|
|||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.scarriffle.vorrania;
|
PRODUCT_BUNDLE_IDENTIFIER = com.scarriffle.vorrania;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
|
||||||
SUPPORTS_MACCATALYST = NO;
|
|
||||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
|
||||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
|
||||||
SWIFT_VERSION = 5.9;
|
SWIFT_VERSION = 5.9;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
};
|
};
|
||||||
@@ -356,7 +355,6 @@
|
|||||||
DEVELOPMENT_TEAM = PP34X97WS3;
|
DEVELOPMENT_TEAM = PP34X97WS3;
|
||||||
GENERATE_INFOPLIST_FILE = NO;
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
INFOPLIST_FILE = Sources/Info.plist;
|
INFOPLIST_FILE = Sources/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@@ -364,10 +362,6 @@
|
|||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.scarriffle.vorrania;
|
PRODUCT_BUNDLE_IDENTIFIER = com.scarriffle.vorrania;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
|
||||||
SUPPORTS_MACCATALYST = NO;
|
|
||||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
|
||||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
|
||||||
SWIFT_VERSION = 5.9;
|
SWIFT_VERSION = 5.9;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user