import SwiftUI import LabWiseKit 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 } struct ChemicalDetailView: View { @State private var chemical: Chemical @State private var showEdit = false init(chemical: Chemical) { self._chemical = State(initialValue: chemical) } var body: some View { Form { let missing = chemical.missingFields if !missing.isEmpty { Section { VStack(alignment: .leading, spacing: 6) { HStack(alignment: .top, spacing: 10) { Image(systemName: "exclamationmark.triangle.fill") .foregroundStyle(.orange) VStack(alignment: .leading, spacing: 2) { Text("Missing \(missing.count) required \(missing.count == 1 ? "field" : "fields")") .font(.subheadline.weight(.semibold)) Text(missing.joined(separator: ", ")) .font(.caption) .foregroundStyle(.secondary) } } Text("Tap Edit to fill them in.") .font(.caption2) .foregroundStyle(.secondary) } } } Section("Identity") { row("Chemical Name", chemical.chemicalName, required: true) row("CAS #", chemical.casNumber, required: true) row("Formula", chemical.chemicalFormula) row("Molecular Weight", chemical.molecularWeight) row("Physical State", chemical.physicalState?.capitalized, required: true) row("Concentration", chemical.concentration) } Section("Storage") { row("Storage Location", chemical.storageLocation, required: true) row("Storage Device", chemical.storageDevice, required: true) row("Building Code", chemical.bldgCode, required: true) row("Lab", chemical.lab, required: true) row("# of Containers", chemical.numberOfContainers, required: true) amountRow 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") { row("PI First Name", chemical.piFirstName, required: true) row("Vendor", chemical.vendor) row("Catalog #", chemical.catalogNumber) row("Lot #", chemical.lotNumber) if let exp = chemical.expirationDate, !exp.isEmpty { LabeledContent("Expiration", value: formatDate(exp)) } row("Barcode", chemical.barcode) row("Contact", chemical.contact) } if let comments = chemical.comments, !comments.isEmpty { Section("Comments") { Text(comments) .font(.body) } } Section("Record") { if let created = chemical.createdAt { LabeledContent("Created", value: formatTimestamp(created)) } if let updated = chemical.updatedAt { LabeledContent("Updated", value: formatTimestamp(updated)) } } } .navigationTitle(chemical.displayName) .navigationBarTitleDisplayMode(.inline) .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 } } /// Combines " " — both required, so we show a Missing /// placeholder if either side is empty. Yellow-tinted when incomplete. @ViewBuilder private var amountRow: some View { let amt = (chemical.amountPerContainer ?? "").trimmingCharacters(in: .whitespaces) let unit = (chemical.unitOfMeasure ?? "").trimmingCharacters(in: .whitespaces) if amt.isEmpty || unit.isEmpty { let parts: [String] = [ amt.isEmpty ? "Amount" : nil, unit.isEmpty ? "Unit" : nil, ].compactMap { $0 } LabeledContent("Amount / Container") { Text("Missing \(parts.joined(separator: " & "))") .italic() .foregroundStyle(.orange) } .listRowBackground(Color.orange.opacity(0.12)) } else { LabeledContent("Amount / Container", value: "\(amt) \(unit)") } } /// Render a labelled row. /// - Optional fields are hidden when empty (matches the web `` helper). /// - Required fields always render: present value normally, missing value /// with a yellow row background and an italic "Missing" placeholder so /// the user can see exactly which field needs attention and where it lives. @ViewBuilder private func row(_ label: String, _ value: String?, required: Bool = false) -> some View { let trimmed = (value ?? "").trimmingCharacters(in: .whitespaces) if !trimmed.isEmpty { LabeledContent(label, value: trimmed) } else if required { LabeledContent(label) { Text("Missing") .italic() .foregroundStyle(.orange) } .listRowBackground(Color.orange.opacity(0.12)) } } }