Fixed type conversion issues in iOS Swift files to handle KotlinDouble for cost fields instead of String. Updated all task-related views to properly convert between String (for TextField input) and KotlinDouble (for API calls). Changes: - TaskCard.swift: Updated preview data with new TaskDetail signature (added residenceName, createdBy, createdByUsername, intervalDays; changed estimatedCost from String to Double; removed actualCost and notes) - TasksSection.swift: Updated two preview TaskDetail instances with new signature - CompleteTaskView.swift: Convert actualCost String to KotlinDouble for API - EditTaskView.swift: Convert estimatedCost between KotlinDouble and String for display and API calls - TaskFormView.swift: Convert estimatedCost String to KotlinDouble for API Pattern used: - Display: KotlinDouble -> String using .doubleValue - API: String -> Double -> KotlinDouble using KotlinDouble(double:) Build now succeeds with all type conversions properly handling Decimal/Double values from backend instead of String. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
340 lines
12 KiB
Swift
340 lines
12 KiB
Swift
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: [Residence]?
|
|
@Binding var isPresented: Bool
|
|
@StateObject private var viewModel = TaskViewModel()
|
|
@FocusState private var focusedField: TaskFormField?
|
|
|
|
private var needsResidenceSelection: Bool {
|
|
residenceId == nil
|
|
}
|
|
|
|
// Lookups from DataCache
|
|
@State private var taskCategories: [TaskCategory] = []
|
|
@State private var taskFrequencies: [TaskFrequency] = []
|
|
@State private var taskPriorities: [TaskPriority] = []
|
|
@State private var taskStatuses: [TaskStatus] = []
|
|
@State private var isLoadingLookups: Bool = true
|
|
|
|
// Form fields
|
|
@State private var selectedResidence: Residence?
|
|
@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 selectedStatus: TaskStatus?
|
|
@State private var dueDate: Date = Date()
|
|
@State private var intervalDays: String = ""
|
|
@State private var estimatedCost: String = ""
|
|
|
|
// Validation errors
|
|
@State private var titleError: String = ""
|
|
@State private var residenceError: String = ""
|
|
|
|
// Error alert state
|
|
@State private var errorAlert: ErrorAlertInfo? = nil
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
ZStack {
|
|
Form {
|
|
// Residence Picker (only if needed)
|
|
if needsResidenceSelection, let residences = residences {
|
|
Section(header: Text("Property")) {
|
|
Picker("Property", selection: $selectedResidence) {
|
|
Text("Select Property").tag(nil as Residence?)
|
|
ForEach(residences, id: \.id) { residence in
|
|
Text(residence.name).tag(residence as Residence?)
|
|
}
|
|
}
|
|
|
|
if !residenceError.isEmpty {
|
|
Text(residenceError)
|
|
.font(.caption)
|
|
.foregroundColor(.red)
|
|
}
|
|
}
|
|
}
|
|
|
|
Section(header: Text("Task Details")) {
|
|
TextField("Title", text: $title)
|
|
.focused($focusedField, equals: .title)
|
|
|
|
if !titleError.isEmpty {
|
|
Text(titleError)
|
|
.font(.caption)
|
|
.foregroundColor(.red)
|
|
}
|
|
|
|
TextField("Description (optional)", text: $description, axis: .vertical)
|
|
.lineLimit(3...6)
|
|
.focused($focusedField, equals: .description)
|
|
}
|
|
|
|
Section(header: Text("Category")) {
|
|
Picker("Category", selection: $selectedCategory) {
|
|
Text("Select Category").tag(nil as TaskCategory?)
|
|
ForEach(taskCategories, id: \.id) { category in
|
|
Text(category.name.capitalized).tag(category as TaskCategory?)
|
|
}
|
|
}
|
|
}
|
|
|
|
Section(header: Text("Scheduling")) {
|
|
Picker("Frequency", selection: $selectedFrequency) {
|
|
Text("Select Frequency").tag(nil as TaskFrequency?)
|
|
ForEach(taskFrequencies, id: \.id) { frequency in
|
|
Text(frequency.displayName).tag(frequency as TaskFrequency?)
|
|
}
|
|
}
|
|
|
|
if selectedFrequency?.name != "once" {
|
|
TextField("Custom Interval (days, optional)", text: $intervalDays)
|
|
.keyboardType(.numberPad)
|
|
.focused($focusedField, equals: .intervalDays)
|
|
}
|
|
|
|
DatePicker("Due Date", selection: $dueDate, displayedComponents: .date)
|
|
}
|
|
|
|
Section(header: Text("Priority & Status")) {
|
|
Picker("Priority", selection: $selectedPriority) {
|
|
Text("Select Priority").tag(nil as TaskPriority?)
|
|
ForEach(taskPriorities, id: \.id) { priority in
|
|
Text(priority.displayName).tag(priority as TaskPriority?)
|
|
}
|
|
}
|
|
|
|
Picker("Status", selection: $selectedStatus) {
|
|
Text("Select Status").tag(nil as TaskStatus?)
|
|
ForEach(taskStatuses, id: \.id) { status in
|
|
Text(status.displayName).tag(status as TaskStatus?)
|
|
}
|
|
}
|
|
}
|
|
|
|
Section(header: Text("Cost")) {
|
|
TextField("Estimated Cost (optional)", text: $estimatedCost)
|
|
.keyboardType(.decimalPad)
|
|
.focused($focusedField, equals: .estimatedCost)
|
|
}
|
|
|
|
if let errorMessage = viewModel.errorMessage {
|
|
Section {
|
|
Text(errorMessage)
|
|
.foregroundColor(.red)
|
|
.font(.caption)
|
|
}
|
|
}
|
|
}
|
|
.disabled(isLoadingLookups)
|
|
.blur(radius: isLoadingLookups ? 3 : 0)
|
|
|
|
if isLoadingLookups {
|
|
VStack(spacing: 16) {
|
|
ProgressView()
|
|
.scaleEffect(1.5)
|
|
Text("Loading...")
|
|
.foregroundColor(.secondary)
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
.background(Color(uiColor: .systemBackground).opacity(0.8))
|
|
}
|
|
}
|
|
.navigationTitle("Add Task")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .navigationBarLeading) {
|
|
Button("Cancel") {
|
|
isPresented = false
|
|
}
|
|
.disabled(isLoadingLookups)
|
|
}
|
|
|
|
ToolbarItem(placement: .navigationBarTrailing) {
|
|
Button("Save") {
|
|
submitForm()
|
|
}
|
|
.disabled(viewModel.isLoading || isLoadingLookups)
|
|
}
|
|
}
|
|
.task {
|
|
await loadLookups()
|
|
}
|
|
.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
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
private func loadLookups() async {
|
|
// Load all lookups from DataCache
|
|
if let categories = DataCache.shared.taskCategories.value as? [TaskCategory] {
|
|
taskCategories = categories
|
|
}
|
|
|
|
if let frequencies = DataCache.shared.taskFrequencies.value as? [TaskFrequency] {
|
|
taskFrequencies = frequencies
|
|
}
|
|
|
|
if let priorities = DataCache.shared.taskPriorities.value as? [TaskPriority] {
|
|
taskPriorities = priorities
|
|
}
|
|
|
|
if let statuses = DataCache.shared.taskStatuses.value as? [TaskStatus] {
|
|
taskStatuses = statuses
|
|
}
|
|
|
|
setDefaults()
|
|
isLoadingLookups = false
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
if selectedStatus == nil && !taskStatuses.isEmpty {
|
|
// Default to "pending"
|
|
selectedStatus = taskStatuses.first { $0.name == "pending" } ?? taskStatuses.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
|
|
}
|
|
|
|
if selectedStatus == nil {
|
|
viewModel.errorMessage = "Please select a status"
|
|
isValid = false
|
|
}
|
|
|
|
return isValid
|
|
}
|
|
|
|
private func submitForm() {
|
|
guard validateForm() else { return }
|
|
|
|
guard let category = selectedCategory,
|
|
let frequency = selectedFrequency,
|
|
let priority = selectedPriority,
|
|
let status = selectedStatus else {
|
|
return
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Format date as yyyy-MM-dd
|
|
let dateFormatter = DateFormatter()
|
|
dateFormatter.dateFormat = "yyyy-MM-dd"
|
|
let dueDateString = dateFormatter.string(from: dueDate)
|
|
|
|
let request = TaskCreateRequest(
|
|
residence: actualResidenceId,
|
|
title: title,
|
|
description: description.isEmpty ? nil : description,
|
|
category: Int32(category.id),
|
|
frequency: Int32(frequency.id),
|
|
intervalDays: intervalDays.isEmpty ? nil : Int32(intervalDays) as? KotlinInt,
|
|
priority: Int32(priority.id),
|
|
status: selectedStatus.map { KotlinInt(value: $0.id) },
|
|
dueDate: dueDateString,
|
|
estimatedCost: estimatedCost.isEmpty ? nil : KotlinDouble(double: Double(estimatedCost) ?? 0.0),
|
|
archived: false
|
|
)
|
|
|
|
viewModel.createTask(request: request) { success in
|
|
if success {
|
|
// 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))
|
|
}
|