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

@@ -289,7 +289,7 @@ fun App(
EditResidenceRoute(
residenceId = residence.id,
name = residence.name,
propertyType = residence.propertyType?.toInt(),
propertyType = residence.propertyTypeId,
streetAddress = residence.streetAddress,
apartmentUnit = residence.apartmentUnit,
city = residence.city,
@@ -297,16 +297,16 @@ fun App(
postalCode = residence.postalCode,
country = residence.country,
bedrooms = residence.bedrooms,
bathrooms = residence.bathrooms,
bathrooms = residence.bathrooms?.toFloat(),
squareFootage = residence.squareFootage,
lotSize = residence.lotSize,
lotSize = residence.lotSize?.toFloat(),
yearBuilt = residence.yearBuilt,
description = residence.description,
isPrimary = residence.isPrimary,
ownerUserName = residence.ownerUsername,
createdAt = residence.createdAt,
updatedAt = residence.updatedAt,
owner = residence.owner
owner = residence.ownerId
)
)
},
@@ -314,15 +314,15 @@ fun App(
navController.navigate(
EditTaskRoute(
taskId = task.id,
residenceId = task.residence,
residenceId = task.residenceId,
title = task.title,
description = task.description,
categoryId = task.category.id,
categoryName = task.category.name,
frequencyId = task.frequency.id,
frequencyName = task.frequency.name,
priorityId = task.priority.id,
priorityName = task.priority.name,
categoryId = task.category?.id ?: 0,
categoryName = task.category?.name ?: "",
frequencyId = task.frequency?.id ?: 0,
frequencyName = task.frequency?.name ?: "",
priorityId = task.priority?.id ?: 0,
priorityName = task.priority?.name ?: "",
statusId = task.status?.id,
statusName = task.status?.name,
dueDate = task.dueDate,
@@ -402,25 +402,24 @@ fun App(
EditResidenceScreen(
residence = Residence(
id = route.residenceId,
ownerId = route.owner ?: 0,
name = route.name,
propertyType = route.propertyType.toString(), // Will be fetched from lookups
streetAddress = route.streetAddress,
apartmentUnit = route.apartmentUnit,
city = route.city,
stateProvince = route.stateProvince,
postalCode = route.postalCode,
country = route.country,
propertyTypeId = route.propertyType,
streetAddress = route.streetAddress ?: "",
apartmentUnit = route.apartmentUnit ?: "",
city = route.city ?: "",
stateProvince = route.stateProvince ?: "",
postalCode = route.postalCode ?: "",
country = route.country ?: "",
bedrooms = route.bedrooms,
bathrooms = route.bathrooms,
bathrooms = route.bathrooms?.toDouble(),
squareFootage = route.squareFootage,
lotSize = route.lotSize,
lotSize = route.lotSize?.toDouble(),
yearBuilt = route.yearBuilt,
description = route.description,
description = route.description ?: "",
purchaseDate = null,
purchasePrice = null,
isPrimary = route.isPrimary,
ownerUsername = route.ownerUserName,
owner = route.owner,
createdAt = route.createdAt,
updatedAt = route.updatedAt
),
@@ -455,7 +454,7 @@ fun App(
EditResidenceRoute(
residenceId = residence.id,
name = residence.name,
propertyType = residence.propertyType?.toInt(),
propertyType = residence.propertyTypeId,
streetAddress = residence.streetAddress,
apartmentUnit = residence.apartmentUnit,
city = residence.city,
@@ -463,16 +462,16 @@ fun App(
postalCode = residence.postalCode,
country = residence.country,
bedrooms = residence.bedrooms,
bathrooms = residence.bathrooms,
bathrooms = residence.bathrooms?.toFloat(),
squareFootage = residence.squareFootage,
lotSize = residence.lotSize,
lotSize = residence.lotSize?.toFloat(),
yearBuilt = residence.yearBuilt,
description = residence.description,
isPrimary = residence.isPrimary,
ownerUserName = residence.ownerUsername,
createdAt = residence.createdAt,
updatedAt = residence.updatedAt,
owner = residence.owner
owner = residence.ownerId
)
)
},
@@ -480,15 +479,15 @@ fun App(
navController.navigate(
EditTaskRoute(
taskId = task.id,
residenceId = task.residence,
residenceId = task.residenceId,
title = task.title,
description = task.description,
categoryId = task.category.id,
categoryName = task.category.name,
frequencyId = task.frequency.id,
frequencyName = task.frequency.name,
priorityId = task.priority.id,
priorityName = task.priority.name,
categoryId = task.category?.id ?: 0,
categoryName = task.category?.name ?: "",
frequencyId = task.frequency?.id ?: 0,
frequencyName = task.frequency?.name ?: "",
priorityId = task.priority?.id ?: 0,
priorityName = task.priority?.name ?: "",
statusId = task.status?.id,
statusName = task.status?.name,
dueDate = task.dueDate,
@@ -506,25 +505,24 @@ fun App(
EditTaskScreen(
task = TaskDetail(
id = route.taskId,
residence = route.residenceId,
residenceId = route.residenceId,
createdById = 0,
title = route.title,
description = route.description,
category = TaskCategory(route.categoryId, route.categoryName),
description = route.description ?: "",
category = TaskCategory(id = route.categoryId, name = route.categoryName),
frequency = TaskFrequency(
route.frequencyId, route.frequencyName, "", route.frequencyName,
daySpan = 0,
notifyDays = 0
id = route.frequencyId,
name = route.frequencyName,
days = null
),
priority = TaskPriority(route.priorityId, route.priorityName, displayName = route.statusName ?: ""),
priority = TaskPriority(id = route.priorityId, name = route.priorityName),
status = route.statusId?.let {
TaskStatus(it, route.statusName ?: "", displayName = route.statusName ?: "")
TaskStatus(id = it, name = route.statusName ?: "")
},
dueDate = route.dueDate,
estimatedCost = route.estimatedCost?.toDoubleOrNull(),
createdAt = route.createdAt,
updatedAt = route.updatedAt,
nextScheduledDate = null,
showCompletedButton = false,
completions = emptyList()
),
onNavigateBack = { navController.popBackStack() },

View File

@@ -3,146 +3,163 @@ package com.example.mycrib.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* User reference for task-related responses - matching Go API TaskUserResponse
*/
@Serializable
data class CustomTask (
data class TaskUserResponse(
val id: Int,
val residence: Int,
@SerialName("created_by") val createdBy: Int,
@SerialName("created_by_username") val createdByUsername: String,
val username: String,
val email: String,
@SerialName("first_name") val firstName: String = "",
@SerialName("last_name") val lastName: String = ""
) {
val displayName: String
get() = when {
firstName.isNotBlank() && lastName.isNotBlank() -> "$firstName $lastName"
firstName.isNotBlank() -> firstName
lastName.isNotBlank() -> lastName
else -> username
}
}
/**
* Task response matching Go API TaskResponse
*/
@Serializable
data class TaskResponse(
val id: Int,
@SerialName("residence_id") val residenceId: Int,
@SerialName("created_by_id") val createdById: Int,
@SerialName("created_by") val createdBy: TaskUserResponse? = null,
@SerialName("assigned_to_id") val assignedToId: Int? = null,
@SerialName("assigned_to") val assignedTo: TaskUserResponse? = null,
val title: String,
val description: String? = null,
val category: TaskCategory?,
val frequency: TaskFrequency,
val priority: TaskPriority,
val description: String = "",
@SerialName("category_id") val categoryId: Int? = null,
val category: TaskCategory? = null,
@SerialName("priority_id") val priorityId: Int? = null,
val priority: TaskPriority? = null,
@SerialName("status_id") val statusId: Int? = null,
val status: TaskStatus? = null,
@SerialName("due_date") val dueDate: String?,
@SerialName("next_scheduled_date") val nextScheduledDate: String? = null,
@SerialName("frequency_id") val frequencyId: Int? = null,
val frequency: TaskFrequency? = null,
@SerialName("due_date") val dueDate: String? = null,
@SerialName("estimated_cost") val estimatedCost: Double? = null,
@SerialName("actual_cost") val actualCost: Double? = null,
@SerialName("completion_count") val completionCount: Int? = null,
val notes: String? = null,
val archived: Boolean = false,
@SerialName("contractor_id") val contractorId: Int? = null,
@SerialName("is_cancelled") val isCancelled: Boolean = false,
@SerialName("is_archived") val isArchived: Boolean = false,
@SerialName("parent_task_id") val parentTaskId: Int? = null,
val completions: List<TaskCompletionResponse> = emptyList(),
@SerialName("created_at") val createdAt: String,
@SerialName("updated_at") val updatedAt: String,
@SerialName("show_completed_button") val showCompletedButton: Boolean = false,
@SerialName("days_until_due") val daysUntilDue: Int? = null,
@SerialName("is_overdue") val isOverdue: Boolean? = null,
@SerialName("last_completion") val lastCompletion: LastCompletion? = null
)
@SerialName("updated_at") val updatedAt: String
) {
// Helper for backwards compatibility with old code
val archived: Boolean get() = isArchived
// Aliases for backwards compatibility with UI code expecting old field names
val residence: Int get() = residenceId
// Helper to get created by username
val createdByUsername: String get() = createdBy?.displayName ?: ""
// Computed properties for UI compatibility (these were in Django API but not Go API)
val categoryName: String? get() = category?.name
val categoryDescription: String? get() = category?.description
val frequencyName: String? get() = frequency?.name
val frequencyDisplayName: String? get() = frequency?.displayName
val frequencyDaySpan: Int? get() = frequency?.days
val priorityName: String? get() = priority?.name
val priorityDisplayName: String? get() = priority?.displayName
val statusName: String? get() = status?.name
// Fields that don't exist in Go API - return null/default
val nextScheduledDate: String? get() = null // Would need calculation based on frequency
val showCompletedButton: Boolean get() = status?.name?.lowercase() != "completed"
val daySpan: Int? get() = frequency?.days
val notifyDays: Int? get() = null // Not in Go API
}
/**
* Task completion response matching Go API TaskCompletionResponse
*/
@Serializable
data class LastCompletion(
@SerialName("completion_date") val completionDate: String,
@SerialName("completed_by") val completedBy: String?,
@SerialName("actual_cost") val actualCost: Double?,
val rating: Int?
)
data class TaskCompletionResponse(
val id: Int,
@SerialName("task_id") val taskId: Int,
@SerialName("completed_by") val completedBy: TaskUserResponse? = null,
@SerialName("completed_at") val completedAt: String,
val notes: String = "",
@SerialName("actual_cost") val actualCost: Double? = null,
@SerialName("photo_url") val photoUrl: String = "",
@SerialName("created_at") val createdAt: String
) {
// Helper for backwards compatibility
val completionDate: String get() = completedAt
val completedByName: String? get() = completedBy?.displayName
// Backwards compatibility for UI that expects these fields
val task: Int get() = taskId
val images: List<TaskCompletionImage> get() = if (photoUrl.isNotEmpty()) {
listOf(TaskCompletionImage(id = 0, imageUrl = photoUrl))
} else {
emptyList()
}
val rating: Int? get() = null // Not in Go API
val contractor: Int? get() = null // Not in Go API - would need to be fetched separately
val contractorDetails: ContractorMinimal? get() = null // Not in Go API
}
/**
* Task create request matching Go API CreateTaskRequest
*/
@Serializable
data class TaskCreateRequest(
val residence: Int,
@SerialName("residence_id") val residenceId: Int,
val title: String,
val description: String? = null,
val category: Int,
val frequency: Int,
@SerialName("interval_days") val intervalDays: Int? = null,
val priority: Int,
val status: Int? = null,
@SerialName("due_date") val dueDate: String,
@SerialName("category_id") val categoryId: Int? = null,
@SerialName("priority_id") val priorityId: Int? = null,
@SerialName("status_id") val statusId: Int? = null,
@SerialName("frequency_id") val frequencyId: Int? = null,
@SerialName("assigned_to_id") val assignedToId: Int? = null,
@SerialName("due_date") val dueDate: String? = null,
@SerialName("estimated_cost") val estimatedCost: Double? = null,
val archived: Boolean = false
)
@Serializable
data class TaskDetail(
val id: Int,
val residence: Int,
@SerialName("residence_name") val residenceName: String? = null,
@SerialName("created_by") val createdBy: Int? = null,
@SerialName("created_by_username") val createdByUsername: String? = null,
val title: String,
val description: String?,
val category: TaskCategory,
val priority: TaskPriority,
val frequency: TaskFrequency,
val status: TaskStatus?,
@SerialName("due_date") val dueDate: String?,
@SerialName("interval_days") val intervalDays: Int? = null,
@SerialName("estimated_cost") val estimatedCost: Double? = null,
val archived: Boolean = false,
@SerialName("created_at") val createdAt: String,
@SerialName("updated_at") val updatedAt: String,
@SerialName("next_scheduled_date") val nextScheduledDate: String? = null,
@SerialName("show_completed_button") val showCompletedButton: Boolean = false,
val completions: List<TaskCompletion>
)
@Serializable
data class TasksByResidenceResponse(
@SerialName("residence_id") val residenceId: String,
@SerialName("days_threshold") val daysThreshold: Int,
val summary: CategorizedTaskSummary,
@SerialName("upcoming_tasks") val upcomingTasks: List<TaskDetail>,
@SerialName("in_progress_tasks") val inProgressTasks: List<TaskDetail>,
@SerialName("done_tasks") val doneTasks: List<TaskDetail>,
@SerialName("archived_tasks") val archivedTasks: List<TaskDetail>
)
@Serializable
data class CategorizedTaskSummary(
val upcoming: Int,
@SerialName("in_progress") val inProgress: Int,
val done: Int,
val archived: Int
)
@Serializable
data class AllTasksResponse(
@SerialName("days_threshold") val daysThreshold: Int,
val summary: CategorizedTaskSummary,
@SerialName("upcoming_tasks") val upcomingTasks: List<TaskDetail>,
@SerialName("in_progress_tasks") val inProgressTasks: List<TaskDetail>,
@SerialName("done_tasks") val doneTasks: List<TaskDetail>,
@SerialName("archived_tasks") val archivedTasks: List<TaskDetail>
)
@Serializable
data class TaskCancelResponse(
val message: String,
val task: TaskDetail
@SerialName("contractor_id") val contractorId: Int? = null
)
/**
* Request model for PATCH updates to a task.
* Used for status changes and archive/unarchive operations.
* Task update request matching Go API UpdateTaskRequest
* All fields are optional - only provided fields will be updated.
*/
@Serializable
data class TaskPatchRequest(
val status: Int? = null, // Status ID to update
val archived: Boolean? = null // Archive/unarchive flag
data class TaskUpdateRequest(
val title: String? = null,
val description: String? = null,
@SerialName("category_id") val categoryId: Int? = null,
@SerialName("priority_id") val priorityId: Int? = null,
@SerialName("status_id") val statusId: Int? = null,
@SerialName("frequency_id") val frequencyId: Int? = null,
@SerialName("assigned_to_id") val assignedToId: Int? = null,
@SerialName("due_date") val dueDate: String? = null,
@SerialName("estimated_cost") val estimatedCost: Double? = null,
@SerialName("actual_cost") val actualCost: Double? = null,
@SerialName("contractor_id") val contractorId: Int? = null
)
/**
* Minimal task model for list/kanban views.
* Uses IDs instead of nested objects for efficiency.
* Resolve IDs to full objects via DataCache.getTaskCategory(), etc.
* Task action response (for cancel, archive, mark in progress, etc.)
*/
@Serializable
data class TaskMinimal(
val id: Int,
val title: String,
val description: String? = null,
@SerialName("due_date") val dueDate: String? = null,
@SerialName("next_scheduled_date") val nextScheduledDate: String? = null,
@SerialName("category_id") val categoryId: Int? = null,
@SerialName("frequency_id") val frequencyId: Int,
@SerialName("priority_id") val priorityId: Int,
@SerialName("status_id") val statusId: Int? = null,
@SerialName("completion_count") val completionCount: Int? = null,
val archived: Boolean = false
data class TaskActionResponse(
val message: String,
val task: TaskResponse
)
/**
* Kanban column response matching Go API KanbanColumnResponse
*/
@Serializable
data class TaskColumn(
val name: String,
@@ -150,13 +167,45 @@ data class TaskColumn(
@SerialName("button_types") val buttonTypes: List<String>,
val icons: Map<String, String>,
val color: String,
val tasks: List<TaskDetail>, // Keep using TaskDetail for now - will be TaskMinimal after full migration
val tasks: List<TaskResponse>,
val count: Int
)
/**
* Kanban board response matching Go API KanbanBoardResponse
*/
@Serializable
data class TaskColumnsResponse(
val columns: List<TaskColumn>,
@SerialName("days_threshold") val daysThreshold: Int? = null,
@SerialName("residence_id") val residenceId: String? = null
@SerialName("days_threshold") val daysThreshold: Int,
@SerialName("residence_id") val residenceId: String
)
/**
* Task patch request for partial updates (status changes, archive/unarchive)
*/
@Serializable
data class TaskPatchRequest(
@SerialName("status_id") val status: Int? = null,
@SerialName("is_archived") val archived: Boolean? = null,
@SerialName("is_cancelled") val cancelled: Boolean? = null
)
/**
* Task completion image model
*/
@Serializable
data class TaskCompletionImage(
val id: Int,
@SerialName("image_url") val imageUrl: String,
val caption: String? = null,
@SerialName("uploaded_at") val uploadedAt: String? = null
) {
// Alias for backwards compatibility
val image: String get() = imageUrl
}
// Type aliases for backwards compatibility with existing code
typealias CustomTask = TaskResponse
typealias TaskDetail = TaskResponse
typealias TaskCompletion = TaskCompletionResponse

View File

@@ -3,97 +3,134 @@ package com.example.mycrib.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Residence type lookup - matching Go API
* Note: Go API returns arrays directly, no wrapper
*/
@Serializable
data class ResidenceType(
val id: Int,
val name: String
)
/**
* Task frequency lookup - matching Go API TaskFrequencyResponse
*/
@Serializable
data class TaskFrequency(
val id: Int,
val name: String,
val days: Int? = null,
@SerialName("display_order") val displayOrder: Int = 0
) {
// Helper for display
val displayName: String
get() = name
}
/**
* Task priority lookup - matching Go API TaskPriorityResponse
*/
@Serializable
data class TaskPriority(
val id: Int,
val name: String,
val level: Int = 0,
val color: String = "",
@SerialName("display_order") val displayOrder: Int = 0
) {
// Helper for display
val displayName: String
get() = name
}
/**
* Task status lookup - matching Go API TaskStatusResponse
*/
@Serializable
data class TaskStatus(
val id: Int,
val name: String,
val description: String = "",
val color: String = "",
@SerialName("display_order") val displayOrder: Int = 0
) {
// Helper for display
val displayName: String
get() = name
}
/**
* Task category lookup - matching Go API TaskCategoryResponse
*/
@Serializable
data class TaskCategory(
val id: Int,
val name: String,
val description: String = "",
val icon: String = "",
val color: String = "",
@SerialName("display_order") val displayOrder: Int = 0
)
/**
* Contractor specialty lookup
*/
@Serializable
data class ContractorSpecialty(
val id: Int,
val name: String
)
/**
* Static data response - all lookups in one call
* Note: This may need adjustment based on Go API implementation
*/
@Serializable
data class StaticDataResponse(
@SerialName("residence_types") val residenceTypes: List<ResidenceType>,
@SerialName("task_frequencies") val taskFrequencies: List<TaskFrequency>,
@SerialName("task_priorities") val taskPriorities: List<TaskPriority>,
@SerialName("task_statuses") val taskStatuses: List<TaskStatus>,
@SerialName("task_categories") val taskCategories: List<TaskCategory>,
@SerialName("contractor_specialties") val contractorSpecialties: List<ContractorSpecialty>
)
// Legacy wrapper responses for backward compatibility
// These can be removed once all code is migrated to use arrays directly
@Serializable
data class ResidenceTypeResponse(
val count: Int,
val results: List<ResidenceType>
)
@Serializable
data class ResidenceType(
val id: Int,
val name: String,
val description: String? = null
)
@Serializable
data class TaskFrequencyResponse(
val count: Int,
val results: List<TaskFrequency>
)
@Serializable
data class TaskFrequency(
val id: Int,
val name: String,
@SerialName("lookup_name") val lookupName: String,
@SerialName("display_name") val displayName: String,
@SerialName("day_span") val daySpan: Int? = null,
@SerialName("notify_days") val notifyDays: Int? = null
)
@Serializable
data class TaskPriorityResponse(
val count: Int,
val results: List<TaskPriority>
)
@Serializable
data class TaskPriority(
val id: Int,
val name: String,
@SerialName("display_name") val displayName: String,
@SerialName("order_id") val orderId: Int = 0,
val description: String? = null
)
@Serializable
data class TaskStatusResponse(
val count: Int,
val results: List<TaskStatus>
)
@Serializable
data class TaskStatus(
val id: Int,
val name: String,
@SerialName("display_name") val displayName: String,
@SerialName("order_id") val orderId: Int = 0,
val description: String? = null
)
@Serializable
data class TaskCategoryResponse(
val count: Int,
val results: List<TaskCategory>
)
@Serializable
data class TaskCategory(
val id: Int,
val name: String,
@SerialName("order_id") val orderId: Int = 0,
val description: String? = null
)
@Serializable
data class ContractorSpecialtyResponse(
val count: Int,
val results: List<ContractorSpecialty>
)
@Serializable
data class ContractorSpecialty(
val id: Int,
val name: String
)
@Serializable
data class StaticDataResponse(
val residenceTypes: List<ResidenceType>,
val taskFrequencies: List<TaskFrequency>,
val taskPriorities: List<TaskPriority>,
val taskStatuses: List<TaskStatus>,
val taskCategories: List<TaskCategory>,
val contractorSpecialties: List<ContractorSpecialty>
)

View File

@@ -3,38 +3,81 @@ package com.example.mycrib.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* User reference for residence responses - matching Go API ResidenceUserResponse
*/
@Serializable
data class Residence(
data class ResidenceUserResponse(
val id: Int,
val owner: Int? = null,
@SerialName("owner_username") val ownerUsername: String,
@SerialName("is_primary_owner") val isPrimaryOwner: Boolean = false,
@SerialName("user_count") val userCount: Int = 1,
val username: String,
val email: String,
@SerialName("first_name") val firstName: String = "",
@SerialName("last_name") val lastName: String = ""
) {
val displayName: String
get() = when {
firstName.isNotBlank() && lastName.isNotBlank() -> "$firstName $lastName"
firstName.isNotBlank() -> firstName
lastName.isNotBlank() -> lastName
else -> username
}
}
/**
* Residence response matching Go API ResidenceResponse
*/
@Serializable
data class ResidenceResponse(
val id: Int,
@SerialName("owner_id") val ownerId: Int,
val owner: ResidenceUserResponse? = null,
val users: List<ResidenceUserResponse> = emptyList(),
val name: String,
@SerialName("property_type") val propertyType: String? = null,
@SerialName("street_address") val streetAddress: String? = null,
@SerialName("apartment_unit") val apartmentUnit: String? = null,
val city: String? = null,
@SerialName("state_province") val stateProvince: String? = null,
@SerialName("postal_code") val postalCode: String? = null,
val country: String? = null,
@SerialName("property_type_id") val propertyTypeId: Int? = null,
@SerialName("property_type") val propertyType: ResidenceType? = null,
@SerialName("street_address") val streetAddress: String = "",
@SerialName("apartment_unit") val apartmentUnit: String = "",
val city: String = "",
@SerialName("state_province") val stateProvince: String = "",
@SerialName("postal_code") val postalCode: String = "",
val country: String = "",
val bedrooms: Int? = null,
val bathrooms: Float? = null,
val bathrooms: Double? = null,
@SerialName("square_footage") val squareFootage: Int? = null,
@SerialName("lot_size") val lotSize: Float? = null,
@SerialName("lot_size") val lotSize: Double? = null,
@SerialName("year_built") val yearBuilt: Int? = null,
val description: String? = null,
val description: String = "",
@SerialName("purchase_date") val purchaseDate: String? = null,
@SerialName("purchase_price") val purchasePrice: Double? = null,
@SerialName("is_primary") val isPrimary: Boolean = false,
@SerialName("is_active") val isActive: Boolean = true,
@SerialName("created_at") val createdAt: String,
@SerialName("updated_at") val updatedAt: String
)
) {
// Helper to get owner username
val ownerUsername: String get() = owner?.displayName ?: ""
// Helper to get property type name
val propertyTypeName: String? get() = propertyType?.name
// Backwards compatibility for UI code
// Note: isPrimaryOwner requires comparing with current user - can't be computed here
// UI components should check ownerId == currentUserId instead
// Stub task summary for UI compatibility (Go API doesn't return this per-residence)
val taskSummary: ResidenceTaskSummary get() = ResidenceTaskSummary()
// Stub summary for UI compatibility
val summary: ResidenceSummaryResponse get() = ResidenceSummaryResponse(id = id, name = name)
}
/**
* Residence create request matching Go API CreateResidenceRequest
*/
@Serializable
data class ResidenceCreateRequest(
val name: String,
@SerialName("property_type") val propertyType: Int? = null,
@SerialName("property_type_id") val propertyTypeId: Int? = null,
@SerialName("street_address") val streetAddress: String? = null,
@SerialName("apartment_unit") val apartmentUnit: String? = null,
val city: String? = null,
@@ -42,189 +85,172 @@ data class ResidenceCreateRequest(
@SerialName("postal_code") val postalCode: String? = null,
val country: String? = null,
val bedrooms: Int? = null,
val bathrooms: Float? = null,
val bathrooms: Double? = null,
@SerialName("square_footage") val squareFootage: Int? = null,
@SerialName("lot_size") val lotSize: Float? = null,
@SerialName("year_built") val yearBuilt: Int? = null,
val description: String? = null,
@SerialName("purchase_date") val purchaseDate: String? = null,
@SerialName("purchase_price") val purchasePrice: String? = null,
@SerialName("is_primary") val isPrimary: Boolean = false
)
@Serializable
data class TaskColumnIcon(
val ios: String,
val android: String,
val web: String
)
@Serializable
data class TaskColumnCategory(
val name: String,
@SerialName("display_name") val displayName: String,
val icons: TaskColumnIcon,
val color: String,
val count: Int
)
@Serializable
data class TaskSummary(
val total: Int,
val categories: List<TaskColumnCategory>
)
@Serializable
data class ResidenceSummary(
val id: Int,
val owner: Int,
@SerialName("owner_username") val ownerUsername: String,
val name: String,
@SerialName("property_type") val propertyType: String? = null,
@SerialName("street_address") val streetAddress: String? = null,
@SerialName("apartment_unit") val apartmentUnit: String? = null,
val city: String? = null,
@SerialName("state_province") val stateProvince: String? = null,
@SerialName("postal_code") val postalCode: String? = null,
val country: String? = null,
@SerialName("is_primary") val isPrimary: Boolean,
@SerialName("task_summary") val taskSummary: TaskSummary,
@SerialName("last_completed_task") val lastCompletedCustomTask: CustomTask?,
@SerialName("next_upcoming_task") val nextUpcomingCustomTask: CustomTask?,
@SerialName("created_at") val createdAt: String,
@SerialName("updated_at") val updatedAt: String
)
@Serializable
data class ResidenceSummaryResponse(
val summary: OverallSummary,
val residences: List<ResidenceSummary>
)
@Serializable
data class OverallSummary(
@SerialName("total_residences") val totalResidences: Int,
@SerialName("total_tasks") val totalTasks: Int,
@SerialName("total_completed") val totalCompleted: Int,
@SerialName("total_pending") val totalPending: Int,
@SerialName("tasks_due_next_week") val tasksDueNextWeek: Int,
@SerialName("tasks_due_next_month") val tasksDueNextMonth: Int
)
@Serializable
data class ResidenceWithTasks(
val id: Int,
val owner: Int,
@SerialName("owner_username") val ownerUsername: String,
@SerialName("is_primary_owner") val isPrimaryOwner: Boolean = false,
@SerialName("user_count") val userCount: Int = 1,
val name: String,
@SerialName("property_type") val propertyType: String? = null,
@SerialName("street_address") val streetAddress: String? = null,
@SerialName("apartment_unit") val apartmentUnit: String? = null,
val city: String? = null,
@SerialName("state_province") val stateProvince: String? = null,
@SerialName("postal_code") val postalCode: String? = null,
val country: String? = null,
val bedrooms: Int? = null,
val bathrooms: Float? = null,
@SerialName("square_footage") val squareFootage: Int? = null,
@SerialName("lot_size") val lotSize: Float? = null,
@SerialName("lot_size") val lotSize: Double? = null,
@SerialName("year_built") val yearBuilt: Int? = null,
val description: String? = null,
@SerialName("purchase_date") val purchaseDate: String? = null,
@SerialName("purchase_price") val purchasePrice: Double? = null,
@SerialName("is_primary") val isPrimary: Boolean,
@SerialName("task_summary") val taskSummary: TaskSummary,
val tasks: List<TaskDetail>,
@SerialName("created_at") val createdAt: String,
@SerialName("updated_at") val updatedAt: String
)
@Serializable
data class MyResidencesSummary(
@SerialName("total_residences") val totalResidences: Int,
@SerialName("total_tasks") val totalTasks: Int,
@SerialName("tasks_due_next_week") val tasksDueNextWeek: Int,
@SerialName("tasks_due_next_month") val tasksDueNextMonth: Int
)
@Serializable
data class MyResidencesResponse(
val summary: MyResidencesSummary,
val residences: List<ResidenceWithTasks>
@SerialName("is_primary") val isPrimary: Boolean? = null
)
/**
* Minimal residence model for list views.
* Uses property_type_id and annotated counts instead of nested objects.
* Resolve property type via DataCache.getResidenceType(residence.propertyTypeId)
* Residence update request matching Go API UpdateResidenceRequest
*/
@Serializable
data class ResidenceMinimal(
val id: Int,
val name: String,
data class ResidenceUpdateRequest(
val name: String? = null,
@SerialName("property_type_id") val propertyTypeId: Int? = null,
@SerialName("street_address") val streetAddress: String? = null,
@SerialName("apartment_unit") val apartmentUnit: String? = null,
val city: String? = null,
@SerialName("state_province") val stateProvince: String? = null,
@SerialName("postal_code") val postalCode: String? = null,
val country: String? = null,
val bedrooms: Int? = null,
val bathrooms: Float? = null,
@SerialName("is_primary") val isPrimary: Boolean = false,
@SerialName("is_primary_owner") val isPrimaryOwner: Boolean = false,
@SerialName("user_count") val userCount: Int = 1,
// Annotated counts from database (no N+1 queries)
@SerialName("task_count") val taskCount: Int = 0,
@SerialName("tasks_pending") val tasksPending: Int = 0,
@SerialName("tasks_overdue") val tasksOverdue: Int = 0,
@SerialName("tasks_due_week") val tasksDueWeek: Int = 0,
// Reference to last/next task (just ID and date, not full object)
@SerialName("last_completed_task_id") val lastCompletedTaskId: Int? = null,
@SerialName("last_completed_date") val lastCompletedDate: String? = null,
@SerialName("next_task_id") val nextTaskId: Int? = null,
@SerialName("next_task_date") val nextTaskDate: String? = null,
val bathrooms: Double? = null,
@SerialName("square_footage") val squareFootage: Int? = null,
@SerialName("lot_size") val lotSize: Double? = null,
@SerialName("year_built") val yearBuilt: Int? = null,
val description: String? = null,
@SerialName("purchase_date") val purchaseDate: String? = null,
@SerialName("purchase_price") val purchasePrice: Double? = null,
@SerialName("is_primary") val isPrimary: Boolean? = null
)
/**
* Share code response matching Go API ShareCodeResponse
*/
@Serializable
data class ShareCodeResponse(
val id: Int,
val code: String,
@SerialName("residence_id") val residenceId: Int,
@SerialName("created_by_id") val createdById: Int,
@SerialName("is_active") val isActive: Boolean,
@SerialName("expires_at") val expiresAt: String?,
@SerialName("created_at") val createdAt: String
)
// Share Code Models
/**
* Generate share code request
*/
@Serializable
data class ResidenceShareCode(
val id: Int,
val code: String,
val residence: Int,
@SerialName("residence_name") val residenceName: String,
@SerialName("created_by") val createdBy: Int,
@SerialName("created_by_username") val createdByUsername: String,
@SerialName("is_active") val isActive: Boolean,
@SerialName("created_at") val createdAt: String,
@SerialName("expires_at") val expiresAt: String?
data class GenerateShareCodeRequest(
@SerialName("expires_in_hours") val expiresInHours: Int? = null
)
/**
* Generate share code response matching Go API GenerateShareCodeResponse
*/
@Serializable
data class GenerateShareCodeResponse(
val message: String,
@SerialName("share_code") val shareCode: ShareCodeResponse
)
/**
* Join residence request
*/
@Serializable
data class JoinResidenceRequest(
val code: String
)
/**
* Join residence response matching Go API JoinResidenceResponse
*/
@Serializable
data class JoinResidenceResponse(
val message: String,
val residence: Residence
val residence: ResidenceResponse
)
// User Management Models
/**
* Total summary for dashboard display
*/
@Serializable
data class ResidenceUser(
val id: Int,
val username: String,
val email: String,
@SerialName("first_name") val firstName: String?,
@SerialName("last_name") val lastName: String?
data class TotalSummary(
@SerialName("total_residences") val totalResidences: Int = 0,
@SerialName("total_tasks") val totalTasks: Int = 0,
@SerialName("total_pending") val totalPending: Int = 0,
@SerialName("total_overdue") val totalOverdue: Int = 0,
@SerialName("tasks_due_next_week") val tasksDueNextWeek: Int = 0,
@SerialName("tasks_due_next_month") val tasksDueNextMonth: Int = 0
)
/**
* My residences response - list of user's residences
* Go API returns array directly, this wraps for consistency
*/
@Serializable
data class MyResidencesResponse(
val residences: List<ResidenceResponse>,
val summary: TotalSummary = TotalSummary()
)
/**
* Residence summary response for dashboard
*/
@Serializable
data class ResidenceSummaryResponse(
val id: Int = 0,
val name: String = "",
@SerialName("task_count") val taskCount: Int = 0,
@SerialName("pending_count") val pendingCount: Int = 0,
@SerialName("overdue_count") val overdueCount: Int = 0
)
/**
* Task category summary for residence
*/
@Serializable
data class TaskCategorySummary(
val name: String,
@SerialName("display_name") val displayName: String,
val icons: TaskCategoryIcons,
val color: String,
val count: Int
)
/**
* Icons for task category (Android/iOS)
*/
@Serializable
data class TaskCategoryIcons(
val android: String = "",
val ios: String = ""
)
/**
* Task summary per residence (for UI backwards compatibility)
*/
@Serializable
data class ResidenceTaskSummary(
val categories: List<TaskCategorySummary> = emptyList()
)
/**
* Residence users response
*/
@Serializable
data class ResidenceUsersResponse(
@SerialName("owner_id") val ownerId: Int,
val users: List<ResidenceUser>
val owner: ResidenceUserResponse,
val users: List<ResidenceUserResponse>
)
/**
* Remove user response
*/
@Serializable
data class RemoveUserResponse(
val message: String
)
)
// Type aliases for backwards compatibility with existing code
typealias Residence = ResidenceResponse
typealias ResidenceShareCode = ShareCodeResponse
typealias ResidenceUser = ResidenceUserResponse
typealias TaskSummary = ResidenceTaskSummary
typealias TaskColumnCategory = TaskCategorySummary

View File

@@ -3,56 +3,15 @@ package com.example.mycrib.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class TaskCompletion(
val id: Int,
val task: Int,
@SerialName("completed_by_user") val completedByUser: Int?,
val contractor: Int?,
@SerialName("contractor_details") val contractorDetails: ContractorDetails?,
@SerialName("completed_by_name") val completedByName: String?,
@SerialName("completed_by_phone") val completedByPhone: String?,
@SerialName("completed_by_email") val completedByEmail: String?,
@SerialName("company_name") val companyName: String?,
@SerialName("completion_date") val completionDate: String,
@SerialName("actual_cost") val actualCost: Double?,
val notes: String?,
val rating: Int?,
@SerialName("completed_by_display") val completedByDisplay: String?,
@SerialName("created_at") val createdAt: String,
val images: List<TaskCompletionImage>? = null
)
@Serializable
data class ContractorDetails(
val id: Int,
val name: String,
val company: String?,
val phone: String,
val specialty: String?,
@SerialName("average_rating") val averageRating: Double?
)
/**
* Task completion create request matching Go API CreateTaskCompletionRequest
*/
@Serializable
data class TaskCompletionCreateRequest(
val task: Int,
@SerialName("completed_by_user") val completedByUser: Int? = null,
val contractor: Int? = null,
@SerialName("completed_by_name") val completedByName: String? = null,
@SerialName("completed_by_phone") val completedByPhone: String? = null,
@SerialName("completed_by_email") val completedByEmail: String? = null,
@SerialName("company_name") val companyName: String? = null,
@SerialName("completion_date") val completionDate: String,
@SerialName("actual_cost") val actualCost: Double? = null,
@SerialName("task_id") val taskId: Int,
@SerialName("completed_at") val completedAt: String? = null, // Defaults to now on server
val notes: String? = null,
val rating: Int? = null
@SerialName("actual_cost") val actualCost: Double? = null,
@SerialName("photo_url") val photoUrl: String? = null
)
@Serializable
data class TaskCompletionImage(
val id: Int,
val completion: Int,
val image: String,
val caption: String?,
@SerialName("uploaded_at") val uploadedAt: String
)

View File

@@ -3,33 +3,57 @@ package com.example.mycrib.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* User model matching Go API UserResponse/CurrentUserResponse
*/
@Serializable
data class User(
val id: Int,
val username: String,
val email: String,
@SerialName("first_name") val firstName: String?,
@SerialName("last_name") val lastName: String?,
@SerialName("is_staff") val isStaff: Boolean = false,
@SerialName("first_name") val firstName: String = "",
@SerialName("last_name") val lastName: String = "",
@SerialName("is_active") val isActive: Boolean = true,
@SerialName("date_joined") val dateJoined: String,
val verified: Boolean = false
)
@SerialName("last_login") val lastLogin: String? = null,
// Profile is included in CurrentUserResponse (/auth/me)
val profile: UserProfile? = null
) {
// Computed property for display name
val displayName: String
get() = when {
firstName.isNotBlank() && lastName.isNotBlank() -> "$firstName $lastName"
firstName.isNotBlank() -> firstName
lastName.isNotBlank() -> lastName
else -> username
}
// Check if user is verified (from profile)
val isVerified: Boolean
get() = profile?.verified ?: false
// Alias for backwards compatibility
val verified: Boolean
get() = isVerified
}
/**
* User profile model matching Go API UserProfileResponse
*/
@Serializable
data class UserProfile(
val id: Int,
val user: Int,
@SerialName("phone_number") val phoneNumber: String?,
val address: String?,
val city: String?,
@SerialName("state_province") val stateProvince: String?,
@SerialName("postal_code") val postalCode: String?,
val country: String?,
@SerialName("profile_picture") val profilePicture: String?,
val bio: String?
@SerialName("user_id") val userId: Int,
val verified: Boolean = false,
val bio: String = "",
@SerialName("phone_number") val phoneNumber: String = "",
@SerialName("date_of_birth") val dateOfBirth: String? = null,
@SerialName("profile_picture") val profilePicture: String = ""
)
/**
* Register request matching Go API
*/
@Serializable
data class RegisterRequest(
val username: String,
@@ -39,30 +63,54 @@ data class RegisterRequest(
@SerialName("last_name") val lastName: String? = null
)
/**
* Login request matching Go API
*/
@Serializable
data class LoginRequest(
val username: String,
val password: String
)
/**
* Auth response for login - matching Go API LoginResponse
*/
@Serializable
data class AuthResponse(
val token: String,
val user: User,
val verified: Boolean = false
val user: User
)
/**
* Auth response for registration - matching Go API RegisterResponse
*/
@Serializable
data class RegisterResponse(
val token: String,
val user: User,
val message: String = ""
)
/**
* Verify email request
*/
@Serializable
data class VerifyEmailRequest(
val code: String
)
/**
* Verify email response
*/
@Serializable
data class VerifyEmailResponse(
val message: String,
val verified: Boolean
)
/**
* Update profile request
*/
@Serializable
data class UpdateProfileRequest(
@SerialName("first_name") val firstName: String? = null,
@@ -71,6 +119,7 @@ data class UpdateProfileRequest(
)
// Password Reset Models
@Serializable
data class ForgotPasswordRequest(
val email: String
@@ -96,11 +145,18 @@ data class VerifyResetCodeResponse(
@Serializable
data class ResetPasswordRequest(
@SerialName("reset_token") val resetToken: String,
@SerialName("new_password") val newPassword: String,
@SerialName("confirm_password") val confirmPassword: String
@SerialName("new_password") val newPassword: String
)
@Serializable
data class ResetPasswordResponse(
val message: String
)
/**
* Generic message response used by many endpoints
*/
@Serializable
data class MessageResponse(
val message: String
)

View File

@@ -252,7 +252,7 @@ object APILayer {
// ==================== Residence Operations ====================
suspend fun getResidences(forceRefresh: Boolean = false): ApiResult<List<Residence>> {
suspend fun getResidences(forceRefresh: Boolean = false): ApiResult<List<ResidenceResponse>> {
// Check cache first
if (!forceRefresh) {
val cached = DataCache.residences.value
@@ -294,7 +294,7 @@ object APILayer {
return result
}
suspend fun getResidence(id: Int, forceRefresh: Boolean = false): ApiResult<Residence> {
suspend fun getResidence(id: Int, forceRefresh: Boolean = false): ApiResult<ResidenceResponse> {
// Check cache first
if (!forceRefresh) {
val cached = DataCache.residences.value.find { it.id == id }
@@ -321,7 +321,7 @@ object APILayer {
return residenceApi.getResidenceSummary(token)
}
suspend fun createResidence(request: ResidenceCreateRequest): ApiResult<Residence> {
suspend fun createResidence(request: ResidenceCreateRequest): ApiResult<ResidenceResponse> {
val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
val result = residenceApi.createResidence(token, request)
@@ -333,7 +333,7 @@ object APILayer {
return result
}
suspend fun updateResidence(id: Int, request: ResidenceCreateRequest): ApiResult<Residence> {
suspend fun updateResidence(id: Int, request: ResidenceCreateRequest): ApiResult<ResidenceResponse> {
val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
val result = residenceApi.updateResidence(token, id, request)
@@ -377,12 +377,12 @@ object APILayer {
return residenceApi.getResidenceUsers(token, residenceId)
}
suspend fun getShareCode(residenceId: Int): ApiResult<ResidenceShareCode> {
suspend fun getShareCode(residenceId: Int): ApiResult<ShareCodeResponse> {
val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
return residenceApi.getShareCode(token, residenceId)
}
suspend fun generateShareCode(residenceId: Int): ApiResult<ResidenceShareCode> {
suspend fun generateShareCode(residenceId: Int): ApiResult<GenerateShareCodeResponse> {
val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
return residenceApi.generateShareCode(token, residenceId)
}
@@ -436,7 +436,7 @@ object APILayer {
return result
}
suspend fun createTask(request: TaskCreateRequest): ApiResult<CustomTask> {
suspend fun createTask(request: TaskCreateRequest): ApiResult<TaskResponse> {
val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
val result = taskApi.createTask(token, request)
@@ -448,7 +448,7 @@ object APILayer {
return result
}
suspend fun updateTask(id: Int, request: TaskCreateRequest): ApiResult<CustomTask> {
suspend fun updateTask(id: Int, request: TaskCreateRequest): ApiResult<TaskResponse> {
val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
val result = taskApi.updateTask(token, id, request)
@@ -470,7 +470,7 @@ object APILayer {
}?.id
}
suspend fun cancelTask(taskId: Int): ApiResult<CustomTask> {
suspend fun cancelTask(taskId: Int): ApiResult<TaskResponse> {
val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
// Look up 'cancelled' status ID from cache
@@ -487,7 +487,7 @@ object APILayer {
return result
}
suspend fun uncancelTask(taskId: Int): ApiResult<CustomTask> {
suspend fun uncancelTask(taskId: Int): ApiResult<TaskResponse> {
val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
// Look up 'pending' status ID from cache
@@ -504,7 +504,7 @@ object APILayer {
return result
}
suspend fun markInProgress(taskId: Int): ApiResult<CustomTask> {
suspend fun markInProgress(taskId: Int): ApiResult<TaskResponse> {
val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
// Look up 'in progress' status ID from cache
@@ -522,7 +522,7 @@ object APILayer {
return result
}
suspend fun archiveTask(taskId: Int): ApiResult<CustomTask> {
suspend fun archiveTask(taskId: Int): ApiResult<TaskResponse> {
val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
val result = taskApi.archiveTask(token, taskId)
@@ -534,7 +534,7 @@ object APILayer {
return result
}
suspend fun unarchiveTask(taskId: Int): ApiResult<CustomTask> {
suspend fun unarchiveTask(taskId: Int): ApiResult<TaskResponse> {
val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
val result = taskApi.unarchiveTask(token, taskId)
@@ -546,7 +546,7 @@ object APILayer {
return result
}
suspend fun createTaskCompletion(request: TaskCompletionCreateRequest): ApiResult<TaskCompletion> {
suspend fun createTaskCompletion(request: TaskCompletionCreateRequest): ApiResult<TaskCompletionResponse> {
val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
val result = taskCompletionApi.createCompletion(token, request)
@@ -562,7 +562,7 @@ object APILayer {
request: TaskCompletionCreateRequest,
images: List<ByteArray>,
imageFileNames: List<String>
): ApiResult<TaskCompletion> {
): ApiResult<TaskCompletionResponse> {
val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
val result = taskCompletionApi.createCompletionWithImages(token, request, images, imageFileNames)

View File

@@ -129,7 +129,7 @@ class ContractorApi(private val client: HttpClient = ApiClient.httpClient) {
}
}
suspend fun getContractorTasks(token: String, id: Int): ApiResult<List<TaskCompletion>> {
suspend fun getContractorTasks(token: String, id: Int): ApiResult<List<TaskResponse>> {
return try {
val response = client.get("$baseUrl/contractors/$id/tasks/") {
header("Authorization", "Token $token")

View File

@@ -105,7 +105,7 @@ class LookupsApi(private val client: HttpClient = ApiClient.httpClient) {
}
}
suspend fun getAllTasks(token: String): ApiResult<List<CustomTask>> {
suspend fun getAllTasks(token: String): ApiResult<List<TaskResponse>> {
return try {
val response = client.get("$baseUrl/tasks/") {
header("Authorization", "Token $token")

View File

@@ -9,7 +9,7 @@ import io.ktor.http.*
class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
private val baseUrl = ApiClient.getBaseUrl()
suspend fun getResidences(token: String): ApiResult<List<Residence>> {
suspend fun getResidences(token: String): ApiResult<List<ResidenceResponse>> {
return try {
val response = client.get("$baseUrl/residences/") {
header("Authorization", "Token $token")
@@ -25,7 +25,7 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
}
}
suspend fun getResidence(token: String, id: Int): ApiResult<Residence> {
suspend fun getResidence(token: String, id: Int): ApiResult<ResidenceResponse> {
return try {
val response = client.get("$baseUrl/residences/$id/") {
header("Authorization", "Token $token")
@@ -41,7 +41,7 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
}
}
suspend fun createResidence(token: String, request: ResidenceCreateRequest): ApiResult<Residence> {
suspend fun createResidence(token: String, request: ResidenceCreateRequest): ApiResult<ResidenceResponse> {
return try {
val response = client.post("$baseUrl/residences/") {
header("Authorization", "Token $token")
@@ -59,7 +59,7 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
}
}
suspend fun updateResidence(token: String, id: Int, request: ResidenceCreateRequest): ApiResult<Residence> {
suspend fun updateResidence(token: String, id: Int, request: ResidenceCreateRequest): ApiResult<ResidenceResponse> {
return try {
val response = client.put("$baseUrl/residences/$id/") {
header("Authorization", "Token $token")
@@ -126,7 +126,7 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
}
// Share Code Management
suspend fun generateShareCode(token: String, residenceId: Int): ApiResult<ResidenceShareCode> {
suspend fun generateShareCode(token: String, residenceId: Int): ApiResult<GenerateShareCodeResponse> {
return try {
val response = client.post("$baseUrl/residences/$residenceId/generate-share-code/") {
header("Authorization", "Token $token")
@@ -143,7 +143,7 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
}
}
suspend fun getShareCode(token: String, residenceId: Int): ApiResult<ResidenceShareCode> {
suspend fun getShareCode(token: String, residenceId: Int): ApiResult<ShareCodeResponse> {
return try {
val response = client.get("$baseUrl/residences/$residenceId/share-code/") {
header("Authorization", "Token $token")

View File

@@ -30,7 +30,7 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) {
}
}
suspend fun getTask(token: String, id: Int): ApiResult<TaskDetail> {
suspend fun getTask(token: String, id: Int): ApiResult<TaskResponse> {
return try {
val response = client.get("$baseUrl/tasks/$id/") {
header("Authorization", "Token $token")
@@ -47,7 +47,7 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) {
}
}
suspend fun createTask(token: String, request: TaskCreateRequest): ApiResult<CustomTask> {
suspend fun createTask(token: String, request: TaskCreateRequest): ApiResult<TaskResponse> {
return try {
val response = client.post("$baseUrl/tasks/") {
header("Authorization", "Token $token")
@@ -66,7 +66,7 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) {
}
}
suspend fun updateTask(token: String, id: Int, request: TaskCreateRequest): ApiResult<CustomTask> {
suspend fun updateTask(token: String, id: Int, request: TaskCreateRequest): ApiResult<TaskResponse> {
return try {
val response = client.put("$baseUrl/tasks/$id/") {
header("Authorization", "Token $token")
@@ -132,7 +132,7 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) {
* archive, unarchive) have been REMOVED from the API.
* All task updates now use PATCH /tasks/{id}/.
*/
suspend fun patchTask(token: String, id: Int, request: TaskPatchRequest): ApiResult<CustomTask> {
suspend fun patchTask(token: String, id: Int, request: TaskPatchRequest): ApiResult<TaskResponse> {
return try {
val response = client.patch("$baseUrl/tasks/$id/") {
header("Authorization", "Token $token")
@@ -155,23 +155,23 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) {
// They're kept for backward compatibility with existing ViewModel calls.
// New code should use patchTask directly with status IDs from DataCache.
suspend fun cancelTask(token: String, id: Int, cancelledStatusId: Int): ApiResult<CustomTask> {
suspend fun cancelTask(token: String, id: Int, cancelledStatusId: Int): ApiResult<TaskResponse> {
return patchTask(token, id, TaskPatchRequest(status = cancelledStatusId))
}
suspend fun uncancelTask(token: String, id: Int, pendingStatusId: Int): ApiResult<CustomTask> {
suspend fun uncancelTask(token: String, id: Int, pendingStatusId: Int): ApiResult<TaskResponse> {
return patchTask(token, id, TaskPatchRequest(status = pendingStatusId))
}
suspend fun markInProgress(token: String, id: Int, inProgressStatusId: Int): ApiResult<CustomTask> {
suspend fun markInProgress(token: String, id: Int, inProgressStatusId: Int): ApiResult<TaskResponse> {
return patchTask(token, id, TaskPatchRequest(status = inProgressStatusId))
}
suspend fun archiveTask(token: String, id: Int): ApiResult<CustomTask> {
suspend fun archiveTask(token: String, id: Int): ApiResult<TaskResponse> {
return patchTask(token, id, TaskPatchRequest(archived = true))
}
suspend fun unarchiveTask(token: String, id: Int): ApiResult<CustomTask> {
suspend fun unarchiveTask(token: String, id: Int): ApiResult<TaskResponse> {
return patchTask(token, id, TaskPatchRequest(archived = false))
}
}

View File

@@ -9,7 +9,7 @@ import io.ktor.http.*
class TaskCompletionApi(private val client: HttpClient = ApiClient.httpClient) {
private val baseUrl = ApiClient.getBaseUrl()
suspend fun getCompletions(token: String): ApiResult<List<TaskCompletion>> {
suspend fun getCompletions(token: String): ApiResult<List<TaskCompletionResponse>> {
return try {
val response = client.get("$baseUrl/task-completions/") {
header("Authorization", "Token $token")
@@ -25,7 +25,7 @@ class TaskCompletionApi(private val client: HttpClient = ApiClient.httpClient) {
}
}
suspend fun getCompletion(token: String, id: Int): ApiResult<TaskCompletion> {
suspend fun getCompletion(token: String, id: Int): ApiResult<TaskCompletionResponse> {
return try {
val response = client.get("$baseUrl/task-completions/$id/") {
header("Authorization", "Token $token")
@@ -41,7 +41,7 @@ class TaskCompletionApi(private val client: HttpClient = ApiClient.httpClient) {
}
}
suspend fun createCompletion(token: String, request: TaskCompletionCreateRequest): ApiResult<TaskCompletion> {
suspend fun createCompletion(token: String, request: TaskCompletionCreateRequest): ApiResult<TaskCompletionResponse> {
return try {
val response = client.post("$baseUrl/task-completions/") {
header("Authorization", "Token $token")
@@ -59,7 +59,7 @@ class TaskCompletionApi(private val client: HttpClient = ApiClient.httpClient) {
}
}
suspend fun updateCompletion(token: String, id: Int, request: TaskCompletionCreateRequest): ApiResult<TaskCompletion> {
suspend fun updateCompletion(token: String, id: Int, request: TaskCompletionCreateRequest): ApiResult<TaskCompletionResponse> {
return try {
val response = client.put("$baseUrl/task-completions/$id/") {
header("Authorization", "Token $token")
@@ -98,7 +98,7 @@ class TaskCompletionApi(private val client: HttpClient = ApiClient.httpClient) {
request: TaskCompletionCreateRequest,
images: List<ByteArray> = emptyList(),
imageFileNames: List<String> = emptyList()
): ApiResult<TaskCompletion> {
): ApiResult<TaskCompletionResponse> {
return try {
val response = client.post("$baseUrl/task-completions/") {
header("Authorization", "Token $token")
@@ -107,13 +107,11 @@ class TaskCompletionApi(private val client: HttpClient = ApiClient.httpClient) {
io.ktor.client.request.forms.MultiPartFormDataContent(
io.ktor.client.request.forms.formData {
// Add text fields
append("task", request.task.toString())
request.contractor?.let { append("contractor", it.toString()) }
request.completedByName?.let { append("completed_by_name", it) }
append("completion_date", request.completionDate)
request.actualCost?.let { append("actual_cost", it) }
append("task_id", request.taskId.toString())
request.completedAt?.let { append("completed_at", it) }
request.actualCost?.let { append("actual_cost", it.toString()) }
request.notes?.let { append("notes", it) }
request.rating?.let { append("rating", it.toString()) }
request.photoUrl?.let { append("photo_url", it) }
// Add image files
images.forEachIndexed { index, imageBytes ->

View File

@@ -41,12 +41,8 @@ fun AddTaskDialog(
)
}
var category by remember { mutableStateOf(TaskCategory(id = 0, name = "")) }
var frequency by remember { mutableStateOf(TaskFrequency(
id = 0, name = "", lookupName = "", displayName = "",
daySpan = 0,
notifyDays = 0
)) }
var priority by remember { mutableStateOf(TaskPriority(id = 0, name = "", displayName = "")) }
var frequency by remember { mutableStateOf(TaskFrequency(id = 0, name = "")) }
var priority by remember { mutableStateOf(TaskPriority(id = 0, name = "")) }
var showResidenceDropdown by remember { mutableStateOf(false) }
var showFrequencyDropdown by remember { mutableStateOf(false) }
@@ -333,14 +329,13 @@ fun AddTaskDialog(
if (!hasError) {
onCreate(
TaskCreateRequest(
residence = selectedResidenceId,
residenceId = selectedResidenceId,
title = title,
description = description.ifBlank { null },
category = category.id,
frequency = frequency.id,
intervalDays = intervalDays.toIntOrNull(),
priority = priority.id,
status = null,
categoryId = if (category.id > 0) category.id else null,
frequencyId = if (frequency.id > 0) frequency.id else null,
priorityId = if (priority.id > 0) priority.id else null,
statusId = null,
dueDate = dueDate,
estimatedCost = estimatedCost.ifBlank { null }?.toDoubleOrNull()
)

View File

@@ -255,15 +255,26 @@ fun CompleteTaskDialog(
// Get current date in ISO format
val currentDate = getCurrentDateTime()
// Build notes with contractor info if selected
val notesWithContractor = buildString {
if (selectedContractorName != null) {
append("Contractor: $selectedContractorName\n")
}
if (completedByName.isNotBlank()) {
append("Completed by: $completedByName\n")
}
if (notes.isNotBlank()) {
append(notes)
}
}.ifBlank { null }
onComplete(
TaskCompletionCreateRequest(
task = taskId,
contractor = selectedContractorId,
completedByName = completedByName.ifBlank { null },
completionDate = currentDate,
taskId = taskId,
completedAt = currentDate,
actualCost = actualCost.ifBlank { null }?.toDoubleOrNull(),
notes = notes.ifBlank { null },
rating = rating
notes = notesWithContractor,
photoUrl = null // Images handled separately
),
selectedImages
)

View File

@@ -47,7 +47,7 @@ fun ManageUsersDialog(
when (val result = residenceApi.getResidenceUsers(token, residenceId)) {
is ApiResult.Success -> {
users = result.data.users
ownerId = result.data.ownerId
ownerId = result.data.owner.id
isLoading = false
}
is ApiResult.Error -> {
@@ -133,7 +133,7 @@ fun ManageUsersDialog(
if (token != null) {
when (val result = residenceApi.generateShareCode(token, residenceId)) {
is ApiResult.Success -> {
shareCode = result.data
shareCode = result.data.shareCode
}
is ApiResult.Error -> {
error = result.message

View File

@@ -65,7 +65,7 @@ fun TaskCard(
shape = RoundedCornerShape(12.dp)
) {
Text(
text = task.category.name.uppercase(),
text = (task.category?.name ?: "").uppercase(),
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
@@ -78,7 +78,7 @@ fun TaskCard(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// Priority badge with semantic colors
val priorityColor = when (task.priority.name.lowercase()) {
val priorityColor = when (task.priority?.name?.lowercase()) {
"urgent", "high" -> MaterialTheme.colorScheme.error
"medium" -> MaterialTheme.colorScheme.tertiary
else -> MaterialTheme.colorScheme.secondary
@@ -100,7 +100,7 @@ fun TaskCard(
.background(priorityColor)
)
Text(
text = task.priority.name.uppercase(),
text = (task.priority?.name ?: "").uppercase(),
style = MaterialTheme.typography.labelSmall,
color = priorityColor
)
@@ -589,23 +589,20 @@ fun TaskCardPreview() {
TaskCard(
task = TaskDetail(
id = 1,
residence = 1,
residenceId = 1,
createdById = 1,
title = "Clean Gutters",
description = "Remove all debris from gutters and downspouts",
category = TaskCategory(id = 1, name = "maintenance", description = ""),
priority = TaskPriority(id = 2, name = "medium", displayName = "Medium", description = ""),
category = TaskCategory(id = 1, name = "maintenance"),
priority = TaskPriority(id = 2, name = "medium"),
frequency = TaskFrequency(
id = 1, name = "monthly", lookupName = "monthly", displayName = "Monthly",
daySpan = 0,
notifyDays = 0
id = 1, name = "monthly", days = 30
),
status = TaskStatus(id = 1, name = "pending", displayName = "Pending", description = ""),
status = TaskStatus(id = 1, name = "pending"),
dueDate = "2024-12-15",
estimatedCost = 150.00,
createdAt = "2024-01-01T00:00:00Z",
updatedAt = "2024-01-01T00:00:00Z",
nextScheduledDate = "2024-12-15",
showCompletedButton = true,
completions = emptyList()
),
onCompleteClick = {},

View File

@@ -301,16 +301,15 @@ fun EditTaskScreen(
viewModel.updateTask(
taskId = task.id,
request = TaskCreateRequest(
residence = task.residence,
residenceId = task.residenceId,
title = title,
description = description.ifBlank { null },
category = selectedCategory!!.id,
frequency = selectedFrequency!!.id,
priority = selectedPriority!!.id,
status = selectedStatus!!.id,
categoryId = selectedCategory!!.id,
frequencyId = selectedFrequency!!.id,
priorityId = selectedPriority!!.id,
statusId = selectedStatus!!.id,
dueDate = dueDate,
estimatedCost = estimatedCost.ifBlank { null }?.toDoubleOrNull(),
archived = task.archived
estimatedCost = estimatedCost.ifBlank { null }?.toDoubleOrNull()
)
)
}

View File

@@ -28,15 +28,15 @@ fun HomeScreen(
onLogout: () -> Unit,
viewModel: ResidenceViewModel = viewModel { ResidenceViewModel() }
) {
val summaryState by viewModel.residenceSummaryState.collectAsState()
val summaryState by viewModel.myResidencesState.collectAsState()
LaunchedEffect(Unit) {
viewModel.loadResidenceSummary()
viewModel.loadMyResidences()
}
// Handle errors for loading summary
summaryState.HandleErrors(
onRetry = { viewModel.loadResidenceSummary() },
onRetry = { viewModel.loadMyResidences() },
errorTitle = "Failed to Load Summary"
)

View File

@@ -33,6 +33,7 @@ import com.example.mycrib.network.ApiResult
import com.example.mycrib.utils.SubscriptionHelper
import com.example.mycrib.ui.subscription.UpgradePromptDialog
import com.example.mycrib.cache.SubscriptionCache
import com.example.mycrib.cache.DataCache
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -69,6 +70,9 @@ fun ResidenceDetailScreen(
var showUpgradePrompt by remember { mutableStateOf(false) }
var upgradeTriggerKey by remember { mutableStateOf<String?>(null) }
// Get current user for ownership checks
val currentUser by DataCache.currentUser.collectAsState()
// Check if tasks are blocked (limit=0) - this hides the FAB
val isTasksBlocked = SubscriptionHelper.isTasksBlocked()
// Get current count for checking when adding
@@ -220,7 +224,7 @@ fun ResidenceDetailScreen(
ManageUsersDialog(
residenceId = residence.id,
residenceName = residence.name,
isPrimaryOwner = residence.isPrimaryOwner,
isPrimaryOwner = residence.ownerId == currentUser?.id,
onDismiss = {
showManageUsersDialog = false
},
@@ -395,7 +399,7 @@ fun ResidenceDetailScreen(
}
// Manage Users button - only show for primary owners
if (residence.isPrimaryOwner) {
if (residence.ownerId == currentUser?.id) {
IconButton(onClick = {
showManageUsersDialog = true
}) {
@@ -410,7 +414,7 @@ fun ResidenceDetailScreen(
}
// Delete button - only show for primary owners
if (residence.isPrimaryOwner) {
if (residence.ownerId == currentUser?.id) {
IconButton(onClick = {
showDeleteConfirmation = true
}) {

View File

@@ -75,8 +75,8 @@ fun ResidenceFormScreen(
// Set default/existing property type when types are loaded
LaunchedEffect(propertyTypes, existingResidence) {
if (propertyTypes.isNotEmpty() && propertyType == null) {
propertyType = if (isEditMode && existingResidence != null && existingResidence.propertyType != null) {
propertyTypes.find { it.id == existingResidence.propertyType.toInt() }
propertyType = if (isEditMode && existingResidence != null && existingResidence.propertyTypeId != null) {
propertyTypes.find { it.id == existingResidence.propertyTypeId }
} else if (!isEditMode && propertyTypes.isNotEmpty()) {
propertyTypes.first()
} else {
@@ -306,7 +306,7 @@ fun ResidenceFormScreen(
if (validateForm()) {
val request = ResidenceCreateRequest(
name = name,
propertyType = propertyType?.id,
propertyTypeId = propertyType?.id,
streetAddress = streetAddress.ifBlank { null },
apartmentUnit = apartmentUnit.ifBlank { null },
city = city.ifBlank { null },
@@ -314,9 +314,9 @@ fun ResidenceFormScreen(
postalCode = postalCode.ifBlank { null },
country = country.ifBlank { null },
bedrooms = bedrooms.toIntOrNull(),
bathrooms = bathrooms.toFloatOrNull(),
bathrooms = bathrooms.toDoubleOrNull(),
squareFootage = squareFootage.toIntOrNull(),
lotSize = lotSize.toFloatOrNull(),
lotSize = lotSize.toDoubleOrNull(),
yearBuilt = yearBuilt.toIntOrNull(),
description = description.ifBlank { null },
isPrimary = isPrimary

View File

@@ -188,11 +188,11 @@ class AuthViewModel : ViewModel() {
fun resetPassword(resetToken: String, newPassword: String, confirmPassword: String) {
viewModelScope.launch {
_resetPasswordState.value = ApiResult.Loading
// Note: confirmPassword is for UI validation only, not sent to API
val result = APILayer.resetPassword(
ResetPasswordRequest(
resetToken = resetToken,
newPassword = newPassword,
confirmPassword = confirmPassword
newPassword = newPassword
)
)
_resetPasswordState.value = when (result) {

View File

@@ -90,11 +90,11 @@ class PasswordResetViewModel(
viewModelScope.launch {
_resetPasswordState.value = ApiResult.Loading
// Note: confirmPassword is for UI validation only, not sent to API
val result = authApi.resetPassword(
ResetPasswordRequest(
resetToken = token,
newPassword = newPassword,
confirmPassword = confirmPassword
newPassword = newPassword
)
)
_resetPasswordState.value = when (result) {

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,