diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index c1e5f3d..6b53bcd 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -179,6 +179,7 @@ Unarchive Task marked as completed Task marked as in progress + In Progress Task cancelled diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/App.kt b/composeApp/src/commonMain/kotlin/com/example/casera/App.kt index f11b519..0b2aec2 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/App.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/App.kt @@ -52,7 +52,6 @@ import com.example.casera.models.TaskCategory import com.example.casera.models.TaskDetail import com.example.casera.models.TaskFrequency import com.example.casera.models.TaskPriority -import com.example.casera.models.TaskStatus import com.example.casera.network.ApiResult import com.example.casera.network.AuthApi import com.example.casera.data.DataManager @@ -413,8 +412,7 @@ fun App( frequencyName = task.frequency?.name ?: "", priorityId = task.priority?.id ?: 0, priorityName = task.priority?.name ?: "", - statusId = task.status?.id, - statusName = task.status?.name, + inProgress = task.inProgress, dueDate = task.dueDate, estimatedCost = task.estimatedCost?.toString(), createdAt = task.createdAt, @@ -576,8 +574,7 @@ fun App( frequencyName = task.frequency?.name ?: "", priorityId = task.priority?.id ?: 0, priorityName = task.priority?.name ?: "", - statusId = task.status?.id, - statusName = task.status?.name, + inProgress = task.inProgress, dueDate = task.dueDate, estimatedCost = task.estimatedCost?.toString(), createdAt = task.createdAt, @@ -607,9 +604,7 @@ fun App( days = null ), priority = TaskPriority(id = route.priorityId, name = route.priorityName), - status = route.statusId?.let { - TaskStatus(id = it, name = route.statusName ?: "") - }, + inProgress = route.inProgress, dueDate = route.dueDate, estimatedCost = route.estimatedCost?.toDoubleOrNull(), createdAt = route.createdAt, diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/data/DataManager.kt b/composeApp/src/commonMain/kotlin/com/example/casera/data/DataManager.kt index a3844c3..9347569 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/data/DataManager.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/data/DataManager.kt @@ -158,9 +158,6 @@ object DataManager { private val _taskPriorities = MutableStateFlow>(emptyList()) val taskPriorities: StateFlow> = _taskPriorities.asStateFlow() - private val _taskStatuses = MutableStateFlow>(emptyList()) - val taskStatuses: StateFlow> = _taskStatuses.asStateFlow() - private val _taskCategories = MutableStateFlow>(emptyList()) val taskCategories: StateFlow> = _taskCategories.asStateFlow() @@ -185,9 +182,6 @@ object DataManager { private val _taskPrioritiesMap = MutableStateFlow>(emptyMap()) val taskPrioritiesMap: StateFlow> = _taskPrioritiesMap.asStateFlow() - private val _taskStatusesMap = MutableStateFlow>(emptyMap()) - val taskStatusesMap: StateFlow> = _taskStatusesMap.asStateFlow() - private val _taskCategoriesMap = MutableStateFlow>(emptyMap()) val taskCategoriesMap: StateFlow> = _taskCategoriesMap.asStateFlow() @@ -247,7 +241,6 @@ object DataManager { fun getResidenceType(id: Int?): ResidenceType? = id?.let { _residenceTypesMap.value[it] } fun getTaskFrequency(id: Int?): TaskFrequency? = id?.let { _taskFrequenciesMap.value[it] } fun getTaskPriority(id: Int?): TaskPriority? = id?.let { _taskPrioritiesMap.value[it] } - fun getTaskStatus(id: Int?): TaskStatus? = id?.let { _taskStatusesMap.value[it] } fun getTaskCategory(id: Int?): TaskCategory? = id?.let { _taskCategoriesMap.value[it] } fun getContractorSpecialty(id: Int?): ContractorSpecialty? = id?.let { _contractorSpecialtiesMap.value[it] } @@ -533,12 +526,6 @@ object DataManager { persistToDisk() } - fun setTaskStatuses(statuses: List) { - _taskStatuses.value = statuses - _taskStatusesMap.value = statuses.associateBy { it.id } - persistToDisk() - } - fun setTaskCategories(categories: List) { _taskCategories.value = categories _taskCategoriesMap.value = categories.associateBy { it.id } @@ -583,7 +570,6 @@ object DataManager { setResidenceTypes(staticData.residenceTypes) setTaskFrequencies(staticData.taskFrequencies) setTaskPriorities(staticData.taskPriorities) - setTaskStatuses(staticData.taskStatuses) setTaskCategories(staticData.taskCategories) setContractorSpecialties(staticData.contractorSpecialties) _lookupsInitialized.value = true @@ -598,7 +584,6 @@ object DataManager { setResidenceTypes(seededData.residenceTypes) setTaskFrequencies(seededData.taskFrequencies) setTaskPriorities(seededData.taskPriorities) - setTaskStatuses(seededData.taskStatuses) setTaskCategories(seededData.taskCategories) setContractorSpecialties(seededData.contractorSpecialties) setTaskTemplatesGrouped(seededData.taskTemplates) @@ -659,8 +644,6 @@ object DataManager { _taskFrequenciesMap.value = emptyMap() _taskPriorities.value = emptyList() _taskPrioritiesMap.value = emptyMap() - _taskStatuses.value = emptyList() - _taskStatusesMap.value = emptyMap() _taskCategories.value = emptyList() _taskCategoriesMap.value = emptyMap() _contractorSpecialties.value = emptyList() @@ -756,9 +739,6 @@ object DataManager { if (_taskPriorities.value.isNotEmpty()) { manager.save(KEY_TASK_PRIORITIES, json.encodeToString(_taskPriorities.value)) } - if (_taskStatuses.value.isNotEmpty()) { - manager.save(KEY_TASK_STATUSES, json.encodeToString(_taskStatuses.value)) - } if (_taskCategories.value.isNotEmpty()) { manager.save(KEY_TASK_CATEGORIES, json.encodeToString(_taskCategories.value)) } @@ -832,12 +812,6 @@ object DataManager { _taskPrioritiesMap.value = priorities.associateBy { it.id } } - manager.load(KEY_TASK_STATUSES)?.let { data -> - val statuses = json.decodeFromString>(data) - _taskStatuses.value = statuses - _taskStatusesMap.value = statuses.associateBy { it.id } - } - manager.load(KEY_TASK_CATEGORIES)?.let { data -> val categories = json.decodeFromString>(data) _taskCategories.value = categories @@ -874,7 +848,6 @@ object DataManager { private const val KEY_RESIDENCE_TYPES = "dm_residence_types" private const val KEY_TASK_FREQUENCIES = "dm_task_frequencies" private const val KEY_TASK_PRIORITIES = "dm_task_priorities" - private const val KEY_TASK_STATUSES = "dm_task_statuses" private const val KEY_TASK_CATEGORIES = "dm_task_categories" private const val KEY_CONTRACTOR_SPECIALTIES = "dm_contractor_specialties" private const val KEY_TASK_TEMPLATES_GROUPED = "dm_task_templates_grouped" diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/models/CustomTask.kt b/composeApp/src/commonMain/kotlin/com/example/casera/models/CustomTask.kt index 5f0f2d4..d38d91d 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/models/CustomTask.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/models/CustomTask.kt @@ -40,8 +40,7 @@ data class TaskResponse( 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("in_progress") val inProgress: Boolean = false, @SerialName("frequency_id") val frequencyId: Int? = null, val frequency: TaskFrequency? = null, @SerialName("due_date") val dueDate: String? = null, @@ -74,11 +73,10 @@ data class TaskResponse( 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 showCompletedButton: Boolean get() = true // Always show complete button since status is now just in_progress boolean val daySpan: Int? get() = frequency?.days val notifyDays: Int? get() = null // Not in Go API } @@ -119,7 +117,7 @@ data class TaskCreateRequest( 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("in_progress") val inProgress: Boolean = false, @SerialName("frequency_id") val frequencyId: Int? = null, @SerialName("assigned_to_id") val assignedToId: Int? = null, @SerialName("due_date") val dueDate: String? = null, @@ -137,7 +135,7 @@ data class TaskUpdateRequest( 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("in_progress") val inProgress: Boolean? = null, @SerialName("frequency_id") val frequencyId: Int? = null, @SerialName("assigned_to_id") val assignedToId: Int? = null, @SerialName("due_date") val dueDate: String? = null, @@ -176,7 +174,8 @@ data class TaskColumn( data class TaskColumnsResponse( val columns: List, @SerialName("days_threshold") val daysThreshold: Int, - @SerialName("residence_id") val residenceId: String + @SerialName("residence_id") val residenceId: String, + val summary: TotalSummary? = null ) /** @@ -184,7 +183,7 @@ data class TaskColumnsResponse( */ @Serializable data class TaskPatchRequest( - @SerialName("status_id") val status: Int? = null, + @SerialName("in_progress") val inProgress: Boolean? = null, @SerialName("is_archived") val archived: Boolean? = null, @SerialName("is_cancelled") val cancelled: Boolean? = null ) diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/models/Lookups.kt b/composeApp/src/commonMain/kotlin/com/example/casera/models/Lookups.kt index f1416c8..4d5d4f7 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/models/Lookups.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/models/Lookups.kt @@ -44,22 +44,6 @@ data class TaskPriority( 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 */ @@ -104,7 +88,6 @@ 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 ) @@ -119,7 +102,6 @@ data class SeededDataResponse( @SerialName("task_categories") val taskCategories: List, @SerialName("task_priorities") val taskPriorities: List, @SerialName("task_frequencies") val taskFrequencies: List, - @SerialName("task_statuses") val taskStatuses: List, @SerialName("contractor_specialties") val contractorSpecialties: List, @SerialName("task_templates") val taskTemplates: TaskTemplatesGroupedResponse ) @@ -145,12 +127,6 @@ data class TaskPriorityResponse( val results: List ) -@Serializable -data class TaskStatusResponse( - val count: Int, - val results: List -) - @Serializable data class TaskCategoryResponse( val count: Int, diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/navigation/Routes.kt b/composeApp/src/commonMain/kotlin/com/example/casera/navigation/Routes.kt index 209750b..f4ea992 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/navigation/Routes.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/navigation/Routes.kt @@ -60,8 +60,7 @@ data class EditTaskRoute( val frequencyName: String, val priorityId: Int, val priorityName: String, - val statusId: Int?, - val statusName: String?, + val inProgress: Boolean, val dueDate: String?, val estimatedCost: String?, val createdAt: String, diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/network/APILayer.kt b/composeApp/src/commonMain/kotlin/com/example/casera/network/APILayer.kt index 46cc63e..cb7e0d1 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/network/APILayer.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/network/APILayer.kt @@ -271,27 +271,6 @@ object APILayer { return result } - /** - * Get task statuses from DataManager. If cache is empty, fetch from API. - */ - suspend fun getTaskStatuses(forceRefresh: Boolean = false): ApiResult> { - if (!forceRefresh) { - val cached = DataManager.taskStatuses.value - if (cached.isNotEmpty()) { - return ApiResult.Success(cached) - } - } - - val token = getToken() ?: return ApiResult.Error("Not authenticated", 401) - val result = lookupsApi.getTaskStatuses(token) - - if (result is ApiResult.Success) { - DataManager.setTaskStatuses(result.data) - } - - return result - } - /** * Get task categories from DataManager. If cache is empty, fetch from API. */ @@ -594,22 +573,9 @@ object APILayer { } } - /** - * Get status ID by name from DataManager. - */ - private fun getStatusIdByName(name: String): Int? { - return DataManager.taskStatuses.value.find { - it.name.equals(name, ignoreCase = true) - }?.id - } - suspend fun cancelTask(taskId: Int): ApiResult { val token = getToken() ?: return ApiResult.Error("Not authenticated", 401) - - val cancelledStatusId = getStatusIdByName("cancelled") - ?: return ApiResult.Error("Cancelled status not found in cache") - - val result = taskApi.cancelTask(token, taskId, cancelledStatusId) + val result = taskApi.cancelTask(token, taskId) if (result is ApiResult.Success) { DataManager.setTotalSummary(result.data.summary) @@ -625,11 +591,7 @@ object APILayer { suspend fun uncancelTask(taskId: Int): ApiResult { val token = getToken() ?: return ApiResult.Error("Not authenticated", 401) - - val pendingStatusId = getStatusIdByName("pending") - ?: return ApiResult.Error("Pending status not found in cache") - - val result = taskApi.uncancelTask(token, taskId, pendingStatusId) + val result = taskApi.uncancelTask(token, taskId) if (result is ApiResult.Success) { DataManager.setTotalSummary(result.data.summary) @@ -645,12 +607,23 @@ object APILayer { suspend fun markInProgress(taskId: Int): ApiResult { val token = getToken() ?: return ApiResult.Error("Not authenticated", 401) + val result = taskApi.markInProgress(token, taskId) - val inProgressStatusId = getStatusIdByName("in progress") - ?: getStatusIdByName("In Progress") - ?: return ApiResult.Error("In Progress status not found in cache") + if (result is ApiResult.Success) { + DataManager.setTotalSummary(result.data.summary) + DataManager.updateTask(result.data.data) + return ApiResult.Success(result.data.data) + } - val result = taskApi.markInProgress(token, taskId, inProgressStatusId) + return when (result) { + is ApiResult.Error -> result + else -> ApiResult.Error("Unknown error") + } + } + + suspend fun clearInProgress(taskId: Int): ApiResult { + val token = getToken() ?: return ApiResult.Error("Not authenticated", 401) + val result = taskApi.clearInProgress(token, taskId) if (result is ApiResult.Success) { DataManager.setTotalSummary(result.data.summary) diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/network/LookupsApi.kt b/composeApp/src/commonMain/kotlin/com/example/casera/network/LookupsApi.kt index 6a18d3b..868bd16 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/network/LookupsApi.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/network/LookupsApi.kt @@ -81,22 +81,6 @@ class LookupsApi(private val client: HttpClient = ApiClient.httpClient) { } } - suspend fun getTaskStatuses(token: String): ApiResult> { - return try { - val response = client.get("$baseUrl/task-statuses/") { - header("Authorization", "Token $token") - } - - if (response.status.isSuccess()) { - ApiResult.Success(response.body()) - } else { - ApiResult.Error("Failed to fetch task statuses", response.status.value) - } - } catch (e: Exception) { - ApiResult.Error(e.message ?: "Unknown error occurred") - } - } - suspend fun getTaskCategories(token: String): ApiResult> { return try { val response = client.get("$baseUrl/task-categories/") { diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/network/TaskApi.kt b/composeApp/src/commonMain/kotlin/com/example/casera/network/TaskApi.kt index 94b594d..92e9832 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/network/TaskApi.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/network/TaskApi.kt @@ -149,26 +149,57 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) { } // Convenience methods for common task actions - // These use PATCH internally to update task status/archived state + // These use dedicated POST endpoints for state changes - suspend fun cancelTask(token: String, id: Int, cancelledStatusId: Int): ApiResult> { - return patchTask(token, id, TaskPatchRequest(status = cancelledStatusId)) + suspend fun cancelTask(token: String, id: Int): ApiResult> { + return postTaskAction(token, id, "cancel") } - suspend fun uncancelTask(token: String, id: Int, pendingStatusId: Int): ApiResult> { - return patchTask(token, id, TaskPatchRequest(status = pendingStatusId)) + suspend fun uncancelTask(token: String, id: Int): ApiResult> { + return postTaskAction(token, id, "uncancel") } - suspend fun markInProgress(token: String, id: Int, inProgressStatusId: Int): ApiResult> { - return patchTask(token, id, TaskPatchRequest(status = inProgressStatusId)) + suspend fun markInProgress(token: String, id: Int): ApiResult> { + return patchTask(token, id, TaskPatchRequest(inProgress = true)) + } + + suspend fun clearInProgress(token: String, id: Int): ApiResult> { + return patchTask(token, id, TaskPatchRequest(inProgress = false)) } suspend fun archiveTask(token: String, id: Int): ApiResult> { - return patchTask(token, id, TaskPatchRequest(archived = true)) + return postTaskAction(token, id, "archive") } suspend fun unarchiveTask(token: String, id: Int): ApiResult> { - return patchTask(token, id, TaskPatchRequest(archived = false)) + return postTaskAction(token, id, "unarchive") + } + + /** + * Helper for POST task action endpoints (cancel, uncancel, archive, unarchive) + */ + private suspend fun postTaskAction(token: String, id: Int, action: String): ApiResult> { + return try { + val response = client.post("$baseUrl/tasks/$id/$action/") { + header("Authorization", "Token $token") + contentType(ContentType.Application.Json) + } + when (response.status) { + HttpStatusCode.OK -> { + val data = response.body>() + ApiResult.Success(data) + } + HttpStatusCode.NotFound -> ApiResult.Error("Task not found", 404) + HttpStatusCode.Forbidden -> ApiResult.Error("Access denied", 403) + HttpStatusCode.BadRequest -> { + val errorBody = response.body() + ApiResult.Error(errorBody, 400) + } + else -> ApiResult.Error("Task $action failed: ${response.status}", response.status.value) + } + } catch (e: Exception) { + ApiResult.Error(e.message ?: "Unknown error occurred") + } } /** diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/repository/LookupsRepository.kt b/composeApp/src/commonMain/kotlin/com/example/casera/repository/LookupsRepository.kt index 31d5e80..65cfb10 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/repository/LookupsRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/repository/LookupsRepository.kt @@ -22,7 +22,6 @@ object LookupsRepository { val residenceTypes: StateFlow> = DataManager.residenceTypes val taskFrequencies: StateFlow> = DataManager.taskFrequencies val taskPriorities: StateFlow> = DataManager.taskPriorities - val taskStatuses: StateFlow> = DataManager.taskStatuses val taskCategories: StateFlow> = DataManager.taskCategories val contractorSpecialties: StateFlow> = DataManager.contractorSpecialties val isInitialized: StateFlow = DataManager.lookupsInitialized diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/components/AddTaskDialog.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/components/AddTaskDialog.kt index b2a293c..7e2bf8a 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/components/AddTaskDialog.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/components/AddTaskDialog.kt @@ -449,7 +449,7 @@ fun AddTaskDialog( 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, + inProgress = false, dueDate = dueDate, estimatedCost = estimatedCost.ifBlank { null }?.toDoubleOrNull() ) diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/components/task/TaskCard.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/components/task/TaskCard.kt index 9263bed..3c1a75f 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/components/task/TaskCard.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/components/task/TaskCard.kt @@ -18,7 +18,6 @@ import com.example.casera.models.TaskDetail import com.example.casera.models.TaskCategory import com.example.casera.models.TaskPriority import com.example.casera.models.TaskFrequency -import com.example.casera.models.TaskStatus import com.example.casera.models.TaskCompletion import com.example.casera.util.DateUtils import org.jetbrains.compose.ui.tooling.preview.Preview @@ -108,21 +107,15 @@ fun TaskCard( ) } - // Status badge with semantic colors - if (task.status != null) { - val statusColor = when (task.status.name.lowercase()) { - "completed" -> MaterialTheme.colorScheme.secondary - "in_progress" -> MaterialTheme.colorScheme.tertiary - "pending" -> MaterialTheme.colorScheme.tertiary - "cancelled" -> MaterialTheme.colorScheme.onSurfaceVariant - else -> MaterialTheme.colorScheme.onSurfaceVariant - } + // In Progress badge + if (task.inProgress) { + val statusColor = MaterialTheme.colorScheme.tertiary Surface( color = statusColor.copy(alpha = 0.15f), shape = RoundedCornerShape(12.dp) ) { Text( - text = task.status.name.replace("_", " ").uppercase(), + text = "IN PROGRESS", modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), style = MaterialTheme.typography.labelSmall, color = statusColor @@ -604,7 +597,7 @@ fun TaskCardPreview() { frequency = TaskFrequency( id = 1, name = "monthly", days = 30 ), - status = TaskStatus(id = 1, name = "pending"), + inProgress = false, dueDate = "2024-12-15", estimatedCost = 150.00, createdAt = "2024-01-01T00:00:00Z", diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/EditTaskScreen.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/EditTaskScreen.kt index 3bc2338..13fc7a3 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/EditTaskScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/EditTaskScreen.kt @@ -33,20 +33,18 @@ fun EditTaskScreen( var selectedCategory by remember { mutableStateOf(task.category) } var selectedFrequency by remember { mutableStateOf(task.frequency) } var selectedPriority by remember { mutableStateOf(task.priority) } - var selectedStatus by remember { mutableStateOf(task.status) } + var inProgress by remember { mutableStateOf(task.inProgress) } var dueDate by remember { mutableStateOf(task.dueDate ?: "") } var estimatedCost by remember { mutableStateOf(task.estimatedCost?.toString() ?: "") } var categoryExpanded by remember { mutableStateOf(false) } var frequencyExpanded by remember { mutableStateOf(false) } var priorityExpanded by remember { mutableStateOf(false) } - var statusExpanded by remember { mutableStateOf(false) } val updateTaskState by viewModel.updateTaskState.collectAsState() val categories by LookupsRepository.taskCategories.collectAsState() val frequencies by LookupsRepository.taskFrequencies.collectAsState() val priorities by LookupsRepository.taskPriorities.collectAsState() - val statuses by LookupsRepository.taskStatuses.collectAsState() // Validation errors var titleError by remember { mutableStateOf("") } @@ -235,36 +233,20 @@ fun EditTaskScreen( } } - // Status dropdown - ExposedDropdownMenuBox( - expanded = statusExpanded, - onExpandedChange = { statusExpanded = it } + // In Progress toggle + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = androidx.compose.ui.Alignment.CenterVertically ) { - OutlinedTextField( - value = selectedStatus?.name?.replaceFirstChar { it.uppercase() } ?: "", - onValueChange = {}, - readOnly = true, - label = { Text(stringResource(Res.string.tasks_status_label)) }, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = statusExpanded) }, - modifier = Modifier - .fillMaxWidth() - .menuAnchor(), - enabled = statuses.isNotEmpty() + Text( + text = stringResource(Res.string.tasks_in_progress_label), + style = MaterialTheme.typography.bodyLarge + ) + Switch( + checked = inProgress, + onCheckedChange = { inProgress = it } ) - ExposedDropdownMenu( - expanded = statusExpanded, - onDismissRequest = { statusExpanded = false } - ) { - statuses.forEach { status -> - DropdownMenuItem( - text = { Text(status.name.replaceFirstChar { it.uppercase() }) }, - onClick = { - selectedStatus = status - statusExpanded = false - } - ) - } - } } OutlinedTextField( @@ -301,8 +283,7 @@ fun EditTaskScreen( Button( onClick = { if (validateForm() && selectedCategory != null && - selectedFrequency != null && selectedPriority != null && - selectedStatus != null) { + selectedFrequency != null && selectedPriority != null) { viewModel.updateTask( taskId = task.id, request = TaskCreateRequest( @@ -312,7 +293,7 @@ fun EditTaskScreen( categoryId = selectedCategory!!.id, frequencyId = selectedFrequency!!.id, priorityId = selectedPriority!!.id, - statusId = selectedStatus!!.id, + inProgress = inProgress, dueDate = dueDate, estimatedCost = estimatedCost.ifBlank { null }?.toDoubleOrNull() ) @@ -321,8 +302,7 @@ fun EditTaskScreen( }, modifier = Modifier.fillMaxWidth(), enabled = validateForm() && selectedCategory != null && - selectedFrequency != null && selectedPriority != null && - selectedStatus != null + selectedFrequency != null && selectedPriority != null ) { if (updateTaskState is ApiResult.Loading) { CircularProgressIndicator( diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingFirstTaskContent.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingFirstTaskContent.kt index 98367af..997cb60 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingFirstTaskContent.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/onboarding/OnboardingFirstTaskContent.kt @@ -346,7 +346,7 @@ fun OnboardingFirstTaskContent( description = null, categoryId = categoryId, priorityId = null, - statusId = null, + inProgress = false, frequencyId = frequencyId, assignedToId = null, dueDate = today, diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/viewmodel/LookupsViewModel.kt b/composeApp/src/commonMain/kotlin/com/example/casera/viewmodel/LookupsViewModel.kt index 29ab72c..3b05a85 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/viewmodel/LookupsViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/viewmodel/LookupsViewModel.kt @@ -21,7 +21,6 @@ class LookupsViewModel : ViewModel() { val residenceTypes: StateFlow> = DataManager.residenceTypes val taskFrequencies: StateFlow> = DataManager.taskFrequencies val taskPriorities: StateFlow> = DataManager.taskPriorities - val taskStatuses: StateFlow> = DataManager.taskStatuses val taskCategories: StateFlow> = DataManager.taskCategories val contractorSpecialties: StateFlow> = DataManager.contractorSpecialties @@ -35,9 +34,6 @@ class LookupsViewModel : ViewModel() { private val _taskPrioritiesState = MutableStateFlow>>(ApiResult.Idle) val taskPrioritiesState: StateFlow>> = _taskPrioritiesState - private val _taskStatusesState = MutableStateFlow>>(ApiResult.Idle) - val taskStatusesState: StateFlow>> = _taskStatusesState - private val _taskCategoriesState = MutableStateFlow>>(ApiResult.Idle) val taskCategoriesState: StateFlow>> = _taskCategoriesState @@ -80,19 +76,6 @@ class LookupsViewModel : ViewModel() { } } - fun loadTaskStatuses() { - viewModelScope.launch { - val cached = DataManager.taskStatuses.value - if (cached.isNotEmpty()) { - _taskStatusesState.value = ApiResult.Success(cached) - return@launch - } - _taskStatusesState.value = ApiResult.Loading - val result = APILayer.getTaskStatuses() - _taskStatusesState.value = result - } - } - fun loadTaskCategories() { viewModelScope.launch { val cached = DataManager.taskCategories.value @@ -111,7 +94,6 @@ class LookupsViewModel : ViewModel() { loadResidenceTypes() loadTaskFrequencies() loadTaskPriorities() - loadTaskStatuses() loadTaskCategories() } } diff --git a/iosApp/Casera/MyCrib.swift b/iosApp/Casera/MyCrib.swift index d3fc87b..58f56b8 100644 --- a/iosApp/Casera/MyCrib.swift +++ b/iosApp/Casera/MyCrib.swift @@ -64,14 +64,15 @@ class CacheManager { let title: String let description: String? let priority: String? - let status: String? + let inProgress: Bool let dueDate: String? let category: String? let residenceName: String? let isOverdue: Bool enum CodingKeys: String, CodingKey { - case id, title, description, priority, status, category + case id, title, description, priority, category + case inProgress = "in_progress" case dueDate = "due_date" case residenceName = "residence_name" case isOverdue = "is_overdue" @@ -126,12 +127,11 @@ class CacheManager { static func getUpcomingTasks() -> [CustomTask] { let allTasks = getData() - // Filter for pending/in-progress tasks, sorted by due date + // Filter for actionable tasks (not completed, including in-progress and overdue) // Also exclude tasks that are pending completion via widget let upcoming = allTasks.filter { task in - let status = task.status?.lowercased() ?? "" - let isActive = status == "pending" || status == "in_progress" || status == "in progress" - return isActive && task.shouldShow + // Include if: not pending completion + return task.shouldShow } // Sort by due date (earliest first), with overdue at top @@ -444,6 +444,10 @@ struct InteractiveTaskRowView: View { } private var priorityColor: Color { + // Overdue tasks are always red + if task.isOverdue { + return .red + } switch task.priority?.lowercased() { case "urgent": return .red case "high": return .orange @@ -565,6 +569,10 @@ struct LargeInteractiveTaskRowView: View { } private var priorityColor: Color { + // Overdue tasks are always red + if task.isOverdue { + return .red + } switch task.priority?.lowercased() { case "urgent": return .red case "high": return .orange @@ -601,7 +609,7 @@ struct Casera: Widget { title: "Fix leaky faucet", description: "Kitchen sink needs repair", priority: "high", - status: "pending", + inProgress: false, dueDate: "2024-12-15", category: "plumbing", residenceName: "Home", @@ -612,7 +620,7 @@ struct Casera: Widget { title: "Paint living room", description: nil, priority: "medium", - status: "pending", + inProgress: false, dueDate: "2024-12-20", category: "painting", residenceName: "Home", @@ -632,7 +640,7 @@ struct Casera: Widget { title: "Fix leaky faucet", description: nil, priority: "high", - status: "pending", + inProgress: false, dueDate: "2024-12-15", category: "plumbing", residenceName: "Home", @@ -643,7 +651,7 @@ struct Casera: Widget { title: "Paint living room", description: nil, priority: "medium", - status: "pending", + inProgress: false, dueDate: "2024-12-20", category: "painting", residenceName: "Home", @@ -654,7 +662,7 @@ struct Casera: Widget { title: "Clean gutters", description: nil, priority: "low", - status: "pending", + inProgress: false, dueDate: "2024-12-25", category: "maintenance", residenceName: "Home", @@ -684,7 +692,7 @@ struct Casera: Widget { title: "Fix leaky faucet", description: "Kitchen sink needs repair", priority: "high", - status: "pending", + inProgress: false, dueDate: "2024-12-15", category: "plumbing", residenceName: "Home", @@ -695,7 +703,7 @@ struct Casera: Widget { title: "Paint living room", description: nil, priority: "medium", - status: "in_progress", + inProgress: true, dueDate: "2024-12-20", category: "painting", residenceName: "Cabin", @@ -706,7 +714,7 @@ struct Casera: Widget { title: "Clean gutters", description: "Remove debris", priority: "low", - status: "pending", + inProgress: false, dueDate: "2024-12-25", category: "maintenance", residenceName: "Home", @@ -726,7 +734,7 @@ struct Casera: Widget { title: "Fix leaky faucet", description: nil, priority: "high", - status: "pending", + inProgress: false, dueDate: nil, category: nil, residenceName: nil, @@ -737,7 +745,7 @@ struct Casera: Widget { title: "Paint living room", description: nil, priority: "medium", - status: "pending", + inProgress: false, dueDate: nil, category: nil, residenceName: nil, @@ -748,7 +756,7 @@ struct Casera: Widget { title: "Clean gutters", description: nil, priority: "low", - status: "pending", + inProgress: false, dueDate: nil, category: nil, residenceName: nil, @@ -759,7 +767,7 @@ struct Casera: Widget { title: "Replace HVAC filter", description: nil, priority: "medium", - status: "pending", + inProgress: false, dueDate: nil, category: nil, residenceName: nil, @@ -770,7 +778,7 @@ struct Casera: Widget { title: "Check smoke detectors", description: nil, priority: "high", - status: "pending", + inProgress: false, dueDate: nil, category: nil, residenceName: nil, @@ -800,7 +808,7 @@ struct Casera: Widget { title: "Fix leaky faucet in kitchen", description: "Kitchen sink needs repair", priority: "high", - status: "pending", + inProgress: false, dueDate: "2024-12-15", category: "plumbing", residenceName: "Home", @@ -811,7 +819,7 @@ struct Casera: Widget { title: "Paint living room walls", description: nil, priority: "medium", - status: "in_progress", + inProgress: true, dueDate: "2024-12-20", category: "painting", residenceName: "Cabin", @@ -822,7 +830,7 @@ struct Casera: Widget { title: "Clean gutters", description: "Remove debris", priority: "low", - status: "pending", + inProgress: false, dueDate: "2024-12-25", category: "maintenance", residenceName: "Home", @@ -833,7 +841,7 @@ struct Casera: Widget { title: "Replace HVAC filter", description: nil, priority: "medium", - status: "pending", + inProgress: false, dueDate: "2024-12-28", category: "hvac", residenceName: "Beach House", @@ -844,7 +852,7 @@ struct Casera: Widget { title: "Check smoke detectors", description: "Replace batteries if needed", priority: "high", - status: "pending", + inProgress: false, dueDate: "2024-12-30", category: "safety", residenceName: "Home", @@ -855,7 +863,7 @@ struct Casera: Widget { title: "Service water heater", description: nil, priority: "medium", - status: "pending", + inProgress: false, dueDate: "2025-01-05", category: "plumbing", residenceName: "Cabin", @@ -866,7 +874,7 @@ struct Casera: Widget { title: "Inspect roof shingles", description: nil, priority: "low", - status: "pending", + inProgress: false, dueDate: "2025-01-10", category: "exterior", residenceName: "Home", @@ -877,7 +885,7 @@ struct Casera: Widget { title: "Clean dryer vent", description: "Fire hazard prevention", priority: "urgent", - status: "pending", + inProgress: false, dueDate: "2025-01-12", category: "appliances", residenceName: "Beach House", @@ -897,7 +905,7 @@ struct Casera: Widget { title: "Task 1", description: nil, priority: "high", - status: "pending", + inProgress: false, dueDate: nil, category: nil, residenceName: nil, @@ -908,7 +916,7 @@ struct Casera: Widget { title: "Task 2", description: nil, priority: "medium", - status: "pending", + inProgress: false, dueDate: nil, category: nil, residenceName: nil, @@ -919,7 +927,7 @@ struct Casera: Widget { title: "Task 3", description: nil, priority: "low", - status: "pending", + inProgress: false, dueDate: nil, category: nil, residenceName: nil, @@ -930,7 +938,7 @@ struct Casera: Widget { title: "Task 4", description: nil, priority: "medium", - status: "pending", + inProgress: false, dueDate: nil, category: nil, residenceName: nil, @@ -941,7 +949,7 @@ struct Casera: Widget { title: "Task 5", description: nil, priority: "high", - status: "pending", + inProgress: false, dueDate: nil, category: nil, residenceName: nil, @@ -952,7 +960,7 @@ struct Casera: Widget { title: "Task 6", description: nil, priority: "low", - status: "pending", + inProgress: false, dueDate: nil, category: nil, residenceName: nil, @@ -963,7 +971,7 @@ struct Casera: Widget { title: "Task 7", description: nil, priority: "medium", - status: "pending", + inProgress: false, dueDate: nil, category: nil, residenceName: nil, diff --git a/iosApp/iosApp/Data/DataManagerObservable.swift b/iosApp/iosApp/Data/DataManagerObservable.swift index ee309db..9dd6bb9 100644 --- a/iosApp/iosApp/Data/DataManagerObservable.swift +++ b/iosApp/iosApp/Data/DataManagerObservable.swift @@ -64,7 +64,6 @@ class DataManagerObservable: ObservableObject { @Published var residenceTypes: [ResidenceType] = [] @Published var taskFrequencies: [TaskFrequency] = [] @Published var taskPriorities: [TaskPriority] = [] - @Published var taskStatuses: [TaskStatus] = [] @Published var taskCategories: [TaskCategory] = [] @Published var contractorSpecialties: [ContractorSpecialty] = [] @@ -292,16 +291,6 @@ class DataManagerObservable: ObservableObject { } observationTasks.append(taskPrioritiesTask) - // Lookups - TaskStatuses - let taskStatusesTask = Task { - for await items in DataManager.shared.taskStatuses { - await MainActor.run { - self.taskStatuses = items - } - } - } - observationTasks.append(taskStatusesTask) - // Lookups - TaskCategories let taskCategoriesTask = Task { for await items in DataManager.shared.taskCategories { @@ -459,12 +448,6 @@ class DataManagerObservable: ObservableObject { return taskPriorities.first { $0.id == id } } - /// Get task status by ID - func getTaskStatus(id: Int32?) -> TaskStatus? { - guard let id = id else { return nil } - return taskStatuses.first { $0.id == id } - } - /// Get task category by ID func getTaskCategory(id: Int32?) -> TaskCategory? { guard let id = id else { return nil } diff --git a/iosApp/iosApp/Helpers/L10n.swift b/iosApp/iosApp/Helpers/L10n.swift index 0a536be..9c2ddda 100644 --- a/iosApp/iosApp/Helpers/L10n.swift +++ b/iosApp/iosApp/Helpers/L10n.swift @@ -251,6 +251,7 @@ enum L10n { // Task Card Actions static var inProgress: String { String(localized: "tasks_in_progress") } + static var inProgressLabel: String { String(localized: "tasks_in_progress_label") } static var complete: String { String(localized: "tasks_complete") } static var edit: String { String(localized: "tasks_edit") } static var cancel: String { String(localized: "tasks_cancel") } diff --git a/iosApp/iosApp/Helpers/WidgetActionProcessor.swift b/iosApp/iosApp/Helpers/WidgetActionProcessor.swift index fde7650..2e2d0f8 100644 --- a/iosApp/iosApp/Helpers/WidgetActionProcessor.swift +++ b/iosApp/iosApp/Helpers/WidgetActionProcessor.swift @@ -93,6 +93,10 @@ final class WidgetActionProcessor { let data = success.data { // Update widget with fresh data WidgetDataManager.shared.saveTasks(from: data) + // Update summary from response (no extra API call needed) + if let summary = data.summary { + DataManager.shared.setTotalSummary(summary: summary) + } } } catch { print("WidgetActionProcessor: Error refreshing tasks: \(error)") diff --git a/iosApp/iosApp/Helpers/WidgetDataManager.swift b/iosApp/iosApp/Helpers/WidgetDataManager.swift index 027be9d..3438791 100644 --- a/iosApp/iosApp/Helpers/WidgetDataManager.swift +++ b/iosApp/iosApp/Helpers/WidgetDataManager.swift @@ -223,14 +223,15 @@ final class WidgetDataManager { let title: String let description: String? let priority: String? - let status: String? + let inProgress: Bool let dueDate: String? let category: String? let residenceName: String? let isOverdue: Bool enum CodingKeys: String, CodingKey { - case id, title, description, priority, status, category + case id, title, description, priority, category + case inProgress = "in_progress" case dueDate = "due_date" case residenceName = "residence_name" case isOverdue = "is_overdue" @@ -273,7 +274,7 @@ final class WidgetDataManager { title: task.title, description: task.description_, priority: task.priority?.name ?? "", - status: task.status?.name, + inProgress: task.inProgress, dueDate: task.dueDate, category: task.category?.name ?? "", residenceName: "", // No longer available in API, residence lookup needed @@ -325,14 +326,9 @@ final class WidgetDataManager { func getUpcomingTasks() -> [WidgetTask] { let allTasks = loadTasks() - // Filter for pending/in-progress tasks (non-archived, non-completed) - let upcoming = allTasks.filter { task in - let status = task.status?.lowercased() ?? "" - return status == "pending" || status == "in_progress" || status == "in progress" - } - + // All loaded tasks are already filtered (archived and completed columns are excluded during save) // Sort by due date (earliest first), with overdue at top - return upcoming.sorted { task1, task2 in + return allTasks.sorted { task1, task2 in // Overdue tasks first if task1.isOverdue != task2.isOverdue { return task1.isOverdue diff --git a/iosApp/iosApp/Localizable.xcstrings b/iosApp/iosApp/Localizable.xcstrings index 3ddb004..990a584 100644 --- a/iosApp/iosApp/Localizable.xcstrings +++ b/iosApp/iosApp/Localizable.xcstrings @@ -27381,6 +27381,71 @@ } } }, + "tasks_in_progress_label" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "In Bearbeitung" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "In Progress" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "En Progreso" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "En Cours" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "In corso" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "進行中" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "진행 중" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "In behandeling" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Em Andamento" + } + }, + "zh" : { + "stringUnit" : { + "state" : "translated", + "value" : "进行中" + } + } + } + }, "tasks_library" : { "extractionState" : "manual", "localizations" : { diff --git a/iosApp/iosApp/Login/AppleSignInViewModel.swift b/iosApp/iosApp/Login/AppleSignInViewModel.swift index a715adb..63bcf19 100644 --- a/iosApp/iosApp/Login/AppleSignInViewModel.swift +++ b/iosApp/iosApp/Login/AppleSignInViewModel.swift @@ -85,6 +85,10 @@ class AppleSignInViewModel: ObservableObject { // - Initializes lookups // - Prefetches all data + // Share token and API URL with widget extension + WidgetDataManager.shared.saveAuthToken(response.token) + WidgetDataManager.shared.saveAPIBaseURL(ApiClient.shared.getBaseUrl()) + // Track Apple Sign In PostHogAnalytics.shared.capture(AnalyticsEvents.userSignedInApple, properties: [ "is_new_user": isNewUser diff --git a/iosApp/iosApp/Onboarding/OnboardingFirstTaskView.swift b/iosApp/iosApp/Onboarding/OnboardingFirstTaskView.swift index e3dea45..0b9b248 100644 --- a/iosApp/iosApp/Onboarding/OnboardingFirstTaskView.swift +++ b/iosApp/iosApp/Onboarding/OnboardingFirstTaskView.swift @@ -361,7 +361,7 @@ struct OnboardingFirstTaskContent: View { description: nil, categoryId: categoryId.map { KotlinInt(int: $0) }, priorityId: nil, - statusId: nil, + inProgress: false, frequencyId: frequencyId.map { KotlinInt(int: $0) }, assignedToId: nil, dueDate: todayString, diff --git a/iosApp/iosApp/Residence/ResidenceDetailView.swift b/iosApp/iosApp/Residence/ResidenceDetailView.swift index f4ad6d5..4e25f88 100644 --- a/iosApp/iosApp/Residence/ResidenceDetailView.swift +++ b/iosApp/iosApp/Residence/ResidenceDetailView.swift @@ -506,8 +506,8 @@ private struct TasksSectionContainer: View { selectedTaskForEdit = task showEditTask = true }, - onCancelTask: { taskId in - taskViewModel.cancelTask(id: taskId) { _ in + onCancelTask: { task in + taskViewModel.cancelTask(id: task.id) { _ in reloadTasks() } }, @@ -526,12 +526,9 @@ private struct TasksSectionContainer: View { onCompleteTask: { task in selectedTaskForComplete = task }, - onArchiveTask: { taskId in - let allTasks = tasksResponse.columns.flatMap { $0.tasks } - if let task = allTasks.first(where: { $0.id == taskId }) { - selectedTaskForArchive = task - showArchiveConfirmation = true - } + onArchiveTask: { task in + selectedTaskForArchive = task + showArchiveConfirmation = true }, onUnarchiveTask: { taskId in taskViewModel.unarchiveTask(id: taskId) { _ in diff --git a/iosApp/iosApp/Subviews/Task/DynamicTaskCard.swift b/iosApp/iosApp/Subviews/Task/DynamicTaskCard.swift index 95e9dc3..19a1398 100644 --- a/iosApp/iosApp/Subviews/Task/DynamicTaskCard.swift +++ b/iosApp/iosApp/Subviews/Task/DynamicTaskCard.swift @@ -25,8 +25,8 @@ struct DynamicTaskCard: View { .font(.title3) .foregroundColor(.primary) - if let status = task.status { - StatusBadge(status: status.name) + if task.inProgress { + StatusBadge(status: "in_progress") } } diff --git a/iosApp/iosApp/Subviews/Task/DynamicTaskColumnView.swift b/iosApp/iosApp/Subviews/Task/DynamicTaskColumnView.swift index 92f5b54..ec8ea29 100644 --- a/iosApp/iosApp/Subviews/Task/DynamicTaskColumnView.swift +++ b/iosApp/iosApp/Subviews/Task/DynamicTaskColumnView.swift @@ -5,11 +5,11 @@ import ComposeApp struct DynamicTaskColumnView: View { let column: TaskColumn let onEditTask: (TaskResponse) -> Void - let onCancelTask: (Int32) -> Void + let onCancelTask: (TaskResponse) -> Void let onUncancelTask: (Int32) -> Void let onMarkInProgress: (Int32) -> Void let onCompleteTask: (TaskResponse) -> Void - let onArchiveTask: (Int32) -> Void + let onArchiveTask: (TaskResponse) -> Void let onUnarchiveTask: (Int32) -> Void // Get icon from API response, with fallback @@ -65,11 +65,11 @@ struct DynamicTaskColumnView: View { task: task, buttonTypes: column.buttonTypes, onEdit: { onEditTask(task) }, - onCancel: { onCancelTask(task.id) }, + onCancel: { onCancelTask(task) }, onUncancel: { onUncancelTask(task.id) }, onMarkInProgress: { onMarkInProgress(task.id) }, onComplete: { onCompleteTask(task) }, - onArchive: { onArchiveTask(task.id) }, + onArchive: { onArchiveTask(task) }, onUnarchive: { onUnarchiveTask(task.id) } ) } diff --git a/iosApp/iosApp/Subviews/Task/TaskCard.swift b/iosApp/iosApp/Subviews/Task/TaskCard.swift index bfa3ec5..0b38802 100644 --- a/iosApp/iosApp/Subviews/Task/TaskCard.swift +++ b/iosApp/iosApp/Subviews/Task/TaskCard.swift @@ -23,8 +23,8 @@ struct TaskCard: View { .foregroundColor(Color.appTextPrimary) .lineLimit(2) - if let status = task.status { - StatusBadge(status: status.name) + if task.inProgress { + StatusBadge(status: "in_progress") } } @@ -116,7 +116,7 @@ struct TaskCard: View { // Primary Actions if task.showCompletedButton { VStack(spacing: AppSpacing.xs) { - if let onMarkInProgress = onMarkInProgress, task.status?.name != "in_progress" { + if let onMarkInProgress = onMarkInProgress, !task.inProgress { Button(action: onMarkInProgress) { HStack(spacing: AppSpacing.xs) { Image(systemName: "play.circle.fill") @@ -258,8 +258,7 @@ struct TaskCard: View { 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), + inProgress: false, frequencyId: 1, frequency: TaskFrequency(id: 1, name: "monthly", days: 30, displayOrder: 0), dueDate: "2024-12-15", diff --git a/iosApp/iosApp/Subviews/Task/TasksSection.swift b/iosApp/iosApp/Subviews/Task/TasksSection.swift index 33fcbd8..bedf0e2 100644 --- a/iosApp/iosApp/Subviews/Task/TasksSection.swift +++ b/iosApp/iosApp/Subviews/Task/TasksSection.swift @@ -4,11 +4,11 @@ import ComposeApp struct TasksSection: View { let tasksResponse: TaskColumnsResponse let onEditTask: (TaskResponse) -> Void - let onCancelTask: (Int32) -> Void + let onCancelTask: (TaskResponse) -> Void let onUncancelTask: (Int32) -> Void let onMarkInProgress: (Int32) -> Void let onCompleteTask: (TaskResponse) -> Void - let onArchiveTask: (Int32) -> Void + let onArchiveTask: (TaskResponse) -> Void let onUnarchiveTask: (Int32) -> Void private var hasNoTasks: Bool { @@ -35,8 +35,8 @@ struct TasksSection: View { onEditTask: { task in onEditTask(task) }, - onCancelTask: { taskId in - onCancelTask(taskId) + onCancelTask: { task in + onCancelTask(task) }, onUncancelTask: { taskId in onUncancelTask(taskId) @@ -47,8 +47,8 @@ struct TasksSection: View { onCompleteTask: { task in onCompleteTask(task) }, - onArchiveTask: { taskId in - onArchiveTask(taskId) + onArchiveTask: { task in + onArchiveTask(task) }, onUnarchiveTask: { taskId in onUnarchiveTask(taskId) @@ -92,8 +92,7 @@ struct TasksSection: View { 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), + inProgress: false, frequencyId: 1, frequency: TaskFrequency(id: 1, name: "monthly", days: 30, displayOrder: 0), dueDate: "2024-12-15", @@ -132,8 +131,7 @@ struct TasksSection: View { 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), + inProgress: false, frequencyId: 6, frequency: TaskFrequency(id: 6, name: "once", days: nil, displayOrder: 0), dueDate: "2024-11-01", @@ -154,7 +152,7 @@ struct TasksSection: View { ) ], daysThreshold: 30, - residenceId: "1" + residenceId: "1", summary: nil ), onEditTask: { _ in }, onCancelTask: { _ in }, diff --git a/iosApp/iosApp/Task/AllTasksView.swift b/iosApp/iosApp/Task/AllTasksView.swift index 702259c..d1bd942 100644 --- a/iosApp/iosApp/Task/AllTasksView.swift +++ b/iosApp/iosApp/Task/AllTasksView.swift @@ -221,12 +221,9 @@ struct AllTasksView: View { selectedTaskForEdit = task showEditTask = true }, - onCancelTask: { taskId in - let allTasks = tasksResponse.columns.flatMap { $0.tasks } - if let task = allTasks.first(where: { $0.id == taskId }) { - selectedTaskForCancel = task - showCancelConfirmation = true - } + onCancelTask: { task in + selectedTaskForCancel = task + showCancelConfirmation = true }, onUncancelTask: { taskId in taskViewModel.uncancelTask(id: taskId) { _ in @@ -243,12 +240,9 @@ struct AllTasksView: View { onCompleteTask: { task in selectedTaskForComplete = task }, - onArchiveTask: { taskId in - let allTasks = tasksResponse.columns.flatMap { $0.tasks } - if let task = allTasks.first(where: { $0.id == taskId }) { - selectedTaskForArchive = task - showArchiveConfirmation = true - } + onArchiveTask: { task in + selectedTaskForArchive = task + showArchiveConfirmation = true }, onUnarchiveTask: { taskId in taskViewModel.unarchiveTask(id: taskId) { _ in diff --git a/iosApp/iosApp/Task/CompleteTaskView.swift b/iosApp/iosApp/Task/CompleteTaskView.swift index b23c267..aaa1bf1 100644 --- a/iosApp/iosApp/Task/CompleteTaskView.swift +++ b/iosApp/iosApp/Task/CompleteTaskView.swift @@ -39,8 +39,8 @@ struct CompleteTaskView: View { Spacer() - if let status = task.status { - Text(status.displayName) + if task.inProgress { + Text(L10n.Tasks.inProgress) .font(.caption) .foregroundStyle(.secondary) .padding(.horizontal, 8) diff --git a/iosApp/iosApp/Task/TaskFormView.swift b/iosApp/iosApp/Task/TaskFormView.swift index b8da411..569a3a8 100644 --- a/iosApp/iosApp/Task/TaskFormView.swift +++ b/iosApp/iosApp/Task/TaskFormView.swift @@ -29,15 +29,13 @@ struct TaskFormView: View { (!needsResidenceSelection || selectedResidence != nil) && selectedCategory != nil && selectedFrequency != nil && - selectedPriority != nil && - selectedStatus != nil + selectedPriority != nil } // Lookups from DataManagerObservable private var taskCategories: [TaskCategory] { dataManager.taskCategories } private var taskFrequencies: [TaskFrequency] { dataManager.taskFrequencies } private var taskPriorities: [TaskPriority] { dataManager.taskPriorities } - private var taskStatuses: [TaskStatus] { dataManager.taskStatuses } private var isLoadingLookups: Bool { !dataManager.lookupsInitialized } // Form fields @@ -47,7 +45,7 @@ struct TaskFormView: View { @State private var selectedCategory: TaskCategory? @State private var selectedFrequency: TaskFrequency? @State private var selectedPriority: TaskPriority? - @State private var selectedStatus: TaskStatus? + @State private var inProgress: Bool @State private var dueDate: Date @State private var intervalDays: String @State private var estimatedCost: String @@ -66,7 +64,7 @@ struct TaskFormView: View { _selectedCategory = State(initialValue: task.category) _selectedFrequency = State(initialValue: task.frequency) _selectedPriority = State(initialValue: task.priority) - _selectedStatus = State(initialValue: task.status) + _inProgress = State(initialValue: task.inProgress) // Parse date from string let formatter = DateFormatter() @@ -78,6 +76,7 @@ struct TaskFormView: View { } else { _title = State(initialValue: "") _description = State(initialValue: "") + _inProgress = State(initialValue: false) _dueDate = State(initialValue: Date()) _intervalDays = State(initialValue: "") _estimatedCost = State(initialValue: "") @@ -246,16 +245,11 @@ struct TaskFormView: View { } } - Picker(L10n.Tasks.status, selection: $selectedStatus) { - Text(L10n.Tasks.selectStatus).tag(nil as TaskStatus?) - ForEach(taskStatuses, id: \.id) { status in - Text(status.displayName).tag(status as TaskStatus?) - } - } + Toggle(L10n.Tasks.inProgressLabel, isOn: $inProgress) } header: { Text(L10n.Tasks.priorityAndStatus) } footer: { - Text(L10n.Tasks.bothRequired) + Text(L10n.Tasks.required) .font(.caption) .foregroundColor(Color.appError) } @@ -409,11 +403,6 @@ struct TaskFormView: View { selectedPriority = taskPriorities.first { $0.name == "medium" } ?? taskPriorities.first } - if selectedStatus == nil && !taskStatuses.isEmpty { - // Default to "pending" - selectedStatus = taskStatuses.first { $0.name == "pending" } ?? taskStatuses.first - } - // Set default residence if provided if needsResidenceSelection && selectedResidence == nil, let residences = residences, !residences.isEmpty { selectedResidence = residences.first @@ -452,11 +441,6 @@ struct TaskFormView: View { isValid = false } - if selectedStatus == nil { - viewModel.errorMessage = "Please select a status" - isValid = false - } - return isValid } @@ -465,8 +449,7 @@ struct TaskFormView: View { guard let category = selectedCategory, let frequency = selectedFrequency, - let priority = selectedPriority, - let status = selectedStatus else { + let priority = selectedPriority else { return } @@ -483,7 +466,7 @@ struct TaskFormView: View { description: description.isEmpty ? nil : description, categoryId: KotlinInt(int: Int32(category.id)), priorityId: KotlinInt(int: Int32(priority.id)), - statusId: KotlinInt(int: Int32(status.id)), + inProgress: inProgress, frequencyId: KotlinInt(int: Int32(frequency.id)), assignedToId: nil, dueDate: dueDateString, @@ -514,7 +497,7 @@ struct TaskFormView: View { description: description.isEmpty ? nil : description, categoryId: KotlinInt(int: Int32(category.id)), priorityId: KotlinInt(int: Int32(priority.id)), - statusId: selectedStatus.map { KotlinInt(int: Int32($0.id)) }, + inProgress: inProgress, frequencyId: KotlinInt(int: Int32(frequency.id)), assignedToId: nil, dueDate: dueDateString, diff --git a/iosApp/iosApp/Task/TaskViewModel.swift b/iosApp/iosApp/Task/TaskViewModel.swift index 553b394..6ccc7ca 100644 --- a/iosApp/iosApp/Task/TaskViewModel.swift +++ b/iosApp/iosApp/Task/TaskViewModel.swift @@ -75,14 +75,13 @@ class TaskViewModel: ObservableObject { do { let result = try await APILayer.shared.createTask(request: request) - if result is ApiResultSuccess { - self.actionState = .success(.create) - // DataManager is updated by APILayer, view updates via observation - completion(true) - } else if let error = result as? ApiResultError { + if let error = result as? ApiResultError { self.actionState = .error(.create, ErrorMessageParser.parse(error.message)) self.errorMessage = ErrorMessageParser.parse(error.message) completion(false) + } else { + self.actionState = .success(.create) + completion(true) } } catch { self.actionState = .error(.create, error.localizedDescription) @@ -100,14 +99,16 @@ class TaskViewModel: ObservableObject { do { let result = try await APILayer.shared.cancelTask(taskId: id) - if result is ApiResultSuccess { - self.actionState = .success(.cancel) - // DataManager is updated by APILayer, view updates via observation - completion(true) - } else if let error = result as? ApiResultError { + // Check for error first, then treat non-error as success + // This handles Kotlin-Swift generic type bridging issues + if let error = result as? ApiResultError { self.actionState = .error(.cancel, ErrorMessageParser.parse(error.message)) self.errorMessage = ErrorMessageParser.parse(error.message) completion(false) + } else { + // Not an error = success (DataManager is updated by APILayer) + self.actionState = .success(.cancel) + completion(true) } } catch { self.actionState = .error(.cancel, error.localizedDescription) @@ -125,14 +126,13 @@ class TaskViewModel: ObservableObject { do { let result = try await APILayer.shared.uncancelTask(taskId: id) - if result is ApiResultSuccess { - self.actionState = .success(.uncancel) - // DataManager is updated by APILayer, view updates via observation - completion(true) - } else if let error = result as? ApiResultError { + if let error = result as? ApiResultError { self.actionState = .error(.uncancel, ErrorMessageParser.parse(error.message)) self.errorMessage = ErrorMessageParser.parse(error.message) completion(false) + } else { + self.actionState = .success(.uncancel) + completion(true) } } catch { self.actionState = .error(.uncancel, error.localizedDescription) @@ -150,14 +150,13 @@ class TaskViewModel: ObservableObject { do { let result = try await APILayer.shared.markInProgress(taskId: id) - if result is ApiResultSuccess { - self.actionState = .success(.markInProgress) - // DataManager is updated by APILayer, view updates via observation - completion(true) - } else if let error = result as? ApiResultError { + if let error = result as? ApiResultError { self.actionState = .error(.markInProgress, ErrorMessageParser.parse(error.message)) self.errorMessage = ErrorMessageParser.parse(error.message) completion(false) + } else { + self.actionState = .success(.markInProgress) + completion(true) } } catch { self.actionState = .error(.markInProgress, error.localizedDescription) @@ -175,14 +174,13 @@ class TaskViewModel: ObservableObject { do { let result = try await APILayer.shared.archiveTask(taskId: id) - if result is ApiResultSuccess { - self.actionState = .success(.archive) - // DataManager is updated by APILayer, view updates via observation - completion(true) - } else if let error = result as? ApiResultError { + if let error = result as? ApiResultError { self.actionState = .error(.archive, ErrorMessageParser.parse(error.message)) self.errorMessage = ErrorMessageParser.parse(error.message) completion(false) + } else { + self.actionState = .success(.archive) + completion(true) } } catch { self.actionState = .error(.archive, error.localizedDescription) @@ -200,14 +198,13 @@ class TaskViewModel: ObservableObject { do { let result = try await APILayer.shared.unarchiveTask(taskId: id) - if result is ApiResultSuccess { - self.actionState = .success(.unarchive) - // DataManager is updated by APILayer, view updates via observation - completion(true) - } else if let error = result as? ApiResultError { + if let error = result as? ApiResultError { self.actionState = .error(.unarchive, ErrorMessageParser.parse(error.message)) self.errorMessage = ErrorMessageParser.parse(error.message) completion(false) + } else { + self.actionState = .success(.unarchive) + completion(true) } } catch { self.actionState = .error(.unarchive, error.localizedDescription) @@ -225,14 +222,13 @@ class TaskViewModel: ObservableObject { do { let result = try await APILayer.shared.updateTask(id: id, request: request) - if result is ApiResultSuccess { - self.actionState = .success(.update) - // DataManager is updated by APILayer, view updates via observation - completion(true) - } else if let error = result as? ApiResultError { + if let error = result as? ApiResultError { self.actionState = .error(.update, ErrorMessageParser.parse(error.message)) self.errorMessage = ErrorMessageParser.parse(error.message) completion(false) + } else { + self.actionState = .success(.update) + completion(true) } } catch { self.actionState = .error(.update, error.localizedDescription) @@ -406,7 +402,7 @@ class TaskViewModel: ObservableObject { tasksResponse = TaskColumnsResponse( columns: newColumns, daysThreshold: currentResponse.daysThreshold, - residenceId: currentResponse.residenceId + residenceId: currentResponse.residenceId, summary: nil ) } @@ -434,7 +430,7 @@ class TaskViewModel: ObservableObject { tasksResponse = TaskColumnsResponse( columns: newColumns, daysThreshold: currentResponse.daysThreshold, - residenceId: currentResponse.residenceId + residenceId: currentResponse.residenceId, summary: nil ) } diff --git a/iosApp/iosApp/iOSApp.swift b/iosApp/iosApp/iOSApp.swift index 17cb820..0101198 100644 --- a/iosApp/iosApp/iOSApp.swift +++ b/iosApp/iosApp/iOSApp.swift @@ -55,6 +55,13 @@ struct iOSApp: App { } .onChange(of: scenePhase) { newPhase in if newPhase == .active { + // Sync auth token to widget if user is logged in + // This ensures widget has credentials even if user logged in before widget support was added + if let token = TokenStorage.shared.getToken() { + WidgetDataManager.shared.saveAuthToken(token) + WidgetDataManager.shared.saveAPIBaseURL(ApiClient.shared.getBaseUrl()) + } + // Check and register device token when app becomes active PushNotificationManager.shared.checkAndRegisterDeviceIfNeeded() @@ -67,6 +74,24 @@ struct iOSApp: App { Task { @MainActor in WidgetActionProcessor.shared.processPendingActions() } + + // Check if widget completed a task - refresh data globally + if WidgetDataManager.shared.areTasksDirty() { + WidgetDataManager.shared.clearDirtyFlag() + Task { + // Refresh tasks - response includes summary for dashboard stats + let result = try? await APILayer.shared.getTasks(forceRefresh: true) + if let success = result as? ApiResultSuccess, + let data = success.data { + // Update widget cache + WidgetDataManager.shared.saveTasks(from: data) + // Update summary from response (no extra API call needed) + if let summary = data.summary { + DataManager.shared.setTotalSummary(summary: summary) + } + } + } + } } else if newPhase == .background { // Refresh widget when app goes to background WidgetCenter.shared.reloadAllTimelines()