126 lines
4.0 KiB
Swift
126 lines
4.0 KiB
Swift
import SwiftUI
|
|
|
|
/// Ported from `src/components/dashboard/StatusBadge.tsx` — the web
|
|
/// dashboard's "3-tier status badges" (see `PROJECT.md`'s feature #12).
|
|
/// Keep this resolver logic in sync with `resolveReceiptStatusTier` /
|
|
/// `receiptNeedsAttention` (`src/lib/ai/recalculate.ts`) on the web side.
|
|
enum ReceiptStatusTier {
|
|
case scanned
|
|
case pendingReview
|
|
case confirmed
|
|
|
|
/// Mirrors `resolveReceiptStatusTier`: a manual confirmation always wins,
|
|
/// then math/confidence problems route to "needs review", and everything
|
|
/// else that at least parsed successfully is "scanned".
|
|
static func resolve(_ receipt: Receipt) -> ReceiptStatusTier {
|
|
let validation = receipt.validation
|
|
if validation.userConfirmed == true { return .confirmed }
|
|
if validation.needsUserReview || !validation.isMathValid { return .pendingReview }
|
|
if receipt.status == .needsReview { return .pendingReview }
|
|
return .scanned
|
|
}
|
|
|
|
var label: String {
|
|
switch self {
|
|
case .scanned: return "Erfasst"
|
|
case .pendingReview: return "Prüfung erforderlich"
|
|
case .confirmed: return "Bestätigt"
|
|
}
|
|
}
|
|
|
|
var systemImage: String {
|
|
switch self {
|
|
case .scanned: return "checkmark.circle.fill"
|
|
case .pendingReview: return "exclamationmark.triangle.fill"
|
|
case .confirmed: return "person.fill.checkmark"
|
|
}
|
|
}
|
|
|
|
var background: Color {
|
|
switch self {
|
|
case .scanned: return .zenithScannedBg
|
|
case .pendingReview: return .zenithPendingBg
|
|
case .confirmed: return .zenithConfirmedBg
|
|
}
|
|
}
|
|
|
|
var foreground: Color {
|
|
switch self {
|
|
case .scanned: return .zenithScannedText
|
|
case .pendingReview: return .zenithPendingText
|
|
case .confirmed: return .zenithConfirmedText
|
|
}
|
|
}
|
|
|
|
var border: Color {
|
|
switch self {
|
|
case .scanned: return .zenithScannedBorder
|
|
case .pendingReview: return .zenithPendingBorder
|
|
case .confirmed: return .zenithConfirmedBorder
|
|
}
|
|
}
|
|
|
|
var dot: Color {
|
|
switch self {
|
|
case .scanned: return .zenithScannedDot
|
|
case .pendingReview: return .zenithPendingDot
|
|
case .confirmed: return .zenithConfirmedDot
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The badge itself — small rectangular (0-radius), bordered, dot + icon +
|
|
/// label, exactly the shape `StatusBadge.tsx` renders on the web.
|
|
struct ZenithStatusBadge: View {
|
|
let tier: ReceiptStatusTier
|
|
var showIcon = true
|
|
var showDot = true
|
|
|
|
init(_ receipt: Receipt, showIcon: Bool = true, showDot: Bool = true) {
|
|
self.tier = .resolve(receipt)
|
|
self.showIcon = showIcon
|
|
self.showDot = showDot
|
|
}
|
|
|
|
init(tier: ReceiptStatusTier, showIcon: Bool = true, showDot: Bool = true) {
|
|
self.tier = tier
|
|
self.showIcon = showIcon
|
|
self.showDot = showDot
|
|
}
|
|
|
|
var body: some View {
|
|
HStack(spacing: 6) {
|
|
if showDot {
|
|
Circle().fill(tier.dot).frame(width: 6, height: 6)
|
|
}
|
|
if showIcon {
|
|
Image(systemName: tier.systemImage)
|
|
.font(.system(size: 10, weight: .semibold))
|
|
}
|
|
Text(tier.label)
|
|
.lineLimit(1)
|
|
}
|
|
.font(.zenithLabelSm)
|
|
.foregroundStyle(tier.foreground)
|
|
.padding(.horizontal, 8)
|
|
.padding(.vertical, 3)
|
|
.background(tier.background)
|
|
.overlay(Rectangle().strokeBorder(tier.border, lineWidth: 1))
|
|
}
|
|
}
|
|
|
|
/// A generic (non-status) rectangular chip — `DESIGN (1).md` → Components →
|
|
/// Chips/Tags: "`label-md` typography, small rectangular boxes with #F5F7F9
|
|
/// fills and no borders." Use for categories, payment methods, etc.
|
|
struct ZenithChip: View {
|
|
let text: String
|
|
|
|
var body: some View {
|
|
Text(text)
|
|
.zenithLabelMdStyle(color: .zenithMuted)
|
|
.padding(.horizontal, 8)
|
|
.padding(.vertical, 3)
|
|
.background(Color.zenithLow)
|
|
}
|
|
}
|