2026-03-20 02:06:19 -05:00
|
|
|
import SwiftUI
|
|
|
|
|
import LabWiseKit
|
|
|
|
|
|
2026-03-20 02:40:51 -05:00
|
|
|
private let isoDateFormatter: ISO8601DateFormatter = {
|
|
|
|
|
let f = ISO8601DateFormatter()
|
|
|
|
|
f.formatOptions = [.withFullDate]
|
|
|
|
|
return f
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
private let isoTimestampFormatter: ISO8601DateFormatter = {
|
|
|
|
|
let f = ISO8601DateFormatter()
|
|
|
|
|
f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
|
|
|
return f
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
private let displayDateFormatter: DateFormatter = {
|
|
|
|
|
let f = DateFormatter()
|
|
|
|
|
f.dateStyle = .medium
|
|
|
|
|
f.timeStyle = .none
|
|
|
|
|
return f
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
private let displayTimestampFormatter: DateFormatter = {
|
|
|
|
|
let f = DateFormatter()
|
|
|
|
|
f.dateStyle = .medium
|
|
|
|
|
f.timeStyle = .short
|
|
|
|
|
return f
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
private func formatDate(_ iso: String?) -> String {
|
|
|
|
|
guard let iso else { return "" }
|
|
|
|
|
if let date = isoDateFormatter.date(from: iso) {
|
|
|
|
|
return displayDateFormatter.string(from: date)
|
|
|
|
|
}
|
|
|
|
|
// Fallback: try full timestamp (e.g. "2025-03-01T00:00:00.000Z")
|
|
|
|
|
if let date = isoTimestampFormatter.date(from: iso) {
|
|
|
|
|
return displayDateFormatter.string(from: date)
|
|
|
|
|
}
|
|
|
|
|
return iso
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func formatTimestamp(_ iso: String?) -> String {
|
|
|
|
|
guard let iso else { return "" }
|
|
|
|
|
if let date = isoTimestampFormatter.date(from: iso) {
|
|
|
|
|
return displayTimestampFormatter.string(from: date)
|
|
|
|
|
}
|
|
|
|
|
return iso
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 02:06:19 -05:00
|
|
|
struct ChemicalDetailView: View {
|
2026-03-20 02:30:15 -05:00
|
|
|
@State private var chemical: Chemical
|
|
|
|
|
@State private var showEdit = false
|
|
|
|
|
|
|
|
|
|
init(chemical: Chemical) {
|
|
|
|
|
self._chemical = State(initialValue: chemical)
|
|
|
|
|
}
|
2026-03-20 02:06:19 -05:00
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
Form {
|
|
|
|
|
Section("Identity") {
|
|
|
|
|
LabeledContent("Name", value: chemical.chemicalName)
|
|
|
|
|
LabeledContent("CAS Number", value: chemical.casNumber)
|
|
|
|
|
if let formula = chemical.chemicalFormula {
|
|
|
|
|
LabeledContent("Formula", value: formula)
|
|
|
|
|
}
|
|
|
|
|
if let mw = chemical.molecularWeight {
|
|
|
|
|
LabeledContent("Molecular Weight", value: mw)
|
|
|
|
|
}
|
|
|
|
|
LabeledContent("Physical State", value: chemical.physicalState.capitalized)
|
|
|
|
|
if let conc = chemical.concentration {
|
|
|
|
|
LabeledContent("Concentration", value: conc)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Section("Storage") {
|
|
|
|
|
LabeledContent("Location", value: chemical.storageLocation)
|
|
|
|
|
LabeledContent("Device", value: chemical.storageDevice)
|
|
|
|
|
LabeledContent("Building", value: chemical.bldgCode)
|
|
|
|
|
LabeledContent("Lab", value: chemical.lab)
|
|
|
|
|
LabeledContent("Containers", value: chemical.numberOfContainers)
|
|
|
|
|
LabeledContent("Amount / Container", value: "\(chemical.amountPerContainer) \(chemical.unitOfMeasure)")
|
|
|
|
|
if let pct = chemical.percentageFull {
|
|
|
|
|
LabeledContent("% Full") {
|
|
|
|
|
HStack(spacing: 8) {
|
|
|
|
|
PercentageBar(value: pct / 100)
|
|
|
|
|
.frame(width: 80, height: 6)
|
|
|
|
|
Text("\(Int(pct))%")
|
|
|
|
|
.font(.callout)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Section("Vendor") {
|
|
|
|
|
LabeledContent("PI", value: chemical.piFirstName)
|
|
|
|
|
if let vendor = chemical.vendor {
|
|
|
|
|
LabeledContent("Vendor", value: vendor)
|
|
|
|
|
}
|
|
|
|
|
if let catalog = chemical.catalogNumber {
|
|
|
|
|
LabeledContent("Catalog #", value: catalog)
|
|
|
|
|
}
|
|
|
|
|
if let lot = chemical.lotNumber {
|
|
|
|
|
LabeledContent("Lot #", value: lot)
|
|
|
|
|
}
|
2026-03-20 02:40:51 -05:00
|
|
|
if let exp = chemical.expirationDate, !exp.isEmpty {
|
|
|
|
|
LabeledContent("Expiration", value: formatDate(exp))
|
2026-03-20 02:06:19 -05:00
|
|
|
}
|
|
|
|
|
if let barcode = chemical.barcode {
|
|
|
|
|
LabeledContent("Barcode", value: barcode)
|
|
|
|
|
}
|
2026-03-20 02:30:15 -05:00
|
|
|
if let contact = chemical.contact {
|
|
|
|
|
LabeledContent("Contact", value: contact)
|
|
|
|
|
}
|
2026-03-20 02:06:19 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let comments = chemical.comments, !comments.isEmpty {
|
|
|
|
|
Section("Comments") {
|
|
|
|
|
Text(comments)
|
|
|
|
|
.font(.body)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Section("Record") {
|
|
|
|
|
if let created = chemical.createdAt {
|
2026-03-20 02:40:51 -05:00
|
|
|
LabeledContent("Created", value: formatTimestamp(created))
|
2026-03-20 02:06:19 -05:00
|
|
|
}
|
|
|
|
|
if let updated = chemical.updatedAt {
|
2026-03-20 02:40:51 -05:00
|
|
|
LabeledContent("Updated", value: formatTimestamp(updated))
|
2026-03-20 02:06:19 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.navigationTitle(chemical.chemicalName)
|
|
|
|
|
.navigationBarTitleDisplayMode(.inline)
|
2026-03-20 02:30:15 -05:00
|
|
|
.toolbar {
|
|
|
|
|
ToolbarItem(placement: .topBarTrailing) {
|
|
|
|
|
Button {
|
|
|
|
|
showEdit = true
|
|
|
|
|
} label: {
|
|
|
|
|
Image(systemName: "pencil")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.sheet(isPresented: $showEdit) {
|
|
|
|
|
AddChemicalView(editing: chemical) { saved in
|
|
|
|
|
showEdit = false
|
|
|
|
|
if saved {
|
|
|
|
|
Task { await reloadChemical() }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reload from server so the detail view reflects the saved changes.
|
|
|
|
|
private func reloadChemical() async {
|
|
|
|
|
let updated = try? await ChemicalsClient().list()
|
|
|
|
|
if let match = updated?.first(where: { $0.id == chemical.id }) {
|
|
|
|
|
chemical = match
|
|
|
|
|
}
|
2026-03-20 02:06:19 -05:00
|
|
|
}
|
|
|
|
|
}
|