editing all fields works and app is actually functional with profile edits

This commit is contained in:
2026-04-10 22:20:12 -05:00
parent b8880af200
commit b79d591395
8 changed files with 69 additions and 52 deletions

View File

@@ -7,7 +7,7 @@
<key>LabWise.xcscheme_^#shared#^_</key> <key>LabWise.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>0</integer> <integer>1</integer>
</dict> </dict>
</dict> </dict>
</dict> </dict>

View File

@@ -120,9 +120,9 @@ final class AddChemicalViewModel {
func loadProfile() async { func loadProfile() async {
guard !isEditing else { return } guard !isEditing else { return }
if let profile = try? await profileClient.get() { if let profile = try? await profileClient.get() {
piFirstName = profile.piFirstName piFirstName = profile.piFirstName ?? ""
bldgCode = profile.bldgCode bldgCode = profile.bldgCode ?? ""
lab = profile.lab lab = profile.lab ?? ""
contact = profile.contact ?? "" contact = profile.contact ?? ""
} }
} }

View File

@@ -61,11 +61,16 @@ struct InventoryView: View {
ProgressView("Loading...") ProgressView("Loading...")
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
} else if viewModel.chemicals.isEmpty { } else if viewModel.chemicals.isEmpty {
ScrollView {
ContentUnavailableView( ContentUnavailableView(
"No Chemicals", "No Chemicals",
systemImage: "flask", systemImage: "flask",
description: Text("Add your first chemical using the + button.") description: Text("Add your first chemical using the + button.")
) )
}
.refreshable {
await viewModel.loadChemicals()
}
} else { } else {
List { List {
ForEach(viewModel.chemicals) { chemical in ForEach(viewModel.chemicals) { chemical in

View File

@@ -24,12 +24,18 @@ final class ProfileViewModel {
func load() async { func load() async {
isLoading = true isLoading = true
defer { isLoading = false } 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 profile = p
populateFields(from: 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 } defer { isSaving = false }
do { do {
let trimmedName = displayName.trimmingCharacters(in: .whitespaces) 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) let updatedUser = try await authClient.updateName(trimmedName)
await MainActor.run { await MainActor.run {
AppState.shared.currentUser = updatedUser AppState.shared.currentUser = updatedUser
} }
} }
let body = UserProfileUpsertBody( let body = UserProfileUpsertBody(
piFirstName: piFirstName, piFirstName: piFirstName.trimmingCharacters(in: .whitespaces).isEmpty ? nil : piFirstName.trimmingCharacters(in: .whitespaces),
bldgCode: bldgCode, bldgCode: bldgCode.trimmingCharacters(in: .whitespaces).isEmpty ? nil : bldgCode.trimmingCharacters(in: .whitespaces),
lab: lab, lab: lab.trimmingCharacters(in: .whitespaces).isEmpty ? nil : lab.trimmingCharacters(in: .whitespaces),
contact: contact.isEmpty ? nil : contact contact: contact.trimmingCharacters(in: .whitespaces).isEmpty ? nil : contact.trimmingCharacters(in: .whitespaces)
) )
let updated = try await profileClient.upsert(body) let updated = try await profileClient.upsert(body)
profile = updated profile = updated
@@ -80,9 +86,9 @@ final class ProfileViewModel {
} }
private func populateFields(from p: UserProfile) { private func populateFields(from p: UserProfile) {
piFirstName = p.piFirstName piFirstName = p.piFirstName ?? ""
bldgCode = p.bldgCode bldgCode = p.bldgCode ?? ""
lab = p.lab lab = p.lab ?? ""
contact = p.contact ?? "" 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 { Section {
Button(role: .destructive) { Button(role: .destructive) {
viewModel.showDeleteConfirm = true viewModel.showDeleteConfirm = true
@@ -139,17 +157,10 @@ struct ProfileView: View {
} footer: { } footer: {
Text("Permanently deletes your account and all data including chemicals and protocols.") 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") .navigationTitle("Profile")
@@ -173,9 +184,9 @@ struct ProfileView: View {
viewModel.isEditing = false viewModel.isEditing = false
viewModel.displayName = appState.currentUser?.name ?? "" viewModel.displayName = appState.currentUser?.name ?? ""
if let p = viewModel.profile { if let p = viewModel.profile {
viewModel.piFirstName = p.piFirstName viewModel.piFirstName = p.piFirstName ?? ""
viewModel.bldgCode = p.bldgCode viewModel.bldgCode = p.bldgCode ?? ""
viewModel.lab = p.lab viewModel.lab = p.lab ?? ""
viewModel.contact = p.contact ?? "" viewModel.contact = p.contact ?? ""
} }
} }

View File

@@ -499,9 +499,9 @@ final class LabelScannerViewModel {
func loadProfile() async { func loadProfile() async {
if let profile = try? await profileClient.get() { if let profile = try? await profileClient.get() {
piFirstName = profile.piFirstName piFirstName = profile.piFirstName ?? ""
bldgCode = profile.bldgCode bldgCode = profile.bldgCode ?? ""
lab = profile.lab lab = profile.lab ?? ""
contact = profile.contact ?? "" contact = profile.contact ?? ""
} }
} }

View File

@@ -44,6 +44,7 @@ public final class APIClient: Sendable {
var req = URLRequest(url: url) var req = URLRequest(url: url)
req.httpMethod = method req.httpMethod = method
req.setValue(contentType, forHTTPHeaderField: "Content-Type") req.setValue(contentType, forHTTPHeaderField: "Content-Type")
req.setValue(baseURL.absoluteString, forHTTPHeaderField: "Origin")
if let body { if let body {
do { do {

View File

@@ -335,13 +335,13 @@ public final class AuthClient: Sendable {
// MARK: - Update Name // MARK: - Update Name
private struct UpdateNameBody: Encodable, Sendable { 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 { public func updateName(_ name: String) async throws -> AuthUser {
let body = UpdateNameBody(name: name) let body = UpdateNameBody(name: name.isEmpty ? nil : name)
_ = try await api.postRaw("/api/auth/update-user", body: body) _ = try await api.postRaw("/api/account/name", body: body)
return try await fetchCurrentUser() return try await fetchCurrentUser()
} }

View File

@@ -3,9 +3,9 @@ import Foundation
/// Profile data returned by the server uses snake_case keys. /// Profile data returned by the server uses snake_case keys.
public struct UserProfile: Codable, Sendable { public struct UserProfile: Codable, Sendable {
public var userId: String public var userId: String
public var piFirstName: String public var piFirstName: String?
public var bldgCode: String public var bldgCode: String?
public var lab: String public var lab: String?
public var contact: String? public var contact: String?
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
@@ -18,9 +18,9 @@ public struct UserProfile: Codable, Sendable {
public init( public init(
userId: String = "", userId: String = "",
piFirstName: String = "", piFirstName: String? = nil,
bldgCode: String = "", bldgCode: String? = nil,
lab: String = "", lab: String? = nil,
contact: String? = nil contact: String? = nil
) { ) {
self.userId = userId self.userId = userId
@@ -33,9 +33,9 @@ public struct UserProfile: Codable, Sendable {
/// Request body for POST /api/profile server expects snake_case keys. /// Request body for POST /api/profile server expects snake_case keys.
public struct UserProfileUpsertBody: Codable, Sendable { public struct UserProfileUpsertBody: Codable, Sendable {
public var piFirstName: String public var piFirstName: String?
public var bldgCode: String public var bldgCode: String?
public var lab: String public var lab: String?
public var contact: String? public var contact: String?
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
@@ -45,7 +45,7 @@ public struct UserProfileUpsertBody: Codable, Sendable {
case contact 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.piFirstName = piFirstName
self.bldgCode = bldgCode self.bldgCode = bldgCode
self.lab = lab self.lab = lab