import SwiftUI import ComposeApp // MARK: - Field Focus Enum enum ContractorFormField: Hashable { case name, company, phone, email, website case streetAddress, city, stateProvince, postalCode, notes } // MARK: - Contractor Form Sheet struct ContractorFormSheet: View { @Environment(\.dismiss) private var dismiss @StateObject private var viewModel = ContractorViewModel() @StateObject private var residenceViewModel = ResidenceViewModel() @ObservedObject private var dataManager = DataManagerObservable.shared let contractor: Contractor? let onSave: () -> Void // Form fields @State private var name = "" @State private var company = "" @State private var phone = "" @State private var email = "" @State private var website = "" @State private var streetAddress = "" @State private var city = "" @State private var stateProvince = "" @State private var postalCode = "" @State private var notes = "" @State private var isFavorite = false // Residence selection (optional) @State private var selectedResidenceId: Int32? @State private var selectedResidenceName: String? @State private var showingResidencePicker = false // Specialty selection (multiple) @State private var selectedSpecialtyIds: Set = [] @State private var showingSpecialtyPicker = false @FocusState private var focusedField: ContractorFormField? private var specialties: [ContractorSpecialty] { return dataManager.contractorSpecialties } private var canSave: Bool { !name.isEmpty } var body: some View { NavigationStack { Form { // Basic Information Section { HStack { Image(systemName: "person") .foregroundColor(Color.appPrimary) .frame(width: 24) TextField(L10n.Contractors.nameLabel, text: $name) .focused($focusedField, equals: .name) .textContentType(.name) .submitLabel(.next) .onSubmit { focusedField = .company } .accessibilityIdentifier(AccessibilityIdentifiers.Contractor.nameField) } HStack { Image(systemName: "building.2") .foregroundColor(Color.appPrimary) .frame(width: 24) TextField(L10n.Contractors.companyLabel, text: $company) .focused($focusedField, equals: .company) .textContentType(.organizationName) .submitLabel(.next) .onSubmit { focusedField = .phone } .accessibilityIdentifier(AccessibilityIdentifiers.Contractor.companyField) } } header: { Text(L10n.Contractors.basicInfoSection) .accessibilityAddTraits(.isHeader) } footer: { Text(L10n.Contractors.basicInfoFooter) .font(.caption) .foregroundColor(Color.appError) } .sectionBackground() // Residence (Optional) Section { Button(action: { showingResidencePicker = true }) { HStack { Image("outline") .renderingMode(.template) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 20, height: 20) .foregroundColor(Color.appPrimary) Text(selectedResidenceName ?? L10n.Contractors.personalNoResidence) .foregroundColor(selectedResidenceName == nil ? Color.appTextSecondary.opacity(0.7) : Color.appTextPrimary) Spacer() Image(systemName: "chevron.down") .font(.caption) .foregroundColor(Color.appTextSecondary.opacity(0.7)) } } } header: { Text(L10n.Contractors.residenceSection) } footer: { Text(selectedResidenceId == nil ? L10n.Contractors.residenceFooterPersonal : String(format: L10n.Contractors.residenceFooterShared, selectedResidenceName ?? "")) .font(.caption) } .sectionBackground() // Contact Information Section { HStack { Image(systemName: "phone.fill") .foregroundColor(Color.appPrimary) .frame(width: 24) TextField(L10n.Contractors.phoneLabel, text: $phone) .keyboardType(.phonePad) .textContentType(.telephoneNumber) .focused($focusedField, equals: .phone) .keyboardDismissToolbar() .accessibilityIdentifier(AccessibilityIdentifiers.Contractor.phoneField) } HStack { Image(systemName: "envelope.fill") .foregroundColor(Color.appAccent) .frame(width: 24) TextField(L10n.Contractors.emailLabel, text: $email) .keyboardType(.emailAddress) .textContentType(.emailAddress) .textInputAutocapitalization(.never) .autocorrectionDisabled() .focused($focusedField, equals: .email) .submitLabel(.next) .onSubmit { focusedField = .website } .accessibilityIdentifier(AccessibilityIdentifiers.Contractor.emailField) } HStack { Image(systemName: "globe") .foregroundColor(Color.appAccent) .frame(width: 24) TextField(L10n.Contractors.websiteLabel, text: $website) .keyboardType(.URL) .textInputAutocapitalization(.never) .autocorrectionDisabled() .focused($focusedField, equals: .website) } } header: { Text(L10n.Contractors.contactInfoSection) .accessibilityAddTraits(.isHeader) } .sectionBackground() // Specialties (Multi-select) Section { Button(action: { showingSpecialtyPicker = true }) { HStack { Image(systemName: "wrench.and.screwdriver") .foregroundColor(Color.appPrimary) .frame(width: 24) if selectedSpecialtyIds.isEmpty { Text(L10n.Contractors.selectSpecialtiesPlaceholder) .foregroundColor(Color.appTextSecondary.opacity(0.5)) } else { let selectedNames = specialties .filter { selectedSpecialtyIds.contains($0.id) } .map { $0.name } Text(selectedNames.joined(separator: ", ")) .foregroundColor(Color.appTextPrimary) .lineLimit(2) } Spacer() Image(systemName: "chevron.down") .font(.caption) .foregroundColor(Color.appTextSecondary.opacity(0.7)) } } .accessibilityIdentifier(AccessibilityIdentifiers.Contractor.specialtyPicker) } header: { Text(L10n.Contractors.specialtiesSection) } .sectionBackground() // Address Section { HStack { Image(systemName: "location.fill") .foregroundColor(Color.appError) .frame(width: 24) TextField(L10n.Contractors.streetAddressLabel, text: $streetAddress) .focused($focusedField, equals: .streetAddress) } HStack { Image(systemName: "building.2.crop.circle") .foregroundColor(Color.appPrimary) .frame(width: 24) TextField(L10n.Contractors.cityLabel, text: $city) .focused($focusedField, equals: .city) } HStack(spacing: AppSpacing.sm) { HStack { Image(systemName: "map") .foregroundColor(Color.appAccent) .frame(width: 24) TextField(L10n.Contractors.stateLabel, text: $stateProvince) .focused($focusedField, equals: .stateProvince) } Divider() .frame(height: 24) TextField(L10n.Contractors.zipLabel, text: $postalCode) .keyboardType(.numberPad) .focused($focusedField, equals: .postalCode) .frame(maxWidth: 100) .keyboardDismissToolbar() } } header: { Text(L10n.Contractors.addressSection) .accessibilityAddTraits(.isHeader) } .sectionBackground() // Notes Section { HStack(alignment: .top) { Image(systemName: "note.text") .foregroundColor(Color.appAccent) .frame(width: 24) .padding(.top, 8) TextEditor(text: $notes) .frame(height: 100) .focused($focusedField, equals: .notes) .keyboardDismissToolbar() .accessibilityIdentifier(AccessibilityIdentifiers.Contractor.notesField) } } header: { Text(L10n.Contractors.notesSection) } footer: { Text(L10n.Contractors.notesFooter) .font(.caption) } .sectionBackground() // Favorite Section { Toggle(isOn: $isFavorite) { Label(L10n.Contractors.favoriteLabel, systemImage: "star.fill") .foregroundColor(isFavorite ? Color.appAccent : Color.appTextPrimary) } .tint(Color.appAccent) } .sectionBackground() // Error Message if let error = viewModel.errorMessage { Section { HStack { Image(systemName: "exclamationmark.triangle.fill") .foregroundColor(Color.appError) Text(error) .font(.callout) .foregroundColor(Color.appError) } } .sectionBackground() } } .listStyle(.plain) .scrollContentBackground(.hidden) .background(Color.appBackgroundPrimary) .navigationTitle(contractor == nil ? L10n.Contractors.addTitle : L10n.Contractors.editTitle) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button(L10n.Common.cancel) { dismiss() } .accessibilityIdentifier(AccessibilityIdentifiers.Contractor.formCancelButton) } ToolbarItem(placement: .confirmationAction) { Button(action: saveContractor) { if viewModel.isCreating || viewModel.isUpdating { ProgressView() } else { Text(contractor == nil ? L10n.Common.add : L10n.Common.save) .bold() } } .disabled(!canSave || viewModel.isCreating || viewModel.isUpdating) .accessibilityIdentifier(AccessibilityIdentifiers.Contractor.saveButton) } } .sheet(isPresented: $showingResidencePicker) { residencePickerSheet } .sheet(isPresented: $showingSpecialtyPicker) { specialtyPickerSheet } .onAppear { // Track screen view for new contractors if contractor == nil { AnalyticsManager.shared.trackScreen(.newContractor) } residenceViewModel.loadMyResidences() loadContractorData() } .onChange(of: residenceViewModel.selectedResidence?.id) { _, _ in if let residence = residenceViewModel.selectedResidence, residence.id == selectedResidenceId { selectedResidenceName = residence.name } } .handleErrors( error: viewModel.errorMessage, onRetry: { saveContractor() } ) } } // MARK: - Residence Picker Sheet private var residencePickerSheet: some View { NavigationStack { List { // Personal (no residence) option Button(action: { selectedResidenceId = nil selectedResidenceName = nil showingResidencePicker = false }) { HStack { Text(L10n.Contractors.personalNoResidence) .foregroundColor(Color.appTextPrimary) Spacer() if selectedResidenceId == nil { Image(systemName: "checkmark") .foregroundColor(Color.appPrimary) } } } .sectionBackground() // Residences if let residences = residenceViewModel.myResidences?.residences { ForEach(residences, id: \.id) { residence in Button(action: { selectedResidenceId = residence.id selectedResidenceName = residence.name showingResidencePicker = false }) { HStack { Text(residence.name) .foregroundColor(Color.appTextPrimary) Spacer() if selectedResidenceId == residence.id { Image(systemName: "checkmark") .foregroundColor(Color.appPrimary) } } } .sectionBackground() } } else if residenceViewModel.isLoading { HStack { Spacer() ProgressView() Spacer() } .sectionBackground() } } .listStyle(.plain) .scrollContentBackground(.hidden) .background(Color.appBackgroundPrimary) .navigationTitle(L10n.Contractors.selectResidence) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .confirmationAction) { Button(L10n.Common.done) { showingResidencePicker = false } } } } .presentationDetents([.medium, .large]) } // MARK: - Specialty Picker Sheet (Multi-select) private var specialtyPickerSheet: some View { NavigationStack { List { ForEach(specialties, id: \.id) { specialty in Button(action: { if selectedSpecialtyIds.contains(specialty.id) { selectedSpecialtyIds.remove(specialty.id) } else { selectedSpecialtyIds.insert(specialty.id) } }) { HStack { Text(specialty.name) .foregroundColor(Color.appTextPrimary) Spacer() if selectedSpecialtyIds.contains(specialty.id) { Image(systemName: "checkmark") .foregroundColor(Color.appPrimary) } } } .sectionBackground() } } .listStyle(.plain) .scrollContentBackground(.hidden) .background(Color.appBackgroundPrimary) .navigationTitle(L10n.Contractors.selectSpecialties) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button(L10n.Contractors.clearAction) { selectedSpecialtyIds.removeAll() } } ToolbarItem(placement: .confirmationAction) { Button(L10n.Common.done) { showingSpecialtyPicker = false } } } } .presentationDetents([.large]) } // MARK: - Data Loading private func loadContractorData() { guard let contractor = contractor else { return } name = contractor.name company = contractor.company ?? "" phone = contractor.phone ?? "" email = contractor.email ?? "" website = contractor.website ?? "" streetAddress = contractor.streetAddress ?? "" city = contractor.city ?? "" stateProvince = contractor.stateProvince ?? "" postalCode = contractor.postalCode ?? "" notes = contractor.notes ?? "" isFavorite = contractor.isFavorite // Set residence if contractor has one if let residenceId = contractor.residenceId { selectedResidenceId = residenceId.int32Value if let selectedResidenceId { residenceViewModel.getResidence(id: selectedResidenceId) } } // Set specialties selectedSpecialtyIds = Set(contractor.specialties.map { $0.id }) } // MARK: - Save Action private func saveContractor() { let specialtyIdsArray = Array(selectedSpecialtyIds).map { KotlinInt(int: $0) } if let contractor = contractor { // Update existing contractor let request = ContractorUpdateRequest( name: name.isEmpty ? nil : name, residenceId: selectedResidenceId.map { KotlinInt(int: $0) }, company: company.isEmpty ? nil : company, phone: phone.isEmpty ? nil : phone, email: email.isEmpty ? nil : email, website: website.isEmpty ? nil : website, streetAddress: streetAddress.isEmpty ? nil : streetAddress, city: city.isEmpty ? nil : city, stateProvince: stateProvince.isEmpty ? nil : stateProvince, postalCode: postalCode.isEmpty ? nil : postalCode, rating: nil, isFavorite: isFavorite.asKotlin, notes: notes.isEmpty ? nil : notes, specialtyIds: specialtyIdsArray.isEmpty ? nil : specialtyIdsArray ) viewModel.updateContractor(id: contractor.id, request: request) { success in if success { onSave() dismiss() } } } else { // Create new contractor let request = ContractorCreateRequest( name: name, residenceId: selectedResidenceId.map { KotlinInt(int: $0) }, company: company.isEmpty ? nil : company, phone: phone.isEmpty ? nil : phone, email: email.isEmpty ? nil : email, website: website.isEmpty ? nil : website, streetAddress: streetAddress.isEmpty ? nil : streetAddress, city: city.isEmpty ? nil : city, stateProvince: stateProvince.isEmpty ? nil : stateProvince, postalCode: postalCode.isEmpty ? nil : postalCode, rating: nil, isFavorite: isFavorite, notes: notes.isEmpty ? nil : notes, specialtyIds: specialtyIdsArray.isEmpty ? nil : specialtyIdsArray ) viewModel.createContractor(request: request) { success in if success { // Track contractor creation AnalyticsManager.shared.track(.contractorCreated) onSave() dismiss() } } } } }