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:
@@ -179,6 +179,7 @@
|
||||
<string name="tasks_unarchive">Unarchive</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_label">In Progress</string>
|
||||
<string name="tasks_cancelled_message">Task cancelled</string>
|
||||
|
||||
<!-- Task Completions -->
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -158,9 +158,6 @@ object DataManager {
|
||||
private val _taskPriorities = MutableStateFlow<List<TaskPriority>>(emptyList())
|
||||
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())
|
||||
val taskCategories: StateFlow<List<TaskCategory>> = _taskCategories.asStateFlow()
|
||||
|
||||
@@ -185,9 +182,6 @@ object DataManager {
|
||||
private val _taskPrioritiesMap = MutableStateFlow<Map<Int, TaskPriority>>(emptyMap())
|
||||
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())
|
||||
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 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<TaskStatus>) {
|
||||
_taskStatuses.value = statuses
|
||||
_taskStatusesMap.value = statuses.associateBy { it.id }
|
||||
persistToDisk()
|
||||
}
|
||||
|
||||
fun setTaskCategories(categories: List<TaskCategory>) {
|
||||
_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<List<TaskStatus>>(data)
|
||||
_taskStatuses.value = statuses
|
||||
_taskStatusesMap.value = statuses.associateBy { it.id }
|
||||
}
|
||||
|
||||
manager.load(KEY_TASK_CATEGORIES)?.let { data ->
|
||||
val categories = json.decodeFromString<List<TaskCategory>>(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"
|
||||
|
||||
@@ -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<TaskColumn>,
|
||||
@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
|
||||
)
|
||||
|
||||
@@ -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<ResidenceType>,
|
||||
@SerialName("task_frequencies") val taskFrequencies: List<TaskFrequency>,
|
||||
@SerialName("task_priorities") val taskPriorities: List<TaskPriority>,
|
||||
@SerialName("task_statuses") val taskStatuses: List<TaskStatus>,
|
||||
@SerialName("task_categories") val taskCategories: List<TaskCategory>,
|
||||
@SerialName("contractor_specialties") val contractorSpecialties: List<ContractorSpecialty>
|
||||
)
|
||||
@@ -119,7 +102,6 @@ data class SeededDataResponse(
|
||||
@SerialName("task_categories") val taskCategories: List<TaskCategory>,
|
||||
@SerialName("task_priorities") val taskPriorities: List<TaskPriority>,
|
||||
@SerialName("task_frequencies") val taskFrequencies: List<TaskFrequency>,
|
||||
@SerialName("task_statuses") val taskStatuses: List<TaskStatus>,
|
||||
@SerialName("contractor_specialties") val contractorSpecialties: List<ContractorSpecialty>,
|
||||
@SerialName("task_templates") val taskTemplates: TaskTemplatesGroupedResponse
|
||||
)
|
||||
@@ -145,12 +127,6 @@ data class TaskPriorityResponse(
|
||||
val results: List<TaskPriority>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TaskStatusResponse(
|
||||
val count: Int,
|
||||
val results: List<TaskStatus>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TaskCategoryResponse(
|
||||
val count: Int,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<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.
|
||||
*/
|
||||
@@ -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> {
|
||||
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<TaskResponse> {
|
||||
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<TaskResponse> {
|
||||
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<TaskResponse> {
|
||||
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)
|
||||
|
||||
@@ -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>> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/task-categories/") {
|
||||
|
||||
@@ -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<WithSummaryResponse<TaskResponse>> {
|
||||
return patchTask(token, id, TaskPatchRequest(status = cancelledStatusId))
|
||||
suspend fun cancelTask(token: String, id: Int): ApiResult<WithSummaryResponse<TaskResponse>> {
|
||||
return postTaskAction(token, id, "cancel")
|
||||
}
|
||||
|
||||
suspend fun uncancelTask(token: String, id: Int, pendingStatusId: Int): ApiResult<WithSummaryResponse<TaskResponse>> {
|
||||
return patchTask(token, id, TaskPatchRequest(status = pendingStatusId))
|
||||
suspend fun uncancelTask(token: String, id: Int): ApiResult<WithSummaryResponse<TaskResponse>> {
|
||||
return postTaskAction(token, id, "uncancel")
|
||||
}
|
||||
|
||||
suspend fun markInProgress(token: String, id: Int, inProgressStatusId: Int): ApiResult<WithSummaryResponse<TaskResponse>> {
|
||||
return patchTask(token, id, TaskPatchRequest(status = inProgressStatusId))
|
||||
suspend fun markInProgress(token: String, id: Int): ApiResult<WithSummaryResponse<TaskResponse>> {
|
||||
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>> {
|
||||
return patchTask(token, id, TaskPatchRequest(archived = true))
|
||||
return postTaskAction(token, id, "archive")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,7 +22,6 @@ object LookupsRepository {
|
||||
val residenceTypes: StateFlow<List<ResidenceType>> = DataManager.residenceTypes
|
||||
val taskFrequencies: StateFlow<List<TaskFrequency>> = DataManager.taskFrequencies
|
||||
val taskPriorities: StateFlow<List<TaskPriority>> = DataManager.taskPriorities
|
||||
val taskStatuses: StateFlow<List<TaskStatus>> = DataManager.taskStatuses
|
||||
val taskCategories: StateFlow<List<TaskCategory>> = DataManager.taskCategories
|
||||
val contractorSpecialties: StateFlow<List<ContractorSpecialty>> = DataManager.contractorSpecialties
|
||||
val isInitialized: StateFlow<Boolean> = DataManager.lookupsInitialized
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -33,20 +33,18 @@ fun EditTaskScreen(
|
||||
var selectedCategory by remember { mutableStateOf<TaskCategory?>(task.category) }
|
||||
var selectedFrequency by remember { mutableStateOf<TaskFrequency?>(task.frequency) }
|
||||
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 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(
|
||||
|
||||
@@ -346,7 +346,7 @@ fun OnboardingFirstTaskContent(
|
||||
description = null,
|
||||
categoryId = categoryId,
|
||||
priorityId = null,
|
||||
statusId = null,
|
||||
inProgress = false,
|
||||
frequencyId = frequencyId,
|
||||
assignedToId = null,
|
||||
dueDate = today,
|
||||
|
||||
@@ -21,7 +21,6 @@ class LookupsViewModel : ViewModel() {
|
||||
val residenceTypes: StateFlow<List<ResidenceType>> = DataManager.residenceTypes
|
||||
val taskFrequencies: StateFlow<List<TaskFrequency>> = DataManager.taskFrequencies
|
||||
val taskPriorities: StateFlow<List<TaskPriority>> = DataManager.taskPriorities
|
||||
val taskStatuses: StateFlow<List<TaskStatus>> = DataManager.taskStatuses
|
||||
val taskCategories: StateFlow<List<TaskCategory>> = DataManager.taskCategories
|
||||
val contractorSpecialties: StateFlow<List<ContractorSpecialty>> = DataManager.contractorSpecialties
|
||||
|
||||
@@ -35,9 +34,6 @@ class LookupsViewModel : ViewModel() {
|
||||
private val _taskPrioritiesState = MutableStateFlow<ApiResult<List<TaskPriority>>>(ApiResult.Idle)
|
||||
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)
|
||||
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() {
|
||||
viewModelScope.launch {
|
||||
val cached = DataManager.taskCategories.value
|
||||
@@ -111,7 +94,6 @@ class LookupsViewModel : ViewModel() {
|
||||
loadResidenceTypes()
|
||||
loadTaskFrequencies()
|
||||
loadTaskPriorities()
|
||||
loadTaskStatuses()
|
||||
loadTaskCategories()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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") }
|
||||
|
||||
@@ -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)")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" : {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -75,14 +75,13 @@ class TaskViewModel: ObservableObject {
|
||||
do {
|
||||
let result = try await APILayer.shared.createTask(request: request)
|
||||
|
||||
if result is ApiResultSuccess<TaskResponse> {
|
||||
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<TaskResponse> {
|
||||
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<TaskResponse> {
|
||||
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<TaskResponse> {
|
||||
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<TaskResponse> {
|
||||
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<TaskResponse> {
|
||||
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<TaskResponse> {
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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<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 {
|
||||
// Refresh widget when app goes to background
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
|
||||
Reference in New Issue
Block a user