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

@@ -11,10 +11,10 @@ import ComposeApp
/// Displays a task summary with dynamic categories from the backend
struct TaskSummaryCard: View {
let taskSummary: TaskSummary
let taskSummary: ResidenceTaskSummary
var visibleCategories: [String]? = nil
private var filteredCategories: [TaskColumnCategory] {
private var filteredCategories: [TaskCategorySummary] {
if let visible = visibleCategories {
return taskSummary.categories.filter { visible.contains($0.name) }
}
@@ -41,7 +41,7 @@ struct TaskSummaryCard: View {
/// Displays a single task category with icon, name, and count
struct TaskCategoryRow: View {
let category: TaskColumnCategory
let category: TaskCategorySummary
private var categoryColor: Color {
Color(hex: category.color) ?? .gray
@@ -103,61 +103,55 @@ struct TaskSummaryCard_Previews: PreviewProvider {
.background(Color(.systemGroupedBackground))
}
static var mockTaskSummary: TaskSummary {
TaskSummary(
total: 25,
static var mockTaskSummary: ResidenceTaskSummary {
ResidenceTaskSummary(
categories: [
TaskColumnCategory(
TaskCategorySummary(
name: "overdue_tasks",
displayName: "Overdue",
icons: TaskColumnIcon(
ios: "exclamationmark.triangle",
icons: TaskCategoryIcons(
android: "Warning",
web: "exclamation-triangle"
ios: "exclamationmark.triangle"
),
color: "#FF3B30",
count: 3
),
TaskColumnCategory(
TaskCategorySummary(
name: "current_tasks",
displayName: "Current",
icons: TaskColumnIcon(
ios: "calendar",
icons: TaskCategoryIcons(
android: "CalendarToday",
web: "calendar"
ios: "calendar"
),
color: "#007AFF",
count: 8
),
TaskColumnCategory(
TaskCategorySummary(
name: "in_progress_tasks",
displayName: "In Progress",
icons: TaskColumnIcon(
ios: "play.circle",
icons: TaskCategoryIcons(
android: "PlayCircle",
web: "play-circle"
ios: "play.circle"
),
color: "#FF9500",
count: 2
),
TaskColumnCategory(
TaskCategorySummary(
name: "backlog_tasks",
displayName: "Backlog",
icons: TaskColumnIcon(
ios: "tray",
icons: TaskCategoryIcons(
android: "Inbox",
web: "inbox"
ios: "tray"
),
color: "#5856D6",
count: 7
),
TaskColumnCategory(
TaskCategorySummary(
name: "done_tasks",
displayName: "Done",
icons: TaskColumnIcon(
ios: "checkmark.circle",
icons: TaskCategoryIcons(
android: "CheckCircle",
web: "check-circle"
ios: "checkmark.circle"
),
color: "#34C759",
count: 5

View File

@@ -2,7 +2,7 @@ import SwiftUI
import ComposeApp
struct EditResidenceView: View {
let residence: Residence
let residence: ResidenceResponse
@Binding var isPresented: Bool
var body: some View {

View File

@@ -1,8 +1,8 @@
import Foundation
import ComposeApp
// Extension to make TaskDetail conform to Identifiable for SwiftUI
extension TaskDetail: Identifiable {
// Extension to make TaskResponse conform to Identifiable for SwiftUI
extension TaskResponse: Identifiable {
// TaskDetail already has an `id` property from Kotlin,
// so we just need to declare conformance to Identifiable
}

View File

@@ -59,11 +59,11 @@ final class WidgetDataManager {
id: Int(task.id),
title: task.title,
description: task.description_,
priority: task.priority.name,
priority: task.priority?.name ?? "",
status: task.status?.name,
dueDate: task.dueDate,
category: task.category.name,
residenceName: task.residenceName,
category: task.category?.name ?? "",
residenceName: "", // No longer available in API, residence lookup needed
isOverdue: isTaskOverdue(dueDate: task.dueDate, status: task.status?.name)
)
allTasks.append(widgetTask)

View File

@@ -7,9 +7,9 @@ struct ManageUsersView: View {
let isPrimaryOwner: Bool
@Environment(\.dismiss) private var dismiss
@State private var users: [ResidenceUser] = []
@State private var users: [ResidenceUserResponse] = []
@State private var ownerId: Int32?
@State private var shareCode: ResidenceShareCode?
@State private var shareCode: ShareCodeResponse?
@State private var isLoading = true
@State private var errorMessage: String?
@State private var isGeneratingCode = false
@@ -100,7 +100,7 @@ struct ManageUsersView: View {
if let successResult = result as? ApiResultSuccess<ResidenceUsersResponse>,
let responseData = successResult.data as? ResidenceUsersResponse {
self.users = Array(responseData.users)
self.ownerId = responseData.ownerId as? Int32
self.ownerId = Int32(responseData.owner.id)
self.isLoading = false
} else if let errorResult = result as? ApiResultError {
self.errorMessage = ErrorMessageParser.parse(errorResult.message)
@@ -127,7 +127,7 @@ struct ManageUsersView: View {
let result = try await APILayer.shared.getShareCode(residenceId: Int32(Int(residenceId)))
await MainActor.run {
if let successResult = result as? ApiResultSuccess<ResidenceShareCode> {
if let successResult = result as? ApiResultSuccess<ShareCodeResponse> {
self.shareCode = successResult.data
}
// It's okay if there's no active share code
@@ -148,7 +148,7 @@ struct ManageUsersView: View {
let result = try await APILayer.shared.generateShareCode(residenceId: Int32(Int(residenceId)))
await MainActor.run {
if let successResult = result as? ApiResultSuccess<ResidenceShareCode> {
if let successResult = result as? ApiResultSuccess<ShareCodeResponse> {
self.shareCode = successResult.data
self.isGeneratingCode = false
} else if let errorResult = result as? ApiResultError {

View File

@@ -15,9 +15,9 @@ struct ResidenceDetailView: View {
@State private var showEditResidence = false
@State private var showEditTask = false
@State private var showManageUsers = false
@State private var selectedTaskForEdit: TaskDetail?
@State private var selectedTaskForComplete: TaskDetail?
@State private var selectedTaskForArchive: TaskDetail?
@State private var selectedTaskForEdit: TaskResponse?
@State private var selectedTaskForComplete: TaskResponse?
@State private var selectedTaskForArchive: TaskResponse?
@State private var showArchiveConfirmation = false
@State private var hasAppeared = false
@@ -29,7 +29,15 @@ struct ResidenceDetailView: View {
@StateObject private var subscriptionCache = SubscriptionCacheWrapper.shared
@Environment(\.dismiss) private var dismiss
// Check if current user is the owner of the residence
private func isCurrentUserOwner(of residence: ResidenceResponse) -> Bool {
guard let currentUser = ComposeApp.DataCache.shared.currentUser.value else {
return false
}
return Int(residence.ownerId) == Int(currentUser.id)
}
var body: some View {
ZStack {
Color.appBackgroundPrimary
@@ -100,7 +108,7 @@ struct ResidenceDetailView: View {
ManageUsersView(
residenceId: residence.id,
residenceName: residence.name,
isPrimaryOwner: residence.isPrimaryOwner
isPrimaryOwner: isCurrentUserOwner(of: residence)
)
}
}
@@ -184,7 +192,7 @@ private extension ResidenceDetailView {
}
@ViewBuilder
func contentView(for residence: Residence) -> some View {
func contentView(for residence: ResidenceResponse) -> some View {
ScrollView {
VStack(spacing: 16) {
PropertyHeaderCard(residence: residence)
@@ -251,7 +259,7 @@ private extension ResidenceDetailView {
.disabled(viewModel.isGeneratingReport)
}
if let residence = viewModel.selectedResidence, residence.isPrimaryOwner {
if let residence = viewModel.selectedResidence, isCurrentUserOwner(of: residence) {
Button {
showManageUsers = true
} label: {
@@ -272,7 +280,7 @@ private extension ResidenceDetailView {
}
.accessibilityIdentifier(AccessibilityIdentifiers.Task.addButton)
if let residence = viewModel.selectedResidence, residence.isPrimaryOwner {
if let residence = viewModel.selectedResidence, isCurrentUserOwner(of: residence) {
Button {
showDeleteConfirmation = true
} label: {
@@ -363,9 +371,9 @@ private struct TasksSectionContainer: View {
let tasksResponse: TaskColumnsResponse
@ObservedObject var taskViewModel: TaskViewModel
@Binding var selectedTaskForEdit: TaskDetail?
@Binding var selectedTaskForComplete: TaskDetail?
@Binding var selectedTaskForArchive: TaskDetail?
@Binding var selectedTaskForEdit: TaskResponse?
@Binding var selectedTaskForComplete: TaskResponse?
@Binding var selectedTaskForArchive: TaskResponse?
@Binding var showArchiveConfirmation: Bool
let reloadTasks: () -> Void

View File

@@ -7,7 +7,7 @@ class ResidenceViewModel: ObservableObject {
// MARK: - Published Properties
@Published var residenceSummary: ResidenceSummaryResponse?
@Published var myResidences: MyResidencesResponse?
@Published var selectedResidence: Residence?
@Published var selectedResidence: ResidenceResponse?
@Published var isLoading: Bool = false
@Published var errorMessage: String?
@Published var isGeneratingReport: Bool = false
@@ -65,7 +65,7 @@ class ResidenceViewModel: ObservableObject {
sharedViewModel.getResidence(id: id) { result in
Task { @MainActor in
if let success = result as? ApiResultSuccess<Residence> {
if let success = result as? ApiResultSuccess<ResidenceResponse> {
self.selectedResidence = success.data
self.isLoading = false
} else if let error = result as? ApiResultError {
@@ -101,7 +101,7 @@ class ResidenceViewModel: ObservableObject {
sharedViewModel.updateResidenceState,
loadingSetter: { [weak self] in self?.isLoading = $0 },
errorSetter: { [weak self] in self?.errorMessage = $0 },
onSuccess: { [weak self] (data: Residence) in
onSuccess: { [weak self] (data: ResidenceResponse) in
self?.selectedResidence = data
},
completion: completion,

View File

@@ -121,7 +121,7 @@ struct ResidencesListView: View {
private struct ResidencesContent: View {
let response: MyResidencesResponse
let residences: [ResidenceWithTasks]
let residences: [ResidenceResponse]
var body: some View {
ScrollView(showsIndicators: false) {

View File

@@ -2,7 +2,7 @@ import SwiftUI
import ComposeApp
struct ResidenceFormView: View {
let existingResidence: Residence?
let existingResidence: ResidenceResponse?
@Binding var isPresented: Bool
var onSuccess: (() -> Void)?
@StateObject private var viewModel = ResidenceViewModel()
@@ -233,8 +233,8 @@ struct ResidenceFormView: View {
isPrimary = residence.isPrimary
// Set the selected property type
if let propertyTypeStr = residence.propertyType, let propertyTypeId = Int(propertyTypeStr) {
selectedPropertyType = residenceTypes.first { $0.id == propertyTypeId }
if let propertyTypeId = residence.propertyTypeId {
selectedPropertyType = residenceTypes.first { $0.id == Int32(propertyTypeId) }
}
}
// In add mode, leave selectedPropertyType as nil to force user to select
@@ -261,17 +261,17 @@ struct ResidenceFormView: View {
guard !bedrooms.isEmpty, let value = Int32(bedrooms) else { return nil }
return KotlinInt(int: value)
}()
let bathroomsValue: KotlinFloat? = {
guard !bathrooms.isEmpty, let value = Float(bathrooms) else { return nil }
return KotlinFloat(float: value)
let bathroomsValue: KotlinDouble? = {
guard !bathrooms.isEmpty, let value = Double(bathrooms) else { return nil }
return KotlinDouble(double: value)
}()
let squareFootageValue: KotlinInt? = {
guard !squareFootage.isEmpty, let value = Int32(squareFootage) else { return nil }
return KotlinInt(int: value)
}()
let lotSizeValue: KotlinFloat? = {
guard !lotSize.isEmpty, let value = Float(lotSize) else { return nil }
return KotlinFloat(float: value)
let lotSizeValue: KotlinDouble? = {
guard !lotSize.isEmpty, let value = Double(lotSize) else { return nil }
return KotlinDouble(double: value)
}()
let yearBuiltValue: KotlinInt? = {
guard !yearBuilt.isEmpty, let value = Int32(yearBuilt) else { return nil }
@@ -286,7 +286,7 @@ struct ResidenceFormView: View {
let request = ResidenceCreateRequest(
name: name,
propertyType: propertyTypeValue,
propertyTypeId: propertyTypeValue,
streetAddress: streetAddress.isEmpty ? nil : streetAddress,
apartmentUnit: apartmentUnit.isEmpty ? nil : apartmentUnit,
city: city.isEmpty ? nil : city,
@@ -301,7 +301,7 @@ struct ResidenceFormView: View {
description: description.isEmpty ? nil : description,
purchaseDate: nil,
purchasePrice: nil,
isPrimary: isPrimary
isPrimary: KotlinBoolean(bool: isPrimary)
)
if let residence = existingResidence {

View File

@@ -2,7 +2,7 @@ import SwiftUI
import ComposeApp
struct OverviewCard: View {
let summary: OverallSummary
let summary: TotalSummary
var body: some View {
VStack(spacing: AppSpacing.lg) {

View File

@@ -2,7 +2,7 @@ import SwiftUI
import ComposeApp
struct PropertyHeaderCard: View {
let residence: Residence
let residence: ResidenceResponse
var body: some View {
VStack(alignment: .leading, spacing: 16) {
@@ -17,8 +17,8 @@ struct PropertyHeaderCard: View {
.fontWeight(.bold)
.foregroundColor(Color.appTextPrimary)
if let propertyType = residence.propertyType {
Text(propertyType)
if let propertyTypeName = residence.propertyTypeName {
Text(propertyTypeName)
.font(.caption)
.foregroundColor(Color.appTextSecondary)
}
@@ -30,20 +30,20 @@ struct PropertyHeaderCard: View {
Divider()
VStack(alignment: .leading, spacing: 4) {
if let streetAddress = residence.streetAddress {
Label(streetAddress, systemImage: "mappin.circle.fill")
if !residence.streetAddress.isEmpty {
Label(residence.streetAddress, systemImage: "mappin.circle.fill")
.font(.subheadline)
.foregroundColor(Color.appTextPrimary)
}
if residence.city != nil || residence.stateProvince != nil || residence.postalCode != nil {
Text("\(residence.city ?? ""), \(residence.stateProvince ?? "") \(residence.postalCode ?? "")")
if !residence.city.isEmpty || !residence.stateProvince.isEmpty || !residence.postalCode.isEmpty {
Text("\(residence.city), \(residence.stateProvince) \(residence.postalCode)")
.font(.subheadline)
.foregroundColor(Color.appTextSecondary)
}
if let country = residence.country, !country.isEmpty {
Text(country)
if !residence.country.isEmpty {
Text(residence.country)
.font(.caption)
.foregroundColor(Color.appTextSecondary)
}

View File

@@ -2,7 +2,7 @@ import SwiftUI
import ComposeApp
struct ResidenceCard: View {
let residence: ResidenceWithTasks
let residence: ResidenceResponse
var body: some View {
VStack(alignment: .leading, spacing: AppSpacing.md) {
@@ -26,8 +26,8 @@ struct ResidenceCard: View {
.foregroundColor(Color.appTextPrimary)
// .lineLimit(1)
if let propertyType = residence.propertyType {
Text(propertyType)
if let propertyTypeName = residence.propertyTypeName {
Text(propertyTypeName)
.font(.caption.weight(.medium))
.foregroundColor(Color.appTextSecondary)
.textCase(.uppercase)
@@ -51,18 +51,18 @@ struct ResidenceCard: View {
// Address
VStack(alignment: .leading, spacing: AppSpacing.xxs) {
if let streetAddress = residence.streetAddress {
if !residence.streetAddress.isEmpty {
HStack(spacing: AppSpacing.xxs) {
Image(systemName: "mappin.circle.fill")
.font(.system(size: 12, weight: .medium))
.foregroundColor(Color.appTextSecondary)
Text(streetAddress)
Text(residence.streetAddress)
.font(.callout)
.foregroundColor(Color.appTextSecondary)
}
}
if residence.city != nil || residence.stateProvince != nil {
if !residence.city.isEmpty || !residence.stateProvince.isEmpty {
HStack(spacing: AppSpacing.xxs) {
Image(systemName: "location.fill")
.font(.system(size: 12, weight: .medium))
@@ -99,16 +99,16 @@ struct ResidenceCard: View {
}
#Preview {
ResidenceCard(residence: ResidenceWithTasks(
ResidenceCard(residence: ResidenceResponse(
id: 1,
owner: 1,
ownerUsername: "testuser",
isPrimaryOwner: false,
userCount: 1,
ownerId: 1,
owner: ResidenceUserResponse(id: 1, username: "testuser", email: "test@test.com", firstName: "", lastName: ""),
users: [],
name: "My Home",
propertyType: "House",
propertyTypeId: 1,
propertyType: ResidenceType(id: 1, name: "House"),
streetAddress: "123 Main St",
apartmentUnit: nil,
apartmentUnit: "",
city: "San Francisco",
stateProvince: "CA",
postalCode: "94102",
@@ -118,44 +118,11 @@ struct ResidenceCard: View {
squareFootage: 1800,
lotSize: 0.25,
yearBuilt: 2010,
description: nil,
description: "",
purchaseDate: nil,
purchasePrice: nil,
isPrimary: true,
taskSummary: TaskSummary(
total: 10,
categories: [
TaskColumnCategory(
name: "overdue_tasks",
displayName: "Overdue",
icons: TaskColumnIcon(ios: "exclamationmark.triangle", android: "Warning", web: "exclamation-triangle"),
color: "#FF3B30",
count: 0
),
TaskColumnCategory(
name: "current_tasks",
displayName: "Current",
icons: TaskColumnIcon(ios: "calendar", android: "CalendarToday", web: "calendar"),
color: "#007AFF",
count: 5
),
TaskColumnCategory(
name: "in_progress_tasks",
displayName: "In Progress",
icons: TaskColumnIcon(ios: "play.circle", android: "PlayCircle", web: "play-circle"),
color: "#FF9500",
count: 2
),
TaskColumnCategory(
name: "done_tasks",
displayName: "Done",
icons: TaskColumnIcon(ios: "checkmark.circle", android: "CheckCircle", web: "check-circle"),
color: "#34C759",
count: 3
)
]
),
tasks: [],
isActive: true,
createdAt: "2024-01-01T00:00:00Z",
updatedAt: "2024-01-01T00:00:00Z"
))

View File

@@ -3,7 +3,7 @@ import ComposeApp
// MARK: - Share Code Card
struct ShareCodeCard: View {
let shareCode: ResidenceShareCode?
let shareCode: ShareCodeResponse?
let residenceName: String
let isGeneratingCode: Bool
let onGenerateCode: () -> Void

View File

@@ -2,7 +2,7 @@ import SwiftUI
import ComposeApp
struct SummaryCard: View {
let summary: MyResidencesSummary
let summary: TotalSummary
var body: some View {
VStack(spacing: 16) {
@@ -53,9 +53,11 @@ struct SummaryCard: View {
}
#Preview {
SummaryCard(summary: MyResidencesSummary(
SummaryCard(summary: TotalSummary(
totalResidences: 3,
totalTasks: 12,
totalPending: 2,
totalOverdue: 1,
tasksDueNextWeek: 4,
tasksDueNextMonth: 8
))

View File

@@ -3,7 +3,7 @@ import ComposeApp
// MARK: - User List Item
struct UserListItem: View {
let user: ResidenceUser
let user: ResidenceUserResponse
let isOwner: Bool
let isPrimaryOwner: Bool
let onRemove: () -> Void

View File

@@ -2,7 +2,7 @@ import SwiftUI
import ComposeApp
struct CompletionCardView: View {
let completion: TaskCompletion
let completion: TaskCompletionResponse
@State private var showPhotoSheet = false
var body: some View {
@@ -64,15 +64,16 @@ struct CompletionCardView: View {
.fontWeight(.medium)
}
if let notes = completion.notes {
Text(notes)
if !completion.notes.isEmpty {
Text(completion.notes)
.font(.caption2)
.foregroundColor(Color.appTextSecondary)
.lineLimit(2)
}
// Show button to view photos if images exist
if let images = completion.images, !images.isEmpty {
if !completion.images.isEmpty {
let images = completion.images
Button(action: {
showPhotoSheet = true
}) {
@@ -95,9 +96,7 @@ struct CompletionCardView: View {
.background(Color.appBackgroundSecondary.opacity(0.5))
.cornerRadius(8)
.sheet(isPresented: $showPhotoSheet) {
if let images = completion.images {
PhotoViewerSheet(images: images)
}
PhotoViewerSheet(images: completion.images)
}
}

View File

@@ -3,7 +3,7 @@ import ComposeApp
/// Task card that dynamically renders buttons based on the column's button types
struct DynamicTaskCard: View {
let task: TaskDetail
let task: TaskResponse
let buttonTypes: [String]
let onEdit: () -> Void
let onCancel: () -> Void
@@ -32,18 +32,18 @@ struct DynamicTaskCard: View {
Spacer()
PriorityBadge(priority: task.priority.name)
PriorityBadge(priority: task.priority?.name ?? "")
}
if let description = task.description_, !description.isEmpty {
Text(description)
if !task.description_.isEmpty {
Text(task.description_)
.font(.subheadline)
.foregroundColor(Color.appTextSecondary)
.lineLimit(2)
}
HStack {
Label(task.frequency.displayName, systemImage: "repeat")
Label(task.frequency?.displayName ?? "", systemImage: "repeat")
.font(.caption)
.foregroundColor(Color.appTextSecondary)

View File

@@ -4,11 +4,11 @@ import ComposeApp
/// Dynamic task column view that adapts based on the column configuration
struct DynamicTaskColumnView: View {
let column: TaskColumn
let onEditTask: (TaskDetail) -> Void
let onEditTask: (TaskResponse) -> Void
let onCancelTask: (Int32) -> Void
let onUncancelTask: (Int32) -> Void
let onMarkInProgress: (Int32) -> Void
let onCompleteTask: (TaskDetail) -> Void
let onCompleteTask: (TaskResponse) -> Void
let onArchiveTask: (Int32) -> Void
let onUnarchiveTask: (Int32) -> Void

View File

@@ -2,7 +2,7 @@ import SwiftUI
import ComposeApp
struct TaskCard: View {
let task: TaskDetail
let task: TaskResponse
let onEdit: () -> Void
let onCancel: (() -> Void)?
let onUncancel: (() -> Void)?
@@ -30,12 +30,12 @@ struct TaskCard: View {
Spacer()
PriorityBadge(priority: task.priority.name)
PriorityBadge(priority: task.priority?.name ?? "")
}
// Description
if let description = task.description_, !description.isEmpty {
Text(description)
if !task.description_.isEmpty {
Text(task.description_)
.font(.callout)
.foregroundColor(Color.appTextSecondary)
.lineLimit(3)
@@ -47,7 +47,7 @@ struct TaskCard: View {
Image(systemName: "repeat")
.font(.system(size: 12, weight: .medium))
.foregroundColor(Color.appTextSecondary.opacity(0.7))
Text(task.frequency.displayName)
Text(task.frequency?.displayName ?? "")
.font(.caption.weight(.medium))
.foregroundColor(Color.appTextSecondary)
}
@@ -255,27 +255,33 @@ struct TaskCard: View {
#Preview {
VStack(spacing: 16) {
TaskCard(
task: TaskDetail(
task: TaskResponse(
id: 1,
residence: 1,
residenceName: "Main House",
createdBy: 1,
createdByUsername: "testuser",
residenceId: 1,
createdById: 1,
createdBy: nil,
assignedToId: nil,
assignedTo: nil,
title: "Clean Gutters",
description: "Remove all debris from gutters",
category: TaskCategory(id: 1, name: "maintenance", orderId: 0, description: ""),
priority: TaskPriority(id: 2, name: "medium", displayName: "", orderId: 0, description: ""),
frequency: TaskFrequency(id: 1, name: "monthly", lookupName: "", displayName: "30", daySpan: 0, notifyDays: 0),
status: TaskStatus(id: 1, name: "pending", displayName: "", orderId: 0, description: ""),
categoryId: 1,
category: TaskCategory(id: 1, name: "maintenance", description: "", icon: "", color: "", displayOrder: 0),
priorityId: 2,
priority: TaskPriority(id: 2, name: "medium", level: 2, color: "", displayOrder: 0),
statusId: 1,
status: TaskStatus(id: 1, name: "pending", description: "", color: "", displayOrder: 0),
frequencyId: 1,
frequency: TaskFrequency(id: 1, name: "monthly", days: 30, displayOrder: 0),
dueDate: "2024-12-15",
intervalDays: 30,
estimatedCost: 150.00,
archived: false,
actualCost: nil,
contractorId: nil,
isCancelled: false,
isArchived: false,
parentTaskId: nil,
completions: [],
createdAt: "2024-01-01T00:00:00Z",
updatedAt: "2024-01-01T00:00:00Z",
nextScheduledDate: nil,
showCompletedButton: true,
completions: []
updatedAt: "2024-01-01T00:00:00Z"
),
onEdit: {},
onCancel: {},

View File

@@ -3,11 +3,11 @@ import ComposeApp
struct TasksSection: View {
let tasksResponse: TaskColumnsResponse
let onEditTask: (TaskDetail) -> Void
let onEditTask: (TaskResponse) -> Void
let onCancelTask: (Int32) -> Void
let onUncancelTask: (Int32) -> Void
let onMarkInProgress: (Int32) -> Void
let onCompleteTask: (TaskDetail) -> Void
let onCompleteTask: (TaskResponse) -> Void
let onArchiveTask: (Int32) -> Void
let onUnarchiveTask: (Int32) -> Void
@@ -79,27 +79,33 @@ struct TasksSection: View {
icons: ["ios": "calendar", "android": "CalendarToday", "web": "calendar"],
color: "#007AFF",
tasks: [
TaskDetail(
TaskResponse(
id: 1,
residence: 1,
residenceName: "Main House",
createdBy: 1,
createdByUsername: "testuser",
residenceId: 1,
createdById: 1,
createdBy: nil,
assignedToId: nil,
assignedTo: nil,
title: "Clean Gutters",
description: "Remove all debris",
category: TaskCategory(id: 1, name: "maintenance", orderId: 1, description: ""),
priority: TaskPriority(id: 2, name: "medium", displayName: "Medium", orderId: 1, description: ""),
frequency: TaskFrequency(id: 1, name: "monthly", lookupName: "", displayName: "Monthly", daySpan: 0, notifyDays: 0),
status: TaskStatus(id: 1, name: "pending", displayName: "Pending", orderId: 1, description: ""),
categoryId: 1,
category: TaskCategory(id: 1, name: "maintenance", description: "", icon: "", color: "", displayOrder: 0),
priorityId: 2,
priority: TaskPriority(id: 2, name: "medium", level: 2, color: "", displayOrder: 0),
statusId: 1,
status: TaskStatus(id: 1, name: "pending", description: "", color: "", displayOrder: 0),
frequencyId: 1,
frequency: TaskFrequency(id: 1, name: "monthly", days: 30, displayOrder: 0),
dueDate: "2024-12-15",
intervalDays: 30,
estimatedCost: 150.00,
archived: false,
actualCost: nil,
contractorId: nil,
isCancelled: false,
isArchived: false,
parentTaskId: nil,
completions: [],
createdAt: "2024-01-01T00:00:00Z",
updatedAt: "2024-01-01T00:00:00Z",
nextScheduledDate: nil,
showCompletedButton: true,
completions: []
updatedAt: "2024-01-01T00:00:00Z"
)
],
count: 1
@@ -111,27 +117,33 @@ struct TasksSection: View {
icons: ["ios": "checkmark.circle", "android": "CheckCircle", "web": "check-circle"],
color: "#34C759",
tasks: [
TaskDetail(
TaskResponse(
id: 2,
residence: 1,
residenceName: "Main House",
createdBy: 1,
createdByUsername: "testuser",
residenceId: 1,
createdById: 1,
createdBy: nil,
assignedToId: nil,
assignedTo: nil,
title: "Fix Leaky Faucet",
description: "Kitchen sink fixed",
category: TaskCategory(id: 2, name: "plumbing", orderId: 1, description: ""),
priority: TaskPriority(id: 3, name: "high", displayName: "High", orderId: 1, description: ""),
frequency: TaskFrequency(id: 6, name: "once", lookupName: "", displayName: "One Time", daySpan: 0, notifyDays: 0),
status: TaskStatus(id: 3, name: "completed", displayName: "Completed", orderId: 1, description: ""),
categoryId: 2,
category: TaskCategory(id: 2, name: "plumbing", description: "", icon: "", color: "", displayOrder: 0),
priorityId: 3,
priority: TaskPriority(id: 3, name: "high", level: 3, color: "", displayOrder: 0),
statusId: 3,
status: TaskStatus(id: 3, name: "completed", description: "", color: "", displayOrder: 0),
frequencyId: 6,
frequency: TaskFrequency(id: 6, name: "once", days: nil, displayOrder: 0),
dueDate: "2024-11-01",
intervalDays: nil,
estimatedCost: 200.00,
archived: false,
actualCost: nil,
contractorId: nil,
isCancelled: false,
isArchived: false,
parentTaskId: nil,
completions: [],
createdAt: "2024-10-01T00:00:00Z",
updatedAt: "2024-11-05T00:00:00Z",
nextScheduledDate: nil,
showCompletedButton: false,
completions: []
updatedAt: "2024-11-05T00:00:00Z"
)
],
count: 1

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,