Missing Field Handling
This commit is contained in:
@@ -431,7 +431,7 @@
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = LabWise/LabWise.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
DEVELOPMENT_TEAM = YK2DB9NT3S;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -469,7 +469,7 @@
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = LabWise/LabWise.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
DEVELOPMENT_TEAM = YK2DB9NT3S;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<key>LabWise.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>1</integer>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
|
||||
@@ -66,19 +66,21 @@ final class AddChemicalViewModel {
|
||||
}
|
||||
|
||||
/// Edit mode — all fields pre-populated from the existing chemical.
|
||||
/// Required fields may be nil on rows that were imported with missing data;
|
||||
/// the form will show them blank and require the user to fill them in to save.
|
||||
init(editing chemical: Chemical) {
|
||||
self.editingID = chemical.id
|
||||
chemicalName = chemical.chemicalName
|
||||
casNumber = chemical.casNumber
|
||||
physicalState = chemical.physicalState
|
||||
storageDevice = chemical.storageDevice
|
||||
piFirstName = chemical.piFirstName
|
||||
bldgCode = chemical.bldgCode
|
||||
lab = chemical.lab
|
||||
storageLocation = chemical.storageLocation
|
||||
numberOfContainers = chemical.numberOfContainers
|
||||
amountPerContainer = chemical.amountPerContainer
|
||||
unitOfMeasure = chemical.unitOfMeasure
|
||||
chemicalName = chemical.chemicalName ?? ""
|
||||
casNumber = chemical.casNumber ?? ""
|
||||
physicalState = chemical.physicalState ?? ""
|
||||
storageDevice = chemical.storageDevice ?? "Glass Bottle"
|
||||
piFirstName = chemical.piFirstName ?? ""
|
||||
bldgCode = chemical.bldgCode ?? ""
|
||||
lab = chemical.lab ?? ""
|
||||
storageLocation = chemical.storageLocation ?? ""
|
||||
numberOfContainers = chemical.numberOfContainers ?? ""
|
||||
amountPerContainer = chemical.amountPerContainer ?? ""
|
||||
unitOfMeasure = chemical.unitOfMeasure ?? ""
|
||||
chemicalFormula = chemical.chemicalFormula ?? ""
|
||||
molecularWeight = chemical.molecularWeight ?? ""
|
||||
concentration = chemical.concentration ?? ""
|
||||
|
||||
@@ -57,28 +57,44 @@ struct ChemicalDetailView: View {
|
||||
|
||||
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)
|
||||
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") {
|
||||
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)")
|
||||
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) {
|
||||
@@ -92,25 +108,15 @@ struct ChemicalDetailView: View {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
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))
|
||||
}
|
||||
if let barcode = chemical.barcode {
|
||||
LabeledContent("Barcode", value: barcode)
|
||||
}
|
||||
if let contact = chemical.contact {
|
||||
LabeledContent("Contact", value: contact)
|
||||
}
|
||||
row("Barcode", chemical.barcode)
|
||||
row("Contact", chemical.contact)
|
||||
}
|
||||
|
||||
if let comments = chemical.comments, !comments.isEmpty {
|
||||
@@ -129,7 +135,7 @@ struct ChemicalDetailView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(chemical.chemicalName)
|
||||
.navigationTitle(chemical.displayName)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
@@ -157,4 +163,46 @@ struct ChemicalDetailView: View {
|
||||
chemical = match
|
||||
}
|
||||
}
|
||||
|
||||
/// Combines "<amount> <unit>" — 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 `<Field>` 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,23 @@ struct ChemicalRowView: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Text(chemical.chemicalName)
|
||||
Text(chemical.displayName)
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
PhysicalStateBadge(state: chemical.physicalState)
|
||||
if let state = chemical.physicalState, !state.isEmpty {
|
||||
PhysicalStateBadge(state: state)
|
||||
}
|
||||
}
|
||||
HStack(spacing: 6) {
|
||||
if let cas = chemical.casNumber, !cas.isEmpty {
|
||||
Text("CAS: \(cas)")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
if chemical.isMissingKeyInfo {
|
||||
MissingInfoBadge()
|
||||
}
|
||||
}
|
||||
Text("CAS: \(chemical.casNumber)")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
if let pct = chemical.percentageFull {
|
||||
PercentageBar(value: pct / 100)
|
||||
.frame(height: 4)
|
||||
@@ -52,6 +61,25 @@ struct PhysicalStateBadge: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Missing info badge
|
||||
|
||||
/// Mirrors the web "Missing info" pill — amber, with a warning glyph.
|
||||
struct MissingInfoBadge: View {
|
||||
var body: some View {
|
||||
HStack(spacing: 3) {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.font(.caption2)
|
||||
Text("Missing info")
|
||||
.font(.caption2.weight(.semibold))
|
||||
}
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(Color.orange.opacity(0.15))
|
||||
.foregroundStyle(Color.orange)
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Percentage bar
|
||||
|
||||
struct PercentageBar: View {
|
||||
|
||||
@@ -215,13 +215,13 @@ private struct SafetyAlertsSection: View {
|
||||
VStack(spacing: 8) {
|
||||
ForEach(expired) { chem in
|
||||
AlertRow(
|
||||
message: "\(chem.chemicalName) expired \(-viewModel.daysUntil(chem.expirationDate ?? ""))d ago — verify disposal",
|
||||
message: "\(chem.displayName) expired \(-viewModel.daysUntil(chem.expirationDate ?? ""))d ago — verify disposal",
|
||||
severity: .critical
|
||||
)
|
||||
}
|
||||
ForEach(lowStockAlerts) { chem in
|
||||
AlertRow(
|
||||
message: "\(chem.chemicalName) is \(Int(chem.percentageFull ?? 0))% full — consider reordering",
|
||||
message: "\(chem.displayName) is \(Int(chem.percentageFull ?? 0))% full — consider reordering",
|
||||
severity: .warning
|
||||
)
|
||||
}
|
||||
@@ -275,8 +275,8 @@ private struct LowStockSection: View {
|
||||
VStack(spacing: 0) {
|
||||
ForEach(viewModel.lowStock.prefix(3)) { chem in
|
||||
ChemAlertRow(
|
||||
name: chem.chemicalName,
|
||||
subtitle: "\(chem.lab) · \(chem.storageLocation)",
|
||||
name: chem.displayName,
|
||||
subtitle: "\(chem.lab ?? "—") · \(chem.storageLocation ?? "—")",
|
||||
badge: "\(Int(chem.percentageFull ?? 0))% full",
|
||||
badgeColor: .orange
|
||||
)
|
||||
@@ -304,8 +304,8 @@ private struct ExpiringSection: View {
|
||||
ForEach(viewModel.expiringSoon.prefix(4)) { chem in
|
||||
let days = viewModel.daysUntil(chem.expirationDate ?? "")
|
||||
ChemAlertRow(
|
||||
name: chem.chemicalName,
|
||||
subtitle: "\(chem.lab) · \(chem.storageLocation)",
|
||||
name: chem.displayName,
|
||||
subtitle: "\(chem.lab ?? "—") · \(chem.storageLocation ?? "—")",
|
||||
badge: days <= 0 ? "Expired \(-days)d ago" : "In \(days)d",
|
||||
badgeColor: days <= 0 ? .red : .orange
|
||||
)
|
||||
@@ -336,7 +336,7 @@ private struct RecentActivitySection: View {
|
||||
.foregroundStyle(Color(.brandPrimary))
|
||||
.font(.title3)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Added \(chem.chemicalName) to inventory")
|
||||
Text("Added \(chem.displayName) to inventory")
|
||||
.font(.subheadline)
|
||||
.lineLimit(1)
|
||||
Text(viewModel.relativeTime(chem.createdAt))
|
||||
|
||||
@@ -7,6 +7,10 @@ final class InventoryViewModel {
|
||||
var isLoading = false
|
||||
var errorMessage: String?
|
||||
|
||||
var missingInfoCount: Int {
|
||||
chemicals.filter(\.isMissingKeyInfo).count
|
||||
}
|
||||
|
||||
private let client = ChemicalsClient()
|
||||
|
||||
func loadChemicals() async {
|
||||
@@ -73,6 +77,12 @@ struct InventoryView: View {
|
||||
}
|
||||
} else {
|
||||
List {
|
||||
if viewModel.missingInfoCount > 0 && !isSelectMode {
|
||||
Section {
|
||||
MissingInfoBanner(count: viewModel.missingInfoCount)
|
||||
}
|
||||
.listRowBackground(Color.orange.opacity(0.08))
|
||||
}
|
||||
ForEach(viewModel.chemicals) { chemical in
|
||||
if isSelectMode {
|
||||
Button {
|
||||
@@ -204,3 +214,27 @@ struct InventoryView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Missing-info banner
|
||||
|
||||
/// Shown above the inventory list when one or more rows are missing required
|
||||
/// fields (typically the result of a web spreadsheet import). Mirrors the
|
||||
/// amber banner on the web inventory page.
|
||||
private struct MissingInfoBanner: View {
|
||||
let count: Int
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: 10) {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.foregroundStyle(.orange)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Missing Key Information")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
Text("\(count) \(count == 1 ? "item is" : "items are") missing required fields. Tap an item flagged with “Missing info” to see exactly which fields and edit them.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,17 +12,17 @@ struct SpreadsheetView: View {
|
||||
}
|
||||
|
||||
private static let columnDefs: [ColumnDef] = [
|
||||
ColumnDef(label: "PI First Name", width: 130) { $0.piFirstName },
|
||||
ColumnDef(label: "Physical State", width: 120) { $0.physicalState },
|
||||
ColumnDef(label: "Chemical Name", width: 180) { $0.chemicalName },
|
||||
ColumnDef(label: "Bldg Code", width: 100) { $0.bldgCode },
|
||||
ColumnDef(label: "LAB", width: 80) { $0.lab },
|
||||
ColumnDef(label: "Storage Location", width: 145) { $0.storageLocation },
|
||||
ColumnDef(label: "Storage Device", width: 140) { $0.storageDevice },
|
||||
ColumnDef(label: "# of Containers", width: 120) { $0.numberOfContainers },
|
||||
ColumnDef(label: "Amount / Container", width: 150) { $0.amountPerContainer },
|
||||
ColumnDef(label: "Unit of Measure", width: 130) { $0.unitOfMeasure },
|
||||
ColumnDef(label: "CAS #", width: 115) { $0.casNumber },
|
||||
ColumnDef(label: "PI First Name", width: 130) { $0.piFirstName ?? "" },
|
||||
ColumnDef(label: "Physical State", width: 120) { $0.physicalState ?? "" },
|
||||
ColumnDef(label: "Chemical Name", width: 180) { $0.chemicalName ?? "" },
|
||||
ColumnDef(label: "Bldg Code", width: 100) { $0.bldgCode ?? "" },
|
||||
ColumnDef(label: "LAB", width: 80) { $0.lab ?? "" },
|
||||
ColumnDef(label: "Storage Location", width: 145) { $0.storageLocation ?? "" },
|
||||
ColumnDef(label: "Storage Device", width: 140) { $0.storageDevice ?? "" },
|
||||
ColumnDef(label: "# of Containers", width: 120) { $0.numberOfContainers ?? "" },
|
||||
ColumnDef(label: "Amount / Container", width: 150) { $0.amountPerContainer ?? "" },
|
||||
ColumnDef(label: "Unit of Measure", width: 130) { $0.unitOfMeasure ?? "" },
|
||||
ColumnDef(label: "CAS #", width: 115) { $0.casNumber ?? "" },
|
||||
ColumnDef(label: "Chemical Formula", width: 140) { $0.chemicalFormula ?? "" },
|
||||
ColumnDef(label: "Molecular Weight", width: 130) { $0.molecularWeight ?? "" },
|
||||
ColumnDef(label: "Vendor", width: 130) { $0.vendor ?? "" },
|
||||
|
||||
@@ -2,17 +2,20 @@ import Foundation
|
||||
|
||||
public struct Chemical: Identifiable, Sendable, Encodable {
|
||||
public let id: String
|
||||
public var piFirstName: String
|
||||
public var physicalState: String
|
||||
public var chemicalName: String
|
||||
public var bldgCode: String
|
||||
public var lab: String
|
||||
public var storageLocation: String
|
||||
public var storageDevice: String
|
||||
public var numberOfContainers: String
|
||||
public var amountPerContainer: String
|
||||
public var unitOfMeasure: String
|
||||
public var casNumber: String
|
||||
// These eleven fields are nullable on the server (rows imported from spreadsheets
|
||||
// can leave any of them blank). Treat them as optional everywhere on read; the
|
||||
// Add/Edit form still validates them as required on write.
|
||||
public var piFirstName: String?
|
||||
public var physicalState: String?
|
||||
public var chemicalName: String?
|
||||
public var bldgCode: String?
|
||||
public var lab: String?
|
||||
public var storageLocation: String?
|
||||
public var storageDevice: String?
|
||||
public var numberOfContainers: String?
|
||||
public var amountPerContainer: String?
|
||||
public var unitOfMeasure: String?
|
||||
public var casNumber: String?
|
||||
public var chemicalFormula: String?
|
||||
public var molecularWeight: String?
|
||||
public var vendor: String?
|
||||
@@ -31,17 +34,17 @@ public struct Chemical: Identifiable, Sendable, Encodable {
|
||||
|
||||
public init(
|
||||
id: String = "",
|
||||
piFirstName: String = "",
|
||||
physicalState: String = "",
|
||||
chemicalName: String = "",
|
||||
bldgCode: String = "",
|
||||
lab: String = "",
|
||||
storageLocation: String = "",
|
||||
storageDevice: String = "",
|
||||
numberOfContainers: String = "",
|
||||
amountPerContainer: String = "",
|
||||
unitOfMeasure: String = "",
|
||||
casNumber: String = "",
|
||||
piFirstName: String? = nil,
|
||||
physicalState: String? = nil,
|
||||
chemicalName: String? = nil,
|
||||
bldgCode: String? = nil,
|
||||
lab: String? = nil,
|
||||
storageLocation: String? = nil,
|
||||
storageDevice: String? = nil,
|
||||
numberOfContainers: String? = nil,
|
||||
amountPerContainer: String? = nil,
|
||||
unitOfMeasure: String? = nil,
|
||||
casNumber: String? = nil,
|
||||
chemicalFormula: String? = nil,
|
||||
molecularWeight: String? = nil,
|
||||
vendor: String? = nil,
|
||||
@@ -92,17 +95,17 @@ extension Chemical: Decodable {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try c.decode(String.self, forKey: .id)
|
||||
piFirstName = try c.decode(String.self, forKey: .piFirstName)
|
||||
physicalState = try c.decode(String.self, forKey: .physicalState)
|
||||
chemicalName = try c.decode(String.self, forKey: .chemicalName)
|
||||
bldgCode = try c.decode(String.self, forKey: .bldgCode)
|
||||
lab = try c.decode(String.self, forKey: .lab)
|
||||
storageLocation = try c.decode(String.self, forKey: .storageLocation)
|
||||
storageDevice = try c.decode(String.self, forKey: .storageDevice)
|
||||
numberOfContainers = try c.decode(String.self, forKey: .numberOfContainers)
|
||||
amountPerContainer = try c.decode(String.self, forKey: .amountPerContainer)
|
||||
unitOfMeasure = try c.decode(String.self, forKey: .unitOfMeasure)
|
||||
casNumber = try c.decode(String.self, forKey: .casNumber)
|
||||
piFirstName = try c.decodeIfPresent(String.self, forKey: .piFirstName)
|
||||
physicalState = try c.decodeIfPresent(String.self, forKey: .physicalState)
|
||||
chemicalName = try c.decodeIfPresent(String.self, forKey: .chemicalName)
|
||||
bldgCode = try c.decodeIfPresent(String.self, forKey: .bldgCode)
|
||||
lab = try c.decodeIfPresent(String.self, forKey: .lab)
|
||||
storageLocation = try c.decodeIfPresent(String.self, forKey: .storageLocation)
|
||||
storageDevice = try c.decodeIfPresent(String.self, forKey: .storageDevice)
|
||||
numberOfContainers = try c.decodeIfPresent(String.self, forKey: .numberOfContainers)
|
||||
amountPerContainer = try c.decodeIfPresent(String.self, forKey: .amountPerContainer)
|
||||
unitOfMeasure = try c.decodeIfPresent(String.self, forKey: .unitOfMeasure)
|
||||
casNumber = try c.decodeIfPresent(String.self, forKey: .casNumber)
|
||||
chemicalFormula = try c.decodeIfPresent(String.self, forKey: .chemicalFormula)
|
||||
molecularWeight = try c.decodeIfPresent(String.self, forKey: .molecularWeight)
|
||||
vendor = try c.decodeIfPresent(String.self, forKey: .vendor)
|
||||
@@ -145,6 +148,38 @@ extension Chemical: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
/// The eleven fields the web app treats as "key information." A row missing any
|
||||
/// of these is flagged with a Missing-info badge in the inventory list.
|
||||
public extension Chemical {
|
||||
/// Names of the required fields whose value is missing or whitespace-only.
|
||||
/// Order matches the visual order in the iOS Add/Edit form so the
|
||||
/// "Missing: …" banner reads top-to-bottom in the same order the user fills.
|
||||
var missingFields: [String] {
|
||||
func empty(_ s: String?) -> Bool { (s ?? "").trimmingCharacters(in: .whitespaces).isEmpty }
|
||||
var out: [String] = []
|
||||
if empty(chemicalName) { out.append("Chemical Name") }
|
||||
if empty(casNumber) { out.append("CAS #") }
|
||||
if empty(physicalState) { out.append("Physical State") }
|
||||
if empty(storageDevice) { out.append("Storage Device") }
|
||||
if empty(piFirstName) { out.append("PI First Name") }
|
||||
if empty(bldgCode) { out.append("Building Code") }
|
||||
if empty(lab) { out.append("Lab") }
|
||||
if empty(storageLocation) { out.append("Storage Location") }
|
||||
if empty(numberOfContainers) { out.append("# of Containers") }
|
||||
if empty(amountPerContainer) { out.append("Amount / Container") }
|
||||
if empty(unitOfMeasure) { out.append("Unit of Measure") }
|
||||
return out
|
||||
}
|
||||
|
||||
var isMissingKeyInfo: Bool { !missingFields.isEmpty }
|
||||
|
||||
/// Best-effort display name; falls back when `chemicalName` is missing.
|
||||
var displayName: String {
|
||||
let trimmed = (chemicalName ?? "").trimmingCharacters(in: .whitespaces)
|
||||
return trimmed.isEmpty ? "Unnamed chemical" : trimmed
|
||||
}
|
||||
}
|
||||
|
||||
public struct ChemicalCreateBody: Codable, Sendable {
|
||||
public var piFirstName: String
|
||||
public var physicalState: String
|
||||
|
||||
Reference in New Issue
Block a user