iOS: Diagramm-Einstellungen der Web-Karten beim Spiegeln uebernehmen

Die gespiegelte Web-Uebersicht ignorierte bisher die Karten-Props der
Diagramme. Jetzt werden uebernommen:
- Ablauf-Ring: 'Ohne MHD einbeziehen' (mit_ohne_mhd)
- Kategorien-Ring: 'Detailansicht' (alle einzeln statt 'Uebrige') (detail)
- Kategorien-Ring: 'Kategorie-Tiefe' (tiefe -> /by-category?depth=N)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-31 22:41:36 +02:00
parent ade3c86747
commit d680614526
4 changed files with 67 additions and 20 deletions

View File

@@ -423,8 +423,9 @@ actor APIClient {
try await send(try makeRequest("/dashboard/expiry-split"), as: ExpirySplit.self) try await send(try makeRequest("/dashboard/expiry-split"), as: ExpirySplit.self)
} }
func byCategory() async throws -> [CategoryShare] { func byCategory(depth: Int? = nil) async throws -> [CategoryShare] {
try await send(try makeRequest("/dashboard/by-category"), as: [CategoryShare].self) let suffix = depth.map { "?depth=\($0)" } ?? ""
return try await send(try makeRequest("/dashboard/by-category" + suffix), as: [CategoryShare].self)
} }
func timeline(days: Int, productId: Int? = nil) async throws -> [TimelinePoint] { func timeline(days: Int, productId: Int? = nil) async throws -> [TimelinePoint] {

View File

@@ -76,11 +76,15 @@ private struct ProportionChart: View {
struct ChartCardView: View { struct ChartCardView: View {
let type: String let type: String
let stand: Int let stand: Int
// Aus den Web-Karten-Einstellungen (props): werden hier angewandt.
var detail: Bool = false
var tiefe: Int? = nil
var mitOhneMhd: Bool = true
var body: some View { var body: some View {
switch type { switch type {
case "expiry-donut": ExpiryDonutCard(stand: stand) case "expiry-donut": ExpiryDonutCard(stand: stand, mitOhneMhd: mitOhneMhd)
case "category-donut": CategoryDonutCard(stand: stand) case "category-donut": CategoryDonutCard(stand: stand, detail: detail, tiefe: tiefe)
case "category-bars": CategoryBarsCard(stand: stand) case "category-bars": CategoryBarsCard(stand: stand)
case "stock-timeline": StockTimelineCard(stand: stand) case "stock-timeline": StockTimelineCard(stand: stand)
case "activity-timeline": ActivityTimelineCard(stand: stand) case "activity-timeline": ActivityTimelineCard(stand: stand)
@@ -122,14 +126,19 @@ private struct ChartLoader<Data, Inhalt: View>: View {
struct ExpiryDonutCard: View { struct ExpiryDonutCard: View {
let stand: Int let stand: Int
// Web-Karte Ohne MHD einbeziehen": aus, blendet den grauen Anteil aus.
var mitOhneMhd: Bool = true
var body: some View { var body: some View {
ChartLoader(laden: { try await APIClient.shared.expirySplit() }, stand: stand) { s in ChartLoader(laden: { try await APIClient.shared.expirySplit() }, stand: stand) { s in
ProportionChart(segments: [ var segmente = [
Segment(label: "In Ordnung", value: s.ok, color: ExpiryColor.ok), Segment(label: "In Ordnung", value: s.ok, color: ExpiryColor.ok),
Segment(label: "Bald ablaufend", value: s.soon, color: ExpiryColor.soon), Segment(label: "Bald ablaufend", value: s.soon, color: ExpiryColor.soon),
Segment(label: "Abgelaufen", value: s.expired, color: ExpiryColor.expired), Segment(label: "Abgelaufen", value: s.expired, color: ExpiryColor.expired),
Segment(label: "Ohne MHD", value: s.noDate, color: ExpiryColor.noDate), ]
], einheit: "Artikeleinheiten") if mitOhneMhd {
segmente.append(Segment(label: "Ohne MHD", value: s.noDate, color: ExpiryColor.noDate))
}
return ProportionChart(segments: segmente, einheit: "Artikeleinheiten")
} }
} }
} }
@@ -138,26 +147,46 @@ struct ExpiryDonutCard: View {
struct CategoryDonutCard: View { struct CategoryDonutCard: View {
let stand: Int let stand: Int
// Web-Karte Detailansicht": alle Kategorien einzeln statt Übrige".
var detail: Bool = false
// Web-Karte Kategorie-Tiefe": auf welche Ebene hochgerollt wird (nil = feinste).
var tiefe: Int? = nil
// Wiederholbare, ruhige Farbfolge fuer die Kategorien. // Wiederholbare, ruhige Farbfolge fuer die Kategorien.
private static let palette: [Color] = [ private static let palette: [Color] = [
.blue, .green, .orange, .purple, .pink, .teal, .indigo, .brown, .blue, .green, .orange, .purple, .pink, .teal, .indigo, .brown,
] ]
// Ab dem Ende der Palette werden Farben ueber den goldenen Winkel erzeugt,
// damit auch viele Kategorien (Detailansicht) unterscheidbar bleiben.
private static func farbe(_ i: Int) -> Color {
if i < palette.count { return palette[i] }
let hue = (Double(i) * 0.61803398875).truncatingRemainder(dividingBy: 1)
return Color(hue: hue, saturation: 0.55, brightness: 0.78)
}
var body: some View { var body: some View {
ChartLoader(laden: { try await APIClient.shared.byCategory() }, stand: stand) { liste in ChartLoader(laden: { try await APIClient.shared.byCategory(depth: tiefe) }, stand: stand) { liste in
let sortiert = liste.filter { $0.articleUnits > 0 } let sortiert = liste.filter { $0.articleUnits > 0 }
.sorted { $0.articleUnits > $1.articleUnits } .sorted { $0.articleUnits > $1.articleUnits }
let segmente: [Segment]
if detail {
// Detailansicht: alle Kategorien einzeln, nichts wird gebuendelt.
segmente = sortiert.enumerated().map { i, c in
Segment(label: c.name, value: c.articleUnits, color: Self.farbe(i))
}
} else {
// Die groessten sechs einzeln, der Rest gebuendelt - sonst wird der // Die groessten sechs einzeln, der Rest gebuendelt - sonst wird der
// Ring unlesbar. // Ring unlesbar.
let kopf = sortiert.prefix(6) let kopf = sortiert.prefix(6)
let restWert = sortiert.dropFirst(6).reduce(0) { $0 + $1.articleUnits } let restWert = sortiert.dropFirst(6).reduce(0) { $0 + $1.articleUnits }
var segmente = kopf.enumerated().map { i, c in var s = kopf.enumerated().map { i, c in
Segment(label: c.name, value: c.articleUnits, Segment(label: c.name, value: c.articleUnits, color: Self.farbe(i))
color: Self.palette[i % Self.palette.count])
} }
if restWert > 0 { if restWert > 0 {
segmente.append(Segment(label: "Übrige", value: restWert, color: .gray)) s.append(Segment(label: "Übrige", value: restWert, color: .gray))
}
segmente = s
} }
return ProportionChart(segments: segmente, einheit: "Artikeleinheiten") return ProportionChart(segments: segmente, einheit: "Artikeleinheiten")
} }

View File

@@ -103,7 +103,10 @@ struct MirrorOverview: View {
ForEach(karten) { karte in ForEach(karten) { karte in
Section { Section {
DashboardCardView(type: karte.type, productId: karte.props?.productId, DashboardCardView(type: karte.type, productId: karte.props?.productId,
stand: stand) { stand += 1 } stand: stand,
detail: karte.props?.detail ?? false,
tiefe: karte.props?.tiefe.flatMap { Int($0) },
mitOhneMhd: karte.props?.mitOhneMhd ?? true) { stand += 1 }
} header: { } header: {
Text(CardCatalog.titel(karte.type)) Text(CardCatalog.titel(karte.type))
} }
@@ -174,6 +177,11 @@ struct DashboardCardView: View {
let type: String let type: String
let productId: Int? let productId: Int?
let stand: Int let stand: Int
// Aus den Web-Karten-Einstellungen (props). Standardwerte = bisheriges Verhalten,
// damit die lokale App-Übersicht unverändert bleibt.
var detail: Bool = false
var tiefe: Int? = nil
var mitOhneMhd: Bool = true
let onChange: () -> Void let onChange: () -> Void
var body: some View { var body: some View {
@@ -198,7 +206,8 @@ struct DashboardCardView: View {
ProductTimelineChart(productId: productId, stand: stand) ProductTimelineChart(productId: productId, stand: stand)
case "expiry-donut", "category-donut", "category-bars", case "expiry-donut", "category-donut", "category-bars",
"stock-timeline", "activity-timeline": "stock-timeline", "activity-timeline":
ChartCardView(type: type, stand: stand) ChartCardView(type: type, stand: stand,
detail: detail, tiefe: tiefe, mitOhneMhd: mitOhneMhd)
default: default:
Text("Unbekannte Karte „\(type)“.") Text("Unbekannte Karte „\(type)“.")
.font(.caption).foregroundStyle(.secondary) .font(.caption).foregroundStyle(.secondary)

View File

@@ -1038,9 +1038,17 @@ struct DashboardCard: Codable, Identifiable, Hashable {
/// Web-Oberflaeche; hier bestimmen sie allein die Reihenfolge. /// Web-Oberflaeche; hier bestimmen sie allein die Reihenfolge.
struct CardProps: Codable, Hashable { struct CardProps: Codable, Hashable {
let productId: Int? let productId: Int?
let locationId: String?
let detail: Bool? // Kategorien-Ring: alle einzeln statt Übrige"
let tiefe: String? // Kategorie-Tiefe ("" = feinste, "1"/"2"/)
let mitOhneMhd: Bool? // Ablauf-Ring: Ohne MHD" einbeziehen
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case productId = "product_id" case productId = "product_id"
case locationId = "location_id"
case detail
case tiefe
case mitOhneMhd = "mit_ohne_mhd"
} }
} }
} }