Replace status_id with in_progress boolean across mobile apps

- Remove TaskStatus model and status_id foreign key references
- Add in_progress boolean field to task models and forms
- Update TaskApi to use dedicated POST endpoints for task actions:
  - POST /tasks/:id/cancel/ instead of PATCH with is_cancelled
  - POST /tasks/:id/uncancel/
  - POST /tasks/:id/archive/
  - POST /tasks/:id/unarchive/
- Fix iOS TaskViewModel to use error-first pattern for Kotlin-Swift
  generic type bridging issues
- Update iOS callback signatures to pass full TaskResponse instead
  of just taskId to avoid stale closure lookups
- Add in_progress localization strings
- Update widget preview data to use inProgress boolean

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-08 20:47:59 -06:00
parent a067228597
commit 4a04aff1e6
33 changed files with 314 additions and 376 deletions

View File

@@ -179,6 +179,7 @@
<string name="tasks_unarchive">Unarchive</string> <string name="tasks_unarchive">Unarchive</string>
<string name="tasks_completed_message">Task marked as completed</string> <string name="tasks_completed_message">Task marked as completed</string>
<string name="tasks_in_progress_message">Task marked as in progress</string> <string name="tasks_in_progress_message">Task marked as in progress</string>
<string name="tasks_in_progress_label">In Progress</string>
<string name="tasks_cancelled_message">Task cancelled</string> <string name="tasks_cancelled_message">Task cancelled</string>
<!-- Task Completions --> <!-- Task Completions -->

View File

@@ -52,7 +52,6 @@ import com.example.casera.models.TaskCategory
import com.example.casera.models.TaskDetail import com.example.casera.models.TaskDetail
import com.example.casera.models.TaskFrequency import com.example.casera.models.TaskFrequency
import com.example.casera.models.TaskPriority import com.example.casera.models.TaskPriority
import com.example.casera.models.TaskStatus
import com.example.casera.network.ApiResult import com.example.casera.network.ApiResult
import com.example.casera.network.AuthApi import com.example.casera.network.AuthApi
import com.example.casera.data.DataManager import com.example.casera.data.DataManager
@@ -413,8 +412,7 @@ fun App(
frequencyName = task.frequency?.name ?: "", frequencyName = task.frequency?.name ?: "",
priorityId = task.priority?.id ?: 0, priorityId = task.priority?.id ?: 0,
priorityName = task.priority?.name ?: "", priorityName = task.priority?.name ?: "",
statusId = task.status?.id, inProgress = task.inProgress,
statusName = task.status?.name,
dueDate = task.dueDate, dueDate = task.dueDate,
estimatedCost = task.estimatedCost?.toString(), estimatedCost = task.estimatedCost?.toString(),
createdAt = task.createdAt, createdAt = task.createdAt,
@@ -576,8 +574,7 @@ fun App(
frequencyName = task.frequency?.name ?: "", frequencyName = task.frequency?.name ?: "",
priorityId = task.priority?.id ?: 0, priorityId = task.priority?.id ?: 0,
priorityName = task.priority?.name ?: "", priorityName = task.priority?.name ?: "",
statusId = task.status?.id, inProgress = task.inProgress,
statusName = task.status?.name,
dueDate = task.dueDate, dueDate = task.dueDate,
estimatedCost = task.estimatedCost?.toString(), estimatedCost = task.estimatedCost?.toString(),
createdAt = task.createdAt, createdAt = task.createdAt,
@@ -607,9 +604,7 @@ fun App(
days = null days = null
), ),
priority = TaskPriority(id = route.priorityId, name = route.priorityName), priority = TaskPriority(id = route.priorityId, name = route.priorityName),
status = route.statusId?.let { inProgress = route.inProgress,
TaskStatus(id = it, name = route.statusName ?: "")
},
dueDate = route.dueDate, dueDate = route.dueDate,
estimatedCost = route.estimatedCost?.toDoubleOrNull(), estimatedCost = route.estimatedCost?.toDoubleOrNull(),
createdAt = route.createdAt, createdAt = route.createdAt,

View File

@@ -158,9 +158,6 @@ object DataManager {
private val _taskPriorities = MutableStateFlow<List<TaskPriority>>(emptyList()) private val _taskPriorities = MutableStateFlow<List<TaskPriority>>(emptyList())
val taskPriorities: StateFlow<List<TaskPriority>> = _taskPriorities.asStateFlow() val taskPriorities: StateFlow<List<TaskPriority>> = _taskPriorities.asStateFlow()
private val _taskStatuses = MutableStateFlow<List<TaskStatus>>(emptyList())
val taskStatuses: StateFlow<List<TaskStatus>> = _taskStatuses.asStateFlow()
private val _taskCategories = MutableStateFlow<List<TaskCategory>>(emptyList()) private val _taskCategories = MutableStateFlow<List<TaskCategory>>(emptyList())
val taskCategories: StateFlow<List<TaskCategory>> = _taskCategories.asStateFlow() val taskCategories: StateFlow<List<TaskCategory>> = _taskCategories.asStateFlow()
@@ -185,9 +182,6 @@ object DataManager {
private val _taskPrioritiesMap = MutableStateFlow<Map<Int, TaskPriority>>(emptyMap()) private val _taskPrioritiesMap = MutableStateFlow<Map<Int, TaskPriority>>(emptyMap())
val taskPrioritiesMap: StateFlow<Map<Int, TaskPriority>> = _taskPrioritiesMap.asStateFlow() val taskPrioritiesMap: StateFlow<Map<Int, TaskPriority>> = _taskPrioritiesMap.asStateFlow()
private val _taskStatusesMap = MutableStateFlow<Map<Int, TaskStatus>>(emptyMap())
val taskStatusesMap: StateFlow<Map<Int, TaskStatus>> = _taskStatusesMap.asStateFlow()
private val _taskCategoriesMap = MutableStateFlow<Map<Int, TaskCategory>>(emptyMap()) private val _taskCategoriesMap = MutableStateFlow<Map<Int, TaskCategory>>(emptyMap())
val taskCategoriesMap: StateFlow<Map<Int, TaskCategory>> = _taskCategoriesMap.asStateFlow() val taskCategoriesMap: StateFlow<Map<Int, TaskCategory>> = _taskCategoriesMap.asStateFlow()
@@ -247,7 +241,6 @@ object DataManager {
fun getResidenceType(id: Int?): ResidenceType? = id?.let { _residenceTypesMap.value[it] } fun getResidenceType(id: Int?): ResidenceType? = id?.let { _residenceTypesMap.value[it] }
fun getTaskFrequency(id: Int?): TaskFrequency? = id?.let { _taskFrequenciesMap.value[it] } fun getTaskFrequency(id: Int?): TaskFrequency? = id?.let { _taskFrequenciesMap.value[it] }
fun getTaskPriority(id: Int?): TaskPriority? = id?.let { _taskPrioritiesMap.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 getTaskCategory(id: Int?): TaskCategory? = id?.let { _taskCategoriesMap.value[it] }
fun getContractorSpecialty(id: Int?): ContractorSpecialty? = id?.let { _contractorSpecialtiesMap.value[it] } fun getContractorSpecialty(id: Int?): ContractorSpecialty? = id?.let { _contractorSpecialtiesMap.value[it] }
@@ -533,12 +526,6 @@ object DataManager {
persistToDisk() persistToDisk()
} }
fun setTaskStatuses(statuses: List<TaskStatus>) {
_taskStatuses.value = statuses
_taskStatusesMap.value = statuses.associateBy { it.id }
persistToDisk()
}
fun setTaskCategories(categories: List<TaskCategory>) { fun setTaskCategories(categories: List<TaskCategory>) {
_taskCategories.value = categories _taskCategories.value = categories
_taskCategoriesMap.value = categories.associateBy { it.id } _taskCategoriesMap.value = categories.associateBy { it.id }
@@ -583,7 +570,6 @@ object DataManager {
setResidenceTypes(staticData.residenceTypes) setResidenceTypes(staticData.residenceTypes)
setTaskFrequencies(staticData.taskFrequencies) setTaskFrequencies(staticData.taskFrequencies)
setTaskPriorities(staticData.taskPriorities) setTaskPriorities(staticData.taskPriorities)
setTaskStatuses(staticData.taskStatuses)
setTaskCategories(staticData.taskCategories) setTaskCategories(staticData.taskCategories)
setContractorSpecialties(staticData.contractorSpecialties) setContractorSpecialties(staticData.contractorSpecialties)
_lookupsInitialized.value = true _lookupsInitialized.value = true
@@ -598,7 +584,6 @@ object DataManager {
setResidenceTypes(seededData.residenceTypes) setResidenceTypes(seededData.residenceTypes)
setTaskFrequencies(seededData.taskFrequencies) setTaskFrequencies(seededData.taskFrequencies)
setTaskPriorities(seededData.taskPriorities) setTaskPriorities(seededData.taskPriorities)
setTaskStatuses(seededData.taskStatuses)
setTaskCategories(seededData.taskCategories) setTaskCategories(seededData.taskCategories)
setContractorSpecialties(seededData.contractorSpecialties) setContractorSpecialties(seededData.contractorSpecialties)
setTaskTemplatesGrouped(seededData.taskTemplates) setTaskTemplatesGrouped(seededData.taskTemplates)
@@ -659,8 +644,6 @@ object DataManager {
_taskFrequenciesMap.value = emptyMap() _taskFrequenciesMap.value = emptyMap()
_taskPriorities.value = emptyList() _taskPriorities.value = emptyList()
_taskPrioritiesMap.value = emptyMap() _taskPrioritiesMap.value = emptyMap()
_taskStatuses.value = emptyList()
_taskStatusesMap.value = emptyMap()
_taskCategories.value = emptyList() _taskCategories.value = emptyList()
_taskCategoriesMap.value = emptyMap() _taskCategoriesMap.value = emptyMap()
_contractorSpecialties.value = emptyList() _contractorSpecialties.value = emptyList()
@@ -756,9 +739,6 @@ object DataManager {
if (_taskPriorities.value.isNotEmpty()) { if (_taskPriorities.value.isNotEmpty()) {
manager.save(KEY_TASK_PRIORITIES, json.encodeToString(_taskPriorities.value)) 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()) { if (_taskCategories.value.isNotEmpty()) {
manager.save(KEY_TASK_CATEGORIES, json.encodeToString(_taskCategories.value)) manager.save(KEY_TASK_CATEGORIES, json.encodeToString(_taskCategories.value))
} }
@@ -832,12 +812,6 @@ object DataManager {
_taskPrioritiesMap.value = priorities.associateBy { it.id } _taskPrioritiesMap.value = priorities.associateBy { it.id }
} }
manager.load(KEY_TASK_STATUSES)?.let { data ->
val statuses = json.decodeFromString<List<TaskStatus>>(data)
_taskStatuses.value = statuses
_taskStatusesMap.value = statuses.associateBy { it.id }
}
manager.load(KEY_TASK_CATEGORIES)?.let { data -> manager.load(KEY_TASK_CATEGORIES)?.let { data ->
val categories = json.decodeFromString<List<TaskCategory>>(data) val categories = json.decodeFromString<List<TaskCategory>>(data)
_taskCategories.value = categories _taskCategories.value = categories
@@ -874,7 +848,6 @@ object DataManager {
private const val KEY_RESIDENCE_TYPES = "dm_residence_types" private const val KEY_RESIDENCE_TYPES = "dm_residence_types"
private const val KEY_TASK_FREQUENCIES = "dm_task_frequencies" private const val KEY_TASK_FREQUENCIES = "dm_task_frequencies"
private const val KEY_TASK_PRIORITIES = "dm_task_priorities" 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_TASK_CATEGORIES = "dm_task_categories"
private const val KEY_CONTRACTOR_SPECIALTIES = "dm_contractor_specialties" private const val KEY_CONTRACTOR_SPECIALTIES = "dm_contractor_specialties"
private const val KEY_TASK_TEMPLATES_GROUPED = "dm_task_templates_grouped" private const val KEY_TASK_TEMPLATES_GROUPED = "dm_task_templates_grouped"

View File

@@ -40,8 +40,7 @@ data class TaskResponse(
val category: TaskCategory? = null, val category: TaskCategory? = null,
@SerialName("priority_id") val priorityId: Int? = null, @SerialName("priority_id") val priorityId: Int? = null,
val priority: TaskPriority? = null, val priority: TaskPriority? = null,
@SerialName("status_id") val statusId: Int? = null, @SerialName("in_progress") val inProgress: Boolean = false,
val status: TaskStatus? = null,
@SerialName("frequency_id") val frequencyId: Int? = null, @SerialName("frequency_id") val frequencyId: Int? = null,
val frequency: TaskFrequency? = null, val frequency: TaskFrequency? = null,
@SerialName("due_date") val dueDate: String? = null, @SerialName("due_date") val dueDate: String? = null,
@@ -74,11 +73,10 @@ data class TaskResponse(
val frequencyDaySpan: Int? get() = frequency?.days val frequencyDaySpan: Int? get() = frequency?.days
val priorityName: String? get() = priority?.name val priorityName: String? get() = priority?.name
val priorityDisplayName: String? get() = priority?.displayName val priorityDisplayName: String? get() = priority?.displayName
val statusName: String? get() = status?.name
// Fields that don't exist in Go API - return null/default // Fields that don't exist in Go API - return null/default
val nextScheduledDate: String? get() = null // Would need calculation based on frequency 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 daySpan: Int? get() = frequency?.days
val notifyDays: Int? get() = null // Not in Go API val notifyDays: Int? get() = null // Not in Go API
} }
@@ -119,7 +117,7 @@ data class TaskCreateRequest(
val description: String? = null, val description: String? = null,
@SerialName("category_id") val categoryId: Int? = null, @SerialName("category_id") val categoryId: Int? = null,
@SerialName("priority_id") val priorityId: 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("frequency_id") val frequencyId: Int? = null,
@SerialName("assigned_to_id") val assignedToId: Int? = null, @SerialName("assigned_to_id") val assignedToId: Int? = null,
@SerialName("due_date") val dueDate: String? = null, @SerialName("due_date") val dueDate: String? = null,
@@ -137,7 +135,7 @@ data class TaskUpdateRequest(
val description: String? = null, val description: String? = null,
@SerialName("category_id") val categoryId: Int? = null, @SerialName("category_id") val categoryId: Int? = null,
@SerialName("priority_id") val priorityId: 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("frequency_id") val frequencyId: Int? = null,
@SerialName("assigned_to_id") val assignedToId: Int? = null, @SerialName("assigned_to_id") val assignedToId: Int? = null,
@SerialName("due_date") val dueDate: String? = null, @SerialName("due_date") val dueDate: String? = null,
@@ -176,7 +174,8 @@ data class TaskColumn(
data class TaskColumnsResponse( data class TaskColumnsResponse(
val columns: List<TaskColumn>, val columns: List<TaskColumn>,
@SerialName("days_threshold") val daysThreshold: Int, @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 @Serializable
data class TaskPatchRequest( 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_archived") val archived: Boolean? = null,
@SerialName("is_cancelled") val cancelled: Boolean? = null @SerialName("is_cancelled") val cancelled: Boolean? = null
) )

View File

@@ -44,22 +44,6 @@ data class TaskPriority(
get() = name 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 * Task category lookup - matching Go API TaskCategoryResponse
*/ */
@@ -104,7 +88,6 @@ data class StaticDataResponse(
@SerialName("residence_types") val residenceTypes: List<ResidenceType>, @SerialName("residence_types") val residenceTypes: List<ResidenceType>,
@SerialName("task_frequencies") val taskFrequencies: List<TaskFrequency>, @SerialName("task_frequencies") val taskFrequencies: List<TaskFrequency>,
@SerialName("task_priorities") val taskPriorities: List<TaskPriority>, @SerialName("task_priorities") val taskPriorities: List<TaskPriority>,
@SerialName("task_statuses") val taskStatuses: List<TaskStatus>,
@SerialName("task_categories") val taskCategories: List<TaskCategory>, @SerialName("task_categories") val taskCategories: List<TaskCategory>,
@SerialName("contractor_specialties") val contractorSpecialties: List<ContractorSpecialty> @SerialName("contractor_specialties") val contractorSpecialties: List<ContractorSpecialty>
) )
@@ -119,7 +102,6 @@ data class SeededDataResponse(
@SerialName("task_categories") val taskCategories: List<TaskCategory>, @SerialName("task_categories") val taskCategories: List<TaskCategory>,
@SerialName("task_priorities") val taskPriorities: List<TaskPriority>, @SerialName("task_priorities") val taskPriorities: List<TaskPriority>,
@SerialName("task_frequencies") val taskFrequencies: List<TaskFrequency>, @SerialName("task_frequencies") val taskFrequencies: List<TaskFrequency>,
@SerialName("task_statuses") val taskStatuses: List<TaskStatus>,
@SerialName("contractor_specialties") val contractorSpecialties: List<ContractorSpecialty>, @SerialName("contractor_specialties") val contractorSpecialties: List<ContractorSpecialty>,
@SerialName("task_templates") val taskTemplates: TaskTemplatesGroupedResponse @SerialName("task_templates") val taskTemplates: TaskTemplatesGroupedResponse
) )
@@ -145,12 +127,6 @@ data class TaskPriorityResponse(
val results: List<TaskPriority> val results: List<TaskPriority>
) )
@Serializable
data class TaskStatusResponse(
val count: Int,
val results: List<TaskStatus>
)
@Serializable @Serializable
data class TaskCategoryResponse( data class TaskCategoryResponse(
val count: Int, val count: Int,

View File

@@ -60,8 +60,7 @@ data class EditTaskRoute(
val frequencyName: String, val frequencyName: String,
val priorityId: Int, val priorityId: Int,
val priorityName: String, val priorityName: String,
val statusId: Int?, val inProgress: Boolean,
val statusName: String?,
val dueDate: String?, val dueDate: String?,
val estimatedCost: String?, val estimatedCost: String?,
val createdAt: String, val createdAt: String,

View File

@@ -271,27 +271,6 @@ object APILayer {
return result return result
} }
/**
* Get task statuses from DataManager. If cache is empty, fetch from API.
*/
suspend fun getTaskStatuses(forceRefresh: Boolean = false): ApiResult<List<TaskStatus>> {
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. * 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<TaskResponse> { suspend fun cancelTask(taskId: Int): ApiResult<TaskResponse> {
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401) val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
val result = taskApi.cancelTask(token, taskId)
val cancelledStatusId = getStatusIdByName("cancelled")
?: return ApiResult.Error("Cancelled status not found in cache")
val result = taskApi.cancelTask(token, taskId, cancelledStatusId)
if (result is ApiResult.Success) { if (result is ApiResult.Success) {
DataManager.setTotalSummary(result.data.summary) DataManager.setTotalSummary(result.data.summary)
@@ -625,11 +591,7 @@ object APILayer {
suspend fun uncancelTask(taskId: Int): ApiResult<TaskResponse> { suspend fun uncancelTask(taskId: Int): ApiResult<TaskResponse> {
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401) val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
val result = taskApi.uncancelTask(token, taskId)
val pendingStatusId = getStatusIdByName("pending")
?: return ApiResult.Error("Pending status not found in cache")
val result = taskApi.uncancelTask(token, taskId, pendingStatusId)
if (result is ApiResult.Success) { if (result is ApiResult.Success) {
DataManager.setTotalSummary(result.data.summary) DataManager.setTotalSummary(result.data.summary)
@@ -645,12 +607,23 @@ object APILayer {
suspend fun markInProgress(taskId: Int): ApiResult<TaskResponse> { suspend fun markInProgress(taskId: Int): ApiResult<TaskResponse> {
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401) val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
val result = taskApi.markInProgress(token, taskId)
val inProgressStatusId = getStatusIdByName("in progress") if (result is ApiResult.Success) {
?: getStatusIdByName("In Progress") DataManager.setTotalSummary(result.data.summary)
?: return ApiResult.Error("In Progress status not found in cache") 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<TaskResponse> {
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
val result = taskApi.clearInProgress(token, taskId)
if (result is ApiResult.Success) { if (result is ApiResult.Success) {
DataManager.setTotalSummary(result.data.summary) DataManager.setTotalSummary(result.data.summary)

View File

@@ -81,22 +81,6 @@ class LookupsApi(private val client: HttpClient = ApiClient.httpClient) {
} }
} }
suspend fun getTaskStatuses(token: String): ApiResult<List<TaskStatus>> {
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<List<TaskCategory>> { suspend fun getTaskCategories(token: String): ApiResult<List<TaskCategory>> {
return try { return try {
val response = client.get("$baseUrl/task-categories/") { val response = client.get("$baseUrl/task-categories/") {

View File

@@ -149,26 +149,57 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) {
} }
// Convenience methods for common task actions // 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<WithSummaryResponse<TaskResponse>> { suspend fun cancelTask(token: String, id: Int): ApiResult<WithSummaryResponse<TaskResponse>> {
return patchTask(token, id, TaskPatchRequest(status = cancelledStatusId)) return postTaskAction(token, id, "cancel")
} }
suspend fun uncancelTask(token: String, id: Int, pendingStatusId: Int): ApiResult<WithSummaryResponse<TaskResponse>> { suspend fun uncancelTask(token: String, id: Int): ApiResult<WithSummaryResponse<TaskResponse>> {
return patchTask(token, id, TaskPatchRequest(status = pendingStatusId)) return postTaskAction(token, id, "uncancel")
} }
suspend fun markInProgress(token: String, id: Int, inProgressStatusId: Int): ApiResult<WithSummaryResponse<TaskResponse>> { suspend fun markInProgress(token: String, id: Int): ApiResult<WithSummaryResponse<TaskResponse>> {
return patchTask(token, id, TaskPatchRequest(status = inProgressStatusId)) return patchTask(token, id, TaskPatchRequest(inProgress = true))
}
suspend fun clearInProgress(token: String, id: Int): ApiResult<WithSummaryResponse<TaskResponse>> {
return patchTask(token, id, TaskPatchRequest(inProgress = false))
} }
suspend fun archiveTask(token: String, id: Int): ApiResult<WithSummaryResponse<TaskResponse>> { suspend fun archiveTask(token: String, id: Int): ApiResult<WithSummaryResponse<TaskResponse>> {
return patchTask(token, id, TaskPatchRequest(archived = true)) return postTaskAction(token, id, "archive")
} }
suspend fun unarchiveTask(token: String, id: Int): ApiResult<WithSummaryResponse<TaskResponse>> { suspend fun unarchiveTask(token: String, id: Int): ApiResult<WithSummaryResponse<TaskResponse>> {
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<WithSummaryResponse<TaskResponse>> {
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<WithSummaryResponse<TaskResponse>>()
ApiResult.Success(data)
}
HttpStatusCode.NotFound -> ApiResult.Error("Task not found", 404)
HttpStatusCode.Forbidden -> ApiResult.Error("Access denied", 403)
HttpStatusCode.BadRequest -> {
val errorBody = response.body<String>()
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")
}
} }
/** /**

View File

@@ -22,7 +22,6 @@ object LookupsRepository {
val residenceTypes: StateFlow<List<ResidenceType>> = DataManager.residenceTypes val residenceTypes: StateFlow<List<ResidenceType>> = DataManager.residenceTypes
val taskFrequencies: StateFlow<List<TaskFrequency>> = DataManager.taskFrequencies val taskFrequencies: StateFlow<List<TaskFrequency>> = DataManager.taskFrequencies
val taskPriorities: StateFlow<List<TaskPriority>> = DataManager.taskPriorities val taskPriorities: StateFlow<List<TaskPriority>> = DataManager.taskPriorities
val taskStatuses: StateFlow<List<TaskStatus>> = DataManager.taskStatuses
val taskCategories: StateFlow<List<TaskCategory>> = DataManager.taskCategories val taskCategories: StateFlow<List<TaskCategory>> = DataManager.taskCategories
val contractorSpecialties: StateFlow<List<ContractorSpecialty>> = DataManager.contractorSpecialties val contractorSpecialties: StateFlow<List<ContractorSpecialty>> = DataManager.contractorSpecialties
val isInitialized: StateFlow<Boolean> = DataManager.lookupsInitialized val isInitialized: StateFlow<Boolean> = DataManager.lookupsInitialized

View File

@@ -449,7 +449,7 @@ fun AddTaskDialog(
categoryId = if (category.id > 0) category.id else null, categoryId = if (category.id > 0) category.id else null,
frequencyId = if (frequency.id > 0) frequency.id else null, frequencyId = if (frequency.id > 0) frequency.id else null,
priorityId = if (priority.id > 0) priority.id else null, priorityId = if (priority.id > 0) priority.id else null,
statusId = null, inProgress = false,
dueDate = dueDate, dueDate = dueDate,
estimatedCost = estimatedCost.ifBlank { null }?.toDoubleOrNull() estimatedCost = estimatedCost.ifBlank { null }?.toDoubleOrNull()
) )

View File

@@ -18,7 +18,6 @@ import com.example.casera.models.TaskDetail
import com.example.casera.models.TaskCategory import com.example.casera.models.TaskCategory
import com.example.casera.models.TaskPriority import com.example.casera.models.TaskPriority
import com.example.casera.models.TaskFrequency import com.example.casera.models.TaskFrequency
import com.example.casera.models.TaskStatus
import com.example.casera.models.TaskCompletion import com.example.casera.models.TaskCompletion
import com.example.casera.util.DateUtils import com.example.casera.util.DateUtils
import org.jetbrains.compose.ui.tooling.preview.Preview import org.jetbrains.compose.ui.tooling.preview.Preview
@@ -108,21 +107,15 @@ fun TaskCard(
) )
} }
// Status badge with semantic colors // In Progress badge
if (task.status != null) { if (task.inProgress) {
val statusColor = when (task.status.name.lowercase()) { val statusColor = MaterialTheme.colorScheme.tertiary
"completed" -> MaterialTheme.colorScheme.secondary
"in_progress" -> MaterialTheme.colorScheme.tertiary
"pending" -> MaterialTheme.colorScheme.tertiary
"cancelled" -> MaterialTheme.colorScheme.onSurfaceVariant
else -> MaterialTheme.colorScheme.onSurfaceVariant
}
Surface( Surface(
color = statusColor.copy(alpha = 0.15f), color = statusColor.copy(alpha = 0.15f),
shape = RoundedCornerShape(12.dp) shape = RoundedCornerShape(12.dp)
) { ) {
Text( Text(
text = task.status.name.replace("_", " ").uppercase(), text = "IN PROGRESS",
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = statusColor color = statusColor
@@ -604,7 +597,7 @@ fun TaskCardPreview() {
frequency = TaskFrequency( frequency = TaskFrequency(
id = 1, name = "monthly", days = 30 id = 1, name = "monthly", days = 30
), ),
status = TaskStatus(id = 1, name = "pending"), inProgress = false,
dueDate = "2024-12-15", dueDate = "2024-12-15",
estimatedCost = 150.00, estimatedCost = 150.00,
createdAt = "2024-01-01T00:00:00Z", createdAt = "2024-01-01T00:00:00Z",

View File

@@ -33,20 +33,18 @@ fun EditTaskScreen(
var selectedCategory by remember { mutableStateOf<TaskCategory?>(task.category) } var selectedCategory by remember { mutableStateOf<TaskCategory?>(task.category) }
var selectedFrequency by remember { mutableStateOf<TaskFrequency?>(task.frequency) } var selectedFrequency by remember { mutableStateOf<TaskFrequency?>(task.frequency) }
var selectedPriority by remember { mutableStateOf<TaskPriority?>(task.priority) } var selectedPriority by remember { mutableStateOf<TaskPriority?>(task.priority) }
var selectedStatus by remember { mutableStateOf<TaskStatus?>(task.status) } var inProgress by remember { mutableStateOf(task.inProgress) }
var dueDate by remember { mutableStateOf(task.dueDate ?: "") } var dueDate by remember { mutableStateOf(task.dueDate ?: "") }
var estimatedCost by remember { mutableStateOf(task.estimatedCost?.toString() ?: "") } var estimatedCost by remember { mutableStateOf(task.estimatedCost?.toString() ?: "") }
var categoryExpanded by remember { mutableStateOf(false) } var categoryExpanded by remember { mutableStateOf(false) }
var frequencyExpanded by remember { mutableStateOf(false) } var frequencyExpanded by remember { mutableStateOf(false) }
var priorityExpanded by remember { mutableStateOf(false) } var priorityExpanded by remember { mutableStateOf(false) }
var statusExpanded by remember { mutableStateOf(false) }
val updateTaskState by viewModel.updateTaskState.collectAsState() val updateTaskState by viewModel.updateTaskState.collectAsState()
val categories by LookupsRepository.taskCategories.collectAsState() val categories by LookupsRepository.taskCategories.collectAsState()
val frequencies by LookupsRepository.taskFrequencies.collectAsState() val frequencies by LookupsRepository.taskFrequencies.collectAsState()
val priorities by LookupsRepository.taskPriorities.collectAsState() val priorities by LookupsRepository.taskPriorities.collectAsState()
val statuses by LookupsRepository.taskStatuses.collectAsState()
// Validation errors // Validation errors
var titleError by remember { mutableStateOf("") } var titleError by remember { mutableStateOf("") }
@@ -235,36 +233,20 @@ fun EditTaskScreen(
} }
} }
// Status dropdown // In Progress toggle
ExposedDropdownMenuBox( Row(
expanded = statusExpanded, modifier = Modifier.fillMaxWidth(),
onExpandedChange = { statusExpanded = it } horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
) { ) {
OutlinedTextField( Text(
value = selectedStatus?.name?.replaceFirstChar { it.uppercase() } ?: "", text = stringResource(Res.string.tasks_in_progress_label),
onValueChange = {}, style = MaterialTheme.typography.bodyLarge
readOnly = true, )
label = { Text(stringResource(Res.string.tasks_status_label)) }, Switch(
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = statusExpanded) }, checked = inProgress,
modifier = Modifier onCheckedChange = { inProgress = it }
.fillMaxWidth()
.menuAnchor(),
enabled = statuses.isNotEmpty()
) )
ExposedDropdownMenu(
expanded = statusExpanded,
onDismissRequest = { statusExpanded = false }
) {
statuses.forEach { status ->
DropdownMenuItem(
text = { Text(status.name.replaceFirstChar { it.uppercase() }) },
onClick = {
selectedStatus = status
statusExpanded = false
}
)
}
}
} }
OutlinedTextField( OutlinedTextField(
@@ -301,8 +283,7 @@ fun EditTaskScreen(
Button( Button(
onClick = { onClick = {
if (validateForm() && selectedCategory != null && if (validateForm() && selectedCategory != null &&
selectedFrequency != null && selectedPriority != null && selectedFrequency != null && selectedPriority != null) {
selectedStatus != null) {
viewModel.updateTask( viewModel.updateTask(
taskId = task.id, taskId = task.id,
request = TaskCreateRequest( request = TaskCreateRequest(
@@ -312,7 +293,7 @@ fun EditTaskScreen(
categoryId = selectedCategory!!.id, categoryId = selectedCategory!!.id,
frequencyId = selectedFrequency!!.id, frequencyId = selectedFrequency!!.id,
priorityId = selectedPriority!!.id, priorityId = selectedPriority!!.id,
statusId = selectedStatus!!.id, inProgress = inProgress,
dueDate = dueDate, dueDate = dueDate,
estimatedCost = estimatedCost.ifBlank { null }?.toDoubleOrNull() estimatedCost = estimatedCost.ifBlank { null }?.toDoubleOrNull()
) )
@@ -321,8 +302,7 @@ fun EditTaskScreen(
}, },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
enabled = validateForm() && selectedCategory != null && enabled = validateForm() && selectedCategory != null &&
selectedFrequency != null && selectedPriority != null && selectedFrequency != null && selectedPriority != null
selectedStatus != null
) { ) {
if (updateTaskState is ApiResult.Loading) { if (updateTaskState is ApiResult.Loading) {
CircularProgressIndicator( CircularProgressIndicator(

View File

@@ -346,7 +346,7 @@ fun OnboardingFirstTaskContent(
description = null, description = null,
categoryId = categoryId, categoryId = categoryId,
priorityId = null, priorityId = null,
statusId = null, inProgress = false,
frequencyId = frequencyId, frequencyId = frequencyId,
assignedToId = null, assignedToId = null,
dueDate = today, dueDate = today,

View File

@@ -21,7 +21,6 @@ class LookupsViewModel : ViewModel() {
val residenceTypes: StateFlow<List<ResidenceType>> = DataManager.residenceTypes val residenceTypes: StateFlow<List<ResidenceType>> = DataManager.residenceTypes
val taskFrequencies: StateFlow<List<TaskFrequency>> = DataManager.taskFrequencies val taskFrequencies: StateFlow<List<TaskFrequency>> = DataManager.taskFrequencies
val taskPriorities: StateFlow<List<TaskPriority>> = DataManager.taskPriorities val taskPriorities: StateFlow<List<TaskPriority>> = DataManager.taskPriorities
val taskStatuses: StateFlow<List<TaskStatus>> = DataManager.taskStatuses
val taskCategories: StateFlow<List<TaskCategory>> = DataManager.taskCategories val taskCategories: StateFlow<List<TaskCategory>> = DataManager.taskCategories
val contractorSpecialties: StateFlow<List<ContractorSpecialty>> = DataManager.contractorSpecialties val contractorSpecialties: StateFlow<List<ContractorSpecialty>> = DataManager.contractorSpecialties
@@ -35,9 +34,6 @@ class LookupsViewModel : ViewModel() {
private val _taskPrioritiesState = MutableStateFlow<ApiResult<List<TaskPriority>>>(ApiResult.Idle) private val _taskPrioritiesState = MutableStateFlow<ApiResult<List<TaskPriority>>>(ApiResult.Idle)
val taskPrioritiesState: StateFlow<ApiResult<List<TaskPriority>>> = _taskPrioritiesState val taskPrioritiesState: StateFlow<ApiResult<List<TaskPriority>>> = _taskPrioritiesState
private val _taskStatusesState = MutableStateFlow<ApiResult<List<TaskStatus>>>(ApiResult.Idle)
val taskStatusesState: StateFlow<ApiResult<List<TaskStatus>>> = _taskStatusesState
private val _taskCategoriesState = MutableStateFlow<ApiResult<List<TaskCategory>>>(ApiResult.Idle) private val _taskCategoriesState = MutableStateFlow<ApiResult<List<TaskCategory>>>(ApiResult.Idle)
val taskCategoriesState: StateFlow<ApiResult<List<TaskCategory>>> = _taskCategoriesState val taskCategoriesState: StateFlow<ApiResult<List<TaskCategory>>> = _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() { fun loadTaskCategories() {
viewModelScope.launch { viewModelScope.launch {
val cached = DataManager.taskCategories.value val cached = DataManager.taskCategories.value
@@ -111,7 +94,6 @@ class LookupsViewModel : ViewModel() {
loadResidenceTypes() loadResidenceTypes()
loadTaskFrequencies() loadTaskFrequencies()
loadTaskPriorities() loadTaskPriorities()
loadTaskStatuses()
loadTaskCategories() loadTaskCategories()
} }
} }

View File

@@ -64,14 +64,15 @@ class CacheManager {
let title: String let title: String
let description: String? let description: String?
let priority: String? let priority: String?
let status: String? let inProgress: Bool
let dueDate: String? let dueDate: String?
let category: String? let category: String?
let residenceName: String? let residenceName: String?
let isOverdue: Bool let isOverdue: Bool
enum CodingKeys: String, CodingKey { 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 dueDate = "due_date"
case residenceName = "residence_name" case residenceName = "residence_name"
case isOverdue = "is_overdue" case isOverdue = "is_overdue"
@@ -126,12 +127,11 @@ class CacheManager {
static func getUpcomingTasks() -> [CustomTask] { static func getUpcomingTasks() -> [CustomTask] {
let allTasks = getData() 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 // Also exclude tasks that are pending completion via widget
let upcoming = allTasks.filter { task in let upcoming = allTasks.filter { task in
let status = task.status?.lowercased() ?? "" // Include if: not pending completion
let isActive = status == "pending" || status == "in_progress" || status == "in progress" return task.shouldShow
return isActive && task.shouldShow
} }
// Sort by due date (earliest first), with overdue at top // Sort by due date (earliest first), with overdue at top
@@ -444,6 +444,10 @@ struct InteractiveTaskRowView: View {
} }
private var priorityColor: Color { private var priorityColor: Color {
// Overdue tasks are always red
if task.isOverdue {
return .red
}
switch task.priority?.lowercased() { switch task.priority?.lowercased() {
case "urgent": return .red case "urgent": return .red
case "high": return .orange case "high": return .orange
@@ -565,6 +569,10 @@ struct LargeInteractiveTaskRowView: View {
} }
private var priorityColor: Color { private var priorityColor: Color {
// Overdue tasks are always red
if task.isOverdue {
return .red
}
switch task.priority?.lowercased() { switch task.priority?.lowercased() {
case "urgent": return .red case "urgent": return .red
case "high": return .orange case "high": return .orange
@@ -601,7 +609,7 @@ struct Casera: Widget {
title: "Fix leaky faucet", title: "Fix leaky faucet",
description: "Kitchen sink needs repair", description: "Kitchen sink needs repair",
priority: "high", priority: "high",
status: "pending", inProgress: false,
dueDate: "2024-12-15", dueDate: "2024-12-15",
category: "plumbing", category: "plumbing",
residenceName: "Home", residenceName: "Home",
@@ -612,7 +620,7 @@ struct Casera: Widget {
title: "Paint living room", title: "Paint living room",
description: nil, description: nil,
priority: "medium", priority: "medium",
status: "pending", inProgress: false,
dueDate: "2024-12-20", dueDate: "2024-12-20",
category: "painting", category: "painting",
residenceName: "Home", residenceName: "Home",
@@ -632,7 +640,7 @@ struct Casera: Widget {
title: "Fix leaky faucet", title: "Fix leaky faucet",
description: nil, description: nil,
priority: "high", priority: "high",
status: "pending", inProgress: false,
dueDate: "2024-12-15", dueDate: "2024-12-15",
category: "plumbing", category: "plumbing",
residenceName: "Home", residenceName: "Home",
@@ -643,7 +651,7 @@ struct Casera: Widget {
title: "Paint living room", title: "Paint living room",
description: nil, description: nil,
priority: "medium", priority: "medium",
status: "pending", inProgress: false,
dueDate: "2024-12-20", dueDate: "2024-12-20",
category: "painting", category: "painting",
residenceName: "Home", residenceName: "Home",
@@ -654,7 +662,7 @@ struct Casera: Widget {
title: "Clean gutters", title: "Clean gutters",
description: nil, description: nil,
priority: "low", priority: "low",
status: "pending", inProgress: false,
dueDate: "2024-12-25", dueDate: "2024-12-25",
category: "maintenance", category: "maintenance",
residenceName: "Home", residenceName: "Home",
@@ -684,7 +692,7 @@ struct Casera: Widget {
title: "Fix leaky faucet", title: "Fix leaky faucet",
description: "Kitchen sink needs repair", description: "Kitchen sink needs repair",
priority: "high", priority: "high",
status: "pending", inProgress: false,
dueDate: "2024-12-15", dueDate: "2024-12-15",
category: "plumbing", category: "plumbing",
residenceName: "Home", residenceName: "Home",
@@ -695,7 +703,7 @@ struct Casera: Widget {
title: "Paint living room", title: "Paint living room",
description: nil, description: nil,
priority: "medium", priority: "medium",
status: "in_progress", inProgress: true,
dueDate: "2024-12-20", dueDate: "2024-12-20",
category: "painting", category: "painting",
residenceName: "Cabin", residenceName: "Cabin",
@@ -706,7 +714,7 @@ struct Casera: Widget {
title: "Clean gutters", title: "Clean gutters",
description: "Remove debris", description: "Remove debris",
priority: "low", priority: "low",
status: "pending", inProgress: false,
dueDate: "2024-12-25", dueDate: "2024-12-25",
category: "maintenance", category: "maintenance",
residenceName: "Home", residenceName: "Home",
@@ -726,7 +734,7 @@ struct Casera: Widget {
title: "Fix leaky faucet", title: "Fix leaky faucet",
description: nil, description: nil,
priority: "high", priority: "high",
status: "pending", inProgress: false,
dueDate: nil, dueDate: nil,
category: nil, category: nil,
residenceName: nil, residenceName: nil,
@@ -737,7 +745,7 @@ struct Casera: Widget {
title: "Paint living room", title: "Paint living room",
description: nil, description: nil,
priority: "medium", priority: "medium",
status: "pending", inProgress: false,
dueDate: nil, dueDate: nil,
category: nil, category: nil,
residenceName: nil, residenceName: nil,
@@ -748,7 +756,7 @@ struct Casera: Widget {
title: "Clean gutters", title: "Clean gutters",
description: nil, description: nil,
priority: "low", priority: "low",
status: "pending", inProgress: false,
dueDate: nil, dueDate: nil,
category: nil, category: nil,
residenceName: nil, residenceName: nil,
@@ -759,7 +767,7 @@ struct Casera: Widget {
title: "Replace HVAC filter", title: "Replace HVAC filter",
description: nil, description: nil,
priority: "medium", priority: "medium",
status: "pending", inProgress: false,
dueDate: nil, dueDate: nil,
category: nil, category: nil,
residenceName: nil, residenceName: nil,
@@ -770,7 +778,7 @@ struct Casera: Widget {
title: "Check smoke detectors", title: "Check smoke detectors",
description: nil, description: nil,
priority: "high", priority: "high",
status: "pending", inProgress: false,
dueDate: nil, dueDate: nil,
category: nil, category: nil,
residenceName: nil, residenceName: nil,
@@ -800,7 +808,7 @@ struct Casera: Widget {
title: "Fix leaky faucet in kitchen", title: "Fix leaky faucet in kitchen",
description: "Kitchen sink needs repair", description: "Kitchen sink needs repair",
priority: "high", priority: "high",
status: "pending", inProgress: false,
dueDate: "2024-12-15", dueDate: "2024-12-15",
category: "plumbing", category: "plumbing",
residenceName: "Home", residenceName: "Home",
@@ -811,7 +819,7 @@ struct Casera: Widget {
title: "Paint living room walls", title: "Paint living room walls",
description: nil, description: nil,
priority: "medium", priority: "medium",
status: "in_progress", inProgress: true,
dueDate: "2024-12-20", dueDate: "2024-12-20",
category: "painting", category: "painting",
residenceName: "Cabin", residenceName: "Cabin",
@@ -822,7 +830,7 @@ struct Casera: Widget {
title: "Clean gutters", title: "Clean gutters",
description: "Remove debris", description: "Remove debris",
priority: "low", priority: "low",
status: "pending", inProgress: false,
dueDate: "2024-12-25", dueDate: "2024-12-25",
category: "maintenance", category: "maintenance",
residenceName: "Home", residenceName: "Home",
@@ -833,7 +841,7 @@ struct Casera: Widget {
title: "Replace HVAC filter", title: "Replace HVAC filter",
description: nil, description: nil,
priority: "medium", priority: "medium",
status: "pending", inProgress: false,
dueDate: "2024-12-28", dueDate: "2024-12-28",
category: "hvac", category: "hvac",
residenceName: "Beach House", residenceName: "Beach House",
@@ -844,7 +852,7 @@ struct Casera: Widget {
title: "Check smoke detectors", title: "Check smoke detectors",
description: "Replace batteries if needed", description: "Replace batteries if needed",
priority: "high", priority: "high",
status: "pending", inProgress: false,
dueDate: "2024-12-30", dueDate: "2024-12-30",
category: "safety", category: "safety",
residenceName: "Home", residenceName: "Home",
@@ -855,7 +863,7 @@ struct Casera: Widget {
title: "Service water heater", title: "Service water heater",
description: nil, description: nil,
priority: "medium", priority: "medium",
status: "pending", inProgress: false,
dueDate: "2025-01-05", dueDate: "2025-01-05",
category: "plumbing", category: "plumbing",
residenceName: "Cabin", residenceName: "Cabin",
@@ -866,7 +874,7 @@ struct Casera: Widget {
title: "Inspect roof shingles", title: "Inspect roof shingles",
description: nil, description: nil,
priority: "low", priority: "low",
status: "pending", inProgress: false,
dueDate: "2025-01-10", dueDate: "2025-01-10",
category: "exterior", category: "exterior",
residenceName: "Home", residenceName: "Home",
@@ -877,7 +885,7 @@ struct Casera: Widget {
title: "Clean dryer vent", title: "Clean dryer vent",
description: "Fire hazard prevention", description: "Fire hazard prevention",
priority: "urgent", priority: "urgent",
status: "pending", inProgress: false,
dueDate: "2025-01-12", dueDate: "2025-01-12",
category: "appliances", category: "appliances",
residenceName: "Beach House", residenceName: "Beach House",
@@ -897,7 +905,7 @@ struct Casera: Widget {
title: "Task 1", title: "Task 1",
description: nil, description: nil,
priority: "high", priority: "high",
status: "pending", inProgress: false,
dueDate: nil, dueDate: nil,
category: nil, category: nil,
residenceName: nil, residenceName: nil,
@@ -908,7 +916,7 @@ struct Casera: Widget {
title: "Task 2", title: "Task 2",
description: nil, description: nil,
priority: "medium", priority: "medium",
status: "pending", inProgress: false,
dueDate: nil, dueDate: nil,
category: nil, category: nil,
residenceName: nil, residenceName: nil,
@@ -919,7 +927,7 @@ struct Casera: Widget {
title: "Task 3", title: "Task 3",
description: nil, description: nil,
priority: "low", priority: "low",
status: "pending", inProgress: false,
dueDate: nil, dueDate: nil,
category: nil, category: nil,
residenceName: nil, residenceName: nil,
@@ -930,7 +938,7 @@ struct Casera: Widget {
title: "Task 4", title: "Task 4",
description: nil, description: nil,
priority: "medium", priority: "medium",
status: "pending", inProgress: false,
dueDate: nil, dueDate: nil,
category: nil, category: nil,
residenceName: nil, residenceName: nil,
@@ -941,7 +949,7 @@ struct Casera: Widget {
title: "Task 5", title: "Task 5",
description: nil, description: nil,
priority: "high", priority: "high",
status: "pending", inProgress: false,
dueDate: nil, dueDate: nil,
category: nil, category: nil,
residenceName: nil, residenceName: nil,
@@ -952,7 +960,7 @@ struct Casera: Widget {
title: "Task 6", title: "Task 6",
description: nil, description: nil,
priority: "low", priority: "low",
status: "pending", inProgress: false,
dueDate: nil, dueDate: nil,
category: nil, category: nil,
residenceName: nil, residenceName: nil,
@@ -963,7 +971,7 @@ struct Casera: Widget {
title: "Task 7", title: "Task 7",
description: nil, description: nil,
priority: "medium", priority: "medium",
status: "pending", inProgress: false,
dueDate: nil, dueDate: nil,
category: nil, category: nil,
residenceName: nil, residenceName: nil,

View File

@@ -64,7 +64,6 @@ class DataManagerObservable: ObservableObject {
@Published var residenceTypes: [ResidenceType] = [] @Published var residenceTypes: [ResidenceType] = []
@Published var taskFrequencies: [TaskFrequency] = [] @Published var taskFrequencies: [TaskFrequency] = []
@Published var taskPriorities: [TaskPriority] = [] @Published var taskPriorities: [TaskPriority] = []
@Published var taskStatuses: [TaskStatus] = []
@Published var taskCategories: [TaskCategory] = [] @Published var taskCategories: [TaskCategory] = []
@Published var contractorSpecialties: [ContractorSpecialty] = [] @Published var contractorSpecialties: [ContractorSpecialty] = []
@@ -292,16 +291,6 @@ class DataManagerObservable: ObservableObject {
} }
observationTasks.append(taskPrioritiesTask) 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 // Lookups - TaskCategories
let taskCategoriesTask = Task { let taskCategoriesTask = Task {
for await items in DataManager.shared.taskCategories { for await items in DataManager.shared.taskCategories {
@@ -459,12 +448,6 @@ class DataManagerObservable: ObservableObject {
return taskPriorities.first { $0.id == id } 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 /// Get task category by ID
func getTaskCategory(id: Int32?) -> TaskCategory? { func getTaskCategory(id: Int32?) -> TaskCategory? {
guard let id = id else { return nil } guard let id = id else { return nil }

View File

@@ -251,6 +251,7 @@ enum L10n {
// Task Card Actions // Task Card Actions
static var inProgress: String { String(localized: "tasks_in_progress") } 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 complete: String { String(localized: "tasks_complete") }
static var edit: String { String(localized: "tasks_edit") } static var edit: String { String(localized: "tasks_edit") }
static var cancel: String { String(localized: "tasks_cancel") } static var cancel: String { String(localized: "tasks_cancel") }

View File

@@ -93,6 +93,10 @@ final class WidgetActionProcessor {
let data = success.data { let data = success.data {
// Update widget with fresh data // Update widget with fresh data
WidgetDataManager.shared.saveTasks(from: 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 { } catch {
print("WidgetActionProcessor: Error refreshing tasks: \(error)") print("WidgetActionProcessor: Error refreshing tasks: \(error)")

View File

@@ -223,14 +223,15 @@ final class WidgetDataManager {
let title: String let title: String
let description: String? let description: String?
let priority: String? let priority: String?
let status: String? let inProgress: Bool
let dueDate: String? let dueDate: String?
let category: String? let category: String?
let residenceName: String? let residenceName: String?
let isOverdue: Bool let isOverdue: Bool
enum CodingKeys: String, CodingKey { 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 dueDate = "due_date"
case residenceName = "residence_name" case residenceName = "residence_name"
case isOverdue = "is_overdue" case isOverdue = "is_overdue"
@@ -273,7 +274,7 @@ final class WidgetDataManager {
title: task.title, title: task.title,
description: task.description_, description: task.description_,
priority: task.priority?.name ?? "", priority: task.priority?.name ?? "",
status: task.status?.name, inProgress: task.inProgress,
dueDate: task.dueDate, dueDate: task.dueDate,
category: task.category?.name ?? "", category: task.category?.name ?? "",
residenceName: "", // No longer available in API, residence lookup needed residenceName: "", // No longer available in API, residence lookup needed
@@ -325,14 +326,9 @@ final class WidgetDataManager {
func getUpcomingTasks() -> [WidgetTask] { func getUpcomingTasks() -> [WidgetTask] {
let allTasks = loadTasks() let allTasks = loadTasks()
// Filter for pending/in-progress tasks (non-archived, non-completed) // All loaded tasks are already filtered (archived and completed columns are excluded during save)
let upcoming = allTasks.filter { task in
let status = task.status?.lowercased() ?? ""
return status == "pending" || status == "in_progress" || status == "in progress"
}
// Sort by due date (earliest first), with overdue at top // 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 // Overdue tasks first
if task1.isOverdue != task2.isOverdue { if task1.isOverdue != task2.isOverdue {
return task1.isOverdue return task1.isOverdue

View File

@@ -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" : { "tasks_library" : {
"extractionState" : "manual", "extractionState" : "manual",
"localizations" : { "localizations" : {

View File

@@ -85,6 +85,10 @@ class AppleSignInViewModel: ObservableObject {
// - Initializes lookups // - Initializes lookups
// - Prefetches all data // - 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 // Track Apple Sign In
PostHogAnalytics.shared.capture(AnalyticsEvents.userSignedInApple, properties: [ PostHogAnalytics.shared.capture(AnalyticsEvents.userSignedInApple, properties: [
"is_new_user": isNewUser "is_new_user": isNewUser

View File

@@ -361,7 +361,7 @@ struct OnboardingFirstTaskContent: View {
description: nil, description: nil,
categoryId: categoryId.map { KotlinInt(int: $0) }, categoryId: categoryId.map { KotlinInt(int: $0) },
priorityId: nil, priorityId: nil,
statusId: nil, inProgress: false,
frequencyId: frequencyId.map { KotlinInt(int: $0) }, frequencyId: frequencyId.map { KotlinInt(int: $0) },
assignedToId: nil, assignedToId: nil,
dueDate: todayString, dueDate: todayString,

View File

@@ -506,8 +506,8 @@ private struct TasksSectionContainer: View {
selectedTaskForEdit = task selectedTaskForEdit = task
showEditTask = true showEditTask = true
}, },
onCancelTask: { taskId in onCancelTask: { task in
taskViewModel.cancelTask(id: taskId) { _ in taskViewModel.cancelTask(id: task.id) { _ in
reloadTasks() reloadTasks()
} }
}, },
@@ -526,12 +526,9 @@ private struct TasksSectionContainer: View {
onCompleteTask: { task in onCompleteTask: { task in
selectedTaskForComplete = task selectedTaskForComplete = task
}, },
onArchiveTask: { taskId in onArchiveTask: { task in
let allTasks = tasksResponse.columns.flatMap { $0.tasks } selectedTaskForArchive = task
if let task = allTasks.first(where: { $0.id == taskId }) { showArchiveConfirmation = true
selectedTaskForArchive = task
showArchiveConfirmation = true
}
}, },
onUnarchiveTask: { taskId in onUnarchiveTask: { taskId in
taskViewModel.unarchiveTask(id: taskId) { _ in taskViewModel.unarchiveTask(id: taskId) { _ in

View File

@@ -25,8 +25,8 @@ struct DynamicTaskCard: View {
.font(.title3) .font(.title3)
.foregroundColor(.primary) .foregroundColor(.primary)
if let status = task.status { if task.inProgress {
StatusBadge(status: status.name) StatusBadge(status: "in_progress")
} }
} }

View File

@@ -5,11 +5,11 @@ import ComposeApp
struct DynamicTaskColumnView: View { struct DynamicTaskColumnView: View {
let column: TaskColumn let column: TaskColumn
let onEditTask: (TaskResponse) -> Void let onEditTask: (TaskResponse) -> Void
let onCancelTask: (Int32) -> Void let onCancelTask: (TaskResponse) -> Void
let onUncancelTask: (Int32) -> Void let onUncancelTask: (Int32) -> Void
let onMarkInProgress: (Int32) -> Void let onMarkInProgress: (Int32) -> Void
let onCompleteTask: (TaskResponse) -> Void let onCompleteTask: (TaskResponse) -> Void
let onArchiveTask: (Int32) -> Void let onArchiveTask: (TaskResponse) -> Void
let onUnarchiveTask: (Int32) -> Void let onUnarchiveTask: (Int32) -> Void
// Get icon from API response, with fallback // Get icon from API response, with fallback
@@ -65,11 +65,11 @@ struct DynamicTaskColumnView: View {
task: task, task: task,
buttonTypes: column.buttonTypes, buttonTypes: column.buttonTypes,
onEdit: { onEditTask(task) }, onEdit: { onEditTask(task) },
onCancel: { onCancelTask(task.id) }, onCancel: { onCancelTask(task) },
onUncancel: { onUncancelTask(task.id) }, onUncancel: { onUncancelTask(task.id) },
onMarkInProgress: { onMarkInProgress(task.id) }, onMarkInProgress: { onMarkInProgress(task.id) },
onComplete: { onCompleteTask(task) }, onComplete: { onCompleteTask(task) },
onArchive: { onArchiveTask(task.id) }, onArchive: { onArchiveTask(task) },
onUnarchive: { onUnarchiveTask(task.id) } onUnarchive: { onUnarchiveTask(task.id) }
) )
} }

View File

@@ -23,8 +23,8 @@ struct TaskCard: View {
.foregroundColor(Color.appTextPrimary) .foregroundColor(Color.appTextPrimary)
.lineLimit(2) .lineLimit(2)
if let status = task.status { if task.inProgress {
StatusBadge(status: status.name) StatusBadge(status: "in_progress")
} }
} }
@@ -116,7 +116,7 @@ struct TaskCard: View {
// Primary Actions // Primary Actions
if task.showCompletedButton { if task.showCompletedButton {
VStack(spacing: AppSpacing.xs) { VStack(spacing: AppSpacing.xs) {
if let onMarkInProgress = onMarkInProgress, task.status?.name != "in_progress" { if let onMarkInProgress = onMarkInProgress, !task.inProgress {
Button(action: onMarkInProgress) { Button(action: onMarkInProgress) {
HStack(spacing: AppSpacing.xs) { HStack(spacing: AppSpacing.xs) {
Image(systemName: "play.circle.fill") Image(systemName: "play.circle.fill")
@@ -258,8 +258,7 @@ struct TaskCard: View {
category: TaskCategory(id: 1, name: "maintenance", description: "", icon: "", color: "", displayOrder: 0), category: TaskCategory(id: 1, name: "maintenance", description: "", icon: "", color: "", displayOrder: 0),
priorityId: 2, priorityId: 2,
priority: TaskPriority(id: 2, name: "medium", level: 2, color: "", displayOrder: 0), priority: TaskPriority(id: 2, name: "medium", level: 2, color: "", displayOrder: 0),
statusId: 1, inProgress: false,
status: TaskStatus(id: 1, name: "pending", description: "", color: "", displayOrder: 0),
frequencyId: 1, frequencyId: 1,
frequency: TaskFrequency(id: 1, name: "monthly", days: 30, displayOrder: 0), frequency: TaskFrequency(id: 1, name: "monthly", days: 30, displayOrder: 0),
dueDate: "2024-12-15", dueDate: "2024-12-15",

View File

@@ -4,11 +4,11 @@ import ComposeApp
struct TasksSection: View { struct TasksSection: View {
let tasksResponse: TaskColumnsResponse let tasksResponse: TaskColumnsResponse
let onEditTask: (TaskResponse) -> Void let onEditTask: (TaskResponse) -> Void
let onCancelTask: (Int32) -> Void let onCancelTask: (TaskResponse) -> Void
let onUncancelTask: (Int32) -> Void let onUncancelTask: (Int32) -> Void
let onMarkInProgress: (Int32) -> Void let onMarkInProgress: (Int32) -> Void
let onCompleteTask: (TaskResponse) -> Void let onCompleteTask: (TaskResponse) -> Void
let onArchiveTask: (Int32) -> Void let onArchiveTask: (TaskResponse) -> Void
let onUnarchiveTask: (Int32) -> Void let onUnarchiveTask: (Int32) -> Void
private var hasNoTasks: Bool { private var hasNoTasks: Bool {
@@ -35,8 +35,8 @@ struct TasksSection: View {
onEditTask: { task in onEditTask: { task in
onEditTask(task) onEditTask(task)
}, },
onCancelTask: { taskId in onCancelTask: { task in
onCancelTask(taskId) onCancelTask(task)
}, },
onUncancelTask: { taskId in onUncancelTask: { taskId in
onUncancelTask(taskId) onUncancelTask(taskId)
@@ -47,8 +47,8 @@ struct TasksSection: View {
onCompleteTask: { task in onCompleteTask: { task in
onCompleteTask(task) onCompleteTask(task)
}, },
onArchiveTask: { taskId in onArchiveTask: { task in
onArchiveTask(taskId) onArchiveTask(task)
}, },
onUnarchiveTask: { taskId in onUnarchiveTask: { taskId in
onUnarchiveTask(taskId) onUnarchiveTask(taskId)
@@ -92,8 +92,7 @@ struct TasksSection: View {
category: TaskCategory(id: 1, name: "maintenance", description: "", icon: "", color: "", displayOrder: 0), category: TaskCategory(id: 1, name: "maintenance", description: "", icon: "", color: "", displayOrder: 0),
priorityId: 2, priorityId: 2,
priority: TaskPriority(id: 2, name: "medium", level: 2, color: "", displayOrder: 0), priority: TaskPriority(id: 2, name: "medium", level: 2, color: "", displayOrder: 0),
statusId: 1, inProgress: false,
status: TaskStatus(id: 1, name: "pending", description: "", color: "", displayOrder: 0),
frequencyId: 1, frequencyId: 1,
frequency: TaskFrequency(id: 1, name: "monthly", days: 30, displayOrder: 0), frequency: TaskFrequency(id: 1, name: "monthly", days: 30, displayOrder: 0),
dueDate: "2024-12-15", dueDate: "2024-12-15",
@@ -132,8 +131,7 @@ struct TasksSection: View {
category: TaskCategory(id: 2, name: "plumbing", description: "", icon: "", color: "", displayOrder: 0), category: TaskCategory(id: 2, name: "plumbing", description: "", icon: "", color: "", displayOrder: 0),
priorityId: 3, priorityId: 3,
priority: TaskPriority(id: 3, name: "high", level: 3, color: "", displayOrder: 0), priority: TaskPriority(id: 3, name: "high", level: 3, color: "", displayOrder: 0),
statusId: 3, inProgress: false,
status: TaskStatus(id: 3, name: "completed", description: "", color: "", displayOrder: 0),
frequencyId: 6, frequencyId: 6,
frequency: TaskFrequency(id: 6, name: "once", days: nil, displayOrder: 0), frequency: TaskFrequency(id: 6, name: "once", days: nil, displayOrder: 0),
dueDate: "2024-11-01", dueDate: "2024-11-01",
@@ -154,7 +152,7 @@ struct TasksSection: View {
) )
], ],
daysThreshold: 30, daysThreshold: 30,
residenceId: "1" residenceId: "1", summary: nil
), ),
onEditTask: { _ in }, onEditTask: { _ in },
onCancelTask: { _ in }, onCancelTask: { _ in },

View File

@@ -221,12 +221,9 @@ struct AllTasksView: View {
selectedTaskForEdit = task selectedTaskForEdit = task
showEditTask = true showEditTask = true
}, },
onCancelTask: { taskId in onCancelTask: { task in
let allTasks = tasksResponse.columns.flatMap { $0.tasks } selectedTaskForCancel = task
if let task = allTasks.first(where: { $0.id == taskId }) { showCancelConfirmation = true
selectedTaskForCancel = task
showCancelConfirmation = true
}
}, },
onUncancelTask: { taskId in onUncancelTask: { taskId in
taskViewModel.uncancelTask(id: taskId) { _ in taskViewModel.uncancelTask(id: taskId) { _ in
@@ -243,12 +240,9 @@ struct AllTasksView: View {
onCompleteTask: { task in onCompleteTask: { task in
selectedTaskForComplete = task selectedTaskForComplete = task
}, },
onArchiveTask: { taskId in onArchiveTask: { task in
let allTasks = tasksResponse.columns.flatMap { $0.tasks } selectedTaskForArchive = task
if let task = allTasks.first(where: { $0.id == taskId }) { showArchiveConfirmation = true
selectedTaskForArchive = task
showArchiveConfirmation = true
}
}, },
onUnarchiveTask: { taskId in onUnarchiveTask: { taskId in
taskViewModel.unarchiveTask(id: taskId) { _ in taskViewModel.unarchiveTask(id: taskId) { _ in

View File

@@ -39,8 +39,8 @@ struct CompleteTaskView: View {
Spacer() Spacer()
if let status = task.status { if task.inProgress {
Text(status.displayName) Text(L10n.Tasks.inProgress)
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.padding(.horizontal, 8) .padding(.horizontal, 8)

View File

@@ -29,15 +29,13 @@ struct TaskFormView: View {
(!needsResidenceSelection || selectedResidence != nil) && (!needsResidenceSelection || selectedResidence != nil) &&
selectedCategory != nil && selectedCategory != nil &&
selectedFrequency != nil && selectedFrequency != nil &&
selectedPriority != nil && selectedPriority != nil
selectedStatus != nil
} }
// Lookups from DataManagerObservable // Lookups from DataManagerObservable
private var taskCategories: [TaskCategory] { dataManager.taskCategories } private var taskCategories: [TaskCategory] { dataManager.taskCategories }
private var taskFrequencies: [TaskFrequency] { dataManager.taskFrequencies } private var taskFrequencies: [TaskFrequency] { dataManager.taskFrequencies }
private var taskPriorities: [TaskPriority] { dataManager.taskPriorities } private var taskPriorities: [TaskPriority] { dataManager.taskPriorities }
private var taskStatuses: [TaskStatus] { dataManager.taskStatuses }
private var isLoadingLookups: Bool { !dataManager.lookupsInitialized } private var isLoadingLookups: Bool { !dataManager.lookupsInitialized }
// Form fields // Form fields
@@ -47,7 +45,7 @@ struct TaskFormView: View {
@State private var selectedCategory: TaskCategory? @State private var selectedCategory: TaskCategory?
@State private var selectedFrequency: TaskFrequency? @State private var selectedFrequency: TaskFrequency?
@State private var selectedPriority: TaskPriority? @State private var selectedPriority: TaskPriority?
@State private var selectedStatus: TaskStatus? @State private var inProgress: Bool
@State private var dueDate: Date @State private var dueDate: Date
@State private var intervalDays: String @State private var intervalDays: String
@State private var estimatedCost: String @State private var estimatedCost: String
@@ -66,7 +64,7 @@ struct TaskFormView: View {
_selectedCategory = State(initialValue: task.category) _selectedCategory = State(initialValue: task.category)
_selectedFrequency = State(initialValue: task.frequency) _selectedFrequency = State(initialValue: task.frequency)
_selectedPriority = State(initialValue: task.priority) _selectedPriority = State(initialValue: task.priority)
_selectedStatus = State(initialValue: task.status) _inProgress = State(initialValue: task.inProgress)
// Parse date from string // Parse date from string
let formatter = DateFormatter() let formatter = DateFormatter()
@@ -78,6 +76,7 @@ struct TaskFormView: View {
} else { } else {
_title = State(initialValue: "") _title = State(initialValue: "")
_description = State(initialValue: "") _description = State(initialValue: "")
_inProgress = State(initialValue: false)
_dueDate = State(initialValue: Date()) _dueDate = State(initialValue: Date())
_intervalDays = State(initialValue: "") _intervalDays = State(initialValue: "")
_estimatedCost = State(initialValue: "") _estimatedCost = State(initialValue: "")
@@ -246,16 +245,11 @@ struct TaskFormView: View {
} }
} }
Picker(L10n.Tasks.status, selection: $selectedStatus) { Toggle(L10n.Tasks.inProgressLabel, isOn: $inProgress)
Text(L10n.Tasks.selectStatus).tag(nil as TaskStatus?)
ForEach(taskStatuses, id: \.id) { status in
Text(status.displayName).tag(status as TaskStatus?)
}
}
} header: { } header: {
Text(L10n.Tasks.priorityAndStatus) Text(L10n.Tasks.priorityAndStatus)
} footer: { } footer: {
Text(L10n.Tasks.bothRequired) Text(L10n.Tasks.required)
.font(.caption) .font(.caption)
.foregroundColor(Color.appError) .foregroundColor(Color.appError)
} }
@@ -409,11 +403,6 @@ struct TaskFormView: View {
selectedPriority = taskPriorities.first { $0.name == "medium" } ?? taskPriorities.first 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 // Set default residence if provided
if needsResidenceSelection && selectedResidence == nil, let residences = residences, !residences.isEmpty { if needsResidenceSelection && selectedResidence == nil, let residences = residences, !residences.isEmpty {
selectedResidence = residences.first selectedResidence = residences.first
@@ -452,11 +441,6 @@ struct TaskFormView: View {
isValid = false isValid = false
} }
if selectedStatus == nil {
viewModel.errorMessage = "Please select a status"
isValid = false
}
return isValid return isValid
} }
@@ -465,8 +449,7 @@ struct TaskFormView: View {
guard let category = selectedCategory, guard let category = selectedCategory,
let frequency = selectedFrequency, let frequency = selectedFrequency,
let priority = selectedPriority, let priority = selectedPriority else {
let status = selectedStatus else {
return return
} }
@@ -483,7 +466,7 @@ struct TaskFormView: View {
description: description.isEmpty ? nil : description, description: description.isEmpty ? nil : description,
categoryId: KotlinInt(int: Int32(category.id)), categoryId: KotlinInt(int: Int32(category.id)),
priorityId: KotlinInt(int: Int32(priority.id)), priorityId: KotlinInt(int: Int32(priority.id)),
statusId: KotlinInt(int: Int32(status.id)), inProgress: inProgress,
frequencyId: KotlinInt(int: Int32(frequency.id)), frequencyId: KotlinInt(int: Int32(frequency.id)),
assignedToId: nil, assignedToId: nil,
dueDate: dueDateString, dueDate: dueDateString,
@@ -514,7 +497,7 @@ struct TaskFormView: View {
description: description.isEmpty ? nil : description, description: description.isEmpty ? nil : description,
categoryId: KotlinInt(int: Int32(category.id)), categoryId: KotlinInt(int: Int32(category.id)),
priorityId: KotlinInt(int: Int32(priority.id)), priorityId: KotlinInt(int: Int32(priority.id)),
statusId: selectedStatus.map { KotlinInt(int: Int32($0.id)) }, inProgress: inProgress,
frequencyId: KotlinInt(int: Int32(frequency.id)), frequencyId: KotlinInt(int: Int32(frequency.id)),
assignedToId: nil, assignedToId: nil,
dueDate: dueDateString, dueDate: dueDateString,

View File

@@ -75,14 +75,13 @@ class TaskViewModel: ObservableObject {
do { do {
let result = try await APILayer.shared.createTask(request: request) let result = try await APILayer.shared.createTask(request: request)
if result is ApiResultSuccess<TaskResponse> { if let error = result as? ApiResultError {
self.actionState = .success(.create)
// DataManager is updated by APILayer, view updates via observation
completion(true)
} else if let error = result as? ApiResultError {
self.actionState = .error(.create, ErrorMessageParser.parse(error.message)) self.actionState = .error(.create, ErrorMessageParser.parse(error.message))
self.errorMessage = ErrorMessageParser.parse(error.message) self.errorMessage = ErrorMessageParser.parse(error.message)
completion(false) completion(false)
} else {
self.actionState = .success(.create)
completion(true)
} }
} catch { } catch {
self.actionState = .error(.create, error.localizedDescription) self.actionState = .error(.create, error.localizedDescription)
@@ -100,14 +99,16 @@ class TaskViewModel: ObservableObject {
do { do {
let result = try await APILayer.shared.cancelTask(taskId: id) let result = try await APILayer.shared.cancelTask(taskId: id)
if result is ApiResultSuccess<TaskResponse> { // Check for error first, then treat non-error as success
self.actionState = .success(.cancel) // This handles Kotlin-Swift generic type bridging issues
// DataManager is updated by APILayer, view updates via observation if let error = result as? ApiResultError {
completion(true)
} else if let error = result as? ApiResultError {
self.actionState = .error(.cancel, ErrorMessageParser.parse(error.message)) self.actionState = .error(.cancel, ErrorMessageParser.parse(error.message))
self.errorMessage = ErrorMessageParser.parse(error.message) self.errorMessage = ErrorMessageParser.parse(error.message)
completion(false) completion(false)
} else {
// Not an error = success (DataManager is updated by APILayer)
self.actionState = .success(.cancel)
completion(true)
} }
} catch { } catch {
self.actionState = .error(.cancel, error.localizedDescription) self.actionState = .error(.cancel, error.localizedDescription)
@@ -125,14 +126,13 @@ class TaskViewModel: ObservableObject {
do { do {
let result = try await APILayer.shared.uncancelTask(taskId: id) let result = try await APILayer.shared.uncancelTask(taskId: id)
if result is ApiResultSuccess<TaskResponse> { if let error = result as? ApiResultError {
self.actionState = .success(.uncancel)
// DataManager is updated by APILayer, view updates via observation
completion(true)
} else if let error = result as? ApiResultError {
self.actionState = .error(.uncancel, ErrorMessageParser.parse(error.message)) self.actionState = .error(.uncancel, ErrorMessageParser.parse(error.message))
self.errorMessage = ErrorMessageParser.parse(error.message) self.errorMessage = ErrorMessageParser.parse(error.message)
completion(false) completion(false)
} else {
self.actionState = .success(.uncancel)
completion(true)
} }
} catch { } catch {
self.actionState = .error(.uncancel, error.localizedDescription) self.actionState = .error(.uncancel, error.localizedDescription)
@@ -150,14 +150,13 @@ class TaskViewModel: ObservableObject {
do { do {
let result = try await APILayer.shared.markInProgress(taskId: id) let result = try await APILayer.shared.markInProgress(taskId: id)
if result is ApiResultSuccess<TaskResponse> { if let error = result as? ApiResultError {
self.actionState = .success(.markInProgress)
// DataManager is updated by APILayer, view updates via observation
completion(true)
} else if let error = result as? ApiResultError {
self.actionState = .error(.markInProgress, ErrorMessageParser.parse(error.message)) self.actionState = .error(.markInProgress, ErrorMessageParser.parse(error.message))
self.errorMessage = ErrorMessageParser.parse(error.message) self.errorMessage = ErrorMessageParser.parse(error.message)
completion(false) completion(false)
} else {
self.actionState = .success(.markInProgress)
completion(true)
} }
} catch { } catch {
self.actionState = .error(.markInProgress, error.localizedDescription) self.actionState = .error(.markInProgress, error.localizedDescription)
@@ -175,14 +174,13 @@ class TaskViewModel: ObservableObject {
do { do {
let result = try await APILayer.shared.archiveTask(taskId: id) let result = try await APILayer.shared.archiveTask(taskId: id)
if result is ApiResultSuccess<TaskResponse> { if let error = result as? ApiResultError {
self.actionState = .success(.archive)
// DataManager is updated by APILayer, view updates via observation
completion(true)
} else if let error = result as? ApiResultError {
self.actionState = .error(.archive, ErrorMessageParser.parse(error.message)) self.actionState = .error(.archive, ErrorMessageParser.parse(error.message))
self.errorMessage = ErrorMessageParser.parse(error.message) self.errorMessage = ErrorMessageParser.parse(error.message)
completion(false) completion(false)
} else {
self.actionState = .success(.archive)
completion(true)
} }
} catch { } catch {
self.actionState = .error(.archive, error.localizedDescription) self.actionState = .error(.archive, error.localizedDescription)
@@ -200,14 +198,13 @@ class TaskViewModel: ObservableObject {
do { do {
let result = try await APILayer.shared.unarchiveTask(taskId: id) let result = try await APILayer.shared.unarchiveTask(taskId: id)
if result is ApiResultSuccess<TaskResponse> { if let error = result as? ApiResultError {
self.actionState = .success(.unarchive)
// DataManager is updated by APILayer, view updates via observation
completion(true)
} else if let error = result as? ApiResultError {
self.actionState = .error(.unarchive, ErrorMessageParser.parse(error.message)) self.actionState = .error(.unarchive, ErrorMessageParser.parse(error.message))
self.errorMessage = ErrorMessageParser.parse(error.message) self.errorMessage = ErrorMessageParser.parse(error.message)
completion(false) completion(false)
} else {
self.actionState = .success(.unarchive)
completion(true)
} }
} catch { } catch {
self.actionState = .error(.unarchive, error.localizedDescription) self.actionState = .error(.unarchive, error.localizedDescription)
@@ -225,14 +222,13 @@ class TaskViewModel: ObservableObject {
do { do {
let result = try await APILayer.shared.updateTask(id: id, request: request) let result = try await APILayer.shared.updateTask(id: id, request: request)
if result is ApiResultSuccess<TaskResponse> { if let error = result as? ApiResultError {
self.actionState = .success(.update)
// DataManager is updated by APILayer, view updates via observation
completion(true)
} else if let error = result as? ApiResultError {
self.actionState = .error(.update, ErrorMessageParser.parse(error.message)) self.actionState = .error(.update, ErrorMessageParser.parse(error.message))
self.errorMessage = ErrorMessageParser.parse(error.message) self.errorMessage = ErrorMessageParser.parse(error.message)
completion(false) completion(false)
} else {
self.actionState = .success(.update)
completion(true)
} }
} catch { } catch {
self.actionState = .error(.update, error.localizedDescription) self.actionState = .error(.update, error.localizedDescription)
@@ -406,7 +402,7 @@ class TaskViewModel: ObservableObject {
tasksResponse = TaskColumnsResponse( tasksResponse = TaskColumnsResponse(
columns: newColumns, columns: newColumns,
daysThreshold: currentResponse.daysThreshold, daysThreshold: currentResponse.daysThreshold,
residenceId: currentResponse.residenceId residenceId: currentResponse.residenceId, summary: nil
) )
} }
@@ -434,7 +430,7 @@ class TaskViewModel: ObservableObject {
tasksResponse = TaskColumnsResponse( tasksResponse = TaskColumnsResponse(
columns: newColumns, columns: newColumns,
daysThreshold: currentResponse.daysThreshold, daysThreshold: currentResponse.daysThreshold,
residenceId: currentResponse.residenceId residenceId: currentResponse.residenceId, summary: nil
) )
} }

View File

@@ -55,6 +55,13 @@ struct iOSApp: App {
} }
.onChange(of: scenePhase) { newPhase in .onChange(of: scenePhase) { newPhase in
if newPhase == .active { 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 // Check and register device token when app becomes active
PushNotificationManager.shared.checkAndRegisterDeviceIfNeeded() PushNotificationManager.shared.checkAndRegisterDeviceIfNeeded()
@@ -67,6 +74,24 @@ struct iOSApp: App {
Task { @MainActor in Task { @MainActor in
WidgetActionProcessor.shared.processPendingActions() 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<TaskColumnsResponse>,
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 { } else if newPhase == .background {
// Refresh widget when app goes to background // Refresh widget when app goes to background
WidgetCenter.shared.reloadAllTimelines() WidgetCenter.shared.reloadAllTimelines()