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:
@@ -289,7 +289,7 @@ fun App(
|
||||
EditResidenceRoute(
|
||||
residenceId = residence.id,
|
||||
name = residence.name,
|
||||
propertyType = residence.propertyType?.toInt(),
|
||||
propertyType = residence.propertyTypeId,
|
||||
streetAddress = residence.streetAddress,
|
||||
apartmentUnit = residence.apartmentUnit,
|
||||
city = residence.city,
|
||||
@@ -297,16 +297,16 @@ fun App(
|
||||
postalCode = residence.postalCode,
|
||||
country = residence.country,
|
||||
bedrooms = residence.bedrooms,
|
||||
bathrooms = residence.bathrooms,
|
||||
bathrooms = residence.bathrooms?.toFloat(),
|
||||
squareFootage = residence.squareFootage,
|
||||
lotSize = residence.lotSize,
|
||||
lotSize = residence.lotSize?.toFloat(),
|
||||
yearBuilt = residence.yearBuilt,
|
||||
description = residence.description,
|
||||
isPrimary = residence.isPrimary,
|
||||
ownerUserName = residence.ownerUsername,
|
||||
createdAt = residence.createdAt,
|
||||
updatedAt = residence.updatedAt,
|
||||
owner = residence.owner
|
||||
owner = residence.ownerId
|
||||
)
|
||||
)
|
||||
},
|
||||
@@ -314,15 +314,15 @@ fun App(
|
||||
navController.navigate(
|
||||
EditTaskRoute(
|
||||
taskId = task.id,
|
||||
residenceId = task.residence,
|
||||
residenceId = task.residenceId,
|
||||
title = task.title,
|
||||
description = task.description,
|
||||
categoryId = task.category.id,
|
||||
categoryName = task.category.name,
|
||||
frequencyId = task.frequency.id,
|
||||
frequencyName = task.frequency.name,
|
||||
priorityId = task.priority.id,
|
||||
priorityName = task.priority.name,
|
||||
categoryId = task.category?.id ?: 0,
|
||||
categoryName = task.category?.name ?: "",
|
||||
frequencyId = task.frequency?.id ?: 0,
|
||||
frequencyName = task.frequency?.name ?: "",
|
||||
priorityId = task.priority?.id ?: 0,
|
||||
priorityName = task.priority?.name ?: "",
|
||||
statusId = task.status?.id,
|
||||
statusName = task.status?.name,
|
||||
dueDate = task.dueDate,
|
||||
@@ -402,25 +402,24 @@ fun App(
|
||||
EditResidenceScreen(
|
||||
residence = Residence(
|
||||
id = route.residenceId,
|
||||
ownerId = route.owner ?: 0,
|
||||
name = route.name,
|
||||
propertyType = route.propertyType.toString(), // Will be fetched from lookups
|
||||
streetAddress = route.streetAddress,
|
||||
apartmentUnit = route.apartmentUnit,
|
||||
city = route.city,
|
||||
stateProvince = route.stateProvince,
|
||||
postalCode = route.postalCode,
|
||||
country = route.country,
|
||||
propertyTypeId = route.propertyType,
|
||||
streetAddress = route.streetAddress ?: "",
|
||||
apartmentUnit = route.apartmentUnit ?: "",
|
||||
city = route.city ?: "",
|
||||
stateProvince = route.stateProvince ?: "",
|
||||
postalCode = route.postalCode ?: "",
|
||||
country = route.country ?: "",
|
||||
bedrooms = route.bedrooms,
|
||||
bathrooms = route.bathrooms,
|
||||
bathrooms = route.bathrooms?.toDouble(),
|
||||
squareFootage = route.squareFootage,
|
||||
lotSize = route.lotSize,
|
||||
lotSize = route.lotSize?.toDouble(),
|
||||
yearBuilt = route.yearBuilt,
|
||||
description = route.description,
|
||||
description = route.description ?: "",
|
||||
purchaseDate = null,
|
||||
purchasePrice = null,
|
||||
isPrimary = route.isPrimary,
|
||||
ownerUsername = route.ownerUserName,
|
||||
owner = route.owner,
|
||||
createdAt = route.createdAt,
|
||||
updatedAt = route.updatedAt
|
||||
),
|
||||
@@ -455,7 +454,7 @@ fun App(
|
||||
EditResidenceRoute(
|
||||
residenceId = residence.id,
|
||||
name = residence.name,
|
||||
propertyType = residence.propertyType?.toInt(),
|
||||
propertyType = residence.propertyTypeId,
|
||||
streetAddress = residence.streetAddress,
|
||||
apartmentUnit = residence.apartmentUnit,
|
||||
city = residence.city,
|
||||
@@ -463,16 +462,16 @@ fun App(
|
||||
postalCode = residence.postalCode,
|
||||
country = residence.country,
|
||||
bedrooms = residence.bedrooms,
|
||||
bathrooms = residence.bathrooms,
|
||||
bathrooms = residence.bathrooms?.toFloat(),
|
||||
squareFootage = residence.squareFootage,
|
||||
lotSize = residence.lotSize,
|
||||
lotSize = residence.lotSize?.toFloat(),
|
||||
yearBuilt = residence.yearBuilt,
|
||||
description = residence.description,
|
||||
isPrimary = residence.isPrimary,
|
||||
ownerUserName = residence.ownerUsername,
|
||||
createdAt = residence.createdAt,
|
||||
updatedAt = residence.updatedAt,
|
||||
owner = residence.owner
|
||||
owner = residence.ownerId
|
||||
)
|
||||
)
|
||||
},
|
||||
@@ -480,15 +479,15 @@ fun App(
|
||||
navController.navigate(
|
||||
EditTaskRoute(
|
||||
taskId = task.id,
|
||||
residenceId = task.residence,
|
||||
residenceId = task.residenceId,
|
||||
title = task.title,
|
||||
description = task.description,
|
||||
categoryId = task.category.id,
|
||||
categoryName = task.category.name,
|
||||
frequencyId = task.frequency.id,
|
||||
frequencyName = task.frequency.name,
|
||||
priorityId = task.priority.id,
|
||||
priorityName = task.priority.name,
|
||||
categoryId = task.category?.id ?: 0,
|
||||
categoryName = task.category?.name ?: "",
|
||||
frequencyId = task.frequency?.id ?: 0,
|
||||
frequencyName = task.frequency?.name ?: "",
|
||||
priorityId = task.priority?.id ?: 0,
|
||||
priorityName = task.priority?.name ?: "",
|
||||
statusId = task.status?.id,
|
||||
statusName = task.status?.name,
|
||||
dueDate = task.dueDate,
|
||||
@@ -506,25 +505,24 @@ fun App(
|
||||
EditTaskScreen(
|
||||
task = TaskDetail(
|
||||
id = route.taskId,
|
||||
residence = route.residenceId,
|
||||
residenceId = route.residenceId,
|
||||
createdById = 0,
|
||||
title = route.title,
|
||||
description = route.description,
|
||||
category = TaskCategory(route.categoryId, route.categoryName),
|
||||
description = route.description ?: "",
|
||||
category = TaskCategory(id = route.categoryId, name = route.categoryName),
|
||||
frequency = TaskFrequency(
|
||||
route.frequencyId, route.frequencyName, "", route.frequencyName,
|
||||
daySpan = 0,
|
||||
notifyDays = 0
|
||||
id = route.frequencyId,
|
||||
name = route.frequencyName,
|
||||
days = null
|
||||
),
|
||||
priority = TaskPriority(route.priorityId, route.priorityName, displayName = route.statusName ?: ""),
|
||||
priority = TaskPriority(id = route.priorityId, name = route.priorityName),
|
||||
status = route.statusId?.let {
|
||||
TaskStatus(it, route.statusName ?: "", displayName = route.statusName ?: "")
|
||||
TaskStatus(id = it, name = route.statusName ?: "")
|
||||
},
|
||||
dueDate = route.dueDate,
|
||||
estimatedCost = route.estimatedCost?.toDoubleOrNull(),
|
||||
createdAt = route.createdAt,
|
||||
updatedAt = route.updatedAt,
|
||||
nextScheduledDate = null,
|
||||
showCompletedButton = false,
|
||||
completions = emptyList()
|
||||
),
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
|
||||
@@ -3,146 +3,163 @@ package com.example.mycrib.models
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* User reference for task-related responses - matching Go API TaskUserResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class CustomTask (
|
||||
data class TaskUserResponse(
|
||||
val id: Int,
|
||||
val residence: Int,
|
||||
@SerialName("created_by") val createdBy: Int,
|
||||
@SerialName("created_by_username") val createdByUsername: String,
|
||||
val username: String,
|
||||
val email: String,
|
||||
@SerialName("first_name") val firstName: String = "",
|
||||
@SerialName("last_name") val lastName: String = ""
|
||||
) {
|
||||
val displayName: String
|
||||
get() = when {
|
||||
firstName.isNotBlank() && lastName.isNotBlank() -> "$firstName $lastName"
|
||||
firstName.isNotBlank() -> firstName
|
||||
lastName.isNotBlank() -> lastName
|
||||
else -> username
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Task response matching Go API TaskResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskResponse(
|
||||
val id: Int,
|
||||
@SerialName("residence_id") val residenceId: Int,
|
||||
@SerialName("created_by_id") val createdById: Int,
|
||||
@SerialName("created_by") val createdBy: TaskUserResponse? = null,
|
||||
@SerialName("assigned_to_id") val assignedToId: Int? = null,
|
||||
@SerialName("assigned_to") val assignedTo: TaskUserResponse? = null,
|
||||
val title: String,
|
||||
val description: String? = null,
|
||||
val category: TaskCategory?,
|
||||
val frequency: TaskFrequency,
|
||||
val priority: TaskPriority,
|
||||
val description: String = "",
|
||||
@SerialName("category_id") val categoryId: Int? = null,
|
||||
val category: TaskCategory? = null,
|
||||
@SerialName("priority_id") val priorityId: Int? = null,
|
||||
val priority: TaskPriority? = null,
|
||||
@SerialName("status_id") val statusId: Int? = null,
|
||||
val status: TaskStatus? = null,
|
||||
@SerialName("due_date") val dueDate: String?,
|
||||
@SerialName("next_scheduled_date") val nextScheduledDate: String? = null,
|
||||
@SerialName("frequency_id") val frequencyId: Int? = null,
|
||||
val frequency: TaskFrequency? = null,
|
||||
@SerialName("due_date") val dueDate: String? = null,
|
||||
@SerialName("estimated_cost") val estimatedCost: Double? = null,
|
||||
@SerialName("actual_cost") val actualCost: Double? = null,
|
||||
@SerialName("completion_count") val completionCount: Int? = null,
|
||||
val notes: String? = null,
|
||||
val archived: Boolean = false,
|
||||
@SerialName("contractor_id") val contractorId: Int? = null,
|
||||
@SerialName("is_cancelled") val isCancelled: Boolean = false,
|
||||
@SerialName("is_archived") val isArchived: Boolean = false,
|
||||
@SerialName("parent_task_id") val parentTaskId: Int? = null,
|
||||
val completions: List<TaskCompletionResponse> = emptyList(),
|
||||
@SerialName("created_at") val createdAt: String,
|
||||
@SerialName("updated_at") val updatedAt: String,
|
||||
@SerialName("show_completed_button") val showCompletedButton: Boolean = false,
|
||||
@SerialName("days_until_due") val daysUntilDue: Int? = null,
|
||||
@SerialName("is_overdue") val isOverdue: Boolean? = null,
|
||||
@SerialName("last_completion") val lastCompletion: LastCompletion? = null
|
||||
)
|
||||
@SerialName("updated_at") val updatedAt: String
|
||||
) {
|
||||
// Helper for backwards compatibility with old code
|
||||
val archived: Boolean get() = isArchived
|
||||
|
||||
// Aliases for backwards compatibility with UI code expecting old field names
|
||||
val residence: Int get() = residenceId
|
||||
|
||||
// Helper to get created by username
|
||||
val createdByUsername: String get() = createdBy?.displayName ?: ""
|
||||
|
||||
// Computed properties for UI compatibility (these were in Django API but not Go API)
|
||||
val categoryName: String? get() = category?.name
|
||||
val categoryDescription: String? get() = category?.description
|
||||
val frequencyName: String? get() = frequency?.name
|
||||
val frequencyDisplayName: String? get() = frequency?.displayName
|
||||
val frequencyDaySpan: Int? get() = frequency?.days
|
||||
val priorityName: String? get() = priority?.name
|
||||
val priorityDisplayName: String? get() = priority?.displayName
|
||||
val statusName: String? get() = status?.name
|
||||
|
||||
// Fields that don't exist in Go API - return null/default
|
||||
val nextScheduledDate: String? get() = null // Would need calculation based on frequency
|
||||
val showCompletedButton: Boolean get() = status?.name?.lowercase() != "completed"
|
||||
val daySpan: Int? get() = frequency?.days
|
||||
val notifyDays: Int? get() = null // Not in Go API
|
||||
}
|
||||
|
||||
/**
|
||||
* Task completion response matching Go API TaskCompletionResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class LastCompletion(
|
||||
@SerialName("completion_date") val completionDate: String,
|
||||
@SerialName("completed_by") val completedBy: String?,
|
||||
@SerialName("actual_cost") val actualCost: Double?,
|
||||
val rating: Int?
|
||||
)
|
||||
data class TaskCompletionResponse(
|
||||
val id: Int,
|
||||
@SerialName("task_id") val taskId: Int,
|
||||
@SerialName("completed_by") val completedBy: TaskUserResponse? = null,
|
||||
@SerialName("completed_at") val completedAt: String,
|
||||
val notes: String = "",
|
||||
@SerialName("actual_cost") val actualCost: Double? = null,
|
||||
@SerialName("photo_url") val photoUrl: String = "",
|
||||
@SerialName("created_at") val createdAt: String
|
||||
) {
|
||||
// Helper for backwards compatibility
|
||||
val completionDate: String get() = completedAt
|
||||
val completedByName: String? get() = completedBy?.displayName
|
||||
|
||||
// Backwards compatibility for UI that expects these fields
|
||||
val task: Int get() = taskId
|
||||
val images: List<TaskCompletionImage> get() = if (photoUrl.isNotEmpty()) {
|
||||
listOf(TaskCompletionImage(id = 0, imageUrl = photoUrl))
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
val rating: Int? get() = null // Not in Go API
|
||||
val contractor: Int? get() = null // Not in Go API - would need to be fetched separately
|
||||
val contractorDetails: ContractorMinimal? get() = null // Not in Go API
|
||||
}
|
||||
|
||||
/**
|
||||
* Task create request matching Go API CreateTaskRequest
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskCreateRequest(
|
||||
val residence: Int,
|
||||
@SerialName("residence_id") val residenceId: Int,
|
||||
val title: String,
|
||||
val description: String? = null,
|
||||
val category: Int,
|
||||
val frequency: Int,
|
||||
@SerialName("interval_days") val intervalDays: Int? = null,
|
||||
val priority: Int,
|
||||
val status: Int? = null,
|
||||
@SerialName("due_date") val dueDate: String,
|
||||
@SerialName("category_id") val categoryId: Int? = null,
|
||||
@SerialName("priority_id") val priorityId: Int? = null,
|
||||
@SerialName("status_id") val statusId: Int? = null,
|
||||
@SerialName("frequency_id") val frequencyId: Int? = null,
|
||||
@SerialName("assigned_to_id") val assignedToId: Int? = null,
|
||||
@SerialName("due_date") val dueDate: String? = null,
|
||||
@SerialName("estimated_cost") val estimatedCost: Double? = null,
|
||||
val archived: Boolean = false
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TaskDetail(
|
||||
val id: Int,
|
||||
val residence: Int,
|
||||
@SerialName("residence_name") val residenceName: String? = null,
|
||||
@SerialName("created_by") val createdBy: Int? = null,
|
||||
@SerialName("created_by_username") val createdByUsername: String? = null,
|
||||
val title: String,
|
||||
val description: String?,
|
||||
val category: TaskCategory,
|
||||
val priority: TaskPriority,
|
||||
val frequency: TaskFrequency,
|
||||
val status: TaskStatus?,
|
||||
@SerialName("due_date") val dueDate: String?,
|
||||
@SerialName("interval_days") val intervalDays: Int? = null,
|
||||
@SerialName("estimated_cost") val estimatedCost: Double? = null,
|
||||
val archived: Boolean = false,
|
||||
@SerialName("created_at") val createdAt: String,
|
||||
@SerialName("updated_at") val updatedAt: String,
|
||||
@SerialName("next_scheduled_date") val nextScheduledDate: String? = null,
|
||||
@SerialName("show_completed_button") val showCompletedButton: Boolean = false,
|
||||
val completions: List<TaskCompletion>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TasksByResidenceResponse(
|
||||
@SerialName("residence_id") val residenceId: String,
|
||||
@SerialName("days_threshold") val daysThreshold: Int,
|
||||
val summary: CategorizedTaskSummary,
|
||||
@SerialName("upcoming_tasks") val upcomingTasks: List<TaskDetail>,
|
||||
@SerialName("in_progress_tasks") val inProgressTasks: List<TaskDetail>,
|
||||
@SerialName("done_tasks") val doneTasks: List<TaskDetail>,
|
||||
@SerialName("archived_tasks") val archivedTasks: List<TaskDetail>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CategorizedTaskSummary(
|
||||
val upcoming: Int,
|
||||
@SerialName("in_progress") val inProgress: Int,
|
||||
val done: Int,
|
||||
val archived: Int
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AllTasksResponse(
|
||||
@SerialName("days_threshold") val daysThreshold: Int,
|
||||
val summary: CategorizedTaskSummary,
|
||||
@SerialName("upcoming_tasks") val upcomingTasks: List<TaskDetail>,
|
||||
@SerialName("in_progress_tasks") val inProgressTasks: List<TaskDetail>,
|
||||
@SerialName("done_tasks") val doneTasks: List<TaskDetail>,
|
||||
@SerialName("archived_tasks") val archivedTasks: List<TaskDetail>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TaskCancelResponse(
|
||||
val message: String,
|
||||
val task: TaskDetail
|
||||
@SerialName("contractor_id") val contractorId: Int? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Request model for PATCH updates to a task.
|
||||
* Used for status changes and archive/unarchive operations.
|
||||
* Task update request matching Go API UpdateTaskRequest
|
||||
* All fields are optional - only provided fields will be updated.
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskPatchRequest(
|
||||
val status: Int? = null, // Status ID to update
|
||||
val archived: Boolean? = null // Archive/unarchive flag
|
||||
data class TaskUpdateRequest(
|
||||
val title: String? = null,
|
||||
val description: String? = null,
|
||||
@SerialName("category_id") val categoryId: Int? = null,
|
||||
@SerialName("priority_id") val priorityId: Int? = null,
|
||||
@SerialName("status_id") val statusId: Int? = null,
|
||||
@SerialName("frequency_id") val frequencyId: Int? = null,
|
||||
@SerialName("assigned_to_id") val assignedToId: Int? = null,
|
||||
@SerialName("due_date") val dueDate: String? = null,
|
||||
@SerialName("estimated_cost") val estimatedCost: Double? = null,
|
||||
@SerialName("actual_cost") val actualCost: Double? = null,
|
||||
@SerialName("contractor_id") val contractorId: Int? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Minimal task model for list/kanban views.
|
||||
* Uses IDs instead of nested objects for efficiency.
|
||||
* Resolve IDs to full objects via DataCache.getTaskCategory(), etc.
|
||||
* Task action response (for cancel, archive, mark in progress, etc.)
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskMinimal(
|
||||
val id: Int,
|
||||
val title: String,
|
||||
val description: String? = null,
|
||||
@SerialName("due_date") val dueDate: String? = null,
|
||||
@SerialName("next_scheduled_date") val nextScheduledDate: String? = null,
|
||||
@SerialName("category_id") val categoryId: Int? = null,
|
||||
@SerialName("frequency_id") val frequencyId: Int,
|
||||
@SerialName("priority_id") val priorityId: Int,
|
||||
@SerialName("status_id") val statusId: Int? = null,
|
||||
@SerialName("completion_count") val completionCount: Int? = null,
|
||||
val archived: Boolean = false
|
||||
data class TaskActionResponse(
|
||||
val message: String,
|
||||
val task: TaskResponse
|
||||
)
|
||||
|
||||
/**
|
||||
* Kanban column response matching Go API KanbanColumnResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskColumn(
|
||||
val name: String,
|
||||
@@ -150,13 +167,45 @@ data class TaskColumn(
|
||||
@SerialName("button_types") val buttonTypes: List<String>,
|
||||
val icons: Map<String, String>,
|
||||
val color: String,
|
||||
val tasks: List<TaskDetail>, // Keep using TaskDetail for now - will be TaskMinimal after full migration
|
||||
val tasks: List<TaskResponse>,
|
||||
val count: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* Kanban board response matching Go API KanbanBoardResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskColumnsResponse(
|
||||
val columns: List<TaskColumn>,
|
||||
@SerialName("days_threshold") val daysThreshold: Int? = null,
|
||||
@SerialName("residence_id") val residenceId: String? = null
|
||||
@SerialName("days_threshold") val daysThreshold: Int,
|
||||
@SerialName("residence_id") val residenceId: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Task patch request for partial updates (status changes, archive/unarchive)
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskPatchRequest(
|
||||
@SerialName("status_id") val status: Int? = null,
|
||||
@SerialName("is_archived") val archived: Boolean? = null,
|
||||
@SerialName("is_cancelled") val cancelled: Boolean? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Task completion image model
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskCompletionImage(
|
||||
val id: Int,
|
||||
@SerialName("image_url") val imageUrl: String,
|
||||
val caption: String? = null,
|
||||
@SerialName("uploaded_at") val uploadedAt: String? = null
|
||||
) {
|
||||
// Alias for backwards compatibility
|
||||
val image: String get() = imageUrl
|
||||
}
|
||||
|
||||
// Type aliases for backwards compatibility with existing code
|
||||
typealias CustomTask = TaskResponse
|
||||
typealias TaskDetail = TaskResponse
|
||||
typealias TaskCompletion = TaskCompletionResponse
|
||||
|
||||
@@ -3,97 +3,134 @@ package com.example.mycrib.models
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Residence type lookup - matching Go API
|
||||
* Note: Go API returns arrays directly, no wrapper
|
||||
*/
|
||||
@Serializable
|
||||
data class ResidenceType(
|
||||
val id: Int,
|
||||
val name: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Task frequency lookup - matching Go API TaskFrequencyResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskFrequency(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val days: Int? = null,
|
||||
@SerialName("display_order") val displayOrder: Int = 0
|
||||
) {
|
||||
// Helper for display
|
||||
val displayName: String
|
||||
get() = name
|
||||
}
|
||||
|
||||
/**
|
||||
* Task priority lookup - matching Go API TaskPriorityResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskPriority(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val level: Int = 0,
|
||||
val color: String = "",
|
||||
@SerialName("display_order") val displayOrder: Int = 0
|
||||
) {
|
||||
// Helper for display
|
||||
val displayName: String
|
||||
get() = name
|
||||
}
|
||||
|
||||
/**
|
||||
* Task status lookup - matching Go API TaskStatusResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskStatus(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val description: String = "",
|
||||
val color: String = "",
|
||||
@SerialName("display_order") val displayOrder: Int = 0
|
||||
) {
|
||||
// Helper for display
|
||||
val displayName: String
|
||||
get() = name
|
||||
}
|
||||
|
||||
/**
|
||||
* Task category lookup - matching Go API TaskCategoryResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskCategory(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val description: String = "",
|
||||
val icon: String = "",
|
||||
val color: String = "",
|
||||
@SerialName("display_order") val displayOrder: Int = 0
|
||||
)
|
||||
|
||||
/**
|
||||
* Contractor specialty lookup
|
||||
*/
|
||||
@Serializable
|
||||
data class ContractorSpecialty(
|
||||
val id: Int,
|
||||
val name: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Static data response - all lookups in one call
|
||||
* Note: This may need adjustment based on Go API implementation
|
||||
*/
|
||||
@Serializable
|
||||
data class StaticDataResponse(
|
||||
@SerialName("residence_types") val residenceTypes: List<ResidenceType>,
|
||||
@SerialName("task_frequencies") val taskFrequencies: List<TaskFrequency>,
|
||||
@SerialName("task_priorities") val taskPriorities: List<TaskPriority>,
|
||||
@SerialName("task_statuses") val taskStatuses: List<TaskStatus>,
|
||||
@SerialName("task_categories") val taskCategories: List<TaskCategory>,
|
||||
@SerialName("contractor_specialties") val contractorSpecialties: List<ContractorSpecialty>
|
||||
)
|
||||
|
||||
// Legacy wrapper responses for backward compatibility
|
||||
// These can be removed once all code is migrated to use arrays directly
|
||||
|
||||
@Serializable
|
||||
data class ResidenceTypeResponse(
|
||||
val count: Int,
|
||||
val results: List<ResidenceType>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ResidenceType(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val description: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TaskFrequencyResponse(
|
||||
val count: Int,
|
||||
val results: List<TaskFrequency>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TaskFrequency(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
@SerialName("lookup_name") val lookupName: String,
|
||||
@SerialName("display_name") val displayName: String,
|
||||
@SerialName("day_span") val daySpan: Int? = null,
|
||||
@SerialName("notify_days") val notifyDays: Int? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TaskPriorityResponse(
|
||||
val count: Int,
|
||||
val results: List<TaskPriority>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TaskPriority(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
@SerialName("display_name") val displayName: String,
|
||||
@SerialName("order_id") val orderId: Int = 0,
|
||||
val description: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TaskStatusResponse(
|
||||
val count: Int,
|
||||
val results: List<TaskStatus>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TaskStatus(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
@SerialName("display_name") val displayName: String,
|
||||
@SerialName("order_id") val orderId: Int = 0,
|
||||
val description: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TaskCategoryResponse(
|
||||
val count: Int,
|
||||
val results: List<TaskCategory>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TaskCategory(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
@SerialName("order_id") val orderId: Int = 0,
|
||||
val description: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ContractorSpecialtyResponse(
|
||||
val count: Int,
|
||||
val results: List<ContractorSpecialty>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ContractorSpecialty(
|
||||
val id: Int,
|
||||
val name: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class StaticDataResponse(
|
||||
val residenceTypes: List<ResidenceType>,
|
||||
val taskFrequencies: List<TaskFrequency>,
|
||||
val taskPriorities: List<TaskPriority>,
|
||||
val taskStatuses: List<TaskStatus>,
|
||||
val taskCategories: List<TaskCategory>,
|
||||
val contractorSpecialties: List<ContractorSpecialty>
|
||||
)
|
||||
|
||||
@@ -3,38 +3,81 @@ package com.example.mycrib.models
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* User reference for residence responses - matching Go API ResidenceUserResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class Residence(
|
||||
data class ResidenceUserResponse(
|
||||
val id: Int,
|
||||
val owner: Int? = null,
|
||||
@SerialName("owner_username") val ownerUsername: String,
|
||||
@SerialName("is_primary_owner") val isPrimaryOwner: Boolean = false,
|
||||
@SerialName("user_count") val userCount: Int = 1,
|
||||
val username: String,
|
||||
val email: String,
|
||||
@SerialName("first_name") val firstName: String = "",
|
||||
@SerialName("last_name") val lastName: String = ""
|
||||
) {
|
||||
val displayName: String
|
||||
get() = when {
|
||||
firstName.isNotBlank() && lastName.isNotBlank() -> "$firstName $lastName"
|
||||
firstName.isNotBlank() -> firstName
|
||||
lastName.isNotBlank() -> lastName
|
||||
else -> username
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Residence response matching Go API ResidenceResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class ResidenceResponse(
|
||||
val id: Int,
|
||||
@SerialName("owner_id") val ownerId: Int,
|
||||
val owner: ResidenceUserResponse? = null,
|
||||
val users: List<ResidenceUserResponse> = emptyList(),
|
||||
val name: String,
|
||||
@SerialName("property_type") val propertyType: String? = null,
|
||||
@SerialName("street_address") val streetAddress: String? = null,
|
||||
@SerialName("apartment_unit") val apartmentUnit: String? = null,
|
||||
val city: String? = null,
|
||||
@SerialName("state_province") val stateProvince: String? = null,
|
||||
@SerialName("postal_code") val postalCode: String? = null,
|
||||
val country: String? = null,
|
||||
@SerialName("property_type_id") val propertyTypeId: Int? = null,
|
||||
@SerialName("property_type") val propertyType: ResidenceType? = null,
|
||||
@SerialName("street_address") val streetAddress: String = "",
|
||||
@SerialName("apartment_unit") val apartmentUnit: String = "",
|
||||
val city: String = "",
|
||||
@SerialName("state_province") val stateProvince: String = "",
|
||||
@SerialName("postal_code") val postalCode: String = "",
|
||||
val country: String = "",
|
||||
val bedrooms: Int? = null,
|
||||
val bathrooms: Float? = null,
|
||||
val bathrooms: Double? = null,
|
||||
@SerialName("square_footage") val squareFootage: Int? = null,
|
||||
@SerialName("lot_size") val lotSize: Float? = null,
|
||||
@SerialName("lot_size") val lotSize: Double? = null,
|
||||
@SerialName("year_built") val yearBuilt: Int? = null,
|
||||
val description: String? = null,
|
||||
val description: String = "",
|
||||
@SerialName("purchase_date") val purchaseDate: String? = null,
|
||||
@SerialName("purchase_price") val purchasePrice: Double? = null,
|
||||
@SerialName("is_primary") val isPrimary: Boolean = false,
|
||||
@SerialName("is_active") val isActive: Boolean = true,
|
||||
@SerialName("created_at") val createdAt: String,
|
||||
@SerialName("updated_at") val updatedAt: String
|
||||
)
|
||||
) {
|
||||
// Helper to get owner username
|
||||
val ownerUsername: String get() = owner?.displayName ?: ""
|
||||
|
||||
// Helper to get property type name
|
||||
val propertyTypeName: String? get() = propertyType?.name
|
||||
|
||||
// Backwards compatibility for UI code
|
||||
// Note: isPrimaryOwner requires comparing with current user - can't be computed here
|
||||
// UI components should check ownerId == currentUserId instead
|
||||
|
||||
// Stub task summary for UI compatibility (Go API doesn't return this per-residence)
|
||||
val taskSummary: ResidenceTaskSummary get() = ResidenceTaskSummary()
|
||||
|
||||
// Stub summary for UI compatibility
|
||||
val summary: ResidenceSummaryResponse get() = ResidenceSummaryResponse(id = id, name = name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Residence create request matching Go API CreateResidenceRequest
|
||||
*/
|
||||
@Serializable
|
||||
data class ResidenceCreateRequest(
|
||||
val name: String,
|
||||
@SerialName("property_type") val propertyType: Int? = null,
|
||||
@SerialName("property_type_id") val propertyTypeId: Int? = null,
|
||||
@SerialName("street_address") val streetAddress: String? = null,
|
||||
@SerialName("apartment_unit") val apartmentUnit: String? = null,
|
||||
val city: String? = null,
|
||||
@@ -42,189 +85,172 @@ data class ResidenceCreateRequest(
|
||||
@SerialName("postal_code") val postalCode: String? = null,
|
||||
val country: String? = null,
|
||||
val bedrooms: Int? = null,
|
||||
val bathrooms: Float? = null,
|
||||
val bathrooms: Double? = null,
|
||||
@SerialName("square_footage") val squareFootage: Int? = null,
|
||||
@SerialName("lot_size") val lotSize: Float? = null,
|
||||
@SerialName("year_built") val yearBuilt: Int? = null,
|
||||
val description: String? = null,
|
||||
@SerialName("purchase_date") val purchaseDate: String? = null,
|
||||
@SerialName("purchase_price") val purchasePrice: String? = null,
|
||||
@SerialName("is_primary") val isPrimary: Boolean = false
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TaskColumnIcon(
|
||||
val ios: String,
|
||||
val android: String,
|
||||
val web: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TaskColumnCategory(
|
||||
val name: String,
|
||||
@SerialName("display_name") val displayName: String,
|
||||
val icons: TaskColumnIcon,
|
||||
val color: String,
|
||||
val count: Int
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TaskSummary(
|
||||
val total: Int,
|
||||
val categories: List<TaskColumnCategory>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ResidenceSummary(
|
||||
val id: Int,
|
||||
val owner: Int,
|
||||
@SerialName("owner_username") val ownerUsername: String,
|
||||
val name: String,
|
||||
@SerialName("property_type") val propertyType: String? = null,
|
||||
@SerialName("street_address") val streetAddress: String? = null,
|
||||
@SerialName("apartment_unit") val apartmentUnit: String? = null,
|
||||
val city: String? = null,
|
||||
@SerialName("state_province") val stateProvince: String? = null,
|
||||
@SerialName("postal_code") val postalCode: String? = null,
|
||||
val country: String? = null,
|
||||
@SerialName("is_primary") val isPrimary: Boolean,
|
||||
@SerialName("task_summary") val taskSummary: TaskSummary,
|
||||
@SerialName("last_completed_task") val lastCompletedCustomTask: CustomTask?,
|
||||
@SerialName("next_upcoming_task") val nextUpcomingCustomTask: CustomTask?,
|
||||
@SerialName("created_at") val createdAt: String,
|
||||
@SerialName("updated_at") val updatedAt: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ResidenceSummaryResponse(
|
||||
val summary: OverallSummary,
|
||||
val residences: List<ResidenceSummary>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class OverallSummary(
|
||||
@SerialName("total_residences") val totalResidences: Int,
|
||||
@SerialName("total_tasks") val totalTasks: Int,
|
||||
@SerialName("total_completed") val totalCompleted: Int,
|
||||
@SerialName("total_pending") val totalPending: Int,
|
||||
@SerialName("tasks_due_next_week") val tasksDueNextWeek: Int,
|
||||
@SerialName("tasks_due_next_month") val tasksDueNextMonth: Int
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ResidenceWithTasks(
|
||||
val id: Int,
|
||||
val owner: Int,
|
||||
@SerialName("owner_username") val ownerUsername: String,
|
||||
@SerialName("is_primary_owner") val isPrimaryOwner: Boolean = false,
|
||||
@SerialName("user_count") val userCount: Int = 1,
|
||||
val name: String,
|
||||
@SerialName("property_type") val propertyType: String? = null,
|
||||
@SerialName("street_address") val streetAddress: String? = null,
|
||||
@SerialName("apartment_unit") val apartmentUnit: String? = null,
|
||||
val city: String? = null,
|
||||
@SerialName("state_province") val stateProvince: String? = null,
|
||||
@SerialName("postal_code") val postalCode: String? = null,
|
||||
val country: String? = null,
|
||||
val bedrooms: Int? = null,
|
||||
val bathrooms: Float? = null,
|
||||
@SerialName("square_footage") val squareFootage: Int? = null,
|
||||
@SerialName("lot_size") val lotSize: Float? = null,
|
||||
@SerialName("lot_size") val lotSize: Double? = null,
|
||||
@SerialName("year_built") val yearBuilt: Int? = null,
|
||||
val description: String? = null,
|
||||
@SerialName("purchase_date") val purchaseDate: String? = null,
|
||||
@SerialName("purchase_price") val purchasePrice: Double? = null,
|
||||
@SerialName("is_primary") val isPrimary: Boolean,
|
||||
@SerialName("task_summary") val taskSummary: TaskSummary,
|
||||
val tasks: List<TaskDetail>,
|
||||
@SerialName("created_at") val createdAt: String,
|
||||
@SerialName("updated_at") val updatedAt: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MyResidencesSummary(
|
||||
@SerialName("total_residences") val totalResidences: Int,
|
||||
@SerialName("total_tasks") val totalTasks: Int,
|
||||
@SerialName("tasks_due_next_week") val tasksDueNextWeek: Int,
|
||||
@SerialName("tasks_due_next_month") val tasksDueNextMonth: Int
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MyResidencesResponse(
|
||||
val summary: MyResidencesSummary,
|
||||
val residences: List<ResidenceWithTasks>
|
||||
@SerialName("is_primary") val isPrimary: Boolean? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Minimal residence model for list views.
|
||||
* Uses property_type_id and annotated counts instead of nested objects.
|
||||
* Resolve property type via DataCache.getResidenceType(residence.propertyTypeId)
|
||||
* Residence update request matching Go API UpdateResidenceRequest
|
||||
*/
|
||||
@Serializable
|
||||
data class ResidenceMinimal(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
data class ResidenceUpdateRequest(
|
||||
val name: String? = null,
|
||||
@SerialName("property_type_id") val propertyTypeId: Int? = null,
|
||||
@SerialName("street_address") val streetAddress: String? = null,
|
||||
@SerialName("apartment_unit") val apartmentUnit: String? = null,
|
||||
val city: String? = null,
|
||||
@SerialName("state_province") val stateProvince: String? = null,
|
||||
@SerialName("postal_code") val postalCode: String? = null,
|
||||
val country: String? = null,
|
||||
val bedrooms: Int? = null,
|
||||
val bathrooms: Float? = null,
|
||||
@SerialName("is_primary") val isPrimary: Boolean = false,
|
||||
@SerialName("is_primary_owner") val isPrimaryOwner: Boolean = false,
|
||||
@SerialName("user_count") val userCount: Int = 1,
|
||||
// Annotated counts from database (no N+1 queries)
|
||||
@SerialName("task_count") val taskCount: Int = 0,
|
||||
@SerialName("tasks_pending") val tasksPending: Int = 0,
|
||||
@SerialName("tasks_overdue") val tasksOverdue: Int = 0,
|
||||
@SerialName("tasks_due_week") val tasksDueWeek: Int = 0,
|
||||
// Reference to last/next task (just ID and date, not full object)
|
||||
@SerialName("last_completed_task_id") val lastCompletedTaskId: Int? = null,
|
||||
@SerialName("last_completed_date") val lastCompletedDate: String? = null,
|
||||
@SerialName("next_task_id") val nextTaskId: Int? = null,
|
||||
@SerialName("next_task_date") val nextTaskDate: String? = null,
|
||||
val bathrooms: Double? = null,
|
||||
@SerialName("square_footage") val squareFootage: Int? = null,
|
||||
@SerialName("lot_size") val lotSize: Double? = null,
|
||||
@SerialName("year_built") val yearBuilt: Int? = null,
|
||||
val description: String? = null,
|
||||
@SerialName("purchase_date") val purchaseDate: String? = null,
|
||||
@SerialName("purchase_price") val purchasePrice: Double? = null,
|
||||
@SerialName("is_primary") val isPrimary: Boolean? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Share code response matching Go API ShareCodeResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class ShareCodeResponse(
|
||||
val id: Int,
|
||||
val code: String,
|
||||
@SerialName("residence_id") val residenceId: Int,
|
||||
@SerialName("created_by_id") val createdById: Int,
|
||||
@SerialName("is_active") val isActive: Boolean,
|
||||
@SerialName("expires_at") val expiresAt: String?,
|
||||
@SerialName("created_at") val createdAt: String
|
||||
)
|
||||
|
||||
// Share Code Models
|
||||
/**
|
||||
* Generate share code request
|
||||
*/
|
||||
@Serializable
|
||||
data class ResidenceShareCode(
|
||||
val id: Int,
|
||||
val code: String,
|
||||
val residence: Int,
|
||||
@SerialName("residence_name") val residenceName: String,
|
||||
@SerialName("created_by") val createdBy: Int,
|
||||
@SerialName("created_by_username") val createdByUsername: String,
|
||||
@SerialName("is_active") val isActive: Boolean,
|
||||
@SerialName("created_at") val createdAt: String,
|
||||
@SerialName("expires_at") val expiresAt: String?
|
||||
data class GenerateShareCodeRequest(
|
||||
@SerialName("expires_in_hours") val expiresInHours: Int? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Generate share code response matching Go API GenerateShareCodeResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class GenerateShareCodeResponse(
|
||||
val message: String,
|
||||
@SerialName("share_code") val shareCode: ShareCodeResponse
|
||||
)
|
||||
|
||||
/**
|
||||
* Join residence request
|
||||
*/
|
||||
@Serializable
|
||||
data class JoinResidenceRequest(
|
||||
val code: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Join residence response matching Go API JoinResidenceResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class JoinResidenceResponse(
|
||||
val message: String,
|
||||
val residence: Residence
|
||||
val residence: ResidenceResponse
|
||||
)
|
||||
|
||||
// User Management Models
|
||||
/**
|
||||
* Total summary for dashboard display
|
||||
*/
|
||||
@Serializable
|
||||
data class ResidenceUser(
|
||||
val id: Int,
|
||||
val username: String,
|
||||
val email: String,
|
||||
@SerialName("first_name") val firstName: String?,
|
||||
@SerialName("last_name") val lastName: String?
|
||||
data class TotalSummary(
|
||||
@SerialName("total_residences") val totalResidences: Int = 0,
|
||||
@SerialName("total_tasks") val totalTasks: Int = 0,
|
||||
@SerialName("total_pending") val totalPending: Int = 0,
|
||||
@SerialName("total_overdue") val totalOverdue: Int = 0,
|
||||
@SerialName("tasks_due_next_week") val tasksDueNextWeek: Int = 0,
|
||||
@SerialName("tasks_due_next_month") val tasksDueNextMonth: Int = 0
|
||||
)
|
||||
|
||||
/**
|
||||
* My residences response - list of user's residences
|
||||
* Go API returns array directly, this wraps for consistency
|
||||
*/
|
||||
@Serializable
|
||||
data class MyResidencesResponse(
|
||||
val residences: List<ResidenceResponse>,
|
||||
val summary: TotalSummary = TotalSummary()
|
||||
)
|
||||
|
||||
/**
|
||||
* Residence summary response for dashboard
|
||||
*/
|
||||
@Serializable
|
||||
data class ResidenceSummaryResponse(
|
||||
val id: Int = 0,
|
||||
val name: String = "",
|
||||
@SerialName("task_count") val taskCount: Int = 0,
|
||||
@SerialName("pending_count") val pendingCount: Int = 0,
|
||||
@SerialName("overdue_count") val overdueCount: Int = 0
|
||||
)
|
||||
|
||||
/**
|
||||
* Task category summary for residence
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskCategorySummary(
|
||||
val name: String,
|
||||
@SerialName("display_name") val displayName: String,
|
||||
val icons: TaskCategoryIcons,
|
||||
val color: String,
|
||||
val count: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* Icons for task category (Android/iOS)
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskCategoryIcons(
|
||||
val android: String = "",
|
||||
val ios: String = ""
|
||||
)
|
||||
|
||||
/**
|
||||
* Task summary per residence (for UI backwards compatibility)
|
||||
*/
|
||||
@Serializable
|
||||
data class ResidenceTaskSummary(
|
||||
val categories: List<TaskCategorySummary> = emptyList()
|
||||
)
|
||||
|
||||
/**
|
||||
* Residence users response
|
||||
*/
|
||||
@Serializable
|
||||
data class ResidenceUsersResponse(
|
||||
@SerialName("owner_id") val ownerId: Int,
|
||||
val users: List<ResidenceUser>
|
||||
val owner: ResidenceUserResponse,
|
||||
val users: List<ResidenceUserResponse>
|
||||
)
|
||||
|
||||
/**
|
||||
* Remove user response
|
||||
*/
|
||||
@Serializable
|
||||
data class RemoveUserResponse(
|
||||
val message: String
|
||||
)
|
||||
)
|
||||
|
||||
// Type aliases for backwards compatibility with existing code
|
||||
typealias Residence = ResidenceResponse
|
||||
typealias ResidenceShareCode = ShareCodeResponse
|
||||
typealias ResidenceUser = ResidenceUserResponse
|
||||
typealias TaskSummary = ResidenceTaskSummary
|
||||
typealias TaskColumnCategory = TaskCategorySummary
|
||||
@@ -3,56 +3,15 @@ package com.example.mycrib.models
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class TaskCompletion(
|
||||
val id: Int,
|
||||
val task: Int,
|
||||
@SerialName("completed_by_user") val completedByUser: Int?,
|
||||
val contractor: Int?,
|
||||
@SerialName("contractor_details") val contractorDetails: ContractorDetails?,
|
||||
@SerialName("completed_by_name") val completedByName: String?,
|
||||
@SerialName("completed_by_phone") val completedByPhone: String?,
|
||||
@SerialName("completed_by_email") val completedByEmail: String?,
|
||||
@SerialName("company_name") val companyName: String?,
|
||||
@SerialName("completion_date") val completionDate: String,
|
||||
@SerialName("actual_cost") val actualCost: Double?,
|
||||
val notes: String?,
|
||||
val rating: Int?,
|
||||
@SerialName("completed_by_display") val completedByDisplay: String?,
|
||||
@SerialName("created_at") val createdAt: String,
|
||||
val images: List<TaskCompletionImage>? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ContractorDetails(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val company: String?,
|
||||
val phone: String,
|
||||
val specialty: String?,
|
||||
@SerialName("average_rating") val averageRating: Double?
|
||||
)
|
||||
|
||||
/**
|
||||
* Task completion create request matching Go API CreateTaskCompletionRequest
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskCompletionCreateRequest(
|
||||
val task: Int,
|
||||
@SerialName("completed_by_user") val completedByUser: Int? = null,
|
||||
val contractor: Int? = null,
|
||||
@SerialName("completed_by_name") val completedByName: String? = null,
|
||||
@SerialName("completed_by_phone") val completedByPhone: String? = null,
|
||||
@SerialName("completed_by_email") val completedByEmail: String? = null,
|
||||
@SerialName("company_name") val companyName: String? = null,
|
||||
@SerialName("completion_date") val completionDate: String,
|
||||
@SerialName("actual_cost") val actualCost: Double? = null,
|
||||
@SerialName("task_id") val taskId: Int,
|
||||
@SerialName("completed_at") val completedAt: String? = null, // Defaults to now on server
|
||||
val notes: String? = null,
|
||||
val rating: Int? = null
|
||||
@SerialName("actual_cost") val actualCost: Double? = null,
|
||||
@SerialName("photo_url") val photoUrl: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TaskCompletionImage(
|
||||
val id: Int,
|
||||
val completion: Int,
|
||||
val image: String,
|
||||
val caption: String?,
|
||||
@SerialName("uploaded_at") val uploadedAt: String
|
||||
)
|
||||
|
||||
@@ -3,33 +3,57 @@ package com.example.mycrib.models
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* User model matching Go API UserResponse/CurrentUserResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class User(
|
||||
val id: Int,
|
||||
val username: String,
|
||||
val email: String,
|
||||
@SerialName("first_name") val firstName: String?,
|
||||
@SerialName("last_name") val lastName: String?,
|
||||
@SerialName("is_staff") val isStaff: Boolean = false,
|
||||
@SerialName("first_name") val firstName: String = "",
|
||||
@SerialName("last_name") val lastName: String = "",
|
||||
@SerialName("is_active") val isActive: Boolean = true,
|
||||
@SerialName("date_joined") val dateJoined: String,
|
||||
val verified: Boolean = false
|
||||
)
|
||||
@SerialName("last_login") val lastLogin: String? = null,
|
||||
// Profile is included in CurrentUserResponse (/auth/me)
|
||||
val profile: UserProfile? = null
|
||||
) {
|
||||
// Computed property for display name
|
||||
val displayName: String
|
||||
get() = when {
|
||||
firstName.isNotBlank() && lastName.isNotBlank() -> "$firstName $lastName"
|
||||
firstName.isNotBlank() -> firstName
|
||||
lastName.isNotBlank() -> lastName
|
||||
else -> username
|
||||
}
|
||||
|
||||
// Check if user is verified (from profile)
|
||||
val isVerified: Boolean
|
||||
get() = profile?.verified ?: false
|
||||
|
||||
// Alias for backwards compatibility
|
||||
val verified: Boolean
|
||||
get() = isVerified
|
||||
}
|
||||
|
||||
/**
|
||||
* User profile model matching Go API UserProfileResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class UserProfile(
|
||||
val id: Int,
|
||||
val user: Int,
|
||||
@SerialName("phone_number") val phoneNumber: String?,
|
||||
val address: String?,
|
||||
val city: String?,
|
||||
@SerialName("state_province") val stateProvince: String?,
|
||||
@SerialName("postal_code") val postalCode: String?,
|
||||
val country: String?,
|
||||
@SerialName("profile_picture") val profilePicture: String?,
|
||||
val bio: String?
|
||||
@SerialName("user_id") val userId: Int,
|
||||
val verified: Boolean = false,
|
||||
val bio: String = "",
|
||||
@SerialName("phone_number") val phoneNumber: String = "",
|
||||
@SerialName("date_of_birth") val dateOfBirth: String? = null,
|
||||
@SerialName("profile_picture") val profilePicture: String = ""
|
||||
)
|
||||
|
||||
/**
|
||||
* Register request matching Go API
|
||||
*/
|
||||
@Serializable
|
||||
data class RegisterRequest(
|
||||
val username: String,
|
||||
@@ -39,30 +63,54 @@ data class RegisterRequest(
|
||||
@SerialName("last_name") val lastName: String? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Login request matching Go API
|
||||
*/
|
||||
@Serializable
|
||||
data class LoginRequest(
|
||||
val username: String,
|
||||
val password: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Auth response for login - matching Go API LoginResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class AuthResponse(
|
||||
val token: String,
|
||||
val user: User,
|
||||
val verified: Boolean = false
|
||||
val user: User
|
||||
)
|
||||
|
||||
/**
|
||||
* Auth response for registration - matching Go API RegisterResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class RegisterResponse(
|
||||
val token: String,
|
||||
val user: User,
|
||||
val message: String = ""
|
||||
)
|
||||
|
||||
/**
|
||||
* Verify email request
|
||||
*/
|
||||
@Serializable
|
||||
data class VerifyEmailRequest(
|
||||
val code: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Verify email response
|
||||
*/
|
||||
@Serializable
|
||||
data class VerifyEmailResponse(
|
||||
val message: String,
|
||||
val verified: Boolean
|
||||
)
|
||||
|
||||
/**
|
||||
* Update profile request
|
||||
*/
|
||||
@Serializable
|
||||
data class UpdateProfileRequest(
|
||||
@SerialName("first_name") val firstName: String? = null,
|
||||
@@ -71,6 +119,7 @@ data class UpdateProfileRequest(
|
||||
)
|
||||
|
||||
// Password Reset Models
|
||||
|
||||
@Serializable
|
||||
data class ForgotPasswordRequest(
|
||||
val email: String
|
||||
@@ -96,11 +145,18 @@ data class VerifyResetCodeResponse(
|
||||
@Serializable
|
||||
data class ResetPasswordRequest(
|
||||
@SerialName("reset_token") val resetToken: String,
|
||||
@SerialName("new_password") val newPassword: String,
|
||||
@SerialName("confirm_password") val confirmPassword: String
|
||||
@SerialName("new_password") val newPassword: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ResetPasswordResponse(
|
||||
val message: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Generic message response used by many endpoints
|
||||
*/
|
||||
@Serializable
|
||||
data class MessageResponse(
|
||||
val message: String
|
||||
)
|
||||
|
||||
@@ -252,7 +252,7 @@ object APILayer {
|
||||
|
||||
// ==================== Residence Operations ====================
|
||||
|
||||
suspend fun getResidences(forceRefresh: Boolean = false): ApiResult<List<Residence>> {
|
||||
suspend fun getResidences(forceRefresh: Boolean = false): ApiResult<List<ResidenceResponse>> {
|
||||
// Check cache first
|
||||
if (!forceRefresh) {
|
||||
val cached = DataCache.residences.value
|
||||
@@ -294,7 +294,7 @@ object APILayer {
|
||||
return result
|
||||
}
|
||||
|
||||
suspend fun getResidence(id: Int, forceRefresh: Boolean = false): ApiResult<Residence> {
|
||||
suspend fun getResidence(id: Int, forceRefresh: Boolean = false): ApiResult<ResidenceResponse> {
|
||||
// Check cache first
|
||||
if (!forceRefresh) {
|
||||
val cached = DataCache.residences.value.find { it.id == id }
|
||||
@@ -321,7 +321,7 @@ object APILayer {
|
||||
return residenceApi.getResidenceSummary(token)
|
||||
}
|
||||
|
||||
suspend fun createResidence(request: ResidenceCreateRequest): ApiResult<Residence> {
|
||||
suspend fun createResidence(request: ResidenceCreateRequest): ApiResult<ResidenceResponse> {
|
||||
val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val result = residenceApi.createResidence(token, request)
|
||||
|
||||
@@ -333,7 +333,7 @@ object APILayer {
|
||||
return result
|
||||
}
|
||||
|
||||
suspend fun updateResidence(id: Int, request: ResidenceCreateRequest): ApiResult<Residence> {
|
||||
suspend fun updateResidence(id: Int, request: ResidenceCreateRequest): ApiResult<ResidenceResponse> {
|
||||
val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val result = residenceApi.updateResidence(token, id, request)
|
||||
|
||||
@@ -377,12 +377,12 @@ object APILayer {
|
||||
return residenceApi.getResidenceUsers(token, residenceId)
|
||||
}
|
||||
|
||||
suspend fun getShareCode(residenceId: Int): ApiResult<ResidenceShareCode> {
|
||||
suspend fun getShareCode(residenceId: Int): ApiResult<ShareCodeResponse> {
|
||||
val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
return residenceApi.getShareCode(token, residenceId)
|
||||
}
|
||||
|
||||
suspend fun generateShareCode(residenceId: Int): ApiResult<ResidenceShareCode> {
|
||||
suspend fun generateShareCode(residenceId: Int): ApiResult<GenerateShareCodeResponse> {
|
||||
val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
return residenceApi.generateShareCode(token, residenceId)
|
||||
}
|
||||
@@ -436,7 +436,7 @@ object APILayer {
|
||||
return result
|
||||
}
|
||||
|
||||
suspend fun createTask(request: TaskCreateRequest): ApiResult<CustomTask> {
|
||||
suspend fun createTask(request: TaskCreateRequest): ApiResult<TaskResponse> {
|
||||
val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val result = taskApi.createTask(token, request)
|
||||
|
||||
@@ -448,7 +448,7 @@ object APILayer {
|
||||
return result
|
||||
}
|
||||
|
||||
suspend fun updateTask(id: Int, request: TaskCreateRequest): ApiResult<CustomTask> {
|
||||
suspend fun updateTask(id: Int, request: TaskCreateRequest): ApiResult<TaskResponse> {
|
||||
val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val result = taskApi.updateTask(token, id, request)
|
||||
|
||||
@@ -470,7 +470,7 @@ object APILayer {
|
||||
}?.id
|
||||
}
|
||||
|
||||
suspend fun cancelTask(taskId: Int): ApiResult<CustomTask> {
|
||||
suspend fun cancelTask(taskId: Int): ApiResult<TaskResponse> {
|
||||
val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
|
||||
// Look up 'cancelled' status ID from cache
|
||||
@@ -487,7 +487,7 @@ object APILayer {
|
||||
return result
|
||||
}
|
||||
|
||||
suspend fun uncancelTask(taskId: Int): ApiResult<CustomTask> {
|
||||
suspend fun uncancelTask(taskId: Int): ApiResult<TaskResponse> {
|
||||
val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
|
||||
// Look up 'pending' status ID from cache
|
||||
@@ -504,7 +504,7 @@ object APILayer {
|
||||
return result
|
||||
}
|
||||
|
||||
suspend fun markInProgress(taskId: Int): ApiResult<CustomTask> {
|
||||
suspend fun markInProgress(taskId: Int): ApiResult<TaskResponse> {
|
||||
val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
|
||||
// Look up 'in progress' status ID from cache
|
||||
@@ -522,7 +522,7 @@ object APILayer {
|
||||
return result
|
||||
}
|
||||
|
||||
suspend fun archiveTask(taskId: Int): ApiResult<CustomTask> {
|
||||
suspend fun archiveTask(taskId: Int): ApiResult<TaskResponse> {
|
||||
val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val result = taskApi.archiveTask(token, taskId)
|
||||
|
||||
@@ -534,7 +534,7 @@ object APILayer {
|
||||
return result
|
||||
}
|
||||
|
||||
suspend fun unarchiveTask(taskId: Int): ApiResult<CustomTask> {
|
||||
suspend fun unarchiveTask(taskId: Int): ApiResult<TaskResponse> {
|
||||
val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val result = taskApi.unarchiveTask(token, taskId)
|
||||
|
||||
@@ -546,7 +546,7 @@ object APILayer {
|
||||
return result
|
||||
}
|
||||
|
||||
suspend fun createTaskCompletion(request: TaskCompletionCreateRequest): ApiResult<TaskCompletion> {
|
||||
suspend fun createTaskCompletion(request: TaskCompletionCreateRequest): ApiResult<TaskCompletionResponse> {
|
||||
val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val result = taskCompletionApi.createCompletion(token, request)
|
||||
|
||||
@@ -562,7 +562,7 @@ object APILayer {
|
||||
request: TaskCompletionCreateRequest,
|
||||
images: List<ByteArray>,
|
||||
imageFileNames: List<String>
|
||||
): ApiResult<TaskCompletion> {
|
||||
): ApiResult<TaskCompletionResponse> {
|
||||
val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||
val result = taskCompletionApi.createCompletionWithImages(token, request, images, imageFileNames)
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ class ContractorApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getContractorTasks(token: String, id: Int): ApiResult<List<TaskCompletion>> {
|
||||
suspend fun getContractorTasks(token: String, id: Int): ApiResult<List<TaskResponse>> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/contractors/$id/tasks/") {
|
||||
header("Authorization", "Token $token")
|
||||
|
||||
@@ -105,7 +105,7 @@ class LookupsApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getAllTasks(token: String): ApiResult<List<CustomTask>> {
|
||||
suspend fun getAllTasks(token: String): ApiResult<List<TaskResponse>> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/tasks/") {
|
||||
header("Authorization", "Token $token")
|
||||
|
||||
@@ -9,7 +9,7 @@ import io.ktor.http.*
|
||||
class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
private val baseUrl = ApiClient.getBaseUrl()
|
||||
|
||||
suspend fun getResidences(token: String): ApiResult<List<Residence>> {
|
||||
suspend fun getResidences(token: String): ApiResult<List<ResidenceResponse>> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/residences/") {
|
||||
header("Authorization", "Token $token")
|
||||
@@ -25,7 +25,7 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getResidence(token: String, id: Int): ApiResult<Residence> {
|
||||
suspend fun getResidence(token: String, id: Int): ApiResult<ResidenceResponse> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/residences/$id/") {
|
||||
header("Authorization", "Token $token")
|
||||
@@ -41,7 +41,7 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun createResidence(token: String, request: ResidenceCreateRequest): ApiResult<Residence> {
|
||||
suspend fun createResidence(token: String, request: ResidenceCreateRequest): ApiResult<ResidenceResponse> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/residences/") {
|
||||
header("Authorization", "Token $token")
|
||||
@@ -59,7 +59,7 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateResidence(token: String, id: Int, request: ResidenceCreateRequest): ApiResult<Residence> {
|
||||
suspend fun updateResidence(token: String, id: Int, request: ResidenceCreateRequest): ApiResult<ResidenceResponse> {
|
||||
return try {
|
||||
val response = client.put("$baseUrl/residences/$id/") {
|
||||
header("Authorization", "Token $token")
|
||||
@@ -126,7 +126,7 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
}
|
||||
|
||||
// Share Code Management
|
||||
suspend fun generateShareCode(token: String, residenceId: Int): ApiResult<ResidenceShareCode> {
|
||||
suspend fun generateShareCode(token: String, residenceId: Int): ApiResult<GenerateShareCodeResponse> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/residences/$residenceId/generate-share-code/") {
|
||||
header("Authorization", "Token $token")
|
||||
@@ -143,7 +143,7 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getShareCode(token: String, residenceId: Int): ApiResult<ResidenceShareCode> {
|
||||
suspend fun getShareCode(token: String, residenceId: Int): ApiResult<ShareCodeResponse> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/residences/$residenceId/share-code/") {
|
||||
header("Authorization", "Token $token")
|
||||
|
||||
@@ -30,7 +30,7 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getTask(token: String, id: Int): ApiResult<TaskDetail> {
|
||||
suspend fun getTask(token: String, id: Int): ApiResult<TaskResponse> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/tasks/$id/") {
|
||||
header("Authorization", "Token $token")
|
||||
@@ -47,7 +47,7 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun createTask(token: String, request: TaskCreateRequest): ApiResult<CustomTask> {
|
||||
suspend fun createTask(token: String, request: TaskCreateRequest): ApiResult<TaskResponse> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/tasks/") {
|
||||
header("Authorization", "Token $token")
|
||||
@@ -66,7 +66,7 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateTask(token: String, id: Int, request: TaskCreateRequest): ApiResult<CustomTask> {
|
||||
suspend fun updateTask(token: String, id: Int, request: TaskCreateRequest): ApiResult<TaskResponse> {
|
||||
return try {
|
||||
val response = client.put("$baseUrl/tasks/$id/") {
|
||||
header("Authorization", "Token $token")
|
||||
@@ -132,7 +132,7 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
* archive, unarchive) have been REMOVED from the API.
|
||||
* All task updates now use PATCH /tasks/{id}/.
|
||||
*/
|
||||
suspend fun patchTask(token: String, id: Int, request: TaskPatchRequest): ApiResult<CustomTask> {
|
||||
suspend fun patchTask(token: String, id: Int, request: TaskPatchRequest): ApiResult<TaskResponse> {
|
||||
return try {
|
||||
val response = client.patch("$baseUrl/tasks/$id/") {
|
||||
header("Authorization", "Token $token")
|
||||
@@ -155,23 +155,23 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
// They're kept for backward compatibility with existing ViewModel calls.
|
||||
// New code should use patchTask directly with status IDs from DataCache.
|
||||
|
||||
suspend fun cancelTask(token: String, id: Int, cancelledStatusId: Int): ApiResult<CustomTask> {
|
||||
suspend fun cancelTask(token: String, id: Int, cancelledStatusId: Int): ApiResult<TaskResponse> {
|
||||
return patchTask(token, id, TaskPatchRequest(status = cancelledStatusId))
|
||||
}
|
||||
|
||||
suspend fun uncancelTask(token: String, id: Int, pendingStatusId: Int): ApiResult<CustomTask> {
|
||||
suspend fun uncancelTask(token: String, id: Int, pendingStatusId: Int): ApiResult<TaskResponse> {
|
||||
return patchTask(token, id, TaskPatchRequest(status = pendingStatusId))
|
||||
}
|
||||
|
||||
suspend fun markInProgress(token: String, id: Int, inProgressStatusId: Int): ApiResult<CustomTask> {
|
||||
suspend fun markInProgress(token: String, id: Int, inProgressStatusId: Int): ApiResult<TaskResponse> {
|
||||
return patchTask(token, id, TaskPatchRequest(status = inProgressStatusId))
|
||||
}
|
||||
|
||||
suspend fun archiveTask(token: String, id: Int): ApiResult<CustomTask> {
|
||||
suspend fun archiveTask(token: String, id: Int): ApiResult<TaskResponse> {
|
||||
return patchTask(token, id, TaskPatchRequest(archived = true))
|
||||
}
|
||||
|
||||
suspend fun unarchiveTask(token: String, id: Int): ApiResult<CustomTask> {
|
||||
suspend fun unarchiveTask(token: String, id: Int): ApiResult<TaskResponse> {
|
||||
return patchTask(token, id, TaskPatchRequest(archived = false))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import io.ktor.http.*
|
||||
class TaskCompletionApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
private val baseUrl = ApiClient.getBaseUrl()
|
||||
|
||||
suspend fun getCompletions(token: String): ApiResult<List<TaskCompletion>> {
|
||||
suspend fun getCompletions(token: String): ApiResult<List<TaskCompletionResponse>> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/task-completions/") {
|
||||
header("Authorization", "Token $token")
|
||||
@@ -25,7 +25,7 @@ class TaskCompletionApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getCompletion(token: String, id: Int): ApiResult<TaskCompletion> {
|
||||
suspend fun getCompletion(token: String, id: Int): ApiResult<TaskCompletionResponse> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/task-completions/$id/") {
|
||||
header("Authorization", "Token $token")
|
||||
@@ -41,7 +41,7 @@ class TaskCompletionApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun createCompletion(token: String, request: TaskCompletionCreateRequest): ApiResult<TaskCompletion> {
|
||||
suspend fun createCompletion(token: String, request: TaskCompletionCreateRequest): ApiResult<TaskCompletionResponse> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/task-completions/") {
|
||||
header("Authorization", "Token $token")
|
||||
@@ -59,7 +59,7 @@ class TaskCompletionApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateCompletion(token: String, id: Int, request: TaskCompletionCreateRequest): ApiResult<TaskCompletion> {
|
||||
suspend fun updateCompletion(token: String, id: Int, request: TaskCompletionCreateRequest): ApiResult<TaskCompletionResponse> {
|
||||
return try {
|
||||
val response = client.put("$baseUrl/task-completions/$id/") {
|
||||
header("Authorization", "Token $token")
|
||||
@@ -98,7 +98,7 @@ class TaskCompletionApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
request: TaskCompletionCreateRequest,
|
||||
images: List<ByteArray> = emptyList(),
|
||||
imageFileNames: List<String> = emptyList()
|
||||
): ApiResult<TaskCompletion> {
|
||||
): ApiResult<TaskCompletionResponse> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/task-completions/") {
|
||||
header("Authorization", "Token $token")
|
||||
@@ -107,13 +107,11 @@ class TaskCompletionApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
io.ktor.client.request.forms.MultiPartFormDataContent(
|
||||
io.ktor.client.request.forms.formData {
|
||||
// Add text fields
|
||||
append("task", request.task.toString())
|
||||
request.contractor?.let { append("contractor", it.toString()) }
|
||||
request.completedByName?.let { append("completed_by_name", it) }
|
||||
append("completion_date", request.completionDate)
|
||||
request.actualCost?.let { append("actual_cost", it) }
|
||||
append("task_id", request.taskId.toString())
|
||||
request.completedAt?.let { append("completed_at", it) }
|
||||
request.actualCost?.let { append("actual_cost", it.toString()) }
|
||||
request.notes?.let { append("notes", it) }
|
||||
request.rating?.let { append("rating", it.toString()) }
|
||||
request.photoUrl?.let { append("photo_url", it) }
|
||||
|
||||
// Add image files
|
||||
images.forEachIndexed { index, imageBytes ->
|
||||
|
||||
@@ -41,12 +41,8 @@ fun AddTaskDialog(
|
||||
)
|
||||
}
|
||||
var category by remember { mutableStateOf(TaskCategory(id = 0, name = "")) }
|
||||
var frequency by remember { mutableStateOf(TaskFrequency(
|
||||
id = 0, name = "", lookupName = "", displayName = "",
|
||||
daySpan = 0,
|
||||
notifyDays = 0
|
||||
)) }
|
||||
var priority by remember { mutableStateOf(TaskPriority(id = 0, name = "", displayName = "")) }
|
||||
var frequency by remember { mutableStateOf(TaskFrequency(id = 0, name = "")) }
|
||||
var priority by remember { mutableStateOf(TaskPriority(id = 0, name = "")) }
|
||||
|
||||
var showResidenceDropdown by remember { mutableStateOf(false) }
|
||||
var showFrequencyDropdown by remember { mutableStateOf(false) }
|
||||
@@ -333,14 +329,13 @@ fun AddTaskDialog(
|
||||
if (!hasError) {
|
||||
onCreate(
|
||||
TaskCreateRequest(
|
||||
residence = selectedResidenceId,
|
||||
residenceId = selectedResidenceId,
|
||||
title = title,
|
||||
description = description.ifBlank { null },
|
||||
category = category.id,
|
||||
frequency = frequency.id,
|
||||
intervalDays = intervalDays.toIntOrNull(),
|
||||
priority = priority.id,
|
||||
status = null,
|
||||
categoryId = if (category.id > 0) category.id else null,
|
||||
frequencyId = if (frequency.id > 0) frequency.id else null,
|
||||
priorityId = if (priority.id > 0) priority.id else null,
|
||||
statusId = null,
|
||||
dueDate = dueDate,
|
||||
estimatedCost = estimatedCost.ifBlank { null }?.toDoubleOrNull()
|
||||
)
|
||||
|
||||
@@ -255,15 +255,26 @@ fun CompleteTaskDialog(
|
||||
// Get current date in ISO format
|
||||
val currentDate = getCurrentDateTime()
|
||||
|
||||
// Build notes with contractor info if selected
|
||||
val notesWithContractor = buildString {
|
||||
if (selectedContractorName != null) {
|
||||
append("Contractor: $selectedContractorName\n")
|
||||
}
|
||||
if (completedByName.isNotBlank()) {
|
||||
append("Completed by: $completedByName\n")
|
||||
}
|
||||
if (notes.isNotBlank()) {
|
||||
append(notes)
|
||||
}
|
||||
}.ifBlank { null }
|
||||
|
||||
onComplete(
|
||||
TaskCompletionCreateRequest(
|
||||
task = taskId,
|
||||
contractor = selectedContractorId,
|
||||
completedByName = completedByName.ifBlank { null },
|
||||
completionDate = currentDate,
|
||||
taskId = taskId,
|
||||
completedAt = currentDate,
|
||||
actualCost = actualCost.ifBlank { null }?.toDoubleOrNull(),
|
||||
notes = notes.ifBlank { null },
|
||||
rating = rating
|
||||
notes = notesWithContractor,
|
||||
photoUrl = null // Images handled separately
|
||||
),
|
||||
selectedImages
|
||||
)
|
||||
|
||||
@@ -47,7 +47,7 @@ fun ManageUsersDialog(
|
||||
when (val result = residenceApi.getResidenceUsers(token, residenceId)) {
|
||||
is ApiResult.Success -> {
|
||||
users = result.data.users
|
||||
ownerId = result.data.ownerId
|
||||
ownerId = result.data.owner.id
|
||||
isLoading = false
|
||||
}
|
||||
is ApiResult.Error -> {
|
||||
@@ -133,7 +133,7 @@ fun ManageUsersDialog(
|
||||
if (token != null) {
|
||||
when (val result = residenceApi.generateShareCode(token, residenceId)) {
|
||||
is ApiResult.Success -> {
|
||||
shareCode = result.data
|
||||
shareCode = result.data.shareCode
|
||||
}
|
||||
is ApiResult.Error -> {
|
||||
error = result.message
|
||||
|
||||
@@ -65,7 +65,7 @@ fun TaskCard(
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = task.category.name.uppercase(),
|
||||
text = (task.category?.name ?: "").uppercase(),
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
@@ -78,7 +78,7 @@ fun TaskCard(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// Priority badge with semantic colors
|
||||
val priorityColor = when (task.priority.name.lowercase()) {
|
||||
val priorityColor = when (task.priority?.name?.lowercase()) {
|
||||
"urgent", "high" -> MaterialTheme.colorScheme.error
|
||||
"medium" -> MaterialTheme.colorScheme.tertiary
|
||||
else -> MaterialTheme.colorScheme.secondary
|
||||
@@ -100,7 +100,7 @@ fun TaskCard(
|
||||
.background(priorityColor)
|
||||
)
|
||||
Text(
|
||||
text = task.priority.name.uppercase(),
|
||||
text = (task.priority?.name ?: "").uppercase(),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = priorityColor
|
||||
)
|
||||
@@ -589,23 +589,20 @@ fun TaskCardPreview() {
|
||||
TaskCard(
|
||||
task = TaskDetail(
|
||||
id = 1,
|
||||
residence = 1,
|
||||
residenceId = 1,
|
||||
createdById = 1,
|
||||
title = "Clean Gutters",
|
||||
description = "Remove all debris from gutters and downspouts",
|
||||
category = TaskCategory(id = 1, name = "maintenance", description = ""),
|
||||
priority = TaskPriority(id = 2, name = "medium", displayName = "Medium", description = ""),
|
||||
category = TaskCategory(id = 1, name = "maintenance"),
|
||||
priority = TaskPriority(id = 2, name = "medium"),
|
||||
frequency = TaskFrequency(
|
||||
id = 1, name = "monthly", lookupName = "monthly", displayName = "Monthly",
|
||||
daySpan = 0,
|
||||
notifyDays = 0
|
||||
id = 1, name = "monthly", days = 30
|
||||
),
|
||||
status = TaskStatus(id = 1, name = "pending", displayName = "Pending", description = ""),
|
||||
status = TaskStatus(id = 1, name = "pending"),
|
||||
dueDate = "2024-12-15",
|
||||
estimatedCost = 150.00,
|
||||
createdAt = "2024-01-01T00:00:00Z",
|
||||
updatedAt = "2024-01-01T00:00:00Z",
|
||||
nextScheduledDate = "2024-12-15",
|
||||
showCompletedButton = true,
|
||||
completions = emptyList()
|
||||
),
|
||||
onCompleteClick = {},
|
||||
|
||||
@@ -301,16 +301,15 @@ fun EditTaskScreen(
|
||||
viewModel.updateTask(
|
||||
taskId = task.id,
|
||||
request = TaskCreateRequest(
|
||||
residence = task.residence,
|
||||
residenceId = task.residenceId,
|
||||
title = title,
|
||||
description = description.ifBlank { null },
|
||||
category = selectedCategory!!.id,
|
||||
frequency = selectedFrequency!!.id,
|
||||
priority = selectedPriority!!.id,
|
||||
status = selectedStatus!!.id,
|
||||
categoryId = selectedCategory!!.id,
|
||||
frequencyId = selectedFrequency!!.id,
|
||||
priorityId = selectedPriority!!.id,
|
||||
statusId = selectedStatus!!.id,
|
||||
dueDate = dueDate,
|
||||
estimatedCost = estimatedCost.ifBlank { null }?.toDoubleOrNull(),
|
||||
archived = task.archived
|
||||
estimatedCost = estimatedCost.ifBlank { null }?.toDoubleOrNull()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,15 +28,15 @@ fun HomeScreen(
|
||||
onLogout: () -> Unit,
|
||||
viewModel: ResidenceViewModel = viewModel { ResidenceViewModel() }
|
||||
) {
|
||||
val summaryState by viewModel.residenceSummaryState.collectAsState()
|
||||
val summaryState by viewModel.myResidencesState.collectAsState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.loadResidenceSummary()
|
||||
viewModel.loadMyResidences()
|
||||
}
|
||||
|
||||
// Handle errors for loading summary
|
||||
summaryState.HandleErrors(
|
||||
onRetry = { viewModel.loadResidenceSummary() },
|
||||
onRetry = { viewModel.loadMyResidences() },
|
||||
errorTitle = "Failed to Load Summary"
|
||||
)
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.example.mycrib.network.ApiResult
|
||||
import com.example.mycrib.utils.SubscriptionHelper
|
||||
import com.example.mycrib.ui.subscription.UpgradePromptDialog
|
||||
import com.example.mycrib.cache.SubscriptionCache
|
||||
import com.example.mycrib.cache.DataCache
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -69,6 +70,9 @@ fun ResidenceDetailScreen(
|
||||
var showUpgradePrompt by remember { mutableStateOf(false) }
|
||||
var upgradeTriggerKey by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
// Get current user for ownership checks
|
||||
val currentUser by DataCache.currentUser.collectAsState()
|
||||
|
||||
// Check if tasks are blocked (limit=0) - this hides the FAB
|
||||
val isTasksBlocked = SubscriptionHelper.isTasksBlocked()
|
||||
// Get current count for checking when adding
|
||||
@@ -220,7 +224,7 @@ fun ResidenceDetailScreen(
|
||||
ManageUsersDialog(
|
||||
residenceId = residence.id,
|
||||
residenceName = residence.name,
|
||||
isPrimaryOwner = residence.isPrimaryOwner,
|
||||
isPrimaryOwner = residence.ownerId == currentUser?.id,
|
||||
onDismiss = {
|
||||
showManageUsersDialog = false
|
||||
},
|
||||
@@ -395,7 +399,7 @@ fun ResidenceDetailScreen(
|
||||
}
|
||||
|
||||
// Manage Users button - only show for primary owners
|
||||
if (residence.isPrimaryOwner) {
|
||||
if (residence.ownerId == currentUser?.id) {
|
||||
IconButton(onClick = {
|
||||
showManageUsersDialog = true
|
||||
}) {
|
||||
@@ -410,7 +414,7 @@ fun ResidenceDetailScreen(
|
||||
}
|
||||
|
||||
// Delete button - only show for primary owners
|
||||
if (residence.isPrimaryOwner) {
|
||||
if (residence.ownerId == currentUser?.id) {
|
||||
IconButton(onClick = {
|
||||
showDeleteConfirmation = true
|
||||
}) {
|
||||
|
||||
@@ -75,8 +75,8 @@ fun ResidenceFormScreen(
|
||||
// Set default/existing property type when types are loaded
|
||||
LaunchedEffect(propertyTypes, existingResidence) {
|
||||
if (propertyTypes.isNotEmpty() && propertyType == null) {
|
||||
propertyType = if (isEditMode && existingResidence != null && existingResidence.propertyType != null) {
|
||||
propertyTypes.find { it.id == existingResidence.propertyType.toInt() }
|
||||
propertyType = if (isEditMode && existingResidence != null && existingResidence.propertyTypeId != null) {
|
||||
propertyTypes.find { it.id == existingResidence.propertyTypeId }
|
||||
} else if (!isEditMode && propertyTypes.isNotEmpty()) {
|
||||
propertyTypes.first()
|
||||
} else {
|
||||
@@ -306,7 +306,7 @@ fun ResidenceFormScreen(
|
||||
if (validateForm()) {
|
||||
val request = ResidenceCreateRequest(
|
||||
name = name,
|
||||
propertyType = propertyType?.id,
|
||||
propertyTypeId = propertyType?.id,
|
||||
streetAddress = streetAddress.ifBlank { null },
|
||||
apartmentUnit = apartmentUnit.ifBlank { null },
|
||||
city = city.ifBlank { null },
|
||||
@@ -314,9 +314,9 @@ fun ResidenceFormScreen(
|
||||
postalCode = postalCode.ifBlank { null },
|
||||
country = country.ifBlank { null },
|
||||
bedrooms = bedrooms.toIntOrNull(),
|
||||
bathrooms = bathrooms.toFloatOrNull(),
|
||||
bathrooms = bathrooms.toDoubleOrNull(),
|
||||
squareFootage = squareFootage.toIntOrNull(),
|
||||
lotSize = lotSize.toFloatOrNull(),
|
||||
lotSize = lotSize.toDoubleOrNull(),
|
||||
yearBuilt = yearBuilt.toIntOrNull(),
|
||||
description = description.ifBlank { null },
|
||||
isPrimary = isPrimary
|
||||
|
||||
@@ -188,11 +188,11 @@ class AuthViewModel : ViewModel() {
|
||||
fun resetPassword(resetToken: String, newPassword: String, confirmPassword: String) {
|
||||
viewModelScope.launch {
|
||||
_resetPasswordState.value = ApiResult.Loading
|
||||
// Note: confirmPassword is for UI validation only, not sent to API
|
||||
val result = APILayer.resetPassword(
|
||||
ResetPasswordRequest(
|
||||
resetToken = resetToken,
|
||||
newPassword = newPassword,
|
||||
confirmPassword = confirmPassword
|
||||
newPassword = newPassword
|
||||
)
|
||||
)
|
||||
_resetPasswordState.value = when (result) {
|
||||
|
||||
@@ -90,11 +90,11 @@ class PasswordResetViewModel(
|
||||
|
||||
viewModelScope.launch {
|
||||
_resetPasswordState.value = ApiResult.Loading
|
||||
// Note: confirmPassword is for UI validation only, not sent to API
|
||||
val result = authApi.resetPassword(
|
||||
ResetPasswordRequest(
|
||||
resetToken = token,
|
||||
newPassword = newPassword,
|
||||
confirmPassword = confirmPassword
|
||||
newPassword = newPassword
|
||||
)
|
||||
)
|
||||
_resetPasswordState.value = when (result) {
|
||||
|
||||
@@ -11,10 +11,10 @@ import ComposeApp
|
||||
|
||||
/// Displays a task summary with dynamic categories from the backend
|
||||
struct TaskSummaryCard: View {
|
||||
let taskSummary: TaskSummary
|
||||
let taskSummary: ResidenceTaskSummary
|
||||
var visibleCategories: [String]? = nil
|
||||
|
||||
private var filteredCategories: [TaskColumnCategory] {
|
||||
private var filteredCategories: [TaskCategorySummary] {
|
||||
if let visible = visibleCategories {
|
||||
return taskSummary.categories.filter { visible.contains($0.name) }
|
||||
}
|
||||
@@ -41,7 +41,7 @@ struct TaskSummaryCard: View {
|
||||
|
||||
/// Displays a single task category with icon, name, and count
|
||||
struct TaskCategoryRow: View {
|
||||
let category: TaskColumnCategory
|
||||
let category: TaskCategorySummary
|
||||
|
||||
private var categoryColor: Color {
|
||||
Color(hex: category.color) ?? .gray
|
||||
@@ -103,61 +103,55 @@ struct TaskSummaryCard_Previews: PreviewProvider {
|
||||
.background(Color(.systemGroupedBackground))
|
||||
}
|
||||
|
||||
static var mockTaskSummary: TaskSummary {
|
||||
TaskSummary(
|
||||
total: 25,
|
||||
static var mockTaskSummary: ResidenceTaskSummary {
|
||||
ResidenceTaskSummary(
|
||||
categories: [
|
||||
TaskColumnCategory(
|
||||
TaskCategorySummary(
|
||||
name: "overdue_tasks",
|
||||
displayName: "Overdue",
|
||||
icons: TaskColumnIcon(
|
||||
ios: "exclamationmark.triangle",
|
||||
icons: TaskCategoryIcons(
|
||||
android: "Warning",
|
||||
web: "exclamation-triangle"
|
||||
ios: "exclamationmark.triangle"
|
||||
),
|
||||
color: "#FF3B30",
|
||||
count: 3
|
||||
),
|
||||
TaskColumnCategory(
|
||||
TaskCategorySummary(
|
||||
name: "current_tasks",
|
||||
displayName: "Current",
|
||||
icons: TaskColumnIcon(
|
||||
ios: "calendar",
|
||||
icons: TaskCategoryIcons(
|
||||
android: "CalendarToday",
|
||||
web: "calendar"
|
||||
ios: "calendar"
|
||||
),
|
||||
color: "#007AFF",
|
||||
count: 8
|
||||
),
|
||||
TaskColumnCategory(
|
||||
TaskCategorySummary(
|
||||
name: "in_progress_tasks",
|
||||
displayName: "In Progress",
|
||||
icons: TaskColumnIcon(
|
||||
ios: "play.circle",
|
||||
icons: TaskCategoryIcons(
|
||||
android: "PlayCircle",
|
||||
web: "play-circle"
|
||||
ios: "play.circle"
|
||||
),
|
||||
color: "#FF9500",
|
||||
count: 2
|
||||
),
|
||||
TaskColumnCategory(
|
||||
TaskCategorySummary(
|
||||
name: "backlog_tasks",
|
||||
displayName: "Backlog",
|
||||
icons: TaskColumnIcon(
|
||||
ios: "tray",
|
||||
icons: TaskCategoryIcons(
|
||||
android: "Inbox",
|
||||
web: "inbox"
|
||||
ios: "tray"
|
||||
),
|
||||
color: "#5856D6",
|
||||
count: 7
|
||||
),
|
||||
TaskColumnCategory(
|
||||
TaskCategorySummary(
|
||||
name: "done_tasks",
|
||||
displayName: "Done",
|
||||
icons: TaskColumnIcon(
|
||||
ios: "checkmark.circle",
|
||||
icons: TaskCategoryIcons(
|
||||
android: "CheckCircle",
|
||||
web: "check-circle"
|
||||
ios: "checkmark.circle"
|
||||
),
|
||||
color: "#34C759",
|
||||
count: 5
|
||||
|
||||
@@ -2,7 +2,7 @@ import SwiftUI
|
||||
import ComposeApp
|
||||
|
||||
struct EditResidenceView: View {
|
||||
let residence: Residence
|
||||
let residence: ResidenceResponse
|
||||
@Binding var isPresented: Bool
|
||||
|
||||
var body: some View {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Foundation
|
||||
import ComposeApp
|
||||
|
||||
// Extension to make TaskDetail conform to Identifiable for SwiftUI
|
||||
extension TaskDetail: Identifiable {
|
||||
// Extension to make TaskResponse conform to Identifiable for SwiftUI
|
||||
extension TaskResponse: Identifiable {
|
||||
// TaskDetail already has an `id` property from Kotlin,
|
||||
// so we just need to declare conformance to Identifiable
|
||||
}
|
||||
|
||||
@@ -59,11 +59,11 @@ final class WidgetDataManager {
|
||||
id: Int(task.id),
|
||||
title: task.title,
|
||||
description: task.description_,
|
||||
priority: task.priority.name,
|
||||
priority: task.priority?.name ?? "",
|
||||
status: task.status?.name,
|
||||
dueDate: task.dueDate,
|
||||
category: task.category.name,
|
||||
residenceName: task.residenceName,
|
||||
category: task.category?.name ?? "",
|
||||
residenceName: "", // No longer available in API, residence lookup needed
|
||||
isOverdue: isTaskOverdue(dueDate: task.dueDate, status: task.status?.name)
|
||||
)
|
||||
allTasks.append(widgetTask)
|
||||
|
||||
@@ -7,9 +7,9 @@ struct ManageUsersView: View {
|
||||
let isPrimaryOwner: Bool
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@State private var users: [ResidenceUser] = []
|
||||
@State private var users: [ResidenceUserResponse] = []
|
||||
@State private var ownerId: Int32?
|
||||
@State private var shareCode: ResidenceShareCode?
|
||||
@State private var shareCode: ShareCodeResponse?
|
||||
@State private var isLoading = true
|
||||
@State private var errorMessage: String?
|
||||
@State private var isGeneratingCode = false
|
||||
@@ -100,7 +100,7 @@ struct ManageUsersView: View {
|
||||
if let successResult = result as? ApiResultSuccess<ResidenceUsersResponse>,
|
||||
let responseData = successResult.data as? ResidenceUsersResponse {
|
||||
self.users = Array(responseData.users)
|
||||
self.ownerId = responseData.ownerId as? Int32
|
||||
self.ownerId = Int32(responseData.owner.id)
|
||||
self.isLoading = false
|
||||
} else if let errorResult = result as? ApiResultError {
|
||||
self.errorMessage = ErrorMessageParser.parse(errorResult.message)
|
||||
@@ -127,7 +127,7 @@ struct ManageUsersView: View {
|
||||
let result = try await APILayer.shared.getShareCode(residenceId: Int32(Int(residenceId)))
|
||||
|
||||
await MainActor.run {
|
||||
if let successResult = result as? ApiResultSuccess<ResidenceShareCode> {
|
||||
if let successResult = result as? ApiResultSuccess<ShareCodeResponse> {
|
||||
self.shareCode = successResult.data
|
||||
}
|
||||
// It's okay if there's no active share code
|
||||
@@ -148,7 +148,7 @@ struct ManageUsersView: View {
|
||||
let result = try await APILayer.shared.generateShareCode(residenceId: Int32(Int(residenceId)))
|
||||
|
||||
await MainActor.run {
|
||||
if let successResult = result as? ApiResultSuccess<ResidenceShareCode> {
|
||||
if let successResult = result as? ApiResultSuccess<ShareCodeResponse> {
|
||||
self.shareCode = successResult.data
|
||||
self.isGeneratingCode = false
|
||||
} else if let errorResult = result as? ApiResultError {
|
||||
|
||||
@@ -15,9 +15,9 @@ struct ResidenceDetailView: View {
|
||||
@State private var showEditResidence = false
|
||||
@State private var showEditTask = false
|
||||
@State private var showManageUsers = false
|
||||
@State private var selectedTaskForEdit: TaskDetail?
|
||||
@State private var selectedTaskForComplete: TaskDetail?
|
||||
@State private var selectedTaskForArchive: TaskDetail?
|
||||
@State private var selectedTaskForEdit: TaskResponse?
|
||||
@State private var selectedTaskForComplete: TaskResponse?
|
||||
@State private var selectedTaskForArchive: TaskResponse?
|
||||
@State private var showArchiveConfirmation = false
|
||||
|
||||
@State private var hasAppeared = false
|
||||
@@ -29,7 +29,15 @@ struct ResidenceDetailView: View {
|
||||
@StateObject private var subscriptionCache = SubscriptionCacheWrapper.shared
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
|
||||
// Check if current user is the owner of the residence
|
||||
private func isCurrentUserOwner(of residence: ResidenceResponse) -> Bool {
|
||||
guard let currentUser = ComposeApp.DataCache.shared.currentUser.value else {
|
||||
return false
|
||||
}
|
||||
return Int(residence.ownerId) == Int(currentUser.id)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.appBackgroundPrimary
|
||||
@@ -100,7 +108,7 @@ struct ResidenceDetailView: View {
|
||||
ManageUsersView(
|
||||
residenceId: residence.id,
|
||||
residenceName: residence.name,
|
||||
isPrimaryOwner: residence.isPrimaryOwner
|
||||
isPrimaryOwner: isCurrentUserOwner(of: residence)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -184,7 +192,7 @@ private extension ResidenceDetailView {
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func contentView(for residence: Residence) -> some View {
|
||||
func contentView(for residence: ResidenceResponse) -> some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
PropertyHeaderCard(residence: residence)
|
||||
@@ -251,7 +259,7 @@ private extension ResidenceDetailView {
|
||||
.disabled(viewModel.isGeneratingReport)
|
||||
}
|
||||
|
||||
if let residence = viewModel.selectedResidence, residence.isPrimaryOwner {
|
||||
if let residence = viewModel.selectedResidence, isCurrentUserOwner(of: residence) {
|
||||
Button {
|
||||
showManageUsers = true
|
||||
} label: {
|
||||
@@ -272,7 +280,7 @@ private extension ResidenceDetailView {
|
||||
}
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Task.addButton)
|
||||
|
||||
if let residence = viewModel.selectedResidence, residence.isPrimaryOwner {
|
||||
if let residence = viewModel.selectedResidence, isCurrentUserOwner(of: residence) {
|
||||
Button {
|
||||
showDeleteConfirmation = true
|
||||
} label: {
|
||||
@@ -363,9 +371,9 @@ private struct TasksSectionContainer: View {
|
||||
let tasksResponse: TaskColumnsResponse
|
||||
|
||||
@ObservedObject var taskViewModel: TaskViewModel
|
||||
@Binding var selectedTaskForEdit: TaskDetail?
|
||||
@Binding var selectedTaskForComplete: TaskDetail?
|
||||
@Binding var selectedTaskForArchive: TaskDetail?
|
||||
@Binding var selectedTaskForEdit: TaskResponse?
|
||||
@Binding var selectedTaskForComplete: TaskResponse?
|
||||
@Binding var selectedTaskForArchive: TaskResponse?
|
||||
@Binding var showArchiveConfirmation: Bool
|
||||
|
||||
let reloadTasks: () -> Void
|
||||
|
||||
@@ -7,7 +7,7 @@ class ResidenceViewModel: ObservableObject {
|
||||
// MARK: - Published Properties
|
||||
@Published var residenceSummary: ResidenceSummaryResponse?
|
||||
@Published var myResidences: MyResidencesResponse?
|
||||
@Published var selectedResidence: Residence?
|
||||
@Published var selectedResidence: ResidenceResponse?
|
||||
@Published var isLoading: Bool = false
|
||||
@Published var errorMessage: String?
|
||||
@Published var isGeneratingReport: Bool = false
|
||||
@@ -65,7 +65,7 @@ class ResidenceViewModel: ObservableObject {
|
||||
|
||||
sharedViewModel.getResidence(id: id) { result in
|
||||
Task { @MainActor in
|
||||
if let success = result as? ApiResultSuccess<Residence> {
|
||||
if let success = result as? ApiResultSuccess<ResidenceResponse> {
|
||||
self.selectedResidence = success.data
|
||||
self.isLoading = false
|
||||
} else if let error = result as? ApiResultError {
|
||||
@@ -101,7 +101,7 @@ class ResidenceViewModel: ObservableObject {
|
||||
sharedViewModel.updateResidenceState,
|
||||
loadingSetter: { [weak self] in self?.isLoading = $0 },
|
||||
errorSetter: { [weak self] in self?.errorMessage = $0 },
|
||||
onSuccess: { [weak self] (data: Residence) in
|
||||
onSuccess: { [weak self] (data: ResidenceResponse) in
|
||||
self?.selectedResidence = data
|
||||
},
|
||||
completion: completion,
|
||||
|
||||
@@ -121,7 +121,7 @@ struct ResidencesListView: View {
|
||||
|
||||
private struct ResidencesContent: View {
|
||||
let response: MyResidencesResponse
|
||||
let residences: [ResidenceWithTasks]
|
||||
let residences: [ResidenceResponse]
|
||||
|
||||
var body: some View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import SwiftUI
|
||||
import ComposeApp
|
||||
|
||||
struct ResidenceFormView: View {
|
||||
let existingResidence: Residence?
|
||||
let existingResidence: ResidenceResponse?
|
||||
@Binding var isPresented: Bool
|
||||
var onSuccess: (() -> Void)?
|
||||
@StateObject private var viewModel = ResidenceViewModel()
|
||||
@@ -233,8 +233,8 @@ struct ResidenceFormView: View {
|
||||
isPrimary = residence.isPrimary
|
||||
|
||||
// Set the selected property type
|
||||
if let propertyTypeStr = residence.propertyType, let propertyTypeId = Int(propertyTypeStr) {
|
||||
selectedPropertyType = residenceTypes.first { $0.id == propertyTypeId }
|
||||
if let propertyTypeId = residence.propertyTypeId {
|
||||
selectedPropertyType = residenceTypes.first { $0.id == Int32(propertyTypeId) }
|
||||
}
|
||||
}
|
||||
// In add mode, leave selectedPropertyType as nil to force user to select
|
||||
@@ -261,17 +261,17 @@ struct ResidenceFormView: View {
|
||||
guard !bedrooms.isEmpty, let value = Int32(bedrooms) else { return nil }
|
||||
return KotlinInt(int: value)
|
||||
}()
|
||||
let bathroomsValue: KotlinFloat? = {
|
||||
guard !bathrooms.isEmpty, let value = Float(bathrooms) else { return nil }
|
||||
return KotlinFloat(float: value)
|
||||
let bathroomsValue: KotlinDouble? = {
|
||||
guard !bathrooms.isEmpty, let value = Double(bathrooms) else { return nil }
|
||||
return KotlinDouble(double: value)
|
||||
}()
|
||||
let squareFootageValue: KotlinInt? = {
|
||||
guard !squareFootage.isEmpty, let value = Int32(squareFootage) else { return nil }
|
||||
return KotlinInt(int: value)
|
||||
}()
|
||||
let lotSizeValue: KotlinFloat? = {
|
||||
guard !lotSize.isEmpty, let value = Float(lotSize) else { return nil }
|
||||
return KotlinFloat(float: value)
|
||||
let lotSizeValue: KotlinDouble? = {
|
||||
guard !lotSize.isEmpty, let value = Double(lotSize) else { return nil }
|
||||
return KotlinDouble(double: value)
|
||||
}()
|
||||
let yearBuiltValue: KotlinInt? = {
|
||||
guard !yearBuilt.isEmpty, let value = Int32(yearBuilt) else { return nil }
|
||||
@@ -286,7 +286,7 @@ struct ResidenceFormView: View {
|
||||
|
||||
let request = ResidenceCreateRequest(
|
||||
name: name,
|
||||
propertyType: propertyTypeValue,
|
||||
propertyTypeId: propertyTypeValue,
|
||||
streetAddress: streetAddress.isEmpty ? nil : streetAddress,
|
||||
apartmentUnit: apartmentUnit.isEmpty ? nil : apartmentUnit,
|
||||
city: city.isEmpty ? nil : city,
|
||||
@@ -301,7 +301,7 @@ struct ResidenceFormView: View {
|
||||
description: description.isEmpty ? nil : description,
|
||||
purchaseDate: nil,
|
||||
purchasePrice: nil,
|
||||
isPrimary: isPrimary
|
||||
isPrimary: KotlinBoolean(bool: isPrimary)
|
||||
)
|
||||
|
||||
if let residence = existingResidence {
|
||||
|
||||
@@ -2,7 +2,7 @@ import SwiftUI
|
||||
import ComposeApp
|
||||
|
||||
struct OverviewCard: View {
|
||||
let summary: OverallSummary
|
||||
let summary: TotalSummary
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: AppSpacing.lg) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import SwiftUI
|
||||
import ComposeApp
|
||||
|
||||
struct PropertyHeaderCard: View {
|
||||
let residence: Residence
|
||||
let residence: ResidenceResponse
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
@@ -17,8 +17,8 @@ struct PropertyHeaderCard: View {
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
|
||||
if let propertyType = residence.propertyType {
|
||||
Text(propertyType)
|
||||
if let propertyTypeName = residence.propertyTypeName {
|
||||
Text(propertyTypeName)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
@@ -30,20 +30,20 @@ struct PropertyHeaderCard: View {
|
||||
Divider()
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
if let streetAddress = residence.streetAddress {
|
||||
Label(streetAddress, systemImage: "mappin.circle.fill")
|
||||
if !residence.streetAddress.isEmpty {
|
||||
Label(residence.streetAddress, systemImage: "mappin.circle.fill")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
}
|
||||
|
||||
if residence.city != nil || residence.stateProvince != nil || residence.postalCode != nil {
|
||||
Text("\(residence.city ?? ""), \(residence.stateProvince ?? "") \(residence.postalCode ?? "")")
|
||||
if !residence.city.isEmpty || !residence.stateProvince.isEmpty || !residence.postalCode.isEmpty {
|
||||
Text("\(residence.city), \(residence.stateProvince) \(residence.postalCode)")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
|
||||
if let country = residence.country, !country.isEmpty {
|
||||
Text(country)
|
||||
if !residence.country.isEmpty {
|
||||
Text(residence.country)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import SwiftUI
|
||||
import ComposeApp
|
||||
|
||||
struct ResidenceCard: View {
|
||||
let residence: ResidenceWithTasks
|
||||
let residence: ResidenceResponse
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: AppSpacing.md) {
|
||||
@@ -26,8 +26,8 @@ struct ResidenceCard: View {
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
// .lineLimit(1)
|
||||
|
||||
if let propertyType = residence.propertyType {
|
||||
Text(propertyType)
|
||||
if let propertyTypeName = residence.propertyTypeName {
|
||||
Text(propertyTypeName)
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
.textCase(.uppercase)
|
||||
@@ -51,18 +51,18 @@ struct ResidenceCard: View {
|
||||
|
||||
// Address
|
||||
VStack(alignment: .leading, spacing: AppSpacing.xxs) {
|
||||
if let streetAddress = residence.streetAddress {
|
||||
if !residence.streetAddress.isEmpty {
|
||||
HStack(spacing: AppSpacing.xxs) {
|
||||
Image(systemName: "mappin.circle.fill")
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
Text(streetAddress)
|
||||
Text(residence.streetAddress)
|
||||
.font(.callout)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
}
|
||||
|
||||
if residence.city != nil || residence.stateProvince != nil {
|
||||
if !residence.city.isEmpty || !residence.stateProvince.isEmpty {
|
||||
HStack(spacing: AppSpacing.xxs) {
|
||||
Image(systemName: "location.fill")
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
@@ -99,16 +99,16 @@ struct ResidenceCard: View {
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ResidenceCard(residence: ResidenceWithTasks(
|
||||
ResidenceCard(residence: ResidenceResponse(
|
||||
id: 1,
|
||||
owner: 1,
|
||||
ownerUsername: "testuser",
|
||||
isPrimaryOwner: false,
|
||||
userCount: 1,
|
||||
ownerId: 1,
|
||||
owner: ResidenceUserResponse(id: 1, username: "testuser", email: "test@test.com", firstName: "", lastName: ""),
|
||||
users: [],
|
||||
name: "My Home",
|
||||
propertyType: "House",
|
||||
propertyTypeId: 1,
|
||||
propertyType: ResidenceType(id: 1, name: "House"),
|
||||
streetAddress: "123 Main St",
|
||||
apartmentUnit: nil,
|
||||
apartmentUnit: "",
|
||||
city: "San Francisco",
|
||||
stateProvince: "CA",
|
||||
postalCode: "94102",
|
||||
@@ -118,44 +118,11 @@ struct ResidenceCard: View {
|
||||
squareFootage: 1800,
|
||||
lotSize: 0.25,
|
||||
yearBuilt: 2010,
|
||||
description: nil,
|
||||
description: "",
|
||||
purchaseDate: nil,
|
||||
purchasePrice: nil,
|
||||
isPrimary: true,
|
||||
taskSummary: TaskSummary(
|
||||
total: 10,
|
||||
categories: [
|
||||
TaskColumnCategory(
|
||||
name: "overdue_tasks",
|
||||
displayName: "Overdue",
|
||||
icons: TaskColumnIcon(ios: "exclamationmark.triangle", android: "Warning", web: "exclamation-triangle"),
|
||||
color: "#FF3B30",
|
||||
count: 0
|
||||
),
|
||||
TaskColumnCategory(
|
||||
name: "current_tasks",
|
||||
displayName: "Current",
|
||||
icons: TaskColumnIcon(ios: "calendar", android: "CalendarToday", web: "calendar"),
|
||||
color: "#007AFF",
|
||||
count: 5
|
||||
),
|
||||
TaskColumnCategory(
|
||||
name: "in_progress_tasks",
|
||||
displayName: "In Progress",
|
||||
icons: TaskColumnIcon(ios: "play.circle", android: "PlayCircle", web: "play-circle"),
|
||||
color: "#FF9500",
|
||||
count: 2
|
||||
),
|
||||
TaskColumnCategory(
|
||||
name: "done_tasks",
|
||||
displayName: "Done",
|
||||
icons: TaskColumnIcon(ios: "checkmark.circle", android: "CheckCircle", web: "check-circle"),
|
||||
color: "#34C759",
|
||||
count: 3
|
||||
)
|
||||
]
|
||||
),
|
||||
tasks: [],
|
||||
isActive: true,
|
||||
createdAt: "2024-01-01T00:00:00Z",
|
||||
updatedAt: "2024-01-01T00:00:00Z"
|
||||
))
|
||||
|
||||
@@ -3,7 +3,7 @@ import ComposeApp
|
||||
|
||||
// MARK: - Share Code Card
|
||||
struct ShareCodeCard: View {
|
||||
let shareCode: ResidenceShareCode?
|
||||
let shareCode: ShareCodeResponse?
|
||||
let residenceName: String
|
||||
let isGeneratingCode: Bool
|
||||
let onGenerateCode: () -> Void
|
||||
|
||||
@@ -2,7 +2,7 @@ import SwiftUI
|
||||
import ComposeApp
|
||||
|
||||
struct SummaryCard: View {
|
||||
let summary: MyResidencesSummary
|
||||
let summary: TotalSummary
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
@@ -53,9 +53,11 @@ struct SummaryCard: View {
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SummaryCard(summary: MyResidencesSummary(
|
||||
SummaryCard(summary: TotalSummary(
|
||||
totalResidences: 3,
|
||||
totalTasks: 12,
|
||||
totalPending: 2,
|
||||
totalOverdue: 1,
|
||||
tasksDueNextWeek: 4,
|
||||
tasksDueNextMonth: 8
|
||||
))
|
||||
|
||||
@@ -3,7 +3,7 @@ import ComposeApp
|
||||
|
||||
// MARK: - User List Item
|
||||
struct UserListItem: View {
|
||||
let user: ResidenceUser
|
||||
let user: ResidenceUserResponse
|
||||
let isOwner: Bool
|
||||
let isPrimaryOwner: Bool
|
||||
let onRemove: () -> Void
|
||||
|
||||
@@ -2,7 +2,7 @@ import SwiftUI
|
||||
import ComposeApp
|
||||
|
||||
struct CompletionCardView: View {
|
||||
let completion: TaskCompletion
|
||||
let completion: TaskCompletionResponse
|
||||
@State private var showPhotoSheet = false
|
||||
|
||||
var body: some View {
|
||||
@@ -64,15 +64,16 @@ struct CompletionCardView: View {
|
||||
.fontWeight(.medium)
|
||||
}
|
||||
|
||||
if let notes = completion.notes {
|
||||
Text(notes)
|
||||
if !completion.notes.isEmpty {
|
||||
Text(completion.notes)
|
||||
.font(.caption2)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
|
||||
// Show button to view photos if images exist
|
||||
if let images = completion.images, !images.isEmpty {
|
||||
if !completion.images.isEmpty {
|
||||
let images = completion.images
|
||||
Button(action: {
|
||||
showPhotoSheet = true
|
||||
}) {
|
||||
@@ -95,9 +96,7 @@ struct CompletionCardView: View {
|
||||
.background(Color.appBackgroundSecondary.opacity(0.5))
|
||||
.cornerRadius(8)
|
||||
.sheet(isPresented: $showPhotoSheet) {
|
||||
if let images = completion.images {
|
||||
PhotoViewerSheet(images: images)
|
||||
}
|
||||
PhotoViewerSheet(images: completion.images)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import ComposeApp
|
||||
|
||||
/// Task card that dynamically renders buttons based on the column's button types
|
||||
struct DynamicTaskCard: View {
|
||||
let task: TaskDetail
|
||||
let task: TaskResponse
|
||||
let buttonTypes: [String]
|
||||
let onEdit: () -> Void
|
||||
let onCancel: () -> Void
|
||||
@@ -32,18 +32,18 @@ struct DynamicTaskCard: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
PriorityBadge(priority: task.priority.name)
|
||||
PriorityBadge(priority: task.priority?.name ?? "")
|
||||
}
|
||||
|
||||
if let description = task.description_, !description.isEmpty {
|
||||
Text(description)
|
||||
if !task.description_.isEmpty {
|
||||
Text(task.description_)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Label(task.frequency.displayName, systemImage: "repeat")
|
||||
Label(task.frequency?.displayName ?? "", systemImage: "repeat")
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@ import ComposeApp
|
||||
/// Dynamic task column view that adapts based on the column configuration
|
||||
struct DynamicTaskColumnView: View {
|
||||
let column: TaskColumn
|
||||
let onEditTask: (TaskDetail) -> Void
|
||||
let onEditTask: (TaskResponse) -> Void
|
||||
let onCancelTask: (Int32) -> Void
|
||||
let onUncancelTask: (Int32) -> Void
|
||||
let onMarkInProgress: (Int32) -> Void
|
||||
let onCompleteTask: (TaskDetail) -> Void
|
||||
let onCompleteTask: (TaskResponse) -> Void
|
||||
let onArchiveTask: (Int32) -> Void
|
||||
let onUnarchiveTask: (Int32) -> Void
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import SwiftUI
|
||||
import ComposeApp
|
||||
|
||||
struct TaskCard: View {
|
||||
let task: TaskDetail
|
||||
let task: TaskResponse
|
||||
let onEdit: () -> Void
|
||||
let onCancel: (() -> Void)?
|
||||
let onUncancel: (() -> Void)?
|
||||
@@ -30,12 +30,12 @@ struct TaskCard: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
PriorityBadge(priority: task.priority.name)
|
||||
PriorityBadge(priority: task.priority?.name ?? "")
|
||||
}
|
||||
|
||||
// Description
|
||||
if let description = task.description_, !description.isEmpty {
|
||||
Text(description)
|
||||
if !task.description_.isEmpty {
|
||||
Text(task.description_)
|
||||
.font(.callout)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
.lineLimit(3)
|
||||
@@ -47,7 +47,7 @@ struct TaskCard: View {
|
||||
Image(systemName: "repeat")
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundColor(Color.appTextSecondary.opacity(0.7))
|
||||
Text(task.frequency.displayName)
|
||||
Text(task.frequency?.displayName ?? "")
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
@@ -255,27 +255,33 @@ struct TaskCard: View {
|
||||
#Preview {
|
||||
VStack(spacing: 16) {
|
||||
TaskCard(
|
||||
task: TaskDetail(
|
||||
task: TaskResponse(
|
||||
id: 1,
|
||||
residence: 1,
|
||||
residenceName: "Main House",
|
||||
createdBy: 1,
|
||||
createdByUsername: "testuser",
|
||||
residenceId: 1,
|
||||
createdById: 1,
|
||||
createdBy: nil,
|
||||
assignedToId: nil,
|
||||
assignedTo: nil,
|
||||
title: "Clean Gutters",
|
||||
description: "Remove all debris from gutters",
|
||||
category: TaskCategory(id: 1, name: "maintenance", orderId: 0, description: ""),
|
||||
priority: TaskPriority(id: 2, name: "medium", displayName: "", orderId: 0, description: ""),
|
||||
frequency: TaskFrequency(id: 1, name: "monthly", lookupName: "", displayName: "30", daySpan: 0, notifyDays: 0),
|
||||
status: TaskStatus(id: 1, name: "pending", displayName: "", orderId: 0, description: ""),
|
||||
categoryId: 1,
|
||||
category: TaskCategory(id: 1, name: "maintenance", description: "", icon: "", color: "", displayOrder: 0),
|
||||
priorityId: 2,
|
||||
priority: TaskPriority(id: 2, name: "medium", level: 2, color: "", displayOrder: 0),
|
||||
statusId: 1,
|
||||
status: TaskStatus(id: 1, name: "pending", description: "", color: "", displayOrder: 0),
|
||||
frequencyId: 1,
|
||||
frequency: TaskFrequency(id: 1, name: "monthly", days: 30, displayOrder: 0),
|
||||
dueDate: "2024-12-15",
|
||||
intervalDays: 30,
|
||||
estimatedCost: 150.00,
|
||||
archived: false,
|
||||
actualCost: nil,
|
||||
contractorId: nil,
|
||||
isCancelled: false,
|
||||
isArchived: false,
|
||||
parentTaskId: nil,
|
||||
completions: [],
|
||||
createdAt: "2024-01-01T00:00:00Z",
|
||||
updatedAt: "2024-01-01T00:00:00Z",
|
||||
nextScheduledDate: nil,
|
||||
showCompletedButton: true,
|
||||
completions: []
|
||||
updatedAt: "2024-01-01T00:00:00Z"
|
||||
),
|
||||
onEdit: {},
|
||||
onCancel: {},
|
||||
|
||||
@@ -3,11 +3,11 @@ import ComposeApp
|
||||
|
||||
struct TasksSection: View {
|
||||
let tasksResponse: TaskColumnsResponse
|
||||
let onEditTask: (TaskDetail) -> Void
|
||||
let onEditTask: (TaskResponse) -> Void
|
||||
let onCancelTask: (Int32) -> Void
|
||||
let onUncancelTask: (Int32) -> Void
|
||||
let onMarkInProgress: (Int32) -> Void
|
||||
let onCompleteTask: (TaskDetail) -> Void
|
||||
let onCompleteTask: (TaskResponse) -> Void
|
||||
let onArchiveTask: (Int32) -> Void
|
||||
let onUnarchiveTask: (Int32) -> Void
|
||||
|
||||
@@ -79,27 +79,33 @@ struct TasksSection: View {
|
||||
icons: ["ios": "calendar", "android": "CalendarToday", "web": "calendar"],
|
||||
color: "#007AFF",
|
||||
tasks: [
|
||||
TaskDetail(
|
||||
TaskResponse(
|
||||
id: 1,
|
||||
residence: 1,
|
||||
residenceName: "Main House",
|
||||
createdBy: 1,
|
||||
createdByUsername: "testuser",
|
||||
residenceId: 1,
|
||||
createdById: 1,
|
||||
createdBy: nil,
|
||||
assignedToId: nil,
|
||||
assignedTo: nil,
|
||||
title: "Clean Gutters",
|
||||
description: "Remove all debris",
|
||||
category: TaskCategory(id: 1, name: "maintenance", orderId: 1, description: ""),
|
||||
priority: TaskPriority(id: 2, name: "medium", displayName: "Medium", orderId: 1, description: ""),
|
||||
frequency: TaskFrequency(id: 1, name: "monthly", lookupName: "", displayName: "Monthly", daySpan: 0, notifyDays: 0),
|
||||
status: TaskStatus(id: 1, name: "pending", displayName: "Pending", orderId: 1, description: ""),
|
||||
categoryId: 1,
|
||||
category: TaskCategory(id: 1, name: "maintenance", description: "", icon: "", color: "", displayOrder: 0),
|
||||
priorityId: 2,
|
||||
priority: TaskPriority(id: 2, name: "medium", level: 2, color: "", displayOrder: 0),
|
||||
statusId: 1,
|
||||
status: TaskStatus(id: 1, name: "pending", description: "", color: "", displayOrder: 0),
|
||||
frequencyId: 1,
|
||||
frequency: TaskFrequency(id: 1, name: "monthly", days: 30, displayOrder: 0),
|
||||
dueDate: "2024-12-15",
|
||||
intervalDays: 30,
|
||||
estimatedCost: 150.00,
|
||||
archived: false,
|
||||
actualCost: nil,
|
||||
contractorId: nil,
|
||||
isCancelled: false,
|
||||
isArchived: false,
|
||||
parentTaskId: nil,
|
||||
completions: [],
|
||||
createdAt: "2024-01-01T00:00:00Z",
|
||||
updatedAt: "2024-01-01T00:00:00Z",
|
||||
nextScheduledDate: nil,
|
||||
showCompletedButton: true,
|
||||
completions: []
|
||||
updatedAt: "2024-01-01T00:00:00Z"
|
||||
)
|
||||
],
|
||||
count: 1
|
||||
@@ -111,27 +117,33 @@ struct TasksSection: View {
|
||||
icons: ["ios": "checkmark.circle", "android": "CheckCircle", "web": "check-circle"],
|
||||
color: "#34C759",
|
||||
tasks: [
|
||||
TaskDetail(
|
||||
TaskResponse(
|
||||
id: 2,
|
||||
residence: 1,
|
||||
residenceName: "Main House",
|
||||
createdBy: 1,
|
||||
createdByUsername: "testuser",
|
||||
residenceId: 1,
|
||||
createdById: 1,
|
||||
createdBy: nil,
|
||||
assignedToId: nil,
|
||||
assignedTo: nil,
|
||||
title: "Fix Leaky Faucet",
|
||||
description: "Kitchen sink fixed",
|
||||
category: TaskCategory(id: 2, name: "plumbing", orderId: 1, description: ""),
|
||||
priority: TaskPriority(id: 3, name: "high", displayName: "High", orderId: 1, description: ""),
|
||||
frequency: TaskFrequency(id: 6, name: "once", lookupName: "", displayName: "One Time", daySpan: 0, notifyDays: 0),
|
||||
status: TaskStatus(id: 3, name: "completed", displayName: "Completed", orderId: 1, description: ""),
|
||||
categoryId: 2,
|
||||
category: TaskCategory(id: 2, name: "plumbing", description: "", icon: "", color: "", displayOrder: 0),
|
||||
priorityId: 3,
|
||||
priority: TaskPriority(id: 3, name: "high", level: 3, color: "", displayOrder: 0),
|
||||
statusId: 3,
|
||||
status: TaskStatus(id: 3, name: "completed", description: "", color: "", displayOrder: 0),
|
||||
frequencyId: 6,
|
||||
frequency: TaskFrequency(id: 6, name: "once", days: nil, displayOrder: 0),
|
||||
dueDate: "2024-11-01",
|
||||
intervalDays: nil,
|
||||
estimatedCost: 200.00,
|
||||
archived: false,
|
||||
actualCost: nil,
|
||||
contractorId: nil,
|
||||
isCancelled: false,
|
||||
isArchived: false,
|
||||
parentTaskId: nil,
|
||||
completions: [],
|
||||
createdAt: "2024-10-01T00:00:00Z",
|
||||
updatedAt: "2024-11-05T00:00:00Z",
|
||||
nextScheduledDate: nil,
|
||||
showCompletedButton: false,
|
||||
completions: []
|
||||
updatedAt: "2024-11-05T00:00:00Z"
|
||||
)
|
||||
],
|
||||
count: 1
|
||||
|
||||
@@ -3,7 +3,7 @@ import ComposeApp
|
||||
|
||||
struct AddTaskWithResidenceView: View {
|
||||
@Binding var isPresented: Bool
|
||||
let residences: [Residence]
|
||||
let residences: [ResidenceResponse]
|
||||
|
||||
var body: some View {
|
||||
TaskFormView(residenceId: nil, residences: residences, isPresented: $isPresented)
|
||||
|
||||
@@ -11,13 +11,13 @@ struct AllTasksView: View {
|
||||
@State private var showAddTask = false
|
||||
@State private var showEditTask = false
|
||||
@State private var showingUpgradePrompt = false
|
||||
@State private var selectedTaskForEdit: TaskDetail?
|
||||
@State private var selectedTaskForComplete: TaskDetail?
|
||||
@State private var selectedTaskForEdit: TaskResponse?
|
||||
@State private var selectedTaskForComplete: TaskResponse?
|
||||
|
||||
@State private var selectedTaskForArchive: TaskDetail?
|
||||
@State private var selectedTaskForArchive: TaskResponse?
|
||||
@State private var showArchiveConfirmation = false
|
||||
|
||||
@State private var selectedTaskForCancel: TaskDetail?
|
||||
@State private var selectedTaskForCancel: TaskResponse?
|
||||
@State private var showCancelConfirmation = false
|
||||
|
||||
// Count total tasks across all columns
|
||||
@@ -334,37 +334,9 @@ struct RoundedCorner: Shape {
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == ResidenceWithTasks {
|
||||
/// Converts an array of ResidenceWithTasks into an array of Residence.
|
||||
/// Adjust the mapping inside as needed to match your model initializers.
|
||||
func toResidences() -> [Residence] {
|
||||
return self.map { item in
|
||||
return Residence(
|
||||
id: item.id,
|
||||
owner: KotlinInt(value: item.owner),
|
||||
ownerUsername: item.ownerUsername,
|
||||
isPrimaryOwner: item.isPrimaryOwner,
|
||||
userCount: item.userCount,
|
||||
name: item.name,
|
||||
propertyType: item.propertyType,
|
||||
streetAddress: item.streetAddress,
|
||||
apartmentUnit: item.apartmentUnit,
|
||||
city: item.city,
|
||||
stateProvince: item.stateProvince,
|
||||
postalCode: item.postalCode,
|
||||
country: item.country,
|
||||
bedrooms: item.bedrooms != nil ? KotlinInt(nonretainedObject: item.bedrooms!) : nil,
|
||||
bathrooms: item.bathrooms != nil ? KotlinFloat(float: Float(item.bathrooms!)) : nil,
|
||||
squareFootage: item.squareFootage != nil ? KotlinInt(nonretainedObject: item.squareFootage!) : nil,
|
||||
lotSize: item.lotSize != nil ? KotlinFloat(float: Float(item.lotSize!)) : nil,
|
||||
yearBuilt: item.yearBuilt != nil ? KotlinInt(nonretainedObject: item.yearBuilt!) : nil,
|
||||
description: item.description,
|
||||
purchaseDate: item.purchaseDate,
|
||||
purchasePrice: item.purchasePrice,
|
||||
isPrimary: item.isPrimary,
|
||||
createdAt: item.createdAt,
|
||||
updatedAt: item.updatedAt
|
||||
)
|
||||
}
|
||||
extension Array where Element == ResidenceResponse {
|
||||
/// Returns the array as-is (for API compatibility)
|
||||
func toResidences() -> [ResidenceResponse] {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@ import PhotosUI
|
||||
import ComposeApp
|
||||
|
||||
struct CompleteTaskView: View {
|
||||
let task: TaskDetail
|
||||
let task: TaskResponse
|
||||
let onComplete: () -> Void
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@StateObject private var taskViewModel = TaskViewModel()
|
||||
@StateObject private var contractorViewModel = ContractorViewModel()
|
||||
private let completionViewModel = ComposeApp.TaskCompletionViewModel()
|
||||
@State private var completedByName: String = ""
|
||||
@State private var actualCost: String = ""
|
||||
@State private var notes: String = ""
|
||||
@@ -32,7 +33,7 @@ struct CompleteTaskView: View {
|
||||
.font(.headline)
|
||||
|
||||
HStack {
|
||||
Label(task.category.name.capitalized, systemImage: "folder")
|
||||
Label((task.category?.name ?? "").capitalized, systemImage: "folder")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
@@ -303,66 +304,53 @@ struct CompleteTaskView: View {
|
||||
|
||||
isSubmitting = true
|
||||
|
||||
// Get current date in ISO format
|
||||
let dateFormatter = ISO8601DateFormatter()
|
||||
let currentDate = dateFormatter.string(from: Date())
|
||||
|
||||
// Create request
|
||||
// Create request with simplified Go API format
|
||||
// Note: completedAt defaults to now on server if not provided
|
||||
let request = TaskCompletionCreateRequest(
|
||||
task: task.id,
|
||||
completedByUser: nil,
|
||||
contractor: selectedContractor != nil ? KotlinInt(int: selectedContractor!.id) : nil,
|
||||
completedByName: completedByName.isEmpty ? nil : completedByName,
|
||||
completedByPhone: selectedContractor?.phone ?? "",
|
||||
completedByEmail: "",
|
||||
companyName: selectedContractor?.company ?? "",
|
||||
completionDate: currentDate,
|
||||
actualCost: actualCost.isEmpty ? nil : KotlinDouble(double: Double(actualCost) ?? 0.0),
|
||||
taskId: task.id,
|
||||
completedAt: nil,
|
||||
notes: notes.isEmpty ? nil : notes,
|
||||
rating: KotlinInt(int: Int32(rating))
|
||||
actualCost: actualCost.isEmpty ? nil : KotlinDouble(double: Double(actualCost) ?? 0.0),
|
||||
photoUrl: nil
|
||||
)
|
||||
|
||||
// Use TaskCompletionViewModel to create completion
|
||||
if !selectedImages.isEmpty {
|
||||
// Convert images to ImageData for Kotlin
|
||||
let imageDataList = selectedImages.compactMap { uiImage -> ComposeApp.ImageData? in
|
||||
guard let jpegData = uiImage.jpegData(compressionQuality: 0.8) else { return nil }
|
||||
let byteArray = KotlinByteArray(data: jpegData)
|
||||
return ComposeApp.ImageData(bytes: byteArray, fileName: "completion_image.jpg")
|
||||
}
|
||||
completionViewModel.createTaskCompletionWithImages(request: request, images: imageDataList)
|
||||
} else {
|
||||
completionViewModel.createTaskCompletion(request: request)
|
||||
}
|
||||
|
||||
// Observe the result
|
||||
Task {
|
||||
do {
|
||||
let result: ApiResult<TaskCompletion>
|
||||
|
||||
// If there are images, upload with images
|
||||
if !selectedImages.isEmpty {
|
||||
// Compress images to meet size requirements
|
||||
let imageDataArray = ImageCompression.compressImages(selectedImages)
|
||||
let imageByteArrays = imageDataArray.map { KotlinByteArray(data: $0) }
|
||||
let fileNames = (0..<imageDataArray.count).map { "image_\($0).jpg" }
|
||||
|
||||
result = try await APILayer.shared.createTaskCompletionWithImages(
|
||||
request: request,
|
||||
images: imageByteArrays,
|
||||
imageFileNames: fileNames
|
||||
)
|
||||
} else {
|
||||
// Upload without images
|
||||
result = try await APILayer.shared.createTaskCompletion(request: request)
|
||||
}
|
||||
|
||||
for await state in completionViewModel.createCompletionState {
|
||||
await MainActor.run {
|
||||
if result is ApiResultSuccess<TaskCompletion> {
|
||||
switch state {
|
||||
case is ApiResultSuccess<TaskCompletionResponse>:
|
||||
self.isSubmitting = false
|
||||
self.dismiss()
|
||||
self.onComplete()
|
||||
} else if let errorResult = result as? ApiResultError {
|
||||
self.errorMessage = errorResult.message
|
||||
self.showError = true
|
||||
self.isSubmitting = false
|
||||
} else {
|
||||
self.errorMessage = "Failed to complete task"
|
||||
case let error as ApiResultError:
|
||||
self.errorMessage = error.message
|
||||
self.showError = true
|
||||
self.isSubmitting = false
|
||||
case is ApiResultLoading:
|
||||
// Still loading, continue waiting
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
self.errorMessage = error.localizedDescription
|
||||
self.showError = true
|
||||
self.isSubmitting = false
|
||||
|
||||
// Break out of loop on terminal states
|
||||
if state is ApiResultSuccess<TaskCompletionResponse> || state is ApiResultError {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import ComposeApp
|
||||
/// Wrapper view for editing an existing task
|
||||
/// This is now just a convenience wrapper around TaskFormView in "edit" mode
|
||||
struct EditTaskView: View {
|
||||
let task: TaskDetail
|
||||
let task: TaskResponse
|
||||
@Binding var isPresented: Bool
|
||||
|
||||
var body: some View {
|
||||
|
||||
@@ -9,8 +9,8 @@ enum TaskFormField {
|
||||
// MARK: - Task Form View
|
||||
struct TaskFormView: View {
|
||||
let residenceId: Int32?
|
||||
let residences: [Residence]?
|
||||
let existingTask: TaskDetail? // nil for add mode, populated for edit mode
|
||||
let residences: [ResidenceResponse]?
|
||||
let existingTask: TaskResponse? // nil for add mode, populated for edit mode
|
||||
@Binding var isPresented: Bool
|
||||
@StateObject private var viewModel = TaskViewModel()
|
||||
@FocusState private var focusedField: TaskFormField?
|
||||
@@ -40,7 +40,7 @@ struct TaskFormView: View {
|
||||
@State private var isLoadingLookups: Bool = true
|
||||
|
||||
// Form fields
|
||||
@State private var selectedResidence: Residence?
|
||||
@State private var selectedResidence: ResidenceResponse?
|
||||
@State private var title: String
|
||||
@State private var description: String
|
||||
@State private var selectedCategory: TaskCategory?
|
||||
@@ -52,7 +52,7 @@ struct TaskFormView: View {
|
||||
@State private var estimatedCost: String
|
||||
|
||||
// Initialize form fields based on mode (add vs edit)
|
||||
init(residenceId: Int32? = nil, residences: [Residence]? = nil, existingTask: TaskDetail? = nil, isPresented: Binding<Bool>) {
|
||||
init(residenceId: Int32? = nil, residences: [ResidenceResponse]? = nil, existingTask: TaskResponse? = nil, isPresented: Binding<Bool>) {
|
||||
self.residenceId = residenceId
|
||||
self.residences = residences
|
||||
self.existingTask = existingTask
|
||||
@@ -72,7 +72,7 @@ struct TaskFormView: View {
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
_dueDate = State(initialValue: formatter.date(from: task.dueDate ?? "") ?? Date())
|
||||
|
||||
_intervalDays = State(initialValue: task.intervalDays != nil ? String(task.intervalDays!.intValue) : "")
|
||||
_intervalDays = State(initialValue: "") // No longer in API
|
||||
_estimatedCost = State(initialValue: task.estimatedCost != nil ? String(task.estimatedCost!.doubleValue) : "")
|
||||
} else {
|
||||
_title = State(initialValue: "")
|
||||
@@ -98,9 +98,9 @@ struct TaskFormView: View {
|
||||
if needsResidenceSelection, let residences = residences {
|
||||
Section {
|
||||
Picker("Property", selection: $selectedResidence) {
|
||||
Text("Select Property").tag(nil as Residence?)
|
||||
Text("Select Property").tag(nil as ResidenceResponse?)
|
||||
ForEach(residences, id: \.id) { residence in
|
||||
Text(residence.name).tag(residence as Residence?)
|
||||
Text(residence.name).tag(residence as ResidenceResponse?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,17 +396,17 @@ struct TaskFormView: View {
|
||||
if isEditMode, let task = existingTask {
|
||||
// UPDATE existing task
|
||||
let request = TaskCreateRequest(
|
||||
residence: task.residence,
|
||||
residenceId: task.residenceId,
|
||||
title: title,
|
||||
description: description.isEmpty ? nil : description,
|
||||
category: Int32(category.id),
|
||||
frequency: Int32(frequency.id),
|
||||
intervalDays: intervalDays.isEmpty ? nil : Int32(intervalDays) as? KotlinInt,
|
||||
priority: Int32(priority.id),
|
||||
status: KotlinInt(value: status.id) as? KotlinInt,
|
||||
categoryId: KotlinInt(int: Int32(category.id)),
|
||||
priorityId: KotlinInt(int: Int32(priority.id)),
|
||||
statusId: KotlinInt(int: Int32(status.id)),
|
||||
frequencyId: KotlinInt(int: Int32(frequency.id)),
|
||||
assignedToId: nil,
|
||||
dueDate: dueDateString,
|
||||
estimatedCost: estimatedCost.isEmpty ? nil : KotlinDouble(double: Double(estimatedCost) ?? 0.0),
|
||||
archived: task.archived
|
||||
contractorId: nil
|
||||
)
|
||||
|
||||
viewModel.updateTask(id: task.id, request: request) { success in
|
||||
@@ -427,17 +427,17 @@ struct TaskFormView: View {
|
||||
}
|
||||
|
||||
let request = TaskCreateRequest(
|
||||
residence: actualResidenceId,
|
||||
residenceId: actualResidenceId,
|
||||
title: title,
|
||||
description: description.isEmpty ? nil : description,
|
||||
category: Int32(category.id),
|
||||
frequency: Int32(frequency.id),
|
||||
intervalDays: intervalDays.isEmpty ? nil : Int32(intervalDays) as? KotlinInt,
|
||||
priority: Int32(priority.id),
|
||||
status: selectedStatus.map { KotlinInt(value: $0.id) },
|
||||
categoryId: KotlinInt(int: Int32(category.id)),
|
||||
priorityId: KotlinInt(int: Int32(priority.id)),
|
||||
statusId: selectedStatus.map { KotlinInt(int: Int32($0.id)) },
|
||||
frequencyId: KotlinInt(int: Int32(frequency.id)),
|
||||
assignedToId: nil,
|
||||
dueDate: dueDateString,
|
||||
estimatedCost: estimatedCost.isEmpty ? nil : KotlinDouble(double: Double(estimatedCost) ?? 0.0),
|
||||
archived: false
|
||||
contractorId: nil
|
||||
)
|
||||
|
||||
viewModel.createTask(request: request) { success in
|
||||
|
||||
@@ -45,7 +45,7 @@ class TaskViewModel: ObservableObject {
|
||||
self?.errorMessage = error
|
||||
}
|
||||
},
|
||||
onSuccess: { [weak self] (_: CustomTask) in
|
||||
onSuccess: { [weak self] (_: TaskResponse) in
|
||||
self?.actionState = .success(.create)
|
||||
},
|
||||
completion: completion,
|
||||
|
||||
Reference in New Issue
Block a user