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:
@@ -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] {
|
||||||
|
|||||||
@@ -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 }
|
||||||
// Die groessten sechs einzeln, der Rest gebuendelt - sonst wird der
|
let segmente: [Segment]
|
||||||
// Ring unlesbar.
|
if detail {
|
||||||
let kopf = sortiert.prefix(6)
|
// Detailansicht: alle Kategorien einzeln, nichts wird gebuendelt.
|
||||||
let restWert = sortiert.dropFirst(6).reduce(0) { $0 + $1.articleUnits }
|
segmente = sortiert.enumerated().map { i, c in
|
||||||
var segmente = kopf.enumerated().map { i, c in
|
Segment(label: c.name, value: c.articleUnits, color: Self.farbe(i))
|
||||||
Segment(label: c.name, value: c.articleUnits,
|
}
|
||||||
color: Self.palette[i % Self.palette.count])
|
} else {
|
||||||
}
|
// Die groessten sechs einzeln, der Rest gebuendelt - sonst wird der
|
||||||
if restWert > 0 {
|
// Ring unlesbar.
|
||||||
segmente.append(Segment(label: "Übrige", value: restWert, color: .gray))
|
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")
|
return ProportionChart(segments: segmente, einheit: "Artikeleinheiten")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user