diff --git a/ios/Sources/APIClient.swift b/ios/Sources/APIClient.swift index 58b11a8..79e2f18 100644 --- a/ios/Sources/APIClient.swift +++ b/ios/Sources/APIClient.swift @@ -423,8 +423,9 @@ actor APIClient { try await send(try makeRequest("/dashboard/expiry-split"), as: ExpirySplit.self) } - func byCategory() async throws -> [CategoryShare] { - try await send(try makeRequest("/dashboard/by-category"), as: [CategoryShare].self) + func byCategory(depth: Int? = nil) async throws -> [CategoryShare] { + 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] { diff --git a/ios/Sources/ChartCards.swift b/ios/Sources/ChartCards.swift index abf46fc..d1df378 100644 --- a/ios/Sources/ChartCards.swift +++ b/ios/Sources/ChartCards.swift @@ -76,11 +76,15 @@ private struct ProportionChart: View { struct ChartCardView: View { let type: String 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 { switch type { - case "expiry-donut": ExpiryDonutCard(stand: stand) - case "category-donut": CategoryDonutCard(stand: stand) + case "expiry-donut": ExpiryDonutCard(stand: stand, mitOhneMhd: mitOhneMhd) + case "category-donut": CategoryDonutCard(stand: stand, detail: detail, tiefe: tiefe) case "category-bars": CategoryBarsCard(stand: stand) case "stock-timeline": StockTimelineCard(stand: stand) case "activity-timeline": ActivityTimelineCard(stand: stand) @@ -122,14 +126,19 @@ private struct ChartLoader: View { struct ExpiryDonutCard: View { let stand: Int + // Web-Karte „Ohne MHD einbeziehen": aus, blendet den grauen Anteil aus. + var mitOhneMhd: Bool = true var body: some View { 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: "Bald ablaufend", value: s.soon, color: ExpiryColor.soon), 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 { 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. private static let palette: [Color] = [ .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 { - 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 } .sorted { $0.articleUnits > $1.articleUnits } - // Die groessten sechs einzeln, der Rest gebuendelt - sonst wird der - // Ring unlesbar. - let kopf = sortiert.prefix(6) - let restWert = sortiert.dropFirst(6).reduce(0) { $0 + $1.articleUnits } - var segmente = kopf.enumerated().map { i, c in - Segment(label: c.name, value: c.articleUnits, - color: Self.palette[i % Self.palette.count]) - } - if restWert > 0 { - segmente.append(Segment(label: "Übrige", value: restWert, color: .gray)) + 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 + // Ring unlesbar. + let kopf = sortiert.prefix(6) + let restWert = sortiert.dropFirst(6).reduce(0) { $0 + $1.articleUnits } + var s = kopf.enumerated().map { i, c in + Segment(label: c.name, value: c.articleUnits, color: Self.farbe(i)) + } + if restWert > 0 { + s.append(Segment(label: "Übrige", value: restWert, color: .gray)) + } + segmente = s } return ProportionChart(segments: segmente, einheit: "Artikeleinheiten") } diff --git a/ios/Sources/DashboardView.swift b/ios/Sources/DashboardView.swift index 278160f..d47cb50 100644 --- a/ios/Sources/DashboardView.swift +++ b/ios/Sources/DashboardView.swift @@ -103,7 +103,10 @@ struct MirrorOverview: View { ForEach(karten) { karte in Section { 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: { Text(CardCatalog.titel(karte.type)) } @@ -174,6 +177,11 @@ struct DashboardCardView: View { let type: String let productId: 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 var body: some View { @@ -198,7 +206,8 @@ struct DashboardCardView: View { ProductTimelineChart(productId: productId, stand: stand) case "expiry-donut", "category-donut", "category-bars", "stock-timeline", "activity-timeline": - ChartCardView(type: type, stand: stand) + ChartCardView(type: type, stand: stand, + detail: detail, tiefe: tiefe, mitOhneMhd: mitOhneMhd) default: Text("Unbekannte Karte „\(type)“.") .font(.caption).foregroundStyle(.secondary) diff --git a/ios/Sources/Models.swift b/ios/Sources/Models.swift index 4c2e307..88643d4 100644 --- a/ios/Sources/Models.swift +++ b/ios/Sources/Models.swift @@ -1038,9 +1038,17 @@ struct DashboardCard: Codable, Identifiable, Hashable { /// Web-Oberflaeche; hier bestimmen sie allein die Reihenfolge. struct CardProps: Codable, Hashable { 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 { case productId = "product_id" + case locationId = "location_id" + case detail + case tiefe + case mitOhneMhd = "mit_ohne_mhd" } } }