Files
LabWiseiOS/LabWise/ChemicalsListView.swift

148 lines
4.5 KiB
Swift

import SwiftUI
import LabWiseKit
@Observable
final class ChemicalsViewModel {
var chemicals: [Chemical] = []
var isLoading = false
var errorMessage: String?
private let client = ChemicalsClient()
func loadChemicals() async {
isLoading = true
errorMessage = nil
defer { isLoading = false }
do {
chemicals = try await client.list()
} catch {
errorMessage = "Failed to load chemicals."
}
}
func delete(chemical: Chemical) async {
do {
try await client.delete(id: chemical.id)
chemicals.removeAll { $0.id == chemical.id }
} catch {
errorMessage = "Failed to delete chemical."
}
}
}
struct ChemicalsListView: View {
@State private var viewModel = ChemicalsViewModel()
var body: some View {
NavigationStack {
Group {
if viewModel.isLoading && viewModel.chemicals.isEmpty {
ProgressView("Loading chemicals...")
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else if viewModel.chemicals.isEmpty {
ContentUnavailableView(
"No Chemicals",
systemImage: "flask",
description: Text("Your chemical inventory is empty.")
)
} else {
List {
ForEach(viewModel.chemicals) { chemical in
NavigationLink(destination: ChemicalDetailView(chemical: chemical)) {
ChemicalRowView(chemical: chemical)
}
}
.onDelete { indexSet in
for index in indexSet {
let chemical = viewModel.chemicals[index]
Task { await viewModel.delete(chemical: chemical) }
}
}
}
.refreshable {
await viewModel.loadChemicals()
}
}
}
.navigationTitle("Chemicals")
.alert("Error", isPresented: .constant(viewModel.errorMessage != nil)) {
Button("OK") { viewModel.errorMessage = nil }
} message: {
Text(viewModel.errorMessage ?? "")
}
}
.task {
await viewModel.loadChemicals()
}
}
}
struct ChemicalRowView: View {
let chemical: Chemical
var body: some View {
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(chemical.chemicalName)
.font(.headline)
Spacer()
PhysicalStateBadge(state: chemical.physicalState)
}
Text("CAS: \(chemical.casNumber)")
.font(.caption)
.foregroundStyle(.secondary)
if let pct = chemical.percentageFull {
PercentageBar(value: pct / 100)
.frame(height: 4)
.padding(.top, 2)
}
}
.padding(.vertical, 2)
}
}
struct PhysicalStateBadge: View {
let state: String
var color: Color {
switch state.lowercased() {
case "liquid": return Color(.brandPrimary)
case "solid": return Color(red: 0.42, green: 0.30, blue: 0.18)
case "gas": return Color(red: 0.22, green: 0.56, blue: 0.52)
default: return Color(.brandMutedForeground)
}
}
var body: some View {
Text(state.capitalized)
.font(.caption2.weight(.semibold))
.padding(.horizontal, 8)
.padding(.vertical, 3)
.background(color.opacity(0.15))
.foregroundStyle(color)
.clipShape(Capsule())
}
}
struct PercentageBar: View {
let value: Double // 0.0 - 1.0
var body: some View {
GeometryReader { geo in
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 2)
.fill(Color.secondary.opacity(0.2))
RoundedRectangle(cornerRadius: 2)
.fill(barColor)
.frame(width: geo.size.width * max(0, min(1, value)))
}
}
}
var barColor: Color {
if value > 0.6 { return .green }
if value > 0.25 { return .yellow }
return .red
}
}