Update Kotlin models and iOS Swift to align with new Go API format

- Update all Kotlin API models to match Go API response structures
- Fix Swift type aliases (TaskDetail→TaskResponse, Residence→ResidenceResponse, etc.)
- Update TaskCompletionCreateRequest to simplified Go API format (taskId, notes, actualCost, photoUrl)
- Fix optional handling for frequency, priority, category, status in task models
- Replace isPrimaryOwner with ownerId comparison against current user
- Update ResidenceUsersResponse to use owner.id instead of ownerId
- Fix non-optional String fields to use isEmpty checks instead of optional binding
- Add type aliases for backwards compatibility in Kotlin models

🤖 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-27 11:03:00 -06:00
parent d3e77326aa
commit 60c824447d
48 changed files with 923 additions and 846 deletions

View File

@@ -3,7 +3,7 @@ import ComposeApp
struct AddTaskWithResidenceView: View {
@Binding var isPresented: Bool
let residences: [Residence]
let residences: [ResidenceResponse]
var body: some View {
TaskFormView(residenceId: nil, residences: residences, isPresented: $isPresented)

View File

@@ -11,13 +11,13 @@ struct AllTasksView: View {
@State private var showAddTask = false
@State private var showEditTask = false
@State private var showingUpgradePrompt = false
@State private var selectedTaskForEdit: TaskDetail?
@State private var selectedTaskForComplete: TaskDetail?
@State private var selectedTaskForEdit: TaskResponse?
@State private var selectedTaskForComplete: TaskResponse?
@State private var selectedTaskForArchive: TaskDetail?
@State private var selectedTaskForArchive: TaskResponse?
@State private var showArchiveConfirmation = false
@State private var selectedTaskForCancel: TaskDetail?
@State private var selectedTaskForCancel: TaskResponse?
@State private var showCancelConfirmation = false
// Count total tasks across all columns
@@ -334,37 +334,9 @@ struct RoundedCorner: Shape {
}
}
extension Array where Element == ResidenceWithTasks {
/// Converts an array of ResidenceWithTasks into an array of Residence.
/// Adjust the mapping inside as needed to match your model initializers.
func toResidences() -> [Residence] {
return self.map { item in
return Residence(
id: item.id,
owner: KotlinInt(value: item.owner),
ownerUsername: item.ownerUsername,
isPrimaryOwner: item.isPrimaryOwner,
userCount: item.userCount,
name: item.name,
propertyType: item.propertyType,
streetAddress: item.streetAddress,
apartmentUnit: item.apartmentUnit,
city: item.city,
stateProvince: item.stateProvince,
postalCode: item.postalCode,
country: item.country,
bedrooms: item.bedrooms != nil ? KotlinInt(nonretainedObject: item.bedrooms!) : nil,
bathrooms: item.bathrooms != nil ? KotlinFloat(float: Float(item.bathrooms!)) : nil,
squareFootage: item.squareFootage != nil ? KotlinInt(nonretainedObject: item.squareFootage!) : nil,
lotSize: item.lotSize != nil ? KotlinFloat(float: Float(item.lotSize!)) : nil,
yearBuilt: item.yearBuilt != nil ? KotlinInt(nonretainedObject: item.yearBuilt!) : nil,
description: item.description,
purchaseDate: item.purchaseDate,
purchasePrice: item.purchasePrice,
isPrimary: item.isPrimary,
createdAt: item.createdAt,
updatedAt: item.updatedAt
)
}
extension Array where Element == ResidenceResponse {
/// Returns the array as-is (for API compatibility)
func toResidences() -> [ResidenceResponse] {
return self
}
}

View File

@@ -3,12 +3,13 @@ import PhotosUI
import ComposeApp
struct CompleteTaskView: View {
let task: TaskDetail
let task: TaskResponse
let onComplete: () -> Void
@Environment(\.dismiss) private var dismiss
@StateObject private var taskViewModel = TaskViewModel()
@StateObject private var contractorViewModel = ContractorViewModel()
private let completionViewModel = ComposeApp.TaskCompletionViewModel()
@State private var completedByName: String = ""
@State private var actualCost: String = ""
@State private var notes: String = ""
@@ -32,7 +33,7 @@ struct CompleteTaskView: View {
.font(.headline)
HStack {
Label(task.category.name.capitalized, systemImage: "folder")
Label((task.category?.name ?? "").capitalized, systemImage: "folder")
.font(.subheadline)
.foregroundStyle(.secondary)
@@ -303,66 +304,53 @@ struct CompleteTaskView: View {
isSubmitting = true
// Get current date in ISO format
let dateFormatter = ISO8601DateFormatter()
let currentDate = dateFormatter.string(from: Date())
// Create request
// Create request with simplified Go API format
// Note: completedAt defaults to now on server if not provided
let request = TaskCompletionCreateRequest(
task: task.id,
completedByUser: nil,
contractor: selectedContractor != nil ? KotlinInt(int: selectedContractor!.id) : nil,
completedByName: completedByName.isEmpty ? nil : completedByName,
completedByPhone: selectedContractor?.phone ?? "",
completedByEmail: "",
companyName: selectedContractor?.company ?? "",
completionDate: currentDate,
actualCost: actualCost.isEmpty ? nil : KotlinDouble(double: Double(actualCost) ?? 0.0),
taskId: task.id,
completedAt: nil,
notes: notes.isEmpty ? nil : notes,
rating: KotlinInt(int: Int32(rating))
actualCost: actualCost.isEmpty ? nil : KotlinDouble(double: Double(actualCost) ?? 0.0),
photoUrl: nil
)
// Use TaskCompletionViewModel to create completion
if !selectedImages.isEmpty {
// Convert images to ImageData for Kotlin
let imageDataList = selectedImages.compactMap { uiImage -> ComposeApp.ImageData? in
guard let jpegData = uiImage.jpegData(compressionQuality: 0.8) else { return nil }
let byteArray = KotlinByteArray(data: jpegData)
return ComposeApp.ImageData(bytes: byteArray, fileName: "completion_image.jpg")
}
completionViewModel.createTaskCompletionWithImages(request: request, images: imageDataList)
} else {
completionViewModel.createTaskCompletion(request: request)
}
// Observe the result
Task {
do {
let result: ApiResult<TaskCompletion>
// If there are images, upload with images
if !selectedImages.isEmpty {
// Compress images to meet size requirements
let imageDataArray = ImageCompression.compressImages(selectedImages)
let imageByteArrays = imageDataArray.map { KotlinByteArray(data: $0) }
let fileNames = (0..<imageDataArray.count).map { "image_\($0).jpg" }
result = try await APILayer.shared.createTaskCompletionWithImages(
request: request,
images: imageByteArrays,
imageFileNames: fileNames
)
} else {
// Upload without images
result = try await APILayer.shared.createTaskCompletion(request: request)
}
for await state in completionViewModel.createCompletionState {
await MainActor.run {
if result is ApiResultSuccess<TaskCompletion> {
switch state {
case is ApiResultSuccess<TaskCompletionResponse>:
self.isSubmitting = false
self.dismiss()
self.onComplete()
} else if let errorResult = result as? ApiResultError {
self.errorMessage = errorResult.message
self.showError = true
self.isSubmitting = false
} else {
self.errorMessage = "Failed to complete task"
case let error as ApiResultError:
self.errorMessage = error.message
self.showError = true
self.isSubmitting = false
case is ApiResultLoading:
// Still loading, continue waiting
break
default:
break
}
}
} catch {
await MainActor.run {
self.errorMessage = error.localizedDescription
self.showError = true
self.isSubmitting = false
// Break out of loop on terminal states
if state is ApiResultSuccess<TaskCompletionResponse> || state is ApiResultError {
break
}
}
}

View File

@@ -4,7 +4,7 @@ import ComposeApp
/// Wrapper view for editing an existing task
/// This is now just a convenience wrapper around TaskFormView in "edit" mode
struct EditTaskView: View {
let task: TaskDetail
let task: TaskResponse
@Binding var isPresented: Bool
var body: some View {

View File

@@ -9,8 +9,8 @@ enum TaskFormField {
// MARK: - Task Form View
struct TaskFormView: View {
let residenceId: Int32?
let residences: [Residence]?
let existingTask: TaskDetail? // nil for add mode, populated for edit mode
let residences: [ResidenceResponse]?
let existingTask: TaskResponse? // nil for add mode, populated for edit mode
@Binding var isPresented: Bool
@StateObject private var viewModel = TaskViewModel()
@FocusState private var focusedField: TaskFormField?
@@ -40,7 +40,7 @@ struct TaskFormView: View {
@State private var isLoadingLookups: Bool = true
// Form fields
@State private var selectedResidence: Residence?
@State private var selectedResidence: ResidenceResponse?
@State private var title: String
@State private var description: String
@State private var selectedCategory: TaskCategory?
@@ -52,7 +52,7 @@ struct TaskFormView: View {
@State private var estimatedCost: String
// Initialize form fields based on mode (add vs edit)
init(residenceId: Int32? = nil, residences: [Residence]? = nil, existingTask: TaskDetail? = nil, isPresented: Binding<Bool>) {
init(residenceId: Int32? = nil, residences: [ResidenceResponse]? = nil, existingTask: TaskResponse? = nil, isPresented: Binding<Bool>) {
self.residenceId = residenceId
self.residences = residences
self.existingTask = existingTask
@@ -72,7 +72,7 @@ struct TaskFormView: View {
formatter.dateFormat = "yyyy-MM-dd"
_dueDate = State(initialValue: formatter.date(from: task.dueDate ?? "") ?? Date())
_intervalDays = State(initialValue: task.intervalDays != nil ? String(task.intervalDays!.intValue) : "")
_intervalDays = State(initialValue: "") // No longer in API
_estimatedCost = State(initialValue: task.estimatedCost != nil ? String(task.estimatedCost!.doubleValue) : "")
} else {
_title = State(initialValue: "")
@@ -98,9 +98,9 @@ struct TaskFormView: View {
if needsResidenceSelection, let residences = residences {
Section {
Picker("Property", selection: $selectedResidence) {
Text("Select Property").tag(nil as Residence?)
Text("Select Property").tag(nil as ResidenceResponse?)
ForEach(residences, id: \.id) { residence in
Text(residence.name).tag(residence as Residence?)
Text(residence.name).tag(residence as ResidenceResponse?)
}
}
@@ -396,17 +396,17 @@ struct TaskFormView: View {
if isEditMode, let task = existingTask {
// UPDATE existing task
let request = TaskCreateRequest(
residence: task.residence,
residenceId: task.residenceId,
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: KotlinInt(value: status.id) as? KotlinInt,
categoryId: KotlinInt(int: Int32(category.id)),
priorityId: KotlinInt(int: Int32(priority.id)),
statusId: KotlinInt(int: Int32(status.id)),
frequencyId: KotlinInt(int: Int32(frequency.id)),
assignedToId: nil,
dueDate: dueDateString,
estimatedCost: estimatedCost.isEmpty ? nil : KotlinDouble(double: Double(estimatedCost) ?? 0.0),
archived: task.archived
contractorId: nil
)
viewModel.updateTask(id: task.id, request: request) { success in
@@ -427,17 +427,17 @@ struct TaskFormView: View {
}
let request = TaskCreateRequest(
residence: actualResidenceId,
residenceId: 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) },
categoryId: KotlinInt(int: Int32(category.id)),
priorityId: KotlinInt(int: Int32(priority.id)),
statusId: selectedStatus.map { KotlinInt(int: Int32($0.id)) },
frequencyId: KotlinInt(int: Int32(frequency.id)),
assignedToId: nil,
dueDate: dueDateString,
estimatedCost: estimatedCost.isEmpty ? nil : KotlinDouble(double: Double(estimatedCost) ?? 0.0),
archived: false
contractorId: nil
)
viewModel.createTask(request: request) { success in

View File

@@ -45,7 +45,7 @@ class TaskViewModel: ObservableObject {
self?.errorMessage = error
}
},
onSuccess: { [weak self] (_: CustomTask) in
onSuccess: { [weak self] (_: TaskResponse) in
self?.actionState = .success(.create)
},
completion: completion,