diff --git a/LabWise.xcodeproj/xcuserdata/adipu.xcuserdatad/xcschemes/xcschememanagement.plist b/LabWise.xcodeproj/xcuserdata/adipu.xcuserdatad/xcschemes/xcschememanagement.plist index 54c14a0..a9c03c8 100644 --- a/LabWise.xcodeproj/xcuserdata/adipu.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/LabWise.xcodeproj/xcuserdata/adipu.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ LabWise.xcscheme_^#shared#^_ orderHint - 0 + 1 diff --git a/LabWise/AddChemicalView.swift b/LabWise/AddChemicalView.swift index 5d7f58d..ef54e89 100644 --- a/LabWise/AddChemicalView.swift +++ b/LabWise/AddChemicalView.swift @@ -120,9 +120,9 @@ final class AddChemicalViewModel { func loadProfile() async { guard !isEditing else { return } if let profile = try? await profileClient.get() { - piFirstName = profile.piFirstName - bldgCode = profile.bldgCode - lab = profile.lab + piFirstName = profile.piFirstName ?? "" + bldgCode = profile.bldgCode ?? "" + lab = profile.lab ?? "" contact = profile.contact ?? "" } } diff --git a/LabWise/InventoryView.swift b/LabWise/InventoryView.swift index 883bbc8..51ff921 100644 --- a/LabWise/InventoryView.swift +++ b/LabWise/InventoryView.swift @@ -61,11 +61,16 @@ struct InventoryView: View { ProgressView("Loading...") .frame(maxWidth: .infinity, maxHeight: .infinity) } else if viewModel.chemicals.isEmpty { - ContentUnavailableView( - "No Chemicals", - systemImage: "flask", - description: Text("Add your first chemical using the + button.") - ) + ScrollView { + ContentUnavailableView( + "No Chemicals", + systemImage: "flask", + description: Text("Add your first chemical using the + button.") + ) + } + .refreshable { + await viewModel.loadChemicals() + } } else { List { ForEach(viewModel.chemicals) { chemical in diff --git a/LabWise/ProfileView.swift b/LabWise/ProfileView.swift index f5bd985..9e480c6 100644 --- a/LabWise/ProfileView.swift +++ b/LabWise/ProfileView.swift @@ -24,12 +24,18 @@ final class ProfileViewModel { func load() async { isLoading = true defer { isLoading = false } - do { - let p = try await profileClient.get() + + async let profileFetch = profileClient.get() + async let userFetch = authClient.fetchCurrentUser() + + if let p = try? await profileFetch { profile = p populateFields(from: p) - } catch { - // Profile may not exist yet — that's OK + } + if let user = try? await userFetch { + await MainActor.run { + AppState.shared.currentUser = user + } } } @@ -38,17 +44,17 @@ final class ProfileViewModel { defer { isSaving = false } do { let trimmedName = displayName.trimmingCharacters(in: .whitespaces) - if !trimmedName.isEmpty && trimmedName != AppState.shared.currentUser?.name { + if trimmedName != (AppState.shared.currentUser?.name ?? "") { let updatedUser = try await authClient.updateName(trimmedName) await MainActor.run { AppState.shared.currentUser = updatedUser } } let body = UserProfileUpsertBody( - piFirstName: piFirstName, - bldgCode: bldgCode, - lab: lab, - contact: contact.isEmpty ? nil : contact + piFirstName: piFirstName.trimmingCharacters(in: .whitespaces).isEmpty ? nil : piFirstName.trimmingCharacters(in: .whitespaces), + bldgCode: bldgCode.trimmingCharacters(in: .whitespaces).isEmpty ? nil : bldgCode.trimmingCharacters(in: .whitespaces), + lab: lab.trimmingCharacters(in: .whitespaces).isEmpty ? nil : lab.trimmingCharacters(in: .whitespaces), + contact: contact.trimmingCharacters(in: .whitespaces).isEmpty ? nil : contact.trimmingCharacters(in: .whitespaces) ) let updated = try await profileClient.upsert(body) profile = updated @@ -80,9 +86,9 @@ final class ProfileViewModel { } private func populateFields(from p: UserProfile) { - piFirstName = p.piFirstName - bldgCode = p.bldgCode - lab = p.lab + piFirstName = p.piFirstName ?? "" + bldgCode = p.bldgCode ?? "" + lab = p.lab ?? "" contact = p.contact ?? "" } } @@ -125,6 +131,18 @@ struct ProfileView: View { } } + Section { + Button(role: .destructive) { + Task { await viewModel.signOut() } + } label: { + HStack { + Spacer() + Text("Sign Out") + Spacer() + } + } + } + Section { Button(role: .destructive) { viewModel.showDeleteConfirm = true @@ -139,17 +157,10 @@ struct ProfileView: View { } footer: { Text("Permanently deletes your account and all data including chemicals and protocols.") } - - Section { - Button(role: .destructive) { - Task { await viewModel.signOut() } - } label: { - HStack { - Spacer() - Text("Sign Out") - Spacer() - } - } + } + .refreshable { + if !viewModel.isEditing { + await viewModel.load() } } .navigationTitle("Profile") @@ -173,9 +184,9 @@ struct ProfileView: View { viewModel.isEditing = false viewModel.displayName = appState.currentUser?.name ?? "" if let p = viewModel.profile { - viewModel.piFirstName = p.piFirstName - viewModel.bldgCode = p.bldgCode - viewModel.lab = p.lab + viewModel.piFirstName = p.piFirstName ?? "" + viewModel.bldgCode = p.bldgCode ?? "" + viewModel.lab = p.lab ?? "" viewModel.contact = p.contact ?? "" } } diff --git a/LabWise/ScanView.swift b/LabWise/ScanView.swift index 73b82c3..d49be6e 100644 --- a/LabWise/ScanView.swift +++ b/LabWise/ScanView.swift @@ -499,9 +499,9 @@ final class LabelScannerViewModel { func loadProfile() async { if let profile = try? await profileClient.get() { - piFirstName = profile.piFirstName - bldgCode = profile.bldgCode - lab = profile.lab + piFirstName = profile.piFirstName ?? "" + bldgCode = profile.bldgCode ?? "" + lab = profile.lab ?? "" contact = profile.contact ?? "" } } diff --git a/LabWiseKit/Sources/LabWiseKit/APIClient.swift b/LabWiseKit/Sources/LabWiseKit/APIClient.swift index 3f860b9..5170c30 100644 --- a/LabWiseKit/Sources/LabWiseKit/APIClient.swift +++ b/LabWiseKit/Sources/LabWiseKit/APIClient.swift @@ -44,6 +44,7 @@ public final class APIClient: Sendable { var req = URLRequest(url: url) req.httpMethod = method req.setValue(contentType, forHTTPHeaderField: "Content-Type") + req.setValue(baseURL.absoluteString, forHTTPHeaderField: "Origin") if let body { do { diff --git a/LabWiseKit/Sources/LabWiseKit/Auth/AuthClient.swift b/LabWiseKit/Sources/LabWiseKit/Auth/AuthClient.swift index 3d4b318..b4ccb7f 100644 --- a/LabWiseKit/Sources/LabWiseKit/Auth/AuthClient.swift +++ b/LabWiseKit/Sources/LabWiseKit/Auth/AuthClient.swift @@ -335,13 +335,13 @@ public final class AuthClient: Sendable { // MARK: - Update Name private struct UpdateNameBody: Encodable, Sendable { - let name: String + let name: String? } - /// Update the authenticated user's display name. + /// Update the authenticated user's display name. Pass an empty string to clear it. public func updateName(_ name: String) async throws -> AuthUser { - let body = UpdateNameBody(name: name) - _ = try await api.postRaw("/api/auth/update-user", body: body) + let body = UpdateNameBody(name: name.isEmpty ? nil : name) + _ = try await api.postRaw("/api/account/name", body: body) return try await fetchCurrentUser() } diff --git a/LabWiseKit/Sources/LabWiseKit/Models/UserProfile.swift b/LabWiseKit/Sources/LabWiseKit/Models/UserProfile.swift index 9f52411..f5d50d5 100644 --- a/LabWiseKit/Sources/LabWiseKit/Models/UserProfile.swift +++ b/LabWiseKit/Sources/LabWiseKit/Models/UserProfile.swift @@ -3,9 +3,9 @@ import Foundation /// Profile data returned by the server uses snake_case keys. public struct UserProfile: Codable, Sendable { public var userId: String - public var piFirstName: String - public var bldgCode: String - public var lab: String + public var piFirstName: String? + public var bldgCode: String? + public var lab: String? public var contact: String? enum CodingKeys: String, CodingKey { @@ -18,9 +18,9 @@ public struct UserProfile: Codable, Sendable { public init( userId: String = "", - piFirstName: String = "", - bldgCode: String = "", - lab: String = "", + piFirstName: String? = nil, + bldgCode: String? = nil, + lab: String? = nil, contact: String? = nil ) { self.userId = userId @@ -33,9 +33,9 @@ public struct UserProfile: Codable, Sendable { /// Request body for POST /api/profile — server expects snake_case keys. public struct UserProfileUpsertBody: Codable, Sendable { - public var piFirstName: String - public var bldgCode: String - public var lab: String + public var piFirstName: String? + public var bldgCode: String? + public var lab: String? public var contact: String? enum CodingKeys: String, CodingKey { @@ -45,7 +45,7 @@ public struct UserProfileUpsertBody: Codable, Sendable { case contact } - public init(piFirstName: String, bldgCode: String, lab: String, contact: String? = nil) { + public init(piFirstName: String? = nil, bldgCode: String? = nil, lab: String? = nil, contact: String? = nil) { self.piFirstName = piFirstName self.bldgCode = bldgCode self.lab = lab