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:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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: {},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user