Verwendungsstatus: in der App umstellen, filtern und nachsehen
Im Einzelstueck steht jetzt ueber dem Verleih ein Abschnitt "Verwendung" mit vier Symbolen: verfuegbar, in Verwendung, in Reinigung, in Reparatur. Ein Tipp bucht sofort, ohne "Speichern" - wie beim Verleih, denn alle Scan-Wege (QR, NFC, vorrania://i/<UID>) landen genau in dieser Ansicht. Vier Woerter waeren auf einem iPhone zu breit, vier Symbole passen immer; darunter steht der Name im Klartext samt "seit wann". Dazu der Verwendungs-Verlauf mit Zeitstempeln und Konto, und in der Einzelstueck-Liste ein Filter ueber alle vier Zustaende plus die Zeile unter dem Namen, solange etwas laeuft. Stuecke ohne Verwendungsstatus fallen aus dem Filter "Verfuegbar" heraus - ein Schraubenzieher ist nicht verfuegbar, er fuehrt den Zustand gar nicht. Der Haken je Kategorie sitzt im Kategorie-Editor und gilt beim Bearbeiten auch fuer die Unterkategorien. Zwei Dinge, die beim Pruefen am Simulator auffielen: Die Leiste haette beim Oeffnen jedes Mal einmal ins Leere gebucht, weil onChange auch beim Laden feuert - dagegen der Abgleich mit dem zuletzt gemeldeten Stand. Und SQLite liefert Zeitstempel ohne Zeitzone, woran ISO8601DateFormatter scheitert; in der App stand deshalb der rohe ISO-Text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b9d097fe96
commit
290ff9341a
@@ -20,6 +20,41 @@ private func anzeigeDatum(_ iso: String?) -> String {
|
||||
return f.string(from: d)
|
||||
}
|
||||
|
||||
/// ISO-Zeitstempel -> "20.09.2026, 18:42". Anders als bei Daten steht hier ein
|
||||
/// voller Zeitpunkt: Ein Hemd wechselt mehrmals am Tag den Zustand.
|
||||
func anzeigeZeitpunkt(_ iso: String?) -> String {
|
||||
guard let iso else { return "–" }
|
||||
guard let d = zeitpunktAus(iso) else { return iso }
|
||||
let f = DateFormatter()
|
||||
f.locale = Locale(identifier: "de_DE")
|
||||
f.dateFormat = "dd.MM.yyyy, HH:mm"
|
||||
return f.string(from: d)
|
||||
}
|
||||
|
||||
/// Zeitstempel des Servers lesen - mit und ohne Zeitzonenangabe.
|
||||
///
|
||||
/// Postgres liefert sie mit Zone ("…+00:00"), SQLite laesst sie weg. Ohne
|
||||
/// Angabe ist es UTC, denn der Server schreibt `datetime.now(UTC)`. Ohne diesen
|
||||
/// Zweig stuende in der App der rohe ISO-Text.
|
||||
private func zeitpunktAus(_ iso: String) -> Date? {
|
||||
let mitZone = ISO8601DateFormatter()
|
||||
mitZone.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
if let d = mitZone.date(from: iso) { return d }
|
||||
mitZone.formatOptions = [.withInternetDateTime]
|
||||
if let d = mitZone.date(from: iso) { return d }
|
||||
|
||||
let ohneZone = DateFormatter()
|
||||
ohneZone.locale = Locale(identifier: "en_US_POSIX")
|
||||
ohneZone.timeZone = TimeZone(identifier: "UTC")
|
||||
for muster in ["yyyy-MM-dd'T'HH:mm:ss.SSSSSS",
|
||||
"yyyy-MM-dd'T'HH:mm:ss.SSS",
|
||||
"yyyy-MM-dd'T'HH:mm:ss"] {
|
||||
ohneZone.dateFormat = muster
|
||||
if let d = ohneZone.date(from: iso) { return d }
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Verleihzustand in einer Zeile: „Markus · zurueck bis 20.09.2026".
|
||||
/// Ueberfaelliges kommt beim Aufrufer rot daher.
|
||||
func verleihZeile(_ loan: Loan) -> String {
|
||||
@@ -242,6 +277,17 @@ struct ItemEditView: View {
|
||||
@State private var hasDue = false
|
||||
@State private var due = Date()
|
||||
@State private var busyLoan = false
|
||||
// Verwendung: der laufende Zustand (nil = verfuegbar), nachgeladen wie der
|
||||
// Verleih. `zeigtVerwendung` kommt vom uebergebenen Stueck und aendert sich
|
||||
// waehrend der Ansicht nicht.
|
||||
@State private var verwendung: VerwendungStatus?
|
||||
// Was der Server zuletzt gemeldet hat. Ohne diesen Abgleich buchte das
|
||||
// onChange der Leiste auch dann, wenn `verwendung` nur beim Laden gesetzt
|
||||
// wurde - also bei jedem Oeffnen einmal ins Leere.
|
||||
@State private var verwendungServer: VerwendungStatus?
|
||||
@State private var verwendungSeit: String?
|
||||
@State private var verwendungAnzahl = 0
|
||||
@State private var busyVerwendung = false
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
@@ -268,6 +314,39 @@ struct ItemEditView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// Woran ist es gerade? Steht oben, weil alle Scan-Wege hier landen
|
||||
// und „gerade aus der Waesche geholt" ein Griff sein soll - also
|
||||
// eine Buchung wie der Verleih, kein Feld, das auf „Speichern"
|
||||
// wartet. Vier Woerter waeren auf einem iPhone zu breit, vier
|
||||
// Symbole passen immer.
|
||||
if item.zeigtVerwendung {
|
||||
Section {
|
||||
Picker("Verwendung", selection: $verwendung) {
|
||||
ForEach(VerwendungStatus.auswahl, id: \.self) { status in
|
||||
Image(systemName: VerwendungStatus.symbol(status))
|
||||
.tag(status)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.disabled(busyVerwendung)
|
||||
.onChange(of: verwendung) { neu in
|
||||
Task { await setzeVerwendung(neu) }
|
||||
}
|
||||
if verwendungAnzahl > 0 {
|
||||
NavigationLink("Verwendungs-Verlauf (\(verwendungAnzahl))") {
|
||||
VerwendungHistorieView(uid: item.uid, itemId: item.id)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Verwendung")
|
||||
} footer: {
|
||||
Text(busyVerwendung
|
||||
? "Moment…"
|
||||
: VerwendungStatus.label(verwendung)
|
||||
+ (verwendungSeit.map { " · seit \($0)" } ?? ""))
|
||||
}
|
||||
}
|
||||
|
||||
// Verleih steht vor allem anderen: Ist das Stueck draussen, ist das
|
||||
// die Antwort auf „wo ist es?". Alle Scan-Wege der App (QR, NFC,
|
||||
// vorrania://i/<UID>) landen in dieser Ansicht - deshalb genuegt der
|
||||
@@ -605,6 +684,42 @@ struct ItemEditView: View {
|
||||
} catch { self.error = error.localizedDescription }
|
||||
}
|
||||
|
||||
/// Verwendungszustand umstellen. Leer = verfuegbar.
|
||||
///
|
||||
/// Die Antwort traegt das ganze Stueck, ein zweites Laden entfaellt. Nur
|
||||
/// der Verlaufszaehler wird nachgeholt.
|
||||
private func setzeVerwendung(_ neu: VerwendungStatus?) async {
|
||||
guard neu != verwendungServer, !busyVerwendung else { return }
|
||||
busyVerwendung = true; defer { busyVerwendung = false }
|
||||
error = nil
|
||||
do {
|
||||
let frisch = try await APIClient.shared.setzeVerwendung(id: item.id, status: neu)
|
||||
uebernimm(frisch.verwendung)
|
||||
await ladeVerwendungen()
|
||||
onChanged?()
|
||||
} catch {
|
||||
self.error = error.localizedDescription
|
||||
// Zurueck auf den Stand des Servers, sonst zeigt die Leiste eine
|
||||
// Stellung an, die nie gebucht wurde.
|
||||
await ladeVerwendungen()
|
||||
}
|
||||
}
|
||||
|
||||
private func uebernimm(_ offen: Verwendung?) {
|
||||
verwendungServer = offen?.status
|
||||
verwendung = offen?.status
|
||||
verwendungSeit = offen.map { anzeigeZeitpunkt($0.startedAt) }
|
||||
}
|
||||
|
||||
/// Verlauf holen - fuer den Zaehler am Verlaufs-Eintrag und um den
|
||||
/// laufenden Zustand gegen den Server abzugleichen.
|
||||
private func ladeVerwendungen() async {
|
||||
guard item.zeigtVerwendung else { return }
|
||||
let verlauf = (try? await APIClient.shared.itemVerwendungen(id: item.id)) ?? []
|
||||
verwendungAnzahl = verlauf.count
|
||||
uebernimm(verlauf.first(where: { $0.laeuft }))
|
||||
}
|
||||
|
||||
/// Offenen Verleih und Historie frisch holen - nach jeder Buchung, damit die
|
||||
/// Ansicht nicht auf dem Stand des uebergebenen `item` haengen bleibt.
|
||||
private func ladeVerleihe() async {
|
||||
@@ -627,7 +742,9 @@ struct ItemEditView: View {
|
||||
loan = item.loan
|
||||
teile = item.teile
|
||||
gehoertZu = item.gehoertZu
|
||||
uebernimm(item.verwendung)
|
||||
await ladeVerleihe()
|
||||
await ladeVerwendungen()
|
||||
}
|
||||
|
||||
private func haengeAn(_ gewaehlt: Item) async {
|
||||
@@ -1075,6 +1192,54 @@ struct ItemAddSheet: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Verwendungs-Verlauf
|
||||
|
||||
/// Alle Verwendungszeitraeume eines Stuecks, neueste zuerst. Abgeschlossene
|
||||
/// bleiben erhalten - so ist nachvollziehbar, seit wann etwas in der Waesche
|
||||
/// liegt und wie oft es getragen wurde.
|
||||
struct VerwendungHistorieView: View {
|
||||
let uid: String
|
||||
let itemId: Int
|
||||
|
||||
@State private var zeilen: [Verwendung] = []
|
||||
@State private var geladen = false
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
if !geladen {
|
||||
Text("Wird geladen…").foregroundStyle(.secondary)
|
||||
} else if zeilen.isEmpty {
|
||||
Text("War noch nie in Verwendung.").foregroundStyle(.secondary)
|
||||
}
|
||||
ForEach(zeilen) { zeile in
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Label(zeile.status.label, systemImage: zeile.status.symbol)
|
||||
.font(.body.bold())
|
||||
Spacer()
|
||||
if zeile.laeuft {
|
||||
Text("läuft").font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
Text(anzeigeZeitpunkt(zeile.startedAt)
|
||||
+ (zeile.endedAt.map { " – " + anzeigeZeitpunkt($0) } ?? ""))
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
if let von = zeile.username {
|
||||
Text("von \(von)").font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Verwendung \(uid)")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.task {
|
||||
zeilen = (try? await APIClient.shared.itemVerwendungen(id: itemId)) ?? []
|
||||
geladen = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Verleih-Verlauf
|
||||
|
||||
/// Alle Verleihe eines Stuecks, neueste zuerst. Zurueckgegebene Vorgaenge bleiben
|
||||
|
||||
Reference in New Issue
Block a user