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"))
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
var request = try makeRequest("/groups/\(id)", method: "PATCH")
|
||||
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(
|
||||
systemImage: "square.stack.3d.up",
|
||||
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() }
|
||||
}
|
||||
}
|
||||
@@ -185,10 +188,15 @@ struct ExpiringView: View {
|
||||
|
||||
// MARK: - Mindestbestände
|
||||
|
||||
/// Übersicht aller Mindestbestände: Produkte und Gruppen, jeweils Gesamt UND je
|
||||
/// Lagerort (Soll in der Anzeige-/Gruppeneinheit). Produktzeilen öffnen den
|
||||
/// Artikel zum Bearbeiten; das Setzen von Gruppen-/Lagerort-Bedarfen läuft übers
|
||||
/// Web.
|
||||
/// Übersicht aller Mindestbestände – nach LAGERORT gegliedert.
|
||||
///
|
||||
/// Ein Mindestbestand ist immer ein Bedarf an einem Ort. „Überall" (egal wo,
|
||||
/// 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 {
|
||||
@State private var products: [Product] = []
|
||||
@State private var groups: [GroupItem] = []
|
||||
@@ -198,71 +206,92 @@ struct MinStockView: View {
|
||||
private struct MinRow: Identifiable {
|
||||
let id: String
|
||||
let title: String
|
||||
let ort: String // "Gesamt" oder Lagerort-Name
|
||||
let soll: String
|
||||
let bestand: String?
|
||||
let productId: Int? // Tippziel (nur Produkte)
|
||||
let isGroup: Bool
|
||||
let productId: Int? // Tippziel (Artikel)
|
||||
let groupId: Int? // Tippziel (Gruppe)
|
||||
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 }) {
|
||||
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 ?? []) {
|
||||
// je Lagerort in Artikeleinheiten gespeichert -> Anzeigeeinheit.
|
||||
out.append(MinRow(
|
||||
id: "p\(p.id)l\(l.locationId)", title: p.name,
|
||||
ort: l.locationName ?? "Lagerort",
|
||||
soll: "\(formatAmount(l.minStock * art / disp)) \(p.unitName)",
|
||||
bestand: nil, productId: p.id, isGroup: false, unter: false))
|
||||
merken(l.locationId, l.locationName, MinRow(
|
||||
id: "p\(p.id)l\(l.id)", title: p.name,
|
||||
soll: "\(formatAmount(l.minStock / disp)) \(p.unitName)",
|
||||
bestand: l.stock.map { "\(formatAmount($0 / disp)) \(p.unitName)" },
|
||||
productId: p.id, groupId: nil,
|
||||
unter: (l.stock ?? 0) < l.minStock, untergruppen: 0))
|
||||
}
|
||||
}
|
||||
for g in groups.sorted(by: { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }) {
|
||||
let unit = g.minStockUnitName ?? ""
|
||||
if let ms = g.minStock, ms > 0 {
|
||||
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))
|
||||
}
|
||||
let faktor = g.minFaktor == 0 ? 1 : g.minFaktor
|
||||
let einheit = g.minEinheit
|
||||
for l in (g.locationMinStocks ?? []) {
|
||||
out.append(MinRow(
|
||||
id: "g\(g.id)l\(l.locationId)", title: g.name,
|
||||
ort: l.locationName ?? "Lagerort",
|
||||
soll: "\(formatAmount(l.minStock)) \(unit)",
|
||||
bestand: nil, productId: nil, isGroup: true, unter: false))
|
||||
merken(l.locationId, l.locationName, MinRow(
|
||||
id: "g\(g.id)l\(l.id)", title: g.name,
|
||||
soll: "\(formatAmount(l.minStock / faktor)) \(einheit)",
|
||||
bestand: l.stock.map { "\(formatAmount($0 / faktor)) \(einheit)" },
|
||||
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 {
|
||||
List {
|
||||
if !busy && rows.isEmpty {
|
||||
if !busy && bloecke.isEmpty {
|
||||
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 }) {
|
||||
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 {
|
||||
zeile(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Mindestbestände")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.overlay { if busy && rows.isEmpty { ProgressView() } }
|
||||
.overlay { if busy && bloecke.isEmpty { ProgressView() } }
|
||||
.refreshable { await load() }
|
||||
.onAppear { Task { await load() } }
|
||||
.alert("Fehler", isPresented: Binding(get: { error != nil }, set: { if !$0 { error = nil } })) {
|
||||
@@ -276,13 +305,18 @@ struct MinStockView: View {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
HStack(spacing: 6) {
|
||||
Text(r.title)
|
||||
if r.isGroup {
|
||||
if r.groupId != nil {
|
||||
Text("Gruppe").font(.caption2)
|
||||
.padding(.horizontal, 6).padding(.vertical, 1)
|
||||
.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)
|
||||
}
|
||||
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 –
|
||||
/// Name plus optionale Website.
|
||||
|
||||
@@ -146,30 +146,50 @@ struct Product: Codable, Identifiable, Hashable {
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
let locationId: String
|
||||
let locationId: String?
|
||||
let locationName: String?
|
||||
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 {
|
||||
case stock
|
||||
case minStock = "min_stock"
|
||||
case locationId = "location_id"
|
||||
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 {
|
||||
let locationId: String
|
||||
let locationId: String?
|
||||
let minStock: Double
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case minStock = "min_stock"
|
||||
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.
|
||||
///
|
||||
/// Anzeige und uebertragener Wert sind bewusst getrennt: Das Backend kennt
|
||||
@@ -390,6 +410,8 @@ struct GroupShoppingItem: Codable, Identifiable {
|
||||
let deficit: Double
|
||||
let unitName: String
|
||||
let productCount: Int
|
||||
/// Wie viele Untergruppen mitgezaehlt werden (0 = keine).
|
||||
var subgroupCount: Int? = nil
|
||||
|
||||
var id: Int { groupId }
|
||||
|
||||
@@ -399,6 +421,7 @@ struct GroupShoppingItem: Codable, Identifiable {
|
||||
case minStock = "min_stock"
|
||||
case unitName = "unit_name"
|
||||
case productCount = "product_count"
|
||||
case subgroupCount = "subgroup_count"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -637,20 +660,86 @@ struct SettingEntry: Codable {
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
let id: Int
|
||||
let name: String
|
||||
// Mindestbestand-Infos (nur aus /groups befuellt; Picker brauchen sie nicht).
|
||||
var minStock: Double? = nil
|
||||
var minStockUnitId: Int? = 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 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 {
|
||||
case id, name, stock
|
||||
case id, name, stock, kind
|
||||
case minStock = "min_stock"
|
||||
case minStockUnitId = "min_stock_unit_id"
|
||||
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 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 minStock: Double?
|
||||
let minStockUnitId: Int?
|
||||
/// Obergruppen (mehrere moeglich) – optional, Standard: keine.
|
||||
var parentIds: [Int]? = nil
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case name
|
||||
case minStock = "min_stock"
|
||||
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 productUrl = ""
|
||||
// Gesamt-Mindestbestand (nur Gegenstände; Menge in Stück/Artikeleinheit).
|
||||
@State private var minStock = ""
|
||||
@State private var shops: [ShopItem] = []
|
||||
@State private var locations: [StorageLocation] = []
|
||||
@State private var fieldDefs: [FieldDefinition] = []
|
||||
@@ -63,8 +62,6 @@ struct ProductDetailView: View {
|
||||
if packageSize != (current.packageSize.map { formatAmount($0) } ?? "") { return true }
|
||||
if packageLabel != (current.packageLabel ?? "") { 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 objMode != currentObjMode { return true }
|
||||
@@ -197,26 +194,31 @@ struct ProductDetailView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// Mindestbestand bei Lebensmitteln UND Verbrauchsgegenständen (Charge) –
|
||||
// in der Anzeigeeinheit (z.B. Gramm/Milliliter), nicht in Packungen.
|
||||
// Mindestbestand bei Lebensmitteln UND Verbrauchsgegenständen (Charge).
|
||||
// Menge je Lagerort und Einzelstücke haben keinen Mindestbestand.
|
||||
if current.foodLike {
|
||||
Section("Mindestbestand") {
|
||||
QuantityField(label: "Gesamt (\(current.unitName))", text: $minStock)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Es gibt kein separates „Gesamt"-Feld mehr: der Bedarf hängt immer
|
||||
// an einem Ort, und „Überall" ist einer davon (der oberste).
|
||||
if current.foodLike {
|
||||
Section {
|
||||
NavigationLink {
|
||||
ProductLocationMinView(product: current) { await reload() }
|
||||
} label: {
|
||||
let n = (current.locationMinStocks ?? []).count
|
||||
Label(n > 0 ? "Mindestbestand je Lagerort (\(n))" : "Mindestbestand je Lagerort",
|
||||
systemImage: "mappin.and.ellipse")
|
||||
let zeilen = current.locationMinStocks ?? []
|
||||
let ueberall = zeilen.first { $0.locationId == nil }
|
||||
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: {
|
||||
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
|
||||
/// den Basiseinheiten umgerechnet, unabhängig davon, wie er erfasst wurde.
|
||||
private var minStockFeld: String {
|
||||
current.minStock.map { formatAmount($0 / max(current.unitFactor, 1)) } ?? ""
|
||||
/// Kurzfassung der hinterlegten Mindestbestände für die Übersichtszeile.
|
||||
private func minStockUntertitel(zeilen: [LocationMinStock], ueberall: LocationMinStock?) -> String {
|
||||
if zeilen.isEmpty { return "keiner hinterlegt" }
|
||||
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.
|
||||
@@ -350,7 +359,6 @@ struct ProductDetailView: View {
|
||||
categoryId = current.categoryId
|
||||
shopId = current.shopId
|
||||
productUrl = current.productUrl ?? ""
|
||||
minStock = minStockFeld
|
||||
objMode = currentObjMode
|
||||
fieldValues = Self.stringValues(current.fieldValues)
|
||||
}
|
||||
@@ -411,9 +419,6 @@ struct ProductDetailView: View {
|
||||
busy = true
|
||||
defer { busy = false }
|
||||
do {
|
||||
// Mindestbestand in der Anzeigeeinheit erfasst → Basiseinheiten.
|
||||
let minBase = Double(minStock.replacingOccurrences(of: ",", with: "."))
|
||||
.map { $0 * current.unitFactor }
|
||||
if current.foodLike {
|
||||
// Lebensmittel ODER Verbrauchsgegenstand: als Charge mit Einheit/
|
||||
// Packung/MHD/Gruppe. Verbrauchsgegenstände (isObject) tragen
|
||||
@@ -427,11 +432,9 @@ struct ProductDetailView: View {
|
||||
groupId: groupId,
|
||||
categoryId: categoryId
|
||||
)
|
||||
// Mindestbestand für Lebensmittel UND Verbrauchsgegenstände.
|
||||
req.minStock = minBase
|
||||
req.minStockInPackages = false
|
||||
req.minStockUnitId = nil
|
||||
req.sendMinStock = true
|
||||
// Der Mindestbestand wird in ProductLocationMinView gepflegt und
|
||||
// hier bewusst NICHT mitgeschickt – sonst überschriebe das
|
||||
// Stammdaten-Speichern die dort gesetzte „Überall"-Zeile.
|
||||
if current.isObject {
|
||||
var fv: [String: String?] = [:]
|
||||
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 error: String?
|
||||
|
||||
// Speicherung in Artikeleinheiten, Anzeige in der Anzeigeeinheit (Gramm/ml/Stück).
|
||||
private var artToDisp: Double {
|
||||
let disp = product.unitFactor == 0 ? 1 : product.unitFactor
|
||||
return product.articleUnitFactor / disp
|
||||
}
|
||||
// Gespeichert wird in Basiseinheiten, erfasst in der Anzeigeeinheit (g/ml/Stück).
|
||||
private var dispFaktor: Double { product.unitFactor == 0 ? 1 : product.unitFactor }
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
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)
|
||||
}
|
||||
ForEach($rows) { $row in
|
||||
HStack {
|
||||
Picker("Lagerort", selection: $row.locationId) {
|
||||
Text("– wählen –").tag(String?.none)
|
||||
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)
|
||||
@@ -691,15 +693,15 @@ struct ProductLocationMinView: View {
|
||||
}
|
||||
}
|
||||
Button {
|
||||
rows.append(MinRow(locationId: nil, amount: ""))
|
||||
rows.append(MinRow(locationId: UEBERALL_ID, amount: ""))
|
||||
} label: {
|
||||
Label("Lagerort hinzufügen", systemImage: "plus")
|
||||
Label("Mindestbestand hinzufügen", systemImage: "plus")
|
||||
}
|
||||
if let error {
|
||||
Section { Text(error).foregroundStyle(.red).font(.callout) }
|
||||
}
|
||||
}
|
||||
.navigationTitle("Bedarf je Lagerort")
|
||||
.navigationTitle("Mindestbestand")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
@@ -712,8 +714,10 @@ struct ProductLocationMinView: View {
|
||||
|
||||
private func load() async {
|
||||
locations = (try? await APIClient.shared.locations()) ?? []
|
||||
// „Überall" ist der Ort nil – im Picker braucht es einen Platzhalter.
|
||||
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
|
||||
if wert > 0 {
|
||||
gesehen.insert(loc)
|
||||
// Eingabe in Anzeigeeinheit → Artikeleinheiten (so gespeichert).
|
||||
list.append(LocationMinStockIn(locationId: loc, minStock: wert / artToDisp))
|
||||
// Eingabe in Anzeigeeinheit → Basiseinheiten (so gespeichert).
|
||||
list.append(LocationMinStockIn(
|
||||
locationId: loc == UEBERALL_ID ? nil : loc,
|
||||
minStock: wert * dispFaktor))
|
||||
}
|
||||
}
|
||||
do {
|
||||
|
||||
@@ -2,5 +2,9 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>applinks:vorrania-ch.scarriffle.com</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
0E0AE6CC6FBA82B190BBC36F /* CheckInFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99E56353CB483EFB1CCD09D8 /* CheckInFormView.swift */; };
|
||||
17032C3F3B89BAD6CB249443 /* NotificationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 939C7B7C2F2F58927ED5F2F1 /* NotificationSettingsView.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 */; };
|
||||
39F1F17DCB4E491652D08DEE /* APIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378F1B2DE567B62169E84426 /* APIClient.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -61,7 +63,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -105,6 +107,7 @@
|
||||
E6E6139CCD0B6C51B3919EE9 /* DashboardView.swift */,
|
||||
717C8EB336170526F5F3E695 /* DateScanView.swift */,
|
||||
AAC97A555856C6C5DD353E25 /* DisplaySettings.swift */,
|
||||
0ED41226394E7F31F37EC3B8 /* GroupViews.swift */,
|
||||
7DACFBB69CB536BA7CBBD484 /* HistoryView.swift */,
|
||||
00288DFA409BAD753AA4CA5D /* ItemListView.swift */,
|
||||
69B59B9F69BC30451D45A3C7 /* ItemViews.swift */,
|
||||
@@ -234,6 +237,7 @@
|
||||
926451E72FD13708C5DD657B /* DashboardView.swift in Sources */,
|
||||
ED61DAE4824165D6D87F8C1D /* DateScanView.swift in Sources */,
|
||||
D70CB183C868FF0143A6A5E7 /* DisplaySettings.swift in Sources */,
|
||||
34787DA16A1AB035BF129BB7 /* GroupViews.swift in Sources */,
|
||||
4C486D190D1AB338925D29B5 /* HistoryView.swift in Sources */,
|
||||
749139BEFF35F9DAFFE87C3B /* ItemListView.swift in Sources */,
|
||||
56A5DFAC86DF972B1481A691 /* ItemViews.swift in Sources */,
|
||||
@@ -329,7 +333,6 @@
|
||||
DEVELOPMENT_TEAM = PP34X97WS3;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = Sources/Info.plist;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -337,10 +340,6 @@
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scarriffle.vorrania;
|
||||
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;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
@@ -356,7 +355,6 @@
|
||||
DEVELOPMENT_TEAM = PP34X97WS3;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = Sources/Info.plist;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -364,10 +362,6 @@
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scarriffle.vorrania;
|
||||
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;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user