feat: add iOS support and harden receipt scanning
This commit is contained in:
125
app/ios/ScanReceipts/Design/ZenithStatusBadge.swift
Normal file
125
app/ios/ScanReceipts/Design/ZenithStatusBadge.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user