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

View File

@@ -3,146 +3,163 @@ package com.example.mycrib.models
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
/**
* User reference for task-related responses - matching Go API TaskUserResponse
*/
@Serializable @Serializable
data class CustomTask ( data class TaskUserResponse(
val id: Int, val id: Int,
val residence: Int, val username: String,
@SerialName("created_by") val createdBy: Int, val email: String,
@SerialName("created_by_username") val createdByUsername: 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 title: String,
val description: String? = null, val description: String = "",
val category: TaskCategory?, @SerialName("category_id") val categoryId: Int? = null,
val frequency: TaskFrequency, val category: TaskCategory? = null,
val priority: TaskPriority, @SerialName("priority_id") val priorityId: Int? = null,
val priority: TaskPriority? = null,
@SerialName("status_id") val statusId: Int? = null,
val status: TaskStatus? = null, val status: TaskStatus? = null,
@SerialName("due_date") val dueDate: String?, @SerialName("frequency_id") val frequencyId: Int? = null,
@SerialName("next_scheduled_date") val nextScheduledDate: String? = null, val frequency: TaskFrequency? = null,
@SerialName("due_date") val dueDate: String? = null,
@SerialName("estimated_cost") val estimatedCost: Double? = null, @SerialName("estimated_cost") val estimatedCost: Double? = null,
@SerialName("actual_cost") val actualCost: Double? = null, @SerialName("actual_cost") val actualCost: Double? = null,
@SerialName("completion_count") val completionCount: Int? = null, @SerialName("contractor_id") val contractorId: Int? = null,
val notes: String? = null, @SerialName("is_cancelled") val isCancelled: Boolean = false,
val archived: 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("created_at") val createdAt: String,
@SerialName("updated_at") val updatedAt: String, @SerialName("updated_at") val updatedAt: String
@SerialName("show_completed_button") val showCompletedButton: Boolean = false, ) {
@SerialName("days_until_due") val daysUntilDue: Int? = null, // Helper for backwards compatibility with old code
@SerialName("is_overdue") val isOverdue: Boolean? = null, val archived: Boolean get() = isArchived
@SerialName("last_completion") val lastCompletion: LastCompletion? = null
)
// 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 @Serializable
data class LastCompletion( data class TaskCompletionResponse(
@SerialName("completion_date") val completionDate: String, val id: Int,
@SerialName("completed_by") val completedBy: String?, @SerialName("task_id") val taskId: Int,
@SerialName("actual_cost") val actualCost: Double?, @SerialName("completed_by") val completedBy: TaskUserResponse? = null,
val rating: Int? @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 @Serializable
data class TaskCreateRequest( data class TaskCreateRequest(
val residence: Int, @SerialName("residence_id") val residenceId: Int,
val title: String, val title: String,
val description: String? = null, val description: String? = null,
val category: Int, @SerialName("category_id") val categoryId: Int? = null,
val frequency: Int, @SerialName("priority_id") val priorityId: Int? = null,
@SerialName("interval_days") val intervalDays: Int? = null, @SerialName("status_id") val statusId: Int? = null,
val priority: Int, @SerialName("frequency_id") val frequencyId: Int? = null,
val status: Int? = null, @SerialName("assigned_to_id") val assignedToId: Int? = null,
@SerialName("due_date") val dueDate: String, @SerialName("due_date") val dueDate: String? = null,
@SerialName("estimated_cost") val estimatedCost: Double? = null, @SerialName("estimated_cost") val estimatedCost: Double? = null,
val archived: Boolean = false @SerialName("contractor_id") val contractorId: Int? = null
)
@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
) )
/** /**
* Request model for PATCH updates to a task. * Task update request matching Go API UpdateTaskRequest
* Used for status changes and archive/unarchive operations.
* All fields are optional - only provided fields will be updated. * All fields are optional - only provided fields will be updated.
*/ */
@Serializable @Serializable
data class TaskPatchRequest( data class TaskUpdateRequest(
val status: Int? = null, // Status ID to update val title: String? = null,
val archived: Boolean? = null // Archive/unarchive flag 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. * Task action response (for cancel, archive, mark in progress, etc.)
* Uses IDs instead of nested objects for efficiency.
* Resolve IDs to full objects via DataCache.getTaskCategory(), etc.
*/ */
@Serializable @Serializable
data class TaskMinimal( data class TaskActionResponse(
val id: Int, val message: String,
val title: String, val task: TaskResponse
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
) )
/**
* Kanban column response matching Go API KanbanColumnResponse
*/
@Serializable @Serializable
data class TaskColumn( data class TaskColumn(
val name: String, val name: String,
@@ -150,13 +167,45 @@ data class TaskColumn(
@SerialName("button_types") val buttonTypes: List<String>, @SerialName("button_types") val buttonTypes: List<String>,
val icons: Map<String, String>, val icons: Map<String, String>,
val color: 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 val count: Int
) )
/**
* Kanban board response matching Go API KanbanBoardResponse
*/
@Serializable @Serializable
data class TaskColumnsResponse( data class TaskColumnsResponse(
val columns: List<TaskColumn>, val columns: List<TaskColumn>,
@SerialName("days_threshold") val daysThreshold: Int? = null, @SerialName("days_threshold") val daysThreshold: Int,
@SerialName("residence_id") val residenceId: String? = null @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.SerialName
import kotlinx.serialization.Serializable 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 @Serializable
data class ResidenceTypeResponse( data class ResidenceTypeResponse(
val count: Int, val count: Int,
val results: List<ResidenceType> val results: List<ResidenceType>
) )
@Serializable
data class ResidenceType(
val id: Int,
val name: String,
val description: String? = null
)
@Serializable @Serializable
data class TaskFrequencyResponse( data class TaskFrequencyResponse(
val count: Int, val count: Int,
val results: List<TaskFrequency> 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 @Serializable
data class TaskPriorityResponse( data class TaskPriorityResponse(
val count: Int, val count: Int,
val results: List<TaskPriority> 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 @Serializable
data class TaskStatusResponse( data class TaskStatusResponse(
val count: Int, val count: Int,
val results: List<TaskStatus> 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 @Serializable
data class TaskCategoryResponse( data class TaskCategoryResponse(
val count: Int, val count: Int,
val results: List<TaskCategory> 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 @Serializable
data class ContractorSpecialtyResponse( data class ContractorSpecialtyResponse(
val count: Int, val count: Int,
val results: List<ContractorSpecialty> 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.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
/**
* User reference for residence responses - matching Go API ResidenceUserResponse
*/
@Serializable @Serializable
data class Residence( data class ResidenceUserResponse(
val id: Int, val id: Int,
val owner: Int? = null, val username: String,
@SerialName("owner_username") val ownerUsername: String, val email: String,
@SerialName("is_primary_owner") val isPrimaryOwner: Boolean = false, @SerialName("first_name") val firstName: String = "",
@SerialName("user_count") val userCount: Int = 1, @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, val name: String,
@SerialName("property_type") val propertyType: String? = null, @SerialName("property_type_id") val propertyTypeId: Int? = null,
@SerialName("street_address") val streetAddress: String? = null, @SerialName("property_type") val propertyType: ResidenceType? = null,
@SerialName("apartment_unit") val apartmentUnit: String? = null, @SerialName("street_address") val streetAddress: String = "",
val city: String? = null, @SerialName("apartment_unit") val apartmentUnit: String = "",
@SerialName("state_province") val stateProvince: String? = null, val city: String = "",
@SerialName("postal_code") val postalCode: String? = null, @SerialName("state_province") val stateProvince: String = "",
val country: String? = null, @SerialName("postal_code") val postalCode: String = "",
val country: String = "",
val bedrooms: Int? = null, val bedrooms: Int? = null,
val bathrooms: Float? = null, val bathrooms: Double? = null,
@SerialName("square_footage") val squareFootage: Int? = 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, @SerialName("year_built") val yearBuilt: Int? = null,
val description: String? = null, val description: String = "",
@SerialName("purchase_date") val purchaseDate: String? = null, @SerialName("purchase_date") val purchaseDate: String? = null,
@SerialName("purchase_price") val purchasePrice: Double? = null, @SerialName("purchase_price") val purchasePrice: Double? = null,
@SerialName("is_primary") val isPrimary: Boolean = false, @SerialName("is_primary") val isPrimary: Boolean = false,
@SerialName("is_active") val isActive: Boolean = true,
@SerialName("created_at") val createdAt: String, @SerialName("created_at") val createdAt: String,
@SerialName("updated_at") val updatedAt: 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 @Serializable
data class ResidenceCreateRequest( data class ResidenceCreateRequest(
val name: String, 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("street_address") val streetAddress: String? = null,
@SerialName("apartment_unit") val apartmentUnit: String? = null, @SerialName("apartment_unit") val apartmentUnit: String? = null,
val city: String? = null, val city: String? = null,
@@ -42,189 +85,172 @@ data class ResidenceCreateRequest(
@SerialName("postal_code") val postalCode: String? = null, @SerialName("postal_code") val postalCode: String? = null,
val country: String? = null, val country: String? = null,
val bedrooms: Int? = null, val bedrooms: Int? = null,
val bathrooms: Float? = null, val bathrooms: Double? = null,
@SerialName("square_footage") val squareFootage: Int? = 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: 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("year_built") val yearBuilt: Int? = null, @SerialName("year_built") val yearBuilt: Int? = null,
val description: String? = null, val description: String? = null,
@SerialName("purchase_date") val purchaseDate: String? = null, @SerialName("purchase_date") val purchaseDate: String? = null,
@SerialName("purchase_price") val purchasePrice: Double? = null, @SerialName("purchase_price") val purchasePrice: Double? = null,
@SerialName("is_primary") val isPrimary: Boolean, @SerialName("is_primary") val isPrimary: Boolean? = null
@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>
) )
/** /**
* Minimal residence model for list views. * Residence update request matching Go API UpdateResidenceRequest
* Uses property_type_id and annotated counts instead of nested objects.
* Resolve property type via DataCache.getResidenceType(residence.propertyTypeId)
*/ */
@Serializable @Serializable
data class ResidenceMinimal( data class ResidenceUpdateRequest(
val id: Int, val name: String? = null,
val name: String,
@SerialName("property_type_id") val propertyTypeId: 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,
@SerialName("state_province") val stateProvince: String? = null,
@SerialName("postal_code") val postalCode: String? = null,
val country: String? = null,
val bedrooms: Int? = null, val bedrooms: Int? = null,
val bathrooms: Float? = null, val bathrooms: Double? = null,
@SerialName("is_primary") val isPrimary: Boolean = false, @SerialName("square_footage") val squareFootage: Int? = null,
@SerialName("is_primary_owner") val isPrimaryOwner: Boolean = false, @SerialName("lot_size") val lotSize: Double? = null,
@SerialName("user_count") val userCount: Int = 1, @SerialName("year_built") val yearBuilt: Int? = null,
// Annotated counts from database (no N+1 queries) val description: String? = null,
@SerialName("task_count") val taskCount: Int = 0, @SerialName("purchase_date") val purchaseDate: String? = null,
@SerialName("tasks_pending") val tasksPending: Int = 0, @SerialName("purchase_price") val purchasePrice: Double? = null,
@SerialName("tasks_overdue") val tasksOverdue: Int = 0, @SerialName("is_primary") val isPrimary: Boolean? = null
@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, * Share code response matching Go API ShareCodeResponse
@SerialName("next_task_id") val nextTaskId: Int? = null, */
@SerialName("next_task_date") val nextTaskDate: String? = null, @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 @SerialName("created_at") val createdAt: String
) )
// Share Code Models /**
* Generate share code request
*/
@Serializable @Serializable
data class ResidenceShareCode( data class GenerateShareCodeRequest(
val id: Int, @SerialName("expires_in_hours") val expiresInHours: Int? = null
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?
) )
/**
* 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 @Serializable
data class JoinResidenceRequest( data class JoinResidenceRequest(
val code: String val code: String
) )
/**
* Join residence response matching Go API JoinResidenceResponse
*/
@Serializable @Serializable
data class JoinResidenceResponse( data class JoinResidenceResponse(
val message: String, val message: String,
val residence: Residence val residence: ResidenceResponse
) )
// User Management Models /**
* Total summary for dashboard display
*/
@Serializable @Serializable
data class ResidenceUser( data class TotalSummary(
val id: Int, @SerialName("total_residences") val totalResidences: Int = 0,
val username: String, @SerialName("total_tasks") val totalTasks: Int = 0,
val email: String, @SerialName("total_pending") val totalPending: Int = 0,
@SerialName("first_name") val firstName: String?, @SerialName("total_overdue") val totalOverdue: Int = 0,
@SerialName("last_name") val lastName: String? @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 @Serializable
data class ResidenceUsersResponse( data class ResidenceUsersResponse(
@SerialName("owner_id") val ownerId: Int, val owner: ResidenceUserResponse,
val users: List<ResidenceUser> val users: List<ResidenceUserResponse>
) )
/**
* Remove user response
*/
@Serializable @Serializable
data class RemoveUserResponse( data class RemoveUserResponse(
val message: String 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.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable /**
data class TaskCompletion( * Task completion create request matching Go API CreateTaskCompletionRequest
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?
)
@Serializable @Serializable
data class TaskCompletionCreateRequest( data class TaskCompletionCreateRequest(
val task: Int, @SerialName("task_id") val taskId: Int,
@SerialName("completed_by_user") val completedByUser: Int? = null, @SerialName("completed_at") val completedAt: String? = null, // Defaults to now on server
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,
val notes: String? = null, 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.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
/**
* User model matching Go API UserResponse/CurrentUserResponse
*/
@Serializable @Serializable
data class User( data class User(
val id: Int, val id: Int,
val username: String, val username: String,
val email: String, val email: String,
@SerialName("first_name") val firstName: String?, @SerialName("first_name") val firstName: String = "",
@SerialName("last_name") val lastName: String?, @SerialName("last_name") val lastName: String = "",
@SerialName("is_staff") val isStaff: Boolean = false,
@SerialName("is_active") val isActive: Boolean = true, @SerialName("is_active") val isActive: Boolean = true,
@SerialName("date_joined") val dateJoined: String, @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 @Serializable
data class UserProfile( data class UserProfile(
val id: Int, val id: Int,
val user: Int, @SerialName("user_id") val userId: Int,
@SerialName("phone_number") val phoneNumber: String?, val verified: Boolean = false,
val address: String?, val bio: String = "",
val city: String?, @SerialName("phone_number") val phoneNumber: String = "",
@SerialName("state_province") val stateProvince: String?, @SerialName("date_of_birth") val dateOfBirth: String? = null,
@SerialName("postal_code") val postalCode: String?, @SerialName("profile_picture") val profilePicture: String = ""
val country: String?,
@SerialName("profile_picture") val profilePicture: String?,
val bio: String?
) )
/**
* Register request matching Go API
*/
@Serializable @Serializable
data class RegisterRequest( data class RegisterRequest(
val username: String, val username: String,
@@ -39,30 +63,54 @@ data class RegisterRequest(
@SerialName("last_name") val lastName: String? = null @SerialName("last_name") val lastName: String? = null
) )
/**
* Login request matching Go API
*/
@Serializable @Serializable
data class LoginRequest( data class LoginRequest(
val username: String, val username: String,
val password: String val password: String
) )
/**
* Auth response for login - matching Go API LoginResponse
*/
@Serializable @Serializable
data class AuthResponse( data class AuthResponse(
val token: String, val token: String,
val user: User, val user: User
val verified: Boolean = false
) )
/**
* 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 @Serializable
data class VerifyEmailRequest( data class VerifyEmailRequest(
val code: String val code: String
) )
/**
* Verify email response
*/
@Serializable @Serializable
data class VerifyEmailResponse( data class VerifyEmailResponse(
val message: String, val message: String,
val verified: Boolean val verified: Boolean
) )
/**
* Update profile request
*/
@Serializable @Serializable
data class UpdateProfileRequest( data class UpdateProfileRequest(
@SerialName("first_name") val firstName: String? = null, @SerialName("first_name") val firstName: String? = null,
@@ -71,6 +119,7 @@ data class UpdateProfileRequest(
) )
// Password Reset Models // Password Reset Models
@Serializable @Serializable
data class ForgotPasswordRequest( data class ForgotPasswordRequest(
val email: String val email: String
@@ -96,11 +145,18 @@ data class VerifyResetCodeResponse(
@Serializable @Serializable
data class ResetPasswordRequest( data class ResetPasswordRequest(
@SerialName("reset_token") val resetToken: String, @SerialName("reset_token") val resetToken: String,
@SerialName("new_password") val newPassword: String, @SerialName("new_password") val newPassword: String
@SerialName("confirm_password") val confirmPassword: String
) )
@Serializable @Serializable
data class ResetPasswordResponse( data class ResetPasswordResponse(
val message: String 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 ==================== // ==================== Residence Operations ====================
suspend fun getResidences(forceRefresh: Boolean = false): ApiResult<List<Residence>> { suspend fun getResidences(forceRefresh: Boolean = false): ApiResult<List<ResidenceResponse>> {
// Check cache first // Check cache first
if (!forceRefresh) { if (!forceRefresh) {
val cached = DataCache.residences.value val cached = DataCache.residences.value
@@ -294,7 +294,7 @@ object APILayer {
return result 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 // Check cache first
if (!forceRefresh) { if (!forceRefresh) {
val cached = DataCache.residences.value.find { it.id == id } val cached = DataCache.residences.value.find { it.id == id }
@@ -321,7 +321,7 @@ object APILayer {
return residenceApi.getResidenceSummary(token) 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 token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
val result = residenceApi.createResidence(token, request) val result = residenceApi.createResidence(token, request)
@@ -333,7 +333,7 @@ object APILayer {
return result 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 token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
val result = residenceApi.updateResidence(token, id, request) val result = residenceApi.updateResidence(token, id, request)
@@ -377,12 +377,12 @@ object APILayer {
return residenceApi.getResidenceUsers(token, residenceId) 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) val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
return residenceApi.getShareCode(token, residenceId) 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) val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
return residenceApi.generateShareCode(token, residenceId) return residenceApi.generateShareCode(token, residenceId)
} }
@@ -436,7 +436,7 @@ object APILayer {
return result 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 token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
val result = taskApi.createTask(token, request) val result = taskApi.createTask(token, request)
@@ -448,7 +448,7 @@ object APILayer {
return result 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 token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
val result = taskApi.updateTask(token, id, request) val result = taskApi.updateTask(token, id, request)
@@ -470,7 +470,7 @@ object APILayer {
}?.id }?.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) val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
// Look up 'cancelled' status ID from cache // Look up 'cancelled' status ID from cache
@@ -487,7 +487,7 @@ object APILayer {
return result 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) val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
// Look up 'pending' status ID from cache // Look up 'pending' status ID from cache
@@ -504,7 +504,7 @@ object APILayer {
return result 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) val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
// Look up 'in progress' status ID from cache // Look up 'in progress' status ID from cache
@@ -522,7 +522,7 @@ object APILayer {
return result 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 token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
val result = taskApi.archiveTask(token, taskId) val result = taskApi.archiveTask(token, taskId)
@@ -534,7 +534,7 @@ object APILayer {
return result 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 token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
val result = taskApi.unarchiveTask(token, taskId) val result = taskApi.unarchiveTask(token, taskId)
@@ -546,7 +546,7 @@ object APILayer {
return result 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 token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
val result = taskCompletionApi.createCompletion(token, request) val result = taskCompletionApi.createCompletion(token, request)
@@ -562,7 +562,7 @@ object APILayer {
request: TaskCompletionCreateRequest, request: TaskCompletionCreateRequest,
images: List<ByteArray>, images: List<ByteArray>,
imageFileNames: List<String> imageFileNames: List<String>
): ApiResult<TaskCompletion> { ): ApiResult<TaskCompletionResponse> {
val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401) val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
val result = taskCompletionApi.createCompletionWithImages(token, request, images, imageFileNames) 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 { return try {
val response = client.get("$baseUrl/contractors/$id/tasks/") { val response = client.get("$baseUrl/contractors/$id/tasks/") {
header("Authorization", "Token $token") 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 { return try {
val response = client.get("$baseUrl/tasks/") { val response = client.get("$baseUrl/tasks/") {
header("Authorization", "Token $token") header("Authorization", "Token $token")

View File

@@ -9,7 +9,7 @@ import io.ktor.http.*
class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) { class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
private val baseUrl = ApiClient.getBaseUrl() private val baseUrl = ApiClient.getBaseUrl()
suspend fun getResidences(token: String): ApiResult<List<Residence>> { suspend fun getResidences(token: String): ApiResult<List<ResidenceResponse>> {
return try { return try {
val response = client.get("$baseUrl/residences/") { val response = client.get("$baseUrl/residences/") {
header("Authorization", "Token $token") 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 { return try {
val response = client.get("$baseUrl/residences/$id/") { val response = client.get("$baseUrl/residences/$id/") {
header("Authorization", "Token $token") 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 { return try {
val response = client.post("$baseUrl/residences/") { val response = client.post("$baseUrl/residences/") {
header("Authorization", "Token $token") 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 { return try {
val response = client.put("$baseUrl/residences/$id/") { val response = client.put("$baseUrl/residences/$id/") {
header("Authorization", "Token $token") header("Authorization", "Token $token")
@@ -126,7 +126,7 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
} }
// Share Code Management // Share Code Management
suspend fun generateShareCode(token: String, residenceId: Int): ApiResult<ResidenceShareCode> { suspend fun generateShareCode(token: String, residenceId: Int): ApiResult<GenerateShareCodeResponse> {
return try { return try {
val response = client.post("$baseUrl/residences/$residenceId/generate-share-code/") { val response = client.post("$baseUrl/residences/$residenceId/generate-share-code/") {
header("Authorization", "Token $token") 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 { return try {
val response = client.get("$baseUrl/residences/$residenceId/share-code/") { val response = client.get("$baseUrl/residences/$residenceId/share-code/") {
header("Authorization", "Token $token") 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 { return try {
val response = client.get("$baseUrl/tasks/$id/") { val response = client.get("$baseUrl/tasks/$id/") {
header("Authorization", "Token $token") 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 { return try {
val response = client.post("$baseUrl/tasks/") { val response = client.post("$baseUrl/tasks/") {
header("Authorization", "Token $token") 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 { return try {
val response = client.put("$baseUrl/tasks/$id/") { val response = client.put("$baseUrl/tasks/$id/") {
header("Authorization", "Token $token") header("Authorization", "Token $token")
@@ -132,7 +132,7 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) {
* archive, unarchive) have been REMOVED from the API. * archive, unarchive) have been REMOVED from the API.
* All task updates now use PATCH /tasks/{id}/. * 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 { return try {
val response = client.patch("$baseUrl/tasks/$id/") { val response = client.patch("$baseUrl/tasks/$id/") {
header("Authorization", "Token $token") 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. // They're kept for backward compatibility with existing ViewModel calls.
// New code should use patchTask directly with status IDs from DataCache. // 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)) 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)) 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)) 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)) 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)) 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) { class TaskCompletionApi(private val client: HttpClient = ApiClient.httpClient) {
private val baseUrl = ApiClient.getBaseUrl() private val baseUrl = ApiClient.getBaseUrl()
suspend fun getCompletions(token: String): ApiResult<List<TaskCompletion>> { suspend fun getCompletions(token: String): ApiResult<List<TaskCompletionResponse>> {
return try { return try {
val response = client.get("$baseUrl/task-completions/") { val response = client.get("$baseUrl/task-completions/") {
header("Authorization", "Token $token") 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 { return try {
val response = client.get("$baseUrl/task-completions/$id/") { val response = client.get("$baseUrl/task-completions/$id/") {
header("Authorization", "Token $token") 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 { return try {
val response = client.post("$baseUrl/task-completions/") { val response = client.post("$baseUrl/task-completions/") {
header("Authorization", "Token $token") 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 { return try {
val response = client.put("$baseUrl/task-completions/$id/") { val response = client.put("$baseUrl/task-completions/$id/") {
header("Authorization", "Token $token") header("Authorization", "Token $token")
@@ -98,7 +98,7 @@ class TaskCompletionApi(private val client: HttpClient = ApiClient.httpClient) {
request: TaskCompletionCreateRequest, request: TaskCompletionCreateRequest,
images: List<ByteArray> = emptyList(), images: List<ByteArray> = emptyList(),
imageFileNames: List<String> = emptyList() imageFileNames: List<String> = emptyList()
): ApiResult<TaskCompletion> { ): ApiResult<TaskCompletionResponse> {
return try { return try {
val response = client.post("$baseUrl/task-completions/") { val response = client.post("$baseUrl/task-completions/") {
header("Authorization", "Token $token") 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.MultiPartFormDataContent(
io.ktor.client.request.forms.formData { io.ktor.client.request.forms.formData {
// Add text fields // Add text fields
append("task", request.task.toString()) append("task_id", request.taskId.toString())
request.contractor?.let { append("contractor", it.toString()) } request.completedAt?.let { append("completed_at", it) }
request.completedByName?.let { append("completed_by_name", it) } request.actualCost?.let { append("actual_cost", it.toString()) }
append("completion_date", request.completionDate)
request.actualCost?.let { append("actual_cost", it) }
request.notes?.let { append("notes", it) } request.notes?.let { append("notes", it) }
request.rating?.let { append("rating", it.toString()) } request.photoUrl?.let { append("photo_url", it) }
// Add image files // Add image files
images.forEachIndexed { index, imageBytes -> images.forEachIndexed { index, imageBytes ->

View File

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

View File

@@ -255,15 +255,26 @@ fun CompleteTaskDialog(
// Get current date in ISO format // Get current date in ISO format
val currentDate = getCurrentDateTime() 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( onComplete(
TaskCompletionCreateRequest( TaskCompletionCreateRequest(
task = taskId, taskId = taskId,
contractor = selectedContractorId, completedAt = currentDate,
completedByName = completedByName.ifBlank { null },
completionDate = currentDate,
actualCost = actualCost.ifBlank { null }?.toDoubleOrNull(), actualCost = actualCost.ifBlank { null }?.toDoubleOrNull(),
notes = notes.ifBlank { null }, notes = notesWithContractor,
rating = rating photoUrl = null // Images handled separately
), ),
selectedImages selectedImages
) )

View File

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

View File

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

View File

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

View File

@@ -28,15 +28,15 @@ fun HomeScreen(
onLogout: () -> Unit, onLogout: () -> Unit,
viewModel: ResidenceViewModel = viewModel { ResidenceViewModel() } viewModel: ResidenceViewModel = viewModel { ResidenceViewModel() }
) { ) {
val summaryState by viewModel.residenceSummaryState.collectAsState() val summaryState by viewModel.myResidencesState.collectAsState()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.loadResidenceSummary() viewModel.loadMyResidences()
} }
// Handle errors for loading summary // Handle errors for loading summary
summaryState.HandleErrors( summaryState.HandleErrors(
onRetry = { viewModel.loadResidenceSummary() }, onRetry = { viewModel.loadMyResidences() },
errorTitle = "Failed to Load Summary" 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.utils.SubscriptionHelper
import com.example.mycrib.ui.subscription.UpgradePromptDialog import com.example.mycrib.ui.subscription.UpgradePromptDialog
import com.example.mycrib.cache.SubscriptionCache import com.example.mycrib.cache.SubscriptionCache
import com.example.mycrib.cache.DataCache
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -69,6 +70,9 @@ fun ResidenceDetailScreen(
var showUpgradePrompt by remember { mutableStateOf(false) } var showUpgradePrompt by remember { mutableStateOf(false) }
var upgradeTriggerKey by remember { mutableStateOf<String?>(null) } 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 // Check if tasks are blocked (limit=0) - this hides the FAB
val isTasksBlocked = SubscriptionHelper.isTasksBlocked() val isTasksBlocked = SubscriptionHelper.isTasksBlocked()
// Get current count for checking when adding // Get current count for checking when adding
@@ -220,7 +224,7 @@ fun ResidenceDetailScreen(
ManageUsersDialog( ManageUsersDialog(
residenceId = residence.id, residenceId = residence.id,
residenceName = residence.name, residenceName = residence.name,
isPrimaryOwner = residence.isPrimaryOwner, isPrimaryOwner = residence.ownerId == currentUser?.id,
onDismiss = { onDismiss = {
showManageUsersDialog = false showManageUsersDialog = false
}, },
@@ -395,7 +399,7 @@ fun ResidenceDetailScreen(
} }
// Manage Users button - only show for primary owners // Manage Users button - only show for primary owners
if (residence.isPrimaryOwner) { if (residence.ownerId == currentUser?.id) {
IconButton(onClick = { IconButton(onClick = {
showManageUsersDialog = true showManageUsersDialog = true
}) { }) {
@@ -410,7 +414,7 @@ fun ResidenceDetailScreen(
} }
// Delete button - only show for primary owners // Delete button - only show for primary owners
if (residence.isPrimaryOwner) { if (residence.ownerId == currentUser?.id) {
IconButton(onClick = { IconButton(onClick = {
showDeleteConfirmation = true showDeleteConfirmation = true
}) { }) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ import SwiftUI
import ComposeApp import ComposeApp
struct ResidenceCard: View { struct ResidenceCard: View {
let residence: ResidenceWithTasks let residence: ResidenceResponse
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: AppSpacing.md) { VStack(alignment: .leading, spacing: AppSpacing.md) {
@@ -26,8 +26,8 @@ struct ResidenceCard: View {
.foregroundColor(Color.appTextPrimary) .foregroundColor(Color.appTextPrimary)
// .lineLimit(1) // .lineLimit(1)
if let propertyType = residence.propertyType { if let propertyTypeName = residence.propertyTypeName {
Text(propertyType) Text(propertyTypeName)
.font(.caption.weight(.medium)) .font(.caption.weight(.medium))
.foregroundColor(Color.appTextSecondary) .foregroundColor(Color.appTextSecondary)
.textCase(.uppercase) .textCase(.uppercase)
@@ -51,18 +51,18 @@ struct ResidenceCard: View {
// Address // Address
VStack(alignment: .leading, spacing: AppSpacing.xxs) { VStack(alignment: .leading, spacing: AppSpacing.xxs) {
if let streetAddress = residence.streetAddress { if !residence.streetAddress.isEmpty {
HStack(spacing: AppSpacing.xxs) { HStack(spacing: AppSpacing.xxs) {
Image(systemName: "mappin.circle.fill") Image(systemName: "mappin.circle.fill")
.font(.system(size: 12, weight: .medium)) .font(.system(size: 12, weight: .medium))
.foregroundColor(Color.appTextSecondary) .foregroundColor(Color.appTextSecondary)
Text(streetAddress) Text(residence.streetAddress)
.font(.callout) .font(.callout)
.foregroundColor(Color.appTextSecondary) .foregroundColor(Color.appTextSecondary)
} }
} }
if residence.city != nil || residence.stateProvince != nil { if !residence.city.isEmpty || !residence.stateProvince.isEmpty {
HStack(spacing: AppSpacing.xxs) { HStack(spacing: AppSpacing.xxs) {
Image(systemName: "location.fill") Image(systemName: "location.fill")
.font(.system(size: 12, weight: .medium)) .font(.system(size: 12, weight: .medium))
@@ -99,16 +99,16 @@ struct ResidenceCard: View {
} }
#Preview { #Preview {
ResidenceCard(residence: ResidenceWithTasks( ResidenceCard(residence: ResidenceResponse(
id: 1, id: 1,
owner: 1, ownerId: 1,
ownerUsername: "testuser", owner: ResidenceUserResponse(id: 1, username: "testuser", email: "test@test.com", firstName: "", lastName: ""),
isPrimaryOwner: false, users: [],
userCount: 1,
name: "My Home", name: "My Home",
propertyType: "House", propertyTypeId: 1,
propertyType: ResidenceType(id: 1, name: "House"),
streetAddress: "123 Main St", streetAddress: "123 Main St",
apartmentUnit: nil, apartmentUnit: "",
city: "San Francisco", city: "San Francisco",
stateProvince: "CA", stateProvince: "CA",
postalCode: "94102", postalCode: "94102",
@@ -118,44 +118,11 @@ struct ResidenceCard: View {
squareFootage: 1800, squareFootage: 1800,
lotSize: 0.25, lotSize: 0.25,
yearBuilt: 2010, yearBuilt: 2010,
description: nil, description: "",
purchaseDate: nil, purchaseDate: nil,
purchasePrice: nil, purchasePrice: nil,
isPrimary: true, isPrimary: true,
taskSummary: TaskSummary( isActive: true,
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: [],
createdAt: "2024-01-01T00:00:00Z", createdAt: "2024-01-01T00:00:00Z",
updatedAt: "2024-01-01T00:00:00Z" updatedAt: "2024-01-01T00:00:00Z"
)) ))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,13 +11,13 @@ struct AllTasksView: View {
@State private var showAddTask = false @State private var showAddTask = false
@State private var showEditTask = false @State private var showEditTask = false
@State private var showingUpgradePrompt = false @State private var showingUpgradePrompt = false
@State private var selectedTaskForEdit: TaskDetail? @State private var selectedTaskForEdit: TaskResponse?
@State private var selectedTaskForComplete: TaskDetail? @State private var selectedTaskForComplete: TaskResponse?
@State private var selectedTaskForArchive: TaskDetail? @State private var selectedTaskForArchive: TaskResponse?
@State private var showArchiveConfirmation = false @State private var showArchiveConfirmation = false
@State private var selectedTaskForCancel: TaskDetail? @State private var selectedTaskForCancel: TaskResponse?
@State private var showCancelConfirmation = false @State private var showCancelConfirmation = false
// Count total tasks across all columns // Count total tasks across all columns
@@ -334,37 +334,9 @@ struct RoundedCorner: Shape {
} }
} }
extension Array where Element == ResidenceWithTasks { extension Array where Element == ResidenceResponse {
/// Converts an array of ResidenceWithTasks into an array of Residence. /// Returns the array as-is (for API compatibility)
/// Adjust the mapping inside as needed to match your model initializers. func toResidences() -> [ResidenceResponse] {
func toResidences() -> [Residence] { return self
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
)
}
} }
} }

View File

@@ -3,12 +3,13 @@ import PhotosUI
import ComposeApp import ComposeApp
struct CompleteTaskView: View { struct CompleteTaskView: View {
let task: TaskDetail let task: TaskResponse
let onComplete: () -> Void let onComplete: () -> Void
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@StateObject private var taskViewModel = TaskViewModel() @StateObject private var taskViewModel = TaskViewModel()
@StateObject private var contractorViewModel = ContractorViewModel() @StateObject private var contractorViewModel = ContractorViewModel()
private let completionViewModel = ComposeApp.TaskCompletionViewModel()
@State private var completedByName: String = "" @State private var completedByName: String = ""
@State private var actualCost: String = "" @State private var actualCost: String = ""
@State private var notes: String = "" @State private var notes: String = ""
@@ -32,7 +33,7 @@ struct CompleteTaskView: View {
.font(.headline) .font(.headline)
HStack { HStack {
Label(task.category.name.capitalized, systemImage: "folder") Label((task.category?.name ?? "").capitalized, systemImage: "folder")
.font(.subheadline) .font(.subheadline)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
@@ -303,66 +304,53 @@ struct CompleteTaskView: View {
isSubmitting = true isSubmitting = true
// Get current date in ISO format // Create request with simplified Go API format
let dateFormatter = ISO8601DateFormatter() // Note: completedAt defaults to now on server if not provided
let currentDate = dateFormatter.string(from: Date())
// Create request
let request = TaskCompletionCreateRequest( let request = TaskCompletionCreateRequest(
task: task.id, taskId: task.id,
completedByUser: nil, completedAt: 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),
notes: notes.isEmpty ? nil : notes, 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 { Task {
do { for await state in completionViewModel.createCompletionState {
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)
}
await MainActor.run { await MainActor.run {
if result is ApiResultSuccess<TaskCompletion> { switch state {
case is ApiResultSuccess<TaskCompletionResponse>:
self.isSubmitting = false self.isSubmitting = false
self.dismiss() self.dismiss()
self.onComplete() self.onComplete()
} else if let errorResult = result as? ApiResultError { case let error as ApiResultError:
self.errorMessage = errorResult.message self.errorMessage = error.message
self.showError = true
self.isSubmitting = false
} else {
self.errorMessage = "Failed to complete task"
self.showError = true self.showError = true
self.isSubmitting = false self.isSubmitting = false
case is ApiResultLoading:
// Still loading, continue waiting
break
default:
break
} }
} }
} catch {
await MainActor.run { // Break out of loop on terminal states
self.errorMessage = error.localizedDescription if state is ApiResultSuccess<TaskCompletionResponse> || state is ApiResultError {
self.showError = true break
self.isSubmitting = false
} }
} }
} }

View File

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

View File

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

View File

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