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:
Scarriffle
2026-08-16 00:02:43 +02:00
parent 0537a2b53f
commit f33054c534
8 changed files with 576 additions and 133 deletions

View File

@@ -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))

View 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)
}
}

View File

@@ -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()

View File

@@ -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.

View File

@@ -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"
} }
} }

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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";
}; };