diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/App.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/App.kt index 9018a37..0a716ce 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/App.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/App.kt @@ -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() }, diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/models/CustomTask.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/models/CustomTask.kt index a6ecd9f..d5c7866 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/models/CustomTask.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/models/CustomTask.kt @@ -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 = 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 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 -) - -@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, - @SerialName("in_progress_tasks") val inProgressTasks: List, - @SerialName("done_tasks") val doneTasks: List, - @SerialName("archived_tasks") val archivedTasks: List -) - -@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, - @SerialName("in_progress_tasks") val inProgressTasks: List, - @SerialName("done_tasks") val doneTasks: List, - @SerialName("archived_tasks") val archivedTasks: List -) - -@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, val icons: Map, val color: String, - val tasks: List, // Keep using TaskDetail for now - will be TaskMinimal after full migration + val tasks: List, val count: Int ) +/** + * Kanban board response matching Go API KanbanBoardResponse + */ @Serializable data class TaskColumnsResponse( val columns: List, - @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 diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/models/Lookups.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/models/Lookups.kt index 7578d25..ee40f06 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/models/Lookups.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/models/Lookups.kt @@ -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, + @SerialName("task_frequencies") val taskFrequencies: List, + @SerialName("task_priorities") val taskPriorities: List, + @SerialName("task_statuses") val taskStatuses: List, + @SerialName("task_categories") val taskCategories: List, + @SerialName("contractor_specialties") val contractorSpecialties: List +) + +// 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 ) -@Serializable -data class ResidenceType( - val id: Int, - val name: String, - val description: String? = null -) - @Serializable data class TaskFrequencyResponse( val count: Int, val results: List ) -@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 ) -@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 ) -@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 ) -@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 ) - -@Serializable -data class ContractorSpecialty( - val id: Int, - val name: String -) - -@Serializable -data class StaticDataResponse( - val residenceTypes: List, - val taskFrequencies: List, - val taskPriorities: List, - val taskStatuses: List, - val taskCategories: List, - val contractorSpecialties: List -) diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/models/Residence.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/models/Residence.kt index 4feeb59..2f17f20 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/models/Residence.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/models/Residence.kt @@ -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 = 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 -) - -@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 -) - -@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, - @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 + @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, + 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 = emptyList() +) + +/** + * Residence users response + */ @Serializable data class ResidenceUsersResponse( - @SerialName("owner_id") val ownerId: Int, - val users: List + val owner: ResidenceUserResponse, + val users: List ) +/** + * Remove user response + */ @Serializable data class RemoveUserResponse( val message: String -) \ No newline at end of file +) + +// Type aliases for backwards compatibility with existing code +typealias Residence = ResidenceResponse +typealias ResidenceShareCode = ShareCodeResponse +typealias ResidenceUser = ResidenceUserResponse +typealias TaskSummary = ResidenceTaskSummary +typealias TaskColumnCategory = TaskCategorySummary \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/models/TaskCompletion.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/models/TaskCompletion.kt index 991455a..fab89ac 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/models/TaskCompletion.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/models/TaskCompletion.kt @@ -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? = 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 -) diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/models/User.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/models/User.kt index c627e33..e60da92 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/models/User.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/models/User.kt @@ -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 +) diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/APILayer.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/APILayer.kt index 84e80d7..2202931 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/APILayer.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/APILayer.kt @@ -252,7 +252,7 @@ object APILayer { // ==================== Residence Operations ==================== - suspend fun getResidences(forceRefresh: Boolean = false): ApiResult> { + suspend fun getResidences(forceRefresh: Boolean = false): ApiResult> { // 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 { + suspend fun getResidence(id: Int, forceRefresh: Boolean = false): ApiResult { // 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 { + suspend fun createResidence(request: ResidenceCreateRequest): ApiResult { 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 { + suspend fun updateResidence(id: Int, request: ResidenceCreateRequest): ApiResult { 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 { + suspend fun getShareCode(residenceId: Int): ApiResult { val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401) return residenceApi.getShareCode(token, residenceId) } - suspend fun generateShareCode(residenceId: Int): ApiResult { + suspend fun generateShareCode(residenceId: Int): ApiResult { 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 { + suspend fun createTask(request: TaskCreateRequest): ApiResult { 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 { + suspend fun updateTask(id: Int, request: TaskCreateRequest): ApiResult { 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 { + suspend fun cancelTask(taskId: Int): ApiResult { 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 { + suspend fun uncancelTask(taskId: Int): ApiResult { 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 { + suspend fun markInProgress(taskId: Int): ApiResult { 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 { + suspend fun archiveTask(taskId: Int): ApiResult { 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 { + suspend fun unarchiveTask(taskId: Int): ApiResult { 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 { + suspend fun createTaskCompletion(request: TaskCompletionCreateRequest): ApiResult { 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, imageFileNames: List - ): ApiResult { + ): ApiResult { val token = TokenStorage.getToken() ?: return ApiResult.Error("Not authenticated", 401) val result = taskCompletionApi.createCompletionWithImages(token, request, images, imageFileNames) diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/ContractorApi.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/ContractorApi.kt index f890ed1..08eeada 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/ContractorApi.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/ContractorApi.kt @@ -129,7 +129,7 @@ class ContractorApi(private val client: HttpClient = ApiClient.httpClient) { } } - suspend fun getContractorTasks(token: String, id: Int): ApiResult> { + suspend fun getContractorTasks(token: String, id: Int): ApiResult> { return try { val response = client.get("$baseUrl/contractors/$id/tasks/") { header("Authorization", "Token $token") diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/LookupsApi.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/LookupsApi.kt index 19b362e..fe88052 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/LookupsApi.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/LookupsApi.kt @@ -105,7 +105,7 @@ class LookupsApi(private val client: HttpClient = ApiClient.httpClient) { } } - suspend fun getAllTasks(token: String): ApiResult> { + suspend fun getAllTasks(token: String): ApiResult> { return try { val response = client.get("$baseUrl/tasks/") { header("Authorization", "Token $token") diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/ResidenceApi.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/ResidenceApi.kt index 4747b9e..361ffcf 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/ResidenceApi.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/ResidenceApi.kt @@ -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> { + suspend fun getResidences(token: String): ApiResult> { 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 { + suspend fun getResidence(token: String, id: Int): ApiResult { 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 { + suspend fun createResidence(token: String, request: ResidenceCreateRequest): ApiResult { 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 { + suspend fun updateResidence(token: String, id: Int, request: ResidenceCreateRequest): ApiResult { 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 { + suspend fun generateShareCode(token: String, residenceId: Int): ApiResult { 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 { + suspend fun getShareCode(token: String, residenceId: Int): ApiResult { return try { val response = client.get("$baseUrl/residences/$residenceId/share-code/") { header("Authorization", "Token $token") diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/TaskApi.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/TaskApi.kt index e7bd1a5..459ffb6 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/TaskApi.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/TaskApi.kt @@ -30,7 +30,7 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) { } } - suspend fun getTask(token: String, id: Int): ApiResult { + suspend fun getTask(token: String, id: Int): ApiResult { 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 { + suspend fun createTask(token: String, request: TaskCreateRequest): ApiResult { 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 { + suspend fun updateTask(token: String, id: Int, request: TaskCreateRequest): ApiResult { 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 { + suspend fun patchTask(token: String, id: Int, request: TaskPatchRequest): ApiResult { 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 { + suspend fun cancelTask(token: String, id: Int, cancelledStatusId: Int): ApiResult { return patchTask(token, id, TaskPatchRequest(status = cancelledStatusId)) } - suspend fun uncancelTask(token: String, id: Int, pendingStatusId: Int): ApiResult { + suspend fun uncancelTask(token: String, id: Int, pendingStatusId: Int): ApiResult { return patchTask(token, id, TaskPatchRequest(status = pendingStatusId)) } - suspend fun markInProgress(token: String, id: Int, inProgressStatusId: Int): ApiResult { + suspend fun markInProgress(token: String, id: Int, inProgressStatusId: Int): ApiResult { return patchTask(token, id, TaskPatchRequest(status = inProgressStatusId)) } - suspend fun archiveTask(token: String, id: Int): ApiResult { + suspend fun archiveTask(token: String, id: Int): ApiResult { return patchTask(token, id, TaskPatchRequest(archived = true)) } - suspend fun unarchiveTask(token: String, id: Int): ApiResult { + suspend fun unarchiveTask(token: String, id: Int): ApiResult { return patchTask(token, id, TaskPatchRequest(archived = false)) } } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/TaskCompletionApi.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/TaskCompletionApi.kt index 4a81356..8ccec13 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/TaskCompletionApi.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/TaskCompletionApi.kt @@ -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> { + suspend fun getCompletions(token: String): ApiResult> { 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 { + suspend fun getCompletion(token: String, id: Int): ApiResult { 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 { + suspend fun createCompletion(token: String, request: TaskCompletionCreateRequest): ApiResult { 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 { + suspend fun updateCompletion(token: String, id: Int, request: TaskCompletionCreateRequest): ApiResult { 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 = emptyList(), imageFileNames: List = emptyList() - ): ApiResult { + ): ApiResult { 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 -> diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/AddTaskDialog.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/AddTaskDialog.kt index 094d787..8297962 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/AddTaskDialog.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/AddTaskDialog.kt @@ -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() ) diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/CompleteTaskDialog.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/CompleteTaskDialog.kt index ef91c54..337d432 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/CompleteTaskDialog.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/CompleteTaskDialog.kt @@ -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 ) diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/ManageUsersDialog.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/ManageUsersDialog.kt index 39147e4..638d3a3 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/ManageUsersDialog.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/ManageUsersDialog.kt @@ -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 diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/task/TaskCard.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/task/TaskCard.kt index 4d92b1b..d272bda 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/task/TaskCard.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/task/TaskCard.kt @@ -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 = {}, diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/EditTaskScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/EditTaskScreen.kt index a36b612..4c15edc 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/EditTaskScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/EditTaskScreen.kt @@ -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() ) ) } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/HomeScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/HomeScreen.kt index c90a8c7..b2805b0 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/HomeScreen.kt @@ -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" ) diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidenceDetailScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidenceDetailScreen.kt index 8594b02..cc6d4bb 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidenceDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidenceDetailScreen.kt @@ -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(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 }) { diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidenceFormScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidenceFormScreen.kt index 5f6c332..05805da 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidenceFormScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidenceFormScreen.kt @@ -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 diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/AuthViewModel.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/AuthViewModel.kt index f5068c1..45d668d 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/AuthViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/AuthViewModel.kt @@ -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) { diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/PasswordResetViewModel.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/PasswordResetViewModel.kt index d99fe08..6a85485 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/PasswordResetViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/PasswordResetViewModel.kt @@ -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) { diff --git a/iosApp/iosApp/Components/TaskSummaryCard.swift b/iosApp/iosApp/Components/TaskSummaryCard.swift index 720d959..37ee0ee 100644 --- a/iosApp/iosApp/Components/TaskSummaryCard.swift +++ b/iosApp/iosApp/Components/TaskSummaryCard.swift @@ -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 diff --git a/iosApp/iosApp/EditResidenceView.swift b/iosApp/iosApp/EditResidenceView.swift index c0b6755..275c968 100644 --- a/iosApp/iosApp/EditResidenceView.swift +++ b/iosApp/iosApp/EditResidenceView.swift @@ -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 { diff --git a/iosApp/iosApp/Extensions/TaskDetailExtensions.swift b/iosApp/iosApp/Extensions/TaskDetailExtensions.swift index 34fbc95..ba629e1 100644 --- a/iosApp/iosApp/Extensions/TaskDetailExtensions.swift +++ b/iosApp/iosApp/Extensions/TaskDetailExtensions.swift @@ -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 } diff --git a/iosApp/iosApp/Helpers/WidgetDataManager.swift b/iosApp/iosApp/Helpers/WidgetDataManager.swift index 6c97dff..3281473 100644 --- a/iosApp/iosApp/Helpers/WidgetDataManager.swift +++ b/iosApp/iosApp/Helpers/WidgetDataManager.swift @@ -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) diff --git a/iosApp/iosApp/Residence/ManageUsersView.swift b/iosApp/iosApp/Residence/ManageUsersView.swift index d245653..b6c8297 100644 --- a/iosApp/iosApp/Residence/ManageUsersView.swift +++ b/iosApp/iosApp/Residence/ManageUsersView.swift @@ -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, 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 { + if let successResult = result as? ApiResultSuccess { 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 { + if let successResult = result as? ApiResultSuccess { self.shareCode = successResult.data self.isGeneratingCode = false } else if let errorResult = result as? ApiResultError { diff --git a/iosApp/iosApp/Residence/ResidenceDetailView.swift b/iosApp/iosApp/Residence/ResidenceDetailView.swift index cd2c07e..592d264 100644 --- a/iosApp/iosApp/Residence/ResidenceDetailView.swift +++ b/iosApp/iosApp/Residence/ResidenceDetailView.swift @@ -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 diff --git a/iosApp/iosApp/Residence/ResidenceViewModel.swift b/iosApp/iosApp/Residence/ResidenceViewModel.swift index a497bc5..2d09ba9 100644 --- a/iosApp/iosApp/Residence/ResidenceViewModel.swift +++ b/iosApp/iosApp/Residence/ResidenceViewModel.swift @@ -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 { + if let success = result as? ApiResultSuccess { 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, diff --git a/iosApp/iosApp/Residence/ResidencesListView.swift b/iosApp/iosApp/Residence/ResidencesListView.swift index 2af9bc4..75d98d4 100644 --- a/iosApp/iosApp/Residence/ResidencesListView.swift +++ b/iosApp/iosApp/Residence/ResidencesListView.swift @@ -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) { diff --git a/iosApp/iosApp/ResidenceFormView.swift b/iosApp/iosApp/ResidenceFormView.swift index 0f98886..000838c 100644 --- a/iosApp/iosApp/ResidenceFormView.swift +++ b/iosApp/iosApp/ResidenceFormView.swift @@ -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 { diff --git a/iosApp/iosApp/Subviews/Common/OverviewCard.swift b/iosApp/iosApp/Subviews/Common/OverviewCard.swift index d46ad91..4d11f06 100644 --- a/iosApp/iosApp/Subviews/Common/OverviewCard.swift +++ b/iosApp/iosApp/Subviews/Common/OverviewCard.swift @@ -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) { diff --git a/iosApp/iosApp/Subviews/Residence/PropertyHeaderCard.swift b/iosApp/iosApp/Subviews/Residence/PropertyHeaderCard.swift index 810057d..8f53c4f 100644 --- a/iosApp/iosApp/Subviews/Residence/PropertyHeaderCard.swift +++ b/iosApp/iosApp/Subviews/Residence/PropertyHeaderCard.swift @@ -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) } diff --git a/iosApp/iosApp/Subviews/Residence/ResidenceCard.swift b/iosApp/iosApp/Subviews/Residence/ResidenceCard.swift index b7f33d8..c049ae3 100644 --- a/iosApp/iosApp/Subviews/Residence/ResidenceCard.swift +++ b/iosApp/iosApp/Subviews/Residence/ResidenceCard.swift @@ -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" )) diff --git a/iosApp/iosApp/Subviews/Residence/ShareCodeCard.swift b/iosApp/iosApp/Subviews/Residence/ShareCodeCard.swift index 9b51ac3..6d394d1 100644 --- a/iosApp/iosApp/Subviews/Residence/ShareCodeCard.swift +++ b/iosApp/iosApp/Subviews/Residence/ShareCodeCard.swift @@ -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 diff --git a/iosApp/iosApp/Subviews/Residence/SummaryCard.swift b/iosApp/iosApp/Subviews/Residence/SummaryCard.swift index 7208eb8..3820766 100644 --- a/iosApp/iosApp/Subviews/Residence/SummaryCard.swift +++ b/iosApp/iosApp/Subviews/Residence/SummaryCard.swift @@ -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 )) diff --git a/iosApp/iosApp/Subviews/Residence/UserListItem.swift b/iosApp/iosApp/Subviews/Residence/UserListItem.swift index 2f4f78a..5a415d8 100644 --- a/iosApp/iosApp/Subviews/Residence/UserListItem.swift +++ b/iosApp/iosApp/Subviews/Residence/UserListItem.swift @@ -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 diff --git a/iosApp/iosApp/Subviews/Task/CompletionCardView.swift b/iosApp/iosApp/Subviews/Task/CompletionCardView.swift index 71f5218..b37d93f 100644 --- a/iosApp/iosApp/Subviews/Task/CompletionCardView.swift +++ b/iosApp/iosApp/Subviews/Task/CompletionCardView.swift @@ -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) } } diff --git a/iosApp/iosApp/Subviews/Task/DynamicTaskCard.swift b/iosApp/iosApp/Subviews/Task/DynamicTaskCard.swift index 0061456..9d6c71a 100644 --- a/iosApp/iosApp/Subviews/Task/DynamicTaskCard.swift +++ b/iosApp/iosApp/Subviews/Task/DynamicTaskCard.swift @@ -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) diff --git a/iosApp/iosApp/Subviews/Task/DynamicTaskColumnView.swift b/iosApp/iosApp/Subviews/Task/DynamicTaskColumnView.swift index b981902..dbdb898 100644 --- a/iosApp/iosApp/Subviews/Task/DynamicTaskColumnView.swift +++ b/iosApp/iosApp/Subviews/Task/DynamicTaskColumnView.swift @@ -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 diff --git a/iosApp/iosApp/Subviews/Task/TaskCard.swift b/iosApp/iosApp/Subviews/Task/TaskCard.swift index fc3f05f..4de291a 100644 --- a/iosApp/iosApp/Subviews/Task/TaskCard.swift +++ b/iosApp/iosApp/Subviews/Task/TaskCard.swift @@ -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: {}, diff --git a/iosApp/iosApp/Subviews/Task/TasksSection.swift b/iosApp/iosApp/Subviews/Task/TasksSection.swift index 5185cc1..86e0395 100644 --- a/iosApp/iosApp/Subviews/Task/TasksSection.swift +++ b/iosApp/iosApp/Subviews/Task/TasksSection.swift @@ -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 diff --git a/iosApp/iosApp/Task/AddTaskWithResidenceView.swift b/iosApp/iosApp/Task/AddTaskWithResidenceView.swift index 8258eed..5f25520 100644 --- a/iosApp/iosApp/Task/AddTaskWithResidenceView.swift +++ b/iosApp/iosApp/Task/AddTaskWithResidenceView.swift @@ -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) diff --git a/iosApp/iosApp/Task/AllTasksView.swift b/iosApp/iosApp/Task/AllTasksView.swift index 39dc33c..e08164b 100644 --- a/iosApp/iosApp/Task/AllTasksView.swift +++ b/iosApp/iosApp/Task/AllTasksView.swift @@ -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 } } diff --git a/iosApp/iosApp/Task/CompleteTaskView.swift b/iosApp/iosApp/Task/CompleteTaskView.swift index 350ab2f..faa3d51 100644 --- a/iosApp/iosApp/Task/CompleteTaskView.swift +++ b/iosApp/iosApp/Task/CompleteTaskView.swift @@ -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 - - // 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.. { + switch state { + case is ApiResultSuccess: 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 || state is ApiResultError { + break } } } diff --git a/iosApp/iosApp/Task/EditTaskView.swift b/iosApp/iosApp/Task/EditTaskView.swift index 7a0e23a..ab1e736 100644 --- a/iosApp/iosApp/Task/EditTaskView.swift +++ b/iosApp/iosApp/Task/EditTaskView.swift @@ -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 { diff --git a/iosApp/iosApp/Task/TaskFormView.swift b/iosApp/iosApp/Task/TaskFormView.swift index 113e6be..0cd4c80 100644 --- a/iosApp/iosApp/Task/TaskFormView.swift +++ b/iosApp/iosApp/Task/TaskFormView.swift @@ -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) { + init(residenceId: Int32? = nil, residences: [ResidenceResponse]? = nil, existingTask: TaskResponse? = nil, isPresented: Binding) { 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 diff --git a/iosApp/iosApp/Task/TaskViewModel.swift b/iosApp/iosApp/Task/TaskViewModel.swift index d013641..92698ee 100644 --- a/iosApp/iosApp/Task/TaskViewModel.swift +++ b/iosApp/iosApp/Task/TaskViewModel.swift @@ -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,