204 lines
7.1 KiB
Swift
204 lines
7.1 KiB
Swift
import SwiftUI
|
|
import LabWiseKit
|
|
|
|
@Observable
|
|
final class ProfileViewModel {
|
|
var profile: UserProfile?
|
|
var isLoading = false
|
|
var isSaving = false
|
|
var isEditing = false
|
|
var errorMessage: String?
|
|
var showDeleteConfirm = false
|
|
var isDeleting = false
|
|
|
|
// Editable fields
|
|
var displayName = ""
|
|
var piFirstName = ""
|
|
var bldgCode = ""
|
|
var lab = ""
|
|
var contact = ""
|
|
|
|
private let profileClient = ProfileClient()
|
|
private let authClient = AuthClient()
|
|
|
|
func load() async {
|
|
isLoading = true
|
|
defer { isLoading = false }
|
|
do {
|
|
let p = try await profileClient.get()
|
|
profile = p
|
|
populateFields(from: p)
|
|
} catch {
|
|
// Profile may not exist yet — that's OK
|
|
}
|
|
}
|
|
|
|
func save() async {
|
|
isSaving = true
|
|
defer { isSaving = false }
|
|
do {
|
|
let trimmedName = displayName.trimmingCharacters(in: .whitespaces)
|
|
if !trimmedName.isEmpty && 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
|
|
)
|
|
let updated = try await profileClient.upsert(body)
|
|
profile = updated
|
|
populateFields(from: updated)
|
|
isEditing = false
|
|
} catch {
|
|
errorMessage = "Failed to save profile."
|
|
}
|
|
}
|
|
|
|
func deleteAccount() async {
|
|
isDeleting = true
|
|
defer { isDeleting = false }
|
|
do {
|
|
try await authClient.deleteAccount()
|
|
await MainActor.run {
|
|
AppState.shared.signedOut()
|
|
}
|
|
} catch {
|
|
errorMessage = "Failed to delete account. Please try again."
|
|
}
|
|
}
|
|
|
|
func signOut() async {
|
|
try? await authClient.signOut()
|
|
await MainActor.run {
|
|
AppState.shared.signedOut()
|
|
}
|
|
}
|
|
|
|
private func populateFields(from p: UserProfile) {
|
|
piFirstName = p.piFirstName
|
|
bldgCode = p.bldgCode
|
|
lab = p.lab
|
|
contact = p.contact ?? ""
|
|
}
|
|
}
|
|
|
|
struct ProfileView: View {
|
|
@Environment(AppState.self) private var appState
|
|
@State private var viewModel = ProfileViewModel()
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Form {
|
|
if let user = appState.currentUser {
|
|
Section("Account") {
|
|
if viewModel.isEditing {
|
|
TextField("Name", text: $viewModel.displayName)
|
|
} else {
|
|
LabeledContent("Name", value: user.name ?? "—")
|
|
}
|
|
LabeledContent("Email", value: user.email)
|
|
}
|
|
}
|
|
|
|
Section("Lab Info") {
|
|
if viewModel.isEditing {
|
|
TextField("PI First Name", text: $viewModel.piFirstName)
|
|
TextField("Building Code", text: $viewModel.bldgCode)
|
|
TextField("Lab", text: $viewModel.lab)
|
|
TextField("Contact", text: $viewModel.contact)
|
|
} else {
|
|
LabeledContent("PI First Name", value: viewModel.piFirstName.isEmpty ? "—" : viewModel.piFirstName)
|
|
LabeledContent("Building Code", value: viewModel.bldgCode.isEmpty ? "—" : viewModel.bldgCode)
|
|
LabeledContent("Lab", value: viewModel.lab.isEmpty ? "—" : viewModel.lab)
|
|
LabeledContent("Contact", value: viewModel.contact.isEmpty ? "—" : viewModel.contact)
|
|
}
|
|
}
|
|
|
|
Section {
|
|
Link(destination: URL(string: "https://labwise.wahwa.com/privacy")!) {
|
|
Text("Privacy Policy")
|
|
}
|
|
}
|
|
|
|
Section {
|
|
Button(role: .destructive) {
|
|
viewModel.showDeleteConfirm = true
|
|
} label: {
|
|
HStack {
|
|
Spacer()
|
|
Text("Delete Account")
|
|
Spacer()
|
|
}
|
|
}
|
|
.disabled(viewModel.isDeleting)
|
|
} 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()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Profile")
|
|
.toolbar {
|
|
ToolbarItem(placement: .topBarTrailing) {
|
|
if viewModel.isEditing {
|
|
Button("Save") {
|
|
Task { await viewModel.save() }
|
|
}
|
|
.disabled(viewModel.isSaving)
|
|
} else {
|
|
Button("Edit") {
|
|
viewModel.displayName = appState.currentUser?.name ?? ""
|
|
viewModel.isEditing = true
|
|
}
|
|
}
|
|
}
|
|
if viewModel.isEditing {
|
|
ToolbarItem(placement: .topBarLeading) {
|
|
Button("Cancel") {
|
|
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.contact = p.contact ?? ""
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.alert("Error", isPresented: .constant(viewModel.errorMessage != nil)) {
|
|
Button("OK") { viewModel.errorMessage = nil }
|
|
} message: {
|
|
Text(viewModel.errorMessage ?? "")
|
|
}
|
|
.alert("Delete Account?", isPresented: $viewModel.showDeleteConfirm) {
|
|
Button("Cancel", role: .cancel) { }
|
|
Button("Delete", role: .destructive) {
|
|
Task { await viewModel.deleteAccount() }
|
|
}
|
|
} message: {
|
|
Text("Are you sure? This will permanently delete your account and all your data. This action cannot be undone.")
|
|
}
|
|
}
|
|
.task {
|
|
await viewModel.load()
|
|
}
|
|
}
|
|
}
|