import SwiftUI import ComposeApp // MARK: - Field Focus Enum enum TaskFormField { case title, description, intervalDays, estimatedCost } // MARK: - Task Form View struct TaskFormView: View { let residenceId: Int32? let residences: [ResidenceResponse]? let existingTask: TaskResponse? // nil for add mode, populated for edit mode @Binding var isPresented: Bool @StateObject private var viewModel = TaskViewModel() @ObservedObject private var dataManager = DataManagerObservable.shared @FocusState private var focusedField: TaskFormField? private var isEditMode: Bool { existingTask != nil } private var needsResidenceSelection: Bool { residenceId == nil && !isEditMode } private var canSave: Bool { !title.isEmpty && (!needsResidenceSelection || selectedResidence != nil) && selectedCategory != nil && selectedFrequency != nil && selectedPriority != nil } // Lookups from DataManagerObservable private var taskCategories: [TaskCategory] { dataManager.taskCategories } private var taskFrequencies: [TaskFrequency] { dataManager.taskFrequencies } private var taskPriorities: [TaskPriority] { dataManager.taskPriorities } private var isLoadingLookups: Bool { !dataManager.lookupsInitialized } // Form fields @State private var selectedResidence: ResidenceResponse? @State private var title: String @State private var description: String @State private var selectedCategory: TaskCategory? @State private var selectedFrequency: TaskFrequency? @State private var selectedPriority: TaskPriority? @State private var inProgress: Bool @State private var dueDate: Date @State private var intervalDays: String @State private var estimatedCost: String // Initialize form fields based on mode (add vs edit) init(residenceId: Int32? = nil, residences: [ResidenceResponse]? = nil, existingTask: TaskResponse? = nil, isPresented: Binding) { self.residenceId = residenceId self.residences = residences self.existingTask = existingTask self._isPresented = isPresented // Initialize fields from existing task or with defaults if let task = existingTask { _title = State(initialValue: task.title) _description = State(initialValue: task.description_ ?? "") _selectedCategory = State(initialValue: task.category) _selectedFrequency = State(initialValue: task.frequency) _selectedPriority = State(initialValue: task.priority) _inProgress = State(initialValue: task.inProgress) // Parse date from string let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd" _dueDate = State(initialValue: formatter.date(from: task.dueDate ?? "") ?? Date()) _intervalDays = State(initialValue: "") // No longer in API _estimatedCost = State(initialValue: task.estimatedCost != nil ? String(task.estimatedCost!.doubleValue) : "") } else { _title = State(initialValue: "") _description = State(initialValue: "") _inProgress = State(initialValue: false) _dueDate = State(initialValue: Date()) _intervalDays = State(initialValue: "") _estimatedCost = State(initialValue: "") } } // Validation errors @State private var titleError: String = "" @State private var residenceError: String = "" // Error alert state @State private var errorAlert: ErrorAlertInfo? = nil // Template suggestions @State private var showingTemplatesBrowser = false @State private var filteredSuggestions: [TaskTemplate] = [] @State private var showSuggestions = false var body: some View { NavigationStack { ZStack { Form { // Residence Picker (only if needed) if needsResidenceSelection, let residences = residences { Section { Picker(L10n.Tasks.property, selection: $selectedResidence) { Text(L10n.Tasks.selectProperty).tag(nil as ResidenceResponse?) ForEach(residences, id: \.id) { residence in Text(residence.name).tag(residence as ResidenceResponse?) } } if !residenceError.isEmpty { Text(residenceError) .font(.caption) .foregroundColor(Color.appError) } } header: { Text(L10n.Tasks.property) } footer: { Text(L10n.Tasks.required) .font(.caption) .foregroundColor(Color.appError) } .listRowBackground(Color.appBackgroundSecondary) } // Browse Templates Button (only for new tasks) if !isEditMode { Section { Button { showingTemplatesBrowser = true } label: { HStack { Image(systemName: "list.bullet.rectangle") .font(.system(size: 18)) .foregroundColor(Color.appPrimary) .frame(width: 28) Text("Browse Task Templates") .foregroundColor(Color.appTextPrimary) Spacer() Text("\(dataManager.taskTemplateCount) tasks") .font(.caption) .foregroundColor(Color.appTextSecondary) Image(systemName: "chevron.right") .font(.caption) .foregroundColor(Color.appTextSecondary) } } } header: { Text("Quick Start") } footer: { Text("Choose from common home maintenance tasks or create your own below") .font(.caption) .foregroundColor(Color.appTextSecondary) } .listRowBackground(Color.appBackgroundSecondary) } Section { VStack(alignment: .leading, spacing: 8) { TextField(L10n.Tasks.titleLabel, text: $title) .focused($focusedField, equals: .title) .onChange(of: title) { newValue in updateSuggestions(query: newValue) } // Inline suggestions dropdown if showSuggestions && !filteredSuggestions.isEmpty && focusedField == .title { TaskSuggestionsView( suggestions: filteredSuggestions, onSelect: { template in selectTaskTemplate(template) } ) } } if !titleError.isEmpty { Text(titleError) .font(.caption) .foregroundColor(Color.appError) } TextField(L10n.Tasks.descriptionOptional, text: $description, axis: .vertical) .lineLimit(3...6) .focused($focusedField, equals: .description) } header: { Text(L10n.Tasks.taskDetails) } footer: { Text(L10n.Tasks.titleRequired) .font(.caption) .foregroundColor(Color.appError) } .listRowBackground(Color.appBackgroundSecondary) Section { Picker(L10n.Tasks.category, selection: $selectedCategory) { Text(L10n.Tasks.selectCategory).tag(nil as TaskCategory?) ForEach(taskCategories, id: \.id) { category in Text(category.name.capitalized).tag(category as TaskCategory?) } } } header: { Text(L10n.Tasks.category) } footer: { Text(L10n.Tasks.required) .font(.caption) .foregroundColor(Color.appError) } .listRowBackground(Color.appBackgroundSecondary) Section { Picker(L10n.Tasks.frequency, selection: $selectedFrequency) { Text(L10n.Tasks.selectFrequency).tag(nil as TaskFrequency?) ForEach(taskFrequencies, id: \.id) { frequency in Text(frequency.displayName).tag(frequency as TaskFrequency?) } } if selectedFrequency?.name != "once" { TextField(L10n.Tasks.customInterval, text: $intervalDays) .keyboardType(.numberPad) .focused($focusedField, equals: .intervalDays) } DatePicker(L10n.Tasks.dueDate, selection: $dueDate, displayedComponents: .date) } header: { Text(L10n.Tasks.scheduling) } footer: { Text(L10n.Tasks.required) .font(.caption) .foregroundColor(Color.appError) } .listRowBackground(Color.appBackgroundSecondary) Section { Picker(L10n.Tasks.priority, selection: $selectedPriority) { Text(L10n.Tasks.selectPriority).tag(nil as TaskPriority?) ForEach(taskPriorities, id: \.id) { priority in Text(priority.displayName).tag(priority as TaskPriority?) } } Toggle(L10n.Tasks.inProgressLabel, isOn: $inProgress) } header: { Text(L10n.Tasks.priorityAndStatus) } footer: { Text(L10n.Tasks.required) .font(.caption) .foregroundColor(Color.appError) } .listRowBackground(Color.appBackgroundSecondary) Section(header: Text(L10n.Tasks.cost)) { TextField(L10n.Tasks.estimatedCost, text: $estimatedCost) .keyboardType(.decimalPad) .focused($focusedField, equals: .estimatedCost) } .listRowBackground(Color.appBackgroundSecondary) if let errorMessage = viewModel.errorMessage { Section { Text(errorMessage) .foregroundColor(Color.appError) .font(.caption) } .listRowBackground(Color.appBackgroundSecondary) } } .disabled(isLoadingLookups) .blur(radius: isLoadingLookups ? 3 : 0) if isLoadingLookups { VStack(spacing: 16) { ProgressView() .scaleEffect(1.5) Text(L10n.Tasks.loading) .foregroundColor(Color.appTextSecondary) } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.appBackgroundPrimary.opacity(0.8)) } } .listStyle(.plain) .scrollContentBackground(.hidden) .background(Color.appBackgroundPrimary) .navigationTitle(isEditMode ? L10n.Tasks.editTitle : L10n.Tasks.addTitle) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button(L10n.Common.cancel) { isPresented = false } .disabled(isLoadingLookups) } ToolbarItem(placement: .navigationBarTrailing) { Button(L10n.Common.save) { submitForm() } .disabled(!canSave || viewModel.isLoading || isLoadingLookups) } } .onAppear { // Track screen view for new tasks if !isEditMode { PostHogAnalytics.shared.screen(AnalyticsEvents.newTaskScreenShown) } // Set defaults when lookups are available if dataManager.lookupsInitialized { setDefaults() } } .onChange(of: dataManager.lookupsInitialized) { initialized in if initialized { setDefaults() } } .onChange(of: viewModel.taskCreated) { created in if created { isPresented = false } } .onChange(of: viewModel.errorMessage) { errorMessage in if let errorMessage = errorMessage, !errorMessage.isEmpty { errorAlert = ErrorAlertInfo(message: errorMessage) } } .errorAlert( error: errorAlert, onRetry: { errorAlert = nil submitForm() }, onDismiss: { errorAlert = nil } ) .sheet(isPresented: $showingTemplatesBrowser) { TaskTemplatesBrowserView { template in selectTaskTemplate(template) } } } } // MARK: - Suggestions Helpers private func updateSuggestions(query: String) { if query.count >= 2 { filteredSuggestions = dataManager.searchTaskTemplates(query: query) showSuggestions = !filteredSuggestions.isEmpty } else { filteredSuggestions = [] showSuggestions = false } } private func selectTaskTemplate(_ template: TaskTemplate) { // Fill in the title title = template.title // Fill in description if available description = template.description_ // Auto-select matching category by ID or name if let categoryId = template.categoryId { selectedCategory = taskCategories.first(where: { $0.id == Int(categoryId.int32Value) }) } else if let category = template.category { selectedCategory = taskCategories.first(where: { $0.name.lowercased() == category.name.lowercased() }) } // Auto-select matching frequency by ID or name if let frequencyId = template.frequencyId { selectedFrequency = taskFrequencies.first(where: { $0.id == Int(frequencyId.int32Value) }) } else if let frequency = template.frequency { selectedFrequency = taskFrequencies.first(where: { $0.name.lowercased() == frequency.name.lowercased() }) } // Clear suggestions and dismiss keyboard showSuggestions = false filteredSuggestions = [] focusedField = nil } private func setDefaults() { // Set default values if not already set if selectedCategory == nil && !taskCategories.isEmpty { selectedCategory = taskCategories.first } if selectedFrequency == nil && !taskFrequencies.isEmpty { // Default to "once" selectedFrequency = taskFrequencies.first { $0.name == "once" } ?? taskFrequencies.first } if selectedPriority == nil && !taskPriorities.isEmpty { // Default to "medium" selectedPriority = taskPriorities.first { $0.name == "medium" } ?? taskPriorities.first } // Set default residence if provided if needsResidenceSelection && selectedResidence == nil, let residences = residences, !residences.isEmpty { selectedResidence = residences.first } } private func validateForm() -> Bool { var isValid = true if title.isEmpty { titleError = "Title is required" isValid = false } else { titleError = "" } if needsResidenceSelection && selectedResidence == nil { residenceError = "Property is required" isValid = false } else { residenceError = "" } if selectedCategory == nil { viewModel.errorMessage = "Please select a category" isValid = false } if selectedFrequency == nil { viewModel.errorMessage = "Please select a frequency" isValid = false } if selectedPriority == nil { viewModel.errorMessage = "Please select a priority" isValid = false } return isValid } private func submitForm() { guard validateForm() else { return } guard let category = selectedCategory, let frequency = selectedFrequency, let priority = selectedPriority else { return } // Format date as yyyy-MM-dd let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd" let dueDateString = dateFormatter.string(from: dueDate) if isEditMode, let task = existingTask { // UPDATE existing task let request = TaskCreateRequest( residenceId: task.residenceId, title: title, description: description.isEmpty ? nil : description, categoryId: KotlinInt(int: Int32(category.id)), priorityId: KotlinInt(int: Int32(priority.id)), inProgress: inProgress, frequencyId: KotlinInt(int: Int32(frequency.id)), assignedToId: nil, dueDate: dueDateString, estimatedCost: estimatedCost.isEmpty ? nil : KotlinDouble(double: Double(estimatedCost) ?? 0.0), contractorId: nil ) viewModel.updateTask(id: task.id, request: request) { success in if success { isPresented = false } } } else { // CREATE new task // Determine the actual residence ID to use let actualResidenceId: Int32 if let providedId = residenceId { actualResidenceId = providedId } else if let selected = selectedResidence { actualResidenceId = Int32(selected.id) } else { return } let request = TaskCreateRequest( residenceId: actualResidenceId, title: title, description: description.isEmpty ? nil : description, categoryId: KotlinInt(int: Int32(category.id)), priorityId: KotlinInt(int: Int32(priority.id)), inProgress: inProgress, frequencyId: KotlinInt(int: Int32(frequency.id)), assignedToId: nil, dueDate: dueDateString, estimatedCost: estimatedCost.isEmpty ? nil : KotlinDouble(double: Double(estimatedCost) ?? 0.0), contractorId: nil ) viewModel.createTask(request: request) { success in if success { // Track task creation PostHogAnalytics.shared.capture(AnalyticsEvents.taskCreated, properties: ["residence_id": actualResidenceId]) // View will dismiss automatically via onChange } } } } } #Preview("With Residence ID") { TaskFormView(residenceId: 1, residences: nil, isPresented: .constant(true)) } #Preview("With Residence Selection") { TaskFormView(residenceId: nil, residences: [], isPresented: .constant(true)) }