This commit is contained in:
Trey t
2025-11-05 13:52:02 -06:00
parent 2be3a5a3a8
commit 5deac95818
10 changed files with 981 additions and 14 deletions

View File

@@ -0,0 +1,365 @@
import SwiftUI
import ComposeApp
struct EditResidenceView: View {
let residence: Residence
@Binding var isPresented: Bool
@StateObject private var viewModel = ResidenceViewModel()
@StateObject private var lookupsManager = LookupsManager.shared
@FocusState private var focusedField: Field?
// Form fields
@State private var name: String = ""
@State private var selectedPropertyType: ResidenceType?
@State private var streetAddress: String = ""
@State private var apartmentUnit: String = ""
@State private var city: String = ""
@State private var stateProvince: String = ""
@State private var postalCode: String = ""
@State private var country: String = "USA"
@State private var bedrooms: String = ""
@State private var bathrooms: String = ""
@State private var squareFootage: String = ""
@State private var lotSize: String = ""
@State private var yearBuilt: String = ""
@State private var description: String = ""
@State private var isPrimary: Bool = false
// Validation errors
@State private var nameError: String = ""
@State private var streetAddressError: String = ""
@State private var cityError: String = ""
@State private var stateProvinceError: String = ""
@State private var postalCodeError: String = ""
// Picker state
@State private var showPropertyTypePicker = false
typealias Field = AddResidenceView.Field
var body: some View {
NavigationView {
ZStack {
Color(.systemGroupedBackground)
.ignoresSafeArea()
ScrollView {
VStack(spacing: 24) {
// Required Information Section
VStack(alignment: .leading, spacing: 16) {
Text("Required Information")
.font(.headline)
.foregroundColor(.blue)
FormTextField(
label: "Property Name",
text: $name,
error: nameError,
placeholder: "My Home",
focusedField: $focusedField,
field: .name
)
// Property Type Picker
VStack(alignment: .leading, spacing: 8) {
Text("Property Type")
.font(.subheadline)
.foregroundColor(.secondary)
Button(action: {
showPropertyTypePicker = true
}) {
HStack {
Text(selectedPropertyType?.name ?? "Select Type")
.foregroundColor(selectedPropertyType == nil ? .gray : .primary)
Spacer()
Image(systemName: "chevron.down")
.foregroundColor(.gray)
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(8)
}
}
FormTextField(
label: "Street Address",
text: $streetAddress,
error: streetAddressError,
placeholder: "123 Main St",
focusedField: $focusedField,
field: .streetAddress
)
FormTextField(
label: "Apartment/Unit (Optional)",
text: $apartmentUnit,
error: "",
placeholder: "Apt 4B",
focusedField: $focusedField,
field: .apartmentUnit
)
FormTextField(
label: "City",
text: $city,
error: cityError,
placeholder: "San Francisco",
focusedField: $focusedField,
field: .city
)
FormTextField(
label: "State/Province",
text: $stateProvince,
error: stateProvinceError,
placeholder: "CA",
focusedField: $focusedField,
field: .stateProvince
)
FormTextField(
label: "Postal Code",
text: $postalCode,
error: postalCodeError,
placeholder: "94102",
focusedField: $focusedField,
field: .postalCode
)
FormTextField(
label: "Country",
text: $country,
error: "",
placeholder: "USA",
focusedField: $focusedField,
field: .country
)
}
// Optional Information Section
VStack(alignment: .leading, spacing: 16) {
Text("Optional Information")
.font(.headline)
.foregroundColor(.blue)
HStack(spacing: 12) {
FormTextField(
label: "Bedrooms",
text: $bedrooms,
error: "",
placeholder: "3",
focusedField: $focusedField,
field: .bedrooms,
keyboardType: .numberPad
)
FormTextField(
label: "Bathrooms",
text: $bathrooms,
error: "",
placeholder: "2.5",
focusedField: $focusedField,
field: .bathrooms,
keyboardType: .decimalPad
)
}
FormTextField(
label: "Square Footage",
text: $squareFootage,
error: "",
placeholder: "1800",
focusedField: $focusedField,
field: .squareFootage,
keyboardType: .numberPad
)
FormTextField(
label: "Lot Size (acres)",
text: $lotSize,
error: "",
placeholder: "0.25",
focusedField: $focusedField,
field: .lotSize,
keyboardType: .decimalPad
)
FormTextField(
label: "Year Built",
text: $yearBuilt,
error: "",
placeholder: "2010",
focusedField: $focusedField,
field: .yearBuilt,
keyboardType: .numberPad
)
VStack(alignment: .leading, spacing: 8) {
Text("Description")
.font(.subheadline)
.foregroundColor(.secondary)
TextEditor(text: $description)
.frame(height: 100)
.padding(8)
.background(Color(.systemBackground))
.cornerRadius(8)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.gray.opacity(0.3), lineWidth: 1)
)
}
Toggle("Primary Residence", isOn: $isPrimary)
.font(.subheadline)
}
// Submit Button
Button(action: submitForm) {
HStack {
if viewModel.isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
} else {
Text("Update Property")
.fontWeight(.semibold)
}
}
.frame(maxWidth: .infinity)
.padding()
.background(viewModel.isLoading ? Color.gray : Color.blue)
.foregroundColor(.white)
.cornerRadius(12)
}
.disabled(viewModel.isLoading)
}
.padding()
}
}
.navigationTitle("Edit Residence")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
isPresented = false
}
}
}
.sheet(isPresented: $showPropertyTypePicker) {
PropertyTypePickerView(
propertyTypes: lookupsManager.residenceTypes,
selectedType: $selectedPropertyType,
isPresented: $showPropertyTypePicker
)
}
.onAppear {
populateFields()
}
.alert("Error", isPresented: .constant(viewModel.errorMessage != nil)) {
Button("OK") {
viewModel.clearError()
}
} message: {
Text(viewModel.errorMessage ?? "")
}
}
}
private func populateFields() {
// Populate fields from the existing residence
name = residence.name
streetAddress = residence.streetAddress
apartmentUnit = residence.apartmentUnit ?? ""
city = residence.city
stateProvince = residence.stateProvince
postalCode = residence.postalCode
country = residence.country
bedrooms = residence.bedrooms != nil ? "\(residence.bedrooms!)" : ""
bathrooms = residence.bathrooms != nil ? "\(residence.bathrooms!)" : ""
squareFootage = residence.squareFootage != nil ? "\(residence.squareFootage!)" : ""
lotSize = residence.lotSize != nil ? "\(residence.lotSize!)" : ""
yearBuilt = residence.yearBuilt != nil ? "\(residence.yearBuilt!)" : ""
description = residence.description_ ?? ""
isPrimary = residence.isPrimary
// Set the selected property type
selectedPropertyType = lookupsManager.residenceTypes.first { $0.id == Int(residence.propertyType) ?? 0 }
}
private func validateForm() -> Bool {
var isValid = true
if name.isEmpty {
nameError = "Name is required"
isValid = false
} else {
nameError = ""
}
if streetAddress.isEmpty {
streetAddressError = "Street address is required"
isValid = false
} else {
streetAddressError = ""
}
if city.isEmpty {
cityError = "City is required"
isValid = false
} else {
cityError = ""
}
if stateProvince.isEmpty {
stateProvinceError = "State/Province is required"
isValid = false
} else {
stateProvinceError = ""
}
if postalCode.isEmpty {
postalCodeError = "Postal code is required"
isValid = false
} else {
postalCodeError = ""
}
return isValid
}
private func submitForm() {
guard validateForm() else { return }
guard let propertyType = selectedPropertyType else {
// Show error
return
}
let request = ResidenceCreateRequest(
name: name,
propertyType: Int32(propertyType.id),
streetAddress: streetAddress,
apartmentUnit: apartmentUnit.isEmpty ? nil : apartmentUnit,
city: city,
stateProvince: stateProvince,
postalCode: postalCode,
country: country,
bedrooms: Int32(bedrooms) as? KotlinInt,
bathrooms: Float(bathrooms) as? KotlinFloat,
squareFootage: Int32(squareFootage) as? KotlinInt,
lotSize: Float(lotSize) as? KotlinFloat,
yearBuilt: Int32(yearBuilt) as? KotlinInt,
description: description.isEmpty ? nil : description,
purchaseDate: nil,
purchasePrice: nil,
isPrimary: isPrimary
)
viewModel.updateResidence(id: residence.id, request: request) { success in
if success {
isPresented = false
}
}
}
}

View File

@@ -8,6 +8,7 @@ struct ResidenceDetailView: View {
@State private var isLoadingTasks = false
@State private var tasksError: String?
@State private var showAddTask = false
@State private var showEditResidence = false
var body: some View {
ZStack {
@@ -47,6 +48,16 @@ struct ResidenceDetailView: View {
.navigationTitle("Property Details")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
if viewModel.selectedResidence != nil {
Button(action: {
showEditResidence = true
}) {
Text("Edit")
}
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
showAddTask = true
@@ -58,12 +69,23 @@ struct ResidenceDetailView: View {
.sheet(isPresented: $showAddTask) {
AddTaskView(residenceId: residenceId, isPresented: $showAddTask)
}
.sheet(isPresented: $showEditResidence) {
if let residence = viewModel.selectedResidence {
EditResidenceView(residence: residence, isPresented: $showEditResidence)
}
}
.onChange(of: showAddTask) { isShowing in
if !isShowing {
// Refresh tasks when sheet is dismissed
loadResidenceWithTasks()
}
}
.onChange(of: showEditResidence) { isShowing in
if !isShowing {
// Refresh residence data when edit sheet is dismissed
loadResidenceData()
}
}
.onAppear {
loadResidenceData()
}
@@ -296,6 +318,69 @@ struct TaskCard: View {
.font(.caption)
.foregroundColor(.secondary)
}
ForEach(task.completions, id: \.id) { completion in
Spacer().frame(height: 12)
// Card equivalent
VStack(alignment: .leading, spacing: 8) {
// Top row: date + rating badge
HStack {
Text(completion.completionDate.components(separatedBy: "T").first ?? "")
.font(.body.weight(.bold))
.foregroundColor(.accentColor)
Spacer()
if let rating = completion.rating {
Text("\(rating)")
.font(.caption.weight(.bold))
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color(.tertiarySystemFill))
)
}
}
// Completed by
if let name = completion.completedByName {
Text("By: \(name)")
.font(.subheadline.weight(.medium))
.padding(.top, 8)
}
// Cost
if let cost = completion.actualCost {
Text("Cost: $\(cost)")
.font(.subheadline.weight(.medium))
.foregroundColor(.teal) // tertiary equivalent
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(16)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(Color.secondary.opacity(0.15)) // surfaceVariant equivalent
)
}
}
if task.showCompletedButton {
Button(action: {}) {
HStack {
Image(systemName: "checkmark.circle.fill") // SF Symbol
.resizable()
.frame(width: 20, height: 20)
Spacer().frame(width: 8)
Text("Complete Task")
.font(.title3.weight(.semibold)) // Material titleSmall + SemiBold
}
.frame(maxWidth: .infinity, alignment: .center)
}
.buttonStyle(.borderedProminent) // gives filled look
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
.padding(16)

View File

@@ -118,6 +118,33 @@ class ResidenceViewModel: ObservableObject {
}
}
func updateResidence(id: Int32, request: ResidenceCreateRequest, completion: @escaping (Bool) -> Void) {
guard let token = tokenStorage.getToken() else {
errorMessage = "Not authenticated"
completion(false)
return
}
isLoading = true
errorMessage = nil
residenceApi.updateResidence(token: token, id: id, request: request) { result, error in
if let successResult = result as? ApiResultSuccess<Residence> {
self.selectedResidence = successResult.data
self.isLoading = false
completion(true)
} else if let errorResult = result as? ApiResultError {
self.errorMessage = errorResult.message
self.isLoading = false
completion(false)
} else if let error = error {
self.errorMessage = error.localizedDescription
self.isLoading = false
completion(false)
}
}
}
func clearError() {
errorMessage = nil
}