Complete iOS document form implementation and improve login error handling

This commit completes the DRY refactoring by implementing the missing document form functionality and enhancing user experience with better error messages.

## iOS Document Forms
- Implemented complete createDocument() method in DocumentViewModel:
  - Support for all warranty-specific fields (itemName, modelNumber, serialNumber, provider, etc.)
  - Multiple image uploads with JPEG compression
  - Proper UIImage to KotlinByteArray conversion
  - Async completion handlers
- Implemented updateDocument() method with full field support
- Completed DocumentFormView submitForm() implementation with proper API calls
- Fixed type conversion issues (Bool/KotlinBoolean, Int32/KotlinInt)
- Added proper error handling and user feedback

## iOS Login Error Handling
- Enhanced error messages to be user-friendly and concise
- Added specific messages for common HTTP error codes (400, 401, 403, 404, 500+)
- Implemented cleanErrorMessage() helper to remove technical jargon
- Added network-specific error handling (connection, timeout)
- Fixed MainActor isolation warnings with proper Task wrapping

## Code Quality
- Removed ~4,086 lines of duplicate code through form consolidation
- Added 429 lines of new shared form components
- Fixed Swift compiler performance issues
- Ensured both iOS and Android builds succeed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-11-12 11:35:41 -06:00
parent ec7c01e92d
commit b888315e0c
22 changed files with 2994 additions and 4086 deletions

View File

@@ -0,0 +1,295 @@
import SwiftUI
import ComposeApp
struct ResidenceFormView: View {
let existingResidence: 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 = ""
enum Field {
case name, streetAddress, apartmentUnit, city, stateProvince, postalCode, country
case bedrooms, bathrooms, squareFootage, lotSize, yearBuilt, description
}
private var isEditMode: Bool {
existingResidence != nil
}
var body: some View {
NavigationView {
Form {
Section(header: Text("Property Details")) {
TextField("Property Name", text: $name)
.focused($focusedField, equals: .name)
if !nameError.isEmpty {
Text(nameError)
.font(.caption)
.foregroundColor(.red)
}
Picker("Property Type", selection: $selectedPropertyType) {
Text("Select Type").tag(nil as ResidenceType?)
ForEach(lookupsManager.residenceTypes, id: \.id) { type in
Text(type.name).tag(type as ResidenceType?)
}
}
}
Section(header: Text("Address")) {
TextField("Street Address", text: $streetAddress)
.focused($focusedField, equals: .streetAddress)
if !streetAddressError.isEmpty {
Text(streetAddressError)
.font(.caption)
.foregroundColor(.red)
}
TextField("Apartment/Unit (optional)", text: $apartmentUnit)
.focused($focusedField, equals: .apartmentUnit)
TextField("City", text: $city)
.focused($focusedField, equals: .city)
if !cityError.isEmpty {
Text(cityError)
.font(.caption)
.foregroundColor(.red)
}
TextField("State/Province", text: $stateProvince)
.focused($focusedField, equals: .stateProvince)
if !stateProvinceError.isEmpty {
Text(stateProvinceError)
.font(.caption)
.foregroundColor(.red)
}
TextField("Postal Code", text: $postalCode)
.focused($focusedField, equals: .postalCode)
if !postalCodeError.isEmpty {
Text(postalCodeError)
.font(.caption)
.foregroundColor(.red)
}
TextField("Country", text: $country)
.focused($focusedField, equals: .country)
}
Section(header: Text("Property Features")) {
HStack {
Text("Bedrooms")
Spacer()
TextField("0", text: $bedrooms)
.keyboardType(.numberPad)
.multilineTextAlignment(.trailing)
.frame(width: 60)
.focused($focusedField, equals: .bedrooms)
}
HStack {
Text("Bathrooms")
Spacer()
TextField("0.0", text: $bathrooms)
.keyboardType(.decimalPad)
.multilineTextAlignment(.trailing)
.frame(width: 60)
.focused($focusedField, equals: .bathrooms)
}
TextField("Square Footage", text: $squareFootage)
.keyboardType(.numberPad)
.focused($focusedField, equals: .squareFootage)
TextField("Lot Size (acres)", text: $lotSize)
.keyboardType(.decimalPad)
.focused($focusedField, equals: .lotSize)
TextField("Year Built", text: $yearBuilt)
.keyboardType(.numberPad)
.focused($focusedField, equals: .yearBuilt)
}
Section(header: Text("Additional Details")) {
TextField("Description (optional)", text: $description, axis: .vertical)
.lineLimit(3...6)
Toggle("Primary Residence", isOn: $isPrimary)
}
if let errorMessage = viewModel.errorMessage {
Section {
Text(errorMessage)
.foregroundColor(.red)
.font(.caption)
}
}
}
.navigationTitle(isEditMode ? "Edit Residence" : "Add Residence")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
isPresented = false
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
submitForm()
}
.disabled(viewModel.isLoading)
}
}
.onAppear {
initializeForm()
}
}
}
private func initializeForm() {
if let residence = existingResidence {
// Edit mode - populate fields from 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 }
} else {
// Add mode - set default property type
if selectedPropertyType == nil && !lookupsManager.residenceTypes.isEmpty {
selectedPropertyType = lookupsManager.residenceTypes.first
}
}
}
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 {
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
)
if let residence = existingResidence {
// Edit mode
viewModel.updateResidence(id: residence.id, request: request) { success in
if success {
isPresented = false
}
}
} else {
// Add mode
viewModel.createResidence(request: request) { success in
if success {
isPresented = false
}
}
}
}
}
#Preview("Add Mode") {
ResidenceFormView(existingResidence: nil, isPresented: .constant(true))
}