feat: add iOS support and harden receipt scanning

This commit is contained in:
Timo
2026-08-20 23:48:26 +02:00
parent f5e06c32b0
commit cf1b799e1b
107 changed files with 11755 additions and 122 deletions

View File

@@ -0,0 +1,125 @@
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)
}
}