Add custom interval days support for task frequency
- Add customIntervalDays field to Kotlin models (TaskResponse, TaskCreateRequest, TaskUpdateRequest) - Update Android AddTaskDialog to show interval field only for "Custom" frequency - Update Android EditTaskScreen for custom frequency support - Update iOS TaskFormView for custom frequency support - Fix preview data in TaskCard and TasksSection to include new field - Add customIntervalDays to OnboardingFirstTaskView 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -219,6 +219,14 @@ class MainActivity : ComponentActivity(), SingletonImageLoader.Factory {
|
|||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
Log.d("MainActivity", "🔄 App resumed, checking for lookup updates...")
|
Log.d("MainActivity", "🔄 App resumed, checking for lookup updates...")
|
||||||
APILayer.refreshLookupsIfChanged()
|
APILayer.refreshLookupsIfChanged()
|
||||||
|
|
||||||
|
// Check if widget completed a task - refresh data if dirty
|
||||||
|
if (TaskCacheStorage.areTasksDirty()) {
|
||||||
|
Log.d("MainActivity", "🔄 Tasks marked dirty by widget, refreshing...")
|
||||||
|
TaskCacheStorage.clearDirtyFlag()
|
||||||
|
// Force refresh tasks from API
|
||||||
|
APILayer.getTasks(forceRefresh = true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,9 +24,32 @@ actual class TaskCacheManager(private val context: Context) {
|
|||||||
prefs.edit().remove(KEY_TASKS).apply()
|
prefs.edit().remove(KEY_TASKS).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if tasks need refresh due to widget interactions.
|
||||||
|
*/
|
||||||
|
actual fun areTasksDirty(): Boolean {
|
||||||
|
return prefs.getBoolean(KEY_DIRTY_FLAG, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark tasks as dirty (needs refresh).
|
||||||
|
* Called when widget modifies task data.
|
||||||
|
*/
|
||||||
|
actual fun markTasksDirty() {
|
||||||
|
prefs.edit().putBoolean(KEY_DIRTY_FLAG, true).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the dirty flag after tasks have been refreshed.
|
||||||
|
*/
|
||||||
|
actual fun clearDirtyFlag() {
|
||||||
|
prefs.edit().putBoolean(KEY_DIRTY_FLAG, false).apply()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val PREFS_NAME = "mycrib_cache"
|
private const val PREFS_NAME = "mycrib_cache"
|
||||||
private const val KEY_TASKS = "cached_tasks"
|
private const val KEY_TASKS = "cached_tasks"
|
||||||
|
private const val KEY_DIRTY_FLAG = "tasks_dirty"
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var instance: TaskCacheManager? = null
|
private var instance: TaskCacheManager? = null
|
||||||
|
|||||||
@@ -195,8 +195,9 @@
|
|||||||
<string name="tasks_browse_templates">Browse Task Templates</string>
|
<string name="tasks_browse_templates">Browse Task Templates</string>
|
||||||
<string name="tasks_common_tasks">%1$d common tasks</string>
|
<string name="tasks_common_tasks">%1$d common tasks</string>
|
||||||
<string name="tasks_category_error">Category is required</string>
|
<string name="tasks_category_error">Category is required</string>
|
||||||
<string name="tasks_interval_days">Interval Days (optional)</string>
|
<string name="tasks_interval_days">Interval Days</string>
|
||||||
<string name="tasks_interval_override">Override default frequency interval</string>
|
<string name="tasks_interval_override">Override default frequency interval</string>
|
||||||
|
<string name="tasks_custom_interval_help">Number of days between each occurrence</string>
|
||||||
<string name="tasks_due_date_format_error">Due date is required (format: YYYY-MM-DD)</string>
|
<string name="tasks_due_date_format_error">Due date is required (format: YYYY-MM-DD)</string>
|
||||||
<string name="tasks_due_date_format">Format: YYYY-MM-DD</string>
|
<string name="tasks_due_date_format">Format: YYYY-MM-DD</string>
|
||||||
<string name="tasks_create">Create Task</string>
|
<string name="tasks_create">Create Task</string>
|
||||||
|
|||||||
@@ -173,9 +173,7 @@ fun App(
|
|||||||
hasCompletedOnboarding = true
|
hasCompletedOnboarding = true
|
||||||
isLoggedIn = true
|
isLoggedIn = true
|
||||||
isVerified = true
|
isVerified = true
|
||||||
|
// Note: Lookups are already initialized by APILayer during login/register
|
||||||
// Initialize lookups after onboarding
|
|
||||||
LookupsRepository.initialize()
|
|
||||||
|
|
||||||
// Navigate to main screen
|
// Navigate to main screen
|
||||||
navController.navigate(MainRoute) {
|
navController.navigate(MainRoute) {
|
||||||
@@ -188,9 +186,7 @@ fun App(
|
|||||||
hasCompletedOnboarding = true
|
hasCompletedOnboarding = true
|
||||||
isLoggedIn = true
|
isLoggedIn = true
|
||||||
isVerified = verified
|
isVerified = verified
|
||||||
|
// Note: Lookups are already initialized by APILayer.login()
|
||||||
// Initialize lookups after login
|
|
||||||
LookupsRepository.initialize()
|
|
||||||
|
|
||||||
if (verified) {
|
if (verified) {
|
||||||
navController.navigate(MainRoute) {
|
navController.navigate(MainRoute) {
|
||||||
@@ -210,8 +206,7 @@ fun App(
|
|||||||
onLoginSuccess = { user ->
|
onLoginSuccess = { user ->
|
||||||
isLoggedIn = true
|
isLoggedIn = true
|
||||||
isVerified = user.verified
|
isVerified = user.verified
|
||||||
// Initialize lookups after successful login
|
// Note: Lookups are already initialized by APILayer.login()
|
||||||
LookupsRepository.initialize()
|
|
||||||
|
|
||||||
// Check if user is verified
|
// Check if user is verified
|
||||||
if (user.verified) {
|
if (user.verified) {
|
||||||
@@ -238,8 +233,7 @@ fun App(
|
|||||||
onRegisterSuccess = {
|
onRegisterSuccess = {
|
||||||
isLoggedIn = true
|
isLoggedIn = true
|
||||||
isVerified = false
|
isVerified = false
|
||||||
// Initialize lookups after successful registration
|
// Note: Lookups are already initialized by APILayer.register()
|
||||||
LookupsRepository.initialize()
|
|
||||||
navController.navigate(VerifyEmailRoute) {
|
navController.navigate(VerifyEmailRoute) {
|
||||||
popUpTo<RegisterRoute> { inclusive = true }
|
popUpTo<RegisterRoute> { inclusive = true }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -351,6 +351,30 @@ object DataManager {
|
|||||||
persistToDisk()
|
persistToDisk()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter cached allTasks by residence ID to avoid separate API call.
|
||||||
|
* Returns null if allTasks not cached.
|
||||||
|
* This enables client-side filtering when we already have all tasks loaded.
|
||||||
|
*/
|
||||||
|
fun getTasksForResidence(residenceId: Int): TaskColumnsResponse? {
|
||||||
|
val allTasksData = _allTasks.value ?: return null
|
||||||
|
|
||||||
|
// Filter each column's tasks by residence ID
|
||||||
|
val filteredColumns = allTasksData.columns.map { column ->
|
||||||
|
column.copy(
|
||||||
|
tasks = column.tasks.filter { it.residenceId == residenceId },
|
||||||
|
count = column.tasks.count { it.residenceId == residenceId }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return TaskColumnsResponse(
|
||||||
|
columns = filteredColumns,
|
||||||
|
daysThreshold = allTasksData.daysThreshold,
|
||||||
|
residenceId = residenceId.toString(),
|
||||||
|
summary = null // Summary is global; residence-specific not available client-side
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update a single task - moves it to the correct kanban column based on kanban_column field.
|
* Update a single task - moves it to the correct kanban column based on kanban_column field.
|
||||||
* This is called after task completion, status change, etc.
|
* This is called after task completion, status change, etc.
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ data class TaskResponse(
|
|||||||
@SerialName("in_progress") val inProgress: Boolean = false,
|
@SerialName("in_progress") val inProgress: Boolean = false,
|
||||||
@SerialName("frequency_id") val frequencyId: Int? = null,
|
@SerialName("frequency_id") val frequencyId: Int? = null,
|
||||||
val frequency: TaskFrequency? = null,
|
val frequency: TaskFrequency? = null,
|
||||||
|
@SerialName("custom_interval_days") val customIntervalDays: Int? = null, // For "Custom" frequency, user-specified days
|
||||||
@SerialName("due_date") val dueDate: String? = null,
|
@SerialName("due_date") val dueDate: String? = null,
|
||||||
@SerialName("next_due_date") val nextDueDate: String? = null, // For recurring tasks, updated after each completion
|
@SerialName("next_due_date") val nextDueDate: String? = null, // For recurring tasks, updated after each completion
|
||||||
@SerialName("estimated_cost") val estimatedCost: Double? = null,
|
@SerialName("estimated_cost") val estimatedCost: Double? = null,
|
||||||
@@ -123,6 +124,7 @@ data class TaskCreateRequest(
|
|||||||
@SerialName("priority_id") val priorityId: Int? = null,
|
@SerialName("priority_id") val priorityId: Int? = null,
|
||||||
@SerialName("in_progress") val inProgress: Boolean = false,
|
@SerialName("in_progress") val inProgress: Boolean = false,
|
||||||
@SerialName("frequency_id") val frequencyId: Int? = null,
|
@SerialName("frequency_id") val frequencyId: Int? = null,
|
||||||
|
@SerialName("custom_interval_days") val customIntervalDays: Int? = null, // For "Custom" frequency
|
||||||
@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,
|
||||||
@SerialName("estimated_cost") val estimatedCost: Double? = null,
|
@SerialName("estimated_cost") val estimatedCost: Double? = null,
|
||||||
@@ -141,6 +143,7 @@ data class TaskUpdateRequest(
|
|||||||
@SerialName("priority_id") val priorityId: Int? = null,
|
@SerialName("priority_id") val priorityId: Int? = null,
|
||||||
@SerialName("in_progress") val inProgress: Boolean? = null,
|
@SerialName("in_progress") val inProgress: Boolean? = null,
|
||||||
@SerialName("frequency_id") val frequencyId: Int? = null,
|
@SerialName("frequency_id") val frequencyId: Int? = null,
|
||||||
|
@SerialName("custom_interval_days") val customIntervalDays: Int? = null, // For "Custom" frequency
|
||||||
@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,
|
||||||
@SerialName("estimated_cost") val estimatedCost: Double? = null,
|
@SerialName("estimated_cost") val estimatedCost: Double? = null,
|
||||||
|
|||||||
@@ -27,6 +27,20 @@ object APILayer {
|
|||||||
private val subscriptionApi = SubscriptionApi()
|
private val subscriptionApi = SubscriptionApi()
|
||||||
private val taskTemplateApi = TaskTemplateApi()
|
private val taskTemplateApi = TaskTemplateApi()
|
||||||
|
|
||||||
|
// ==================== Initialization Guards ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guard to prevent concurrent initialization calls.
|
||||||
|
* This prevents multiple initializeLookups() calls from running simultaneously.
|
||||||
|
*/
|
||||||
|
private var isInitializingLookups = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guard to prevent concurrent prefetch calls.
|
||||||
|
* This prevents multiple prefetchAllData() calls from running simultaneously.
|
||||||
|
*/
|
||||||
|
private var isPrefetchingData = false
|
||||||
|
|
||||||
// ==================== Authentication Helper ====================
|
// ==================== Authentication Helper ====================
|
||||||
|
|
||||||
private fun getToken(): String? = DataManager.authToken.value
|
private fun getToken(): String? = DataManager.authToken.value
|
||||||
@@ -69,6 +83,12 @@ object APILayer {
|
|||||||
* - /subscription/status/ requires auth and is only called if user is authenticated
|
* - /subscription/status/ requires auth and is only called if user is authenticated
|
||||||
*/
|
*/
|
||||||
suspend fun initializeLookups(): ApiResult<Unit> {
|
suspend fun initializeLookups(): ApiResult<Unit> {
|
||||||
|
// Guard: prevent concurrent initialization
|
||||||
|
if (isInitializingLookups) {
|
||||||
|
println("📋 [APILayer] Lookups initialization already in progress, skipping...")
|
||||||
|
return ApiResult.Success(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
val token = getToken()
|
val token = getToken()
|
||||||
val currentETag = DataManager.seededDataETag.value
|
val currentETag = DataManager.seededDataETag.value
|
||||||
|
|
||||||
@@ -78,6 +98,7 @@ object APILayer {
|
|||||||
return refreshLookupsIfChanged()
|
return refreshLookupsIfChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isInitializingLookups = true
|
||||||
try {
|
try {
|
||||||
// Use seeded data endpoint with ETag support (PUBLIC - no auth required)
|
// Use seeded data endpoint with ETag support (PUBLIC - no auth required)
|
||||||
// Only send ETag if lookups are already in memory - otherwise we need full data
|
// Only send ETag if lookups are already in memory - otherwise we need full data
|
||||||
@@ -138,6 +159,8 @@ object APILayer {
|
|||||||
return ApiResult.Success(Unit)
|
return ApiResult.Success(Unit)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return ApiResult.Error("Failed to initialize lookups: ${e.message}")
|
return ApiResult.Error("Failed to initialize lookups: ${e.message}")
|
||||||
|
} finally {
|
||||||
|
isInitializingLookups = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -517,7 +540,7 @@ object APILayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getTasksByResidence(residenceId: Int, forceRefresh: Boolean = false): ApiResult<TaskColumnsResponse> {
|
suspend fun getTasksByResidence(residenceId: Int, forceRefresh: Boolean = false): ApiResult<TaskColumnsResponse> {
|
||||||
// Check DataManager first - return cached if valid and not forcing refresh
|
// 1. Check residence-specific cache first
|
||||||
if (!forceRefresh && DataManager.isCacheValid(DataManager.tasksByResidenceCacheTime[residenceId] ?: 0L)) {
|
if (!forceRefresh && DataManager.isCacheValid(DataManager.tasksByResidenceCacheTime[residenceId] ?: 0L)) {
|
||||||
val cached = DataManager.tasksByResidence.value[residenceId]
|
val cached = DataManager.tasksByResidence.value[residenceId]
|
||||||
if (cached != null) {
|
if (cached != null) {
|
||||||
@@ -525,7 +548,18 @@ object APILayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch from API
|
// 2. Try filtering from allTasks cache before hitting API (optimization)
|
||||||
|
// This avoids a redundant API call when we already have all tasks loaded
|
||||||
|
if (!forceRefresh && DataManager.isCacheValid(DataManager.tasksCacheTime)) {
|
||||||
|
val filtered = DataManager.getTasksForResidence(residenceId)
|
||||||
|
if (filtered != null) {
|
||||||
|
// Cache the filtered result for future use
|
||||||
|
DataManager.setTasksForResidence(residenceId, filtered)
|
||||||
|
return ApiResult.Success(filtered)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fallback: Fetch from API
|
||||||
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
|
||||||
val result = taskApi.getTasksByResidence(token, residenceId)
|
val result = taskApi.getTasksByResidence(token, residenceId)
|
||||||
|
|
||||||
@@ -1112,6 +1146,8 @@ object APILayer {
|
|||||||
DataManager.setCurrentUser(result.data.user)
|
DataManager.setCurrentUser(result.data.user)
|
||||||
// Initialize lookups after successful registration
|
// Initialize lookups after successful registration
|
||||||
initializeLookups()
|
initializeLookups()
|
||||||
|
// Prefetch all data (user may have shared residences)
|
||||||
|
prefetchAllData()
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@@ -1285,15 +1321,40 @@ object APILayer {
|
|||||||
// ==================== Helper Methods ====================
|
// ==================== Helper Methods ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prefetch all data after login
|
* Prefetch all data after login.
|
||||||
|
* Uses guards to prevent concurrent calls and skips if data is already cached.
|
||||||
*/
|
*/
|
||||||
private suspend fun prefetchAllData() {
|
private suspend fun prefetchAllData() {
|
||||||
|
// Guard: prevent concurrent prefetch calls
|
||||||
|
if (isPrefetchingData) {
|
||||||
|
println("📋 [APILayer] Data prefetch already in progress, skipping...")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if data is already cached (within cache validity period)
|
||||||
|
val residencesCached = DataManager.isCacheValid(DataManager.myResidencesCacheTime) &&
|
||||||
|
DataManager.myResidences.value != null
|
||||||
|
val tasksCached = DataManager.isCacheValid(DataManager.tasksCacheTime) &&
|
||||||
|
DataManager.allTasks.value != null
|
||||||
|
|
||||||
|
if (residencesCached && tasksCached) {
|
||||||
|
println("📋 [APILayer] Data already cached, skipping prefetch...")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isPrefetchingData = true
|
||||||
try {
|
try {
|
||||||
// Fetch key data in parallel - these all update DataManager
|
// Fetch key data - these all update DataManager
|
||||||
getMyResidences(forceRefresh = true)
|
if (!residencesCached) {
|
||||||
getTasks(forceRefresh = true)
|
getMyResidences(forceRefresh = true)
|
||||||
|
}
|
||||||
|
if (!tasksCached) {
|
||||||
|
getTasks(forceRefresh = true)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("Error prefetching data: ${e.message}")
|
println("Error prefetching data: ${e.message}")
|
||||||
|
} finally {
|
||||||
|
isPrefetchingData = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ package com.example.casera.network
|
|||||||
*/
|
*/
|
||||||
object ApiConfig {
|
object ApiConfig {
|
||||||
// ⚠️ CHANGE THIS TO TOGGLE ENVIRONMENT ⚠️
|
// ⚠️ CHANGE THIS TO TOGGLE ENVIRONMENT ⚠️
|
||||||
val CURRENT_ENV = Environment.LOCAL
|
val CURRENT_ENV = Environment.DEV
|
||||||
|
|
||||||
enum class Environment {
|
enum class Environment {
|
||||||
LOCAL,
|
LOCAL,
|
||||||
|
|||||||
@@ -9,4 +9,21 @@ expect class TaskCacheManager {
|
|||||||
fun saveTasks(tasksJson: String)
|
fun saveTasks(tasksJson: String)
|
||||||
fun getTasks(): String?
|
fun getTasks(): String?
|
||||||
fun clearTasks()
|
fun clearTasks()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if tasks need refresh due to widget interactions.
|
||||||
|
* Returns true if data was modified externally (e.g., by a widget).
|
||||||
|
*/
|
||||||
|
fun areTasksDirty(): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark tasks as dirty (needs refresh).
|
||||||
|
* Called when widget modifies task data.
|
||||||
|
*/
|
||||||
|
fun markTasksDirty()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the dirty flag after tasks have been refreshed.
|
||||||
|
*/
|
||||||
|
fun clearDirtyFlag()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,31 @@ object TaskCacheStorage {
|
|||||||
ensureInitialized()
|
ensureInitialized()
|
||||||
cacheManager?.clearTasks()
|
cacheManager?.clearTasks()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if tasks need refresh due to widget interactions.
|
||||||
|
*/
|
||||||
|
fun areTasksDirty(): Boolean {
|
||||||
|
ensureInitialized()
|
||||||
|
return cacheManager?.areTasksDirty() ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark tasks as dirty (needs refresh).
|
||||||
|
* Called when widget modifies task data.
|
||||||
|
*/
|
||||||
|
fun markTasksDirty() {
|
||||||
|
ensureInitialized()
|
||||||
|
cacheManager?.markTasksDirty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the dirty flag after tasks have been refreshed.
|
||||||
|
*/
|
||||||
|
fun clearDirtyFlag() {
|
||||||
|
ensureInitialized()
|
||||||
|
cacheManager?.clearDirtyFlag()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -326,8 +326,8 @@ fun AddTaskDialog(
|
|||||||
onClick = {
|
onClick = {
|
||||||
frequency = freq
|
frequency = freq
|
||||||
showFrequencyDropdown = false
|
showFrequencyDropdown = false
|
||||||
// Clear interval days if frequency is "once"
|
// Clear interval days if frequency is not "Custom"
|
||||||
if (freq.name == "once") {
|
if (!freq.name.equals("Custom", ignoreCase = true)) {
|
||||||
intervalDays = ""
|
intervalDays = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -336,15 +336,15 @@ fun AddTaskDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interval Days (only for recurring tasks)
|
// Custom Interval Days (only for "Custom" frequency)
|
||||||
if (frequency.name != "once") {
|
if (frequency.name.equals("Custom", ignoreCase = true)) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = intervalDays,
|
value = intervalDays,
|
||||||
onValueChange = { intervalDays = it.filter { char -> char.isDigit() } },
|
onValueChange = { intervalDays = it.filter { char -> char.isDigit() } },
|
||||||
label = { Text(stringResource(Res.string.tasks_interval_days)) },
|
label = { Text(stringResource(Res.string.tasks_interval_days)) },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
supportingText = { Text(stringResource(Res.string.tasks_interval_override)) },
|
supportingText = { Text(stringResource(Res.string.tasks_custom_interval_help)) },
|
||||||
singleLine = true
|
singleLine = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -450,6 +450,9 @@ fun AddTaskDialog(
|
|||||||
description = description.ifBlank { null },
|
description = description.ifBlank { null },
|
||||||
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,
|
||||||
|
customIntervalDays = if (frequency.name.equals("Custom", ignoreCase = true) && intervalDays.isNotBlank()) {
|
||||||
|
intervalDays.toIntOrNull()
|
||||||
|
} else null,
|
||||||
priorityId = if (priority.id > 0) priority.id else null,
|
priorityId = if (priority.id > 0) priority.id else null,
|
||||||
inProgress = false,
|
inProgress = false,
|
||||||
dueDate = dueDate,
|
dueDate = dueDate,
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ fun EditTaskScreen(
|
|||||||
var inProgress by remember { mutableStateOf(task.inProgress) }
|
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 customIntervalDays by remember { mutableStateOf(task.customIntervalDays?.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) }
|
||||||
@@ -195,12 +196,29 @@ fun EditTaskScreen(
|
|||||||
onClick = {
|
onClick = {
|
||||||
selectedFrequency = frequency
|
selectedFrequency = frequency
|
||||||
frequencyExpanded = false
|
frequencyExpanded = false
|
||||||
|
// Clear custom interval if not Custom frequency
|
||||||
|
if (!frequency.name.equals("Custom", ignoreCase = true)) {
|
||||||
|
customIntervalDays = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Custom Interval Days (only for "Custom" frequency)
|
||||||
|
if (selectedFrequency?.name?.equals("Custom", ignoreCase = true) == true) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = customIntervalDays,
|
||||||
|
onValueChange = { customIntervalDays = it.filter { char -> char.isDigit() } },
|
||||||
|
label = { Text(stringResource(Res.string.tasks_interval_days)) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
supportingText = { Text(stringResource(Res.string.tasks_custom_interval_help)) },
|
||||||
|
singleLine = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Priority dropdown
|
// Priority dropdown
|
||||||
ExposedDropdownMenuBox(
|
ExposedDropdownMenuBox(
|
||||||
expanded = priorityExpanded,
|
expanded = priorityExpanded,
|
||||||
@@ -292,6 +310,9 @@ fun EditTaskScreen(
|
|||||||
description = description.ifBlank { null },
|
description = description.ifBlank { null },
|
||||||
categoryId = selectedCategory!!.id,
|
categoryId = selectedCategory!!.id,
|
||||||
frequencyId = selectedFrequency!!.id,
|
frequencyId = selectedFrequency!!.id,
|
||||||
|
customIntervalDays = if (selectedFrequency?.name?.equals("Custom", ignoreCase = true) == true && customIntervalDays.isNotBlank()) {
|
||||||
|
customIntervalDays.toIntOrNull()
|
||||||
|
} else null,
|
||||||
priorityId = selectedPriority!!.id,
|
priorityId = selectedPriority!!.id,
|
||||||
inProgress = inProgress,
|
inProgress = inProgress,
|
||||||
dueDate = dueDate,
|
dueDate = dueDate,
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import kotlin.concurrent.Volatile
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* iOS implementation of TaskCacheManager using NSUserDefaults.
|
* iOS implementation of TaskCacheManager using NSUserDefaults.
|
||||||
|
* Note: iOS widget dirty flag is handled by native Swift WidgetDataManager
|
||||||
|
* using App Groups shared storage, but we provide these methods for KMM compatibility.
|
||||||
*/
|
*/
|
||||||
actual class TaskCacheManager {
|
actual class TaskCacheManager {
|
||||||
private val userDefaults = NSUserDefaults.standardUserDefaults
|
private val userDefaults = NSUserDefaults.standardUserDefaults
|
||||||
@@ -23,8 +25,33 @@ actual class TaskCacheManager {
|
|||||||
userDefaults.synchronize()
|
userDefaults.synchronize()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if tasks need refresh due to widget interactions.
|
||||||
|
* Note: iOS primarily uses native Swift WidgetDataManager for this.
|
||||||
|
*/
|
||||||
|
actual fun areTasksDirty(): Boolean {
|
||||||
|
return userDefaults.boolForKey(KEY_DIRTY_FLAG)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark tasks as dirty (needs refresh).
|
||||||
|
*/
|
||||||
|
actual fun markTasksDirty() {
|
||||||
|
userDefaults.setBool(true, KEY_DIRTY_FLAG)
|
||||||
|
userDefaults.synchronize()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the dirty flag after tasks have been refreshed.
|
||||||
|
*/
|
||||||
|
actual fun clearDirtyFlag() {
|
||||||
|
userDefaults.setBool(false, KEY_DIRTY_FLAG)
|
||||||
|
userDefaults.synchronize()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val KEY_TASKS = "cached_tasks"
|
private const val KEY_TASKS = "cached_tasks"
|
||||||
|
private const val KEY_DIRTY_FLAG = "tasks_dirty"
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var instance: TaskCacheManager? = null
|
private var instance: TaskCacheManager? = null
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package com.casera.storage
|
package com.example.casera.storage
|
||||||
|
|
||||||
import java.io.File
|
|
||||||
import java.util.prefs.Preferences
|
import java.util.prefs.Preferences
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JVM implementation of TaskCacheManager using Java Preferences.
|
* JVM implementation of TaskCacheManager using Java Preferences.
|
||||||
|
* Note: JVM/Desktop doesn't have widgets, so dirty flag methods are no-ops.
|
||||||
*/
|
*/
|
||||||
actual class TaskCacheManager {
|
actual class TaskCacheManager {
|
||||||
private val prefs = Preferences.userRoot().node(NODE_NAME)
|
private val prefs = Preferences.userRoot().node(NODE_NAME)
|
||||||
@@ -23,9 +23,33 @@ actual class TaskCacheManager {
|
|||||||
prefs.flush()
|
prefs.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if tasks need refresh. JVM doesn't have widgets, always returns false.
|
||||||
|
*/
|
||||||
|
actual fun areTasksDirty(): Boolean {
|
||||||
|
return prefs.getBoolean(KEY_DIRTY_FLAG, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark tasks as dirty. JVM doesn't have widgets but supports the interface.
|
||||||
|
*/
|
||||||
|
actual fun markTasksDirty() {
|
||||||
|
prefs.putBoolean(KEY_DIRTY_FLAG, true)
|
||||||
|
prefs.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the dirty flag.
|
||||||
|
*/
|
||||||
|
actual fun clearDirtyFlag() {
|
||||||
|
prefs.putBoolean(KEY_DIRTY_FLAG, false)
|
||||||
|
prefs.flush()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val NODE_NAME = "com.casera.cache"
|
private const val NODE_NAME = "com.casera.cache"
|
||||||
private const val KEY_TASKS = "cached_tasks"
|
private const val KEY_TASKS = "cached_tasks"
|
||||||
|
private const val KEY_DIRTY_FLAG = "tasks_dirty"
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var instance: TaskCacheManager? = null
|
private var instance: TaskCacheManager? = null
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
package com.casera.storage
|
package com.example.casera.storage
|
||||||
|
|
||||||
import kotlinx.browser.localStorage
|
import kotlinx.browser.localStorage
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WASM implementation of TaskCacheManager using browser's localStorage.
|
* WASM implementation of TaskCacheManager using browser's localStorage.
|
||||||
|
* Note: Web doesn't have widgets, so dirty flag methods use localStorage for consistency.
|
||||||
*/
|
*/
|
||||||
actual class TaskCacheManager {
|
actual class TaskCacheManager {
|
||||||
actual fun saveTasks(tasksJson: String) {
|
actual fun saveTasks(tasksJson: String) {
|
||||||
@@ -18,8 +19,30 @@ actual class TaskCacheManager {
|
|||||||
localStorage.removeItem(KEY_TASKS)
|
localStorage.removeItem(KEY_TASKS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if tasks need refresh. Web doesn't have widgets, always returns false.
|
||||||
|
*/
|
||||||
|
actual fun areTasksDirty(): Boolean {
|
||||||
|
return localStorage.getItem(KEY_DIRTY_FLAG) == "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark tasks as dirty. Web doesn't have widgets but supports the interface.
|
||||||
|
*/
|
||||||
|
actual fun markTasksDirty() {
|
||||||
|
localStorage.setItem(KEY_DIRTY_FLAG, "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the dirty flag.
|
||||||
|
*/
|
||||||
|
actual fun clearDirtyFlag() {
|
||||||
|
localStorage.setItem(KEY_DIRTY_FLAG, "false")
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val KEY_TASKS = "cached_tasks"
|
private const val KEY_TASKS = "cached_tasks"
|
||||||
|
private const val KEY_DIRTY_FLAG = "tasks_dirty"
|
||||||
|
|
||||||
private var instance: TaskCacheManager? = null
|
private var instance: TaskCacheManager? = null
|
||||||
|
|
||||||
|
|||||||
@@ -16977,6 +16977,9 @@
|
|||||||
"Enter the 6-digit code from your email" : {
|
"Enter the 6-digit code from your email" : {
|
||||||
"comment" : "A footer label explaining that users should enter the 6-digit code they received in their email.",
|
"comment" : "A footer label explaining that users should enter the 6-digit code they received in their email.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Enter the number of days between each occurrence" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Enter your email address and we'll send you a verification code" : {
|
"Enter your email address and we'll send you a verification code" : {
|
||||||
"comment" : "A description below the email input field, instructing the user to enter their email address to receive a password reset code.",
|
"comment" : "A description below the email input field, instructing the user to enter their email address to receive a password reset code.",
|
||||||
@@ -17747,6 +17750,72 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"profile_benefit_actionable_notifications" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Actionable Notifications"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profile_benefit_contractor_sharing" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Contractor Sharing"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profile_benefit_document_vault" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Document & Warranty Storage"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profile_benefit_residence_sharing" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Residence Sharing"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profile_benefit_unlimited_properties" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Unlimited Properties"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profile_benefit_widgets" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Home Screen Widgets"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"profile_contact" : {
|
"profile_contact" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -17834,6 +17903,136 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"profile_daily_digest" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Tägliche Zusammenfassung"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Daily Summary"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Resumen diario"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Résumé quotidien"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"it" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Riepilogo giornaliero"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "デイリーサマリー"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ko" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "일일 요약"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nl" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Dagelijkse samenvatting"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pt" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Resumo diário"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zh" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "每日摘要"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profile_daily_digest_description" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Tägliche Übersicht über fällige und überfällige Aufgaben"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Daily overview of tasks due and overdue"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Resumen diario de tareas pendientes y vencidas"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Aperçu quotidien des tâches à faire et en retard"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"it" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Panoramica giornaliera delle attività in scadenza e scadute"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "期限が近いタスクと期限切れのタスクの毎日の概要"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ko" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "마감 예정 및 지연된 작업의 일일 개요"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nl" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Dagelijks overzicht van taken die binnenkort verlopen en achterstallig zijn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pt" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Resumo diário de tarefas a vencer e atrasadas"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zh" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "即将到期和逾期任务的每日概览"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"profile_edit_profile" : {
|
"profile_edit_profile" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -19557,83 +19756,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"profile_unlock_premium_features" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Unlock Premium Features"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"profile_benefit_unlimited_properties" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Unlimited Properties"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"profile_benefit_document_vault" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Document & Warranty Storage"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"profile_benefit_residence_sharing" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Residence Sharing"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"profile_benefit_contractor_sharing" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Contractor Sharing"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"profile_benefit_actionable_notifications" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Actionable Notifications"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"profile_benefit_widgets" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Home Screen Widgets"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"profile_support" : {
|
"profile_support" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -20360,6 +20482,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"profile_unlock_premium_features" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Unlock Premium Features"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"profile_upgrade_to_pro" : {
|
"profile_upgrade_to_pro" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -20620,136 +20753,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"profile_daily_digest" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"de" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Tägliche Zusammenfassung"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Daily Summary"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"es" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Resumen diario"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fr" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Résumé quotidien"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"it" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Riepilogo giornaliero"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ja" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "デイリーサマリー"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "일일 요약"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nl" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Dagelijkse samenvatting"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pt" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Resumo diário"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"zh" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "每日摘要"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"profile_daily_digest_description" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"de" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Tägliche Übersicht über fällige und überfällige Aufgaben"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Daily overview of tasks due and overdue"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"es" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Resumen diario de tareas pendientes y vencidas"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fr" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Aperçu quotidien des tâches à faire et en retard"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"it" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Panoramica giornaliera delle attività in scadenza e scadute"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ja" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "期限が近いタスクと期限切れのタスクの毎日の概要"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "마감 예정 및 지연된 작업의 일일 개요"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nl" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Dagelijks overzicht van taken die binnenkort verlopen en achterstallig zijn"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pt" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Resumo diário de tarefas a vencer e atrasadas"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"zh" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "即将到期和逾期任务的每日概览"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"properties_add_button" : {
|
"properties_add_button" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|||||||
@@ -363,6 +363,7 @@ struct OnboardingFirstTaskContent: View {
|
|||||||
priorityId: nil,
|
priorityId: nil,
|
||||||
inProgress: false,
|
inProgress: false,
|
||||||
frequencyId: frequencyId.map { KotlinInt(int: $0) },
|
frequencyId: frequencyId.map { KotlinInt(int: $0) },
|
||||||
|
customIntervalDays: nil,
|
||||||
assignedToId: nil,
|
assignedToId: nil,
|
||||||
dueDate: todayString,
|
dueDate: todayString,
|
||||||
estimatedCost: nil,
|
estimatedCost: nil,
|
||||||
|
|||||||
@@ -261,6 +261,7 @@ struct TaskCard: View {
|
|||||||
inProgress: false,
|
inProgress: false,
|
||||||
frequencyId: 1,
|
frequencyId: 1,
|
||||||
frequency: TaskFrequency(id: 1, name: "monthly", days: 30, displayOrder: 0),
|
frequency: TaskFrequency(id: 1, name: "monthly", days: 30, displayOrder: 0),
|
||||||
|
customIntervalDays: nil,
|
||||||
dueDate: "2024-12-15",
|
dueDate: "2024-12-15",
|
||||||
nextDueDate: nil,
|
nextDueDate: nil,
|
||||||
estimatedCost: 150.00,
|
estimatedCost: 150.00,
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ struct TasksSection: View {
|
|||||||
inProgress: false,
|
inProgress: false,
|
||||||
frequencyId: 1,
|
frequencyId: 1,
|
||||||
frequency: TaskFrequency(id: 1, name: "monthly", days: 30, displayOrder: 0),
|
frequency: TaskFrequency(id: 1, name: "monthly", days: 30, displayOrder: 0),
|
||||||
|
customIntervalDays: nil,
|
||||||
dueDate: "2024-12-15",
|
dueDate: "2024-12-15",
|
||||||
nextDueDate: nil,
|
nextDueDate: nil,
|
||||||
estimatedCost: 150.00,
|
estimatedCost: 150.00,
|
||||||
@@ -135,6 +136,7 @@ struct TasksSection: View {
|
|||||||
inProgress: false,
|
inProgress: false,
|
||||||
frequencyId: 6,
|
frequencyId: 6,
|
||||||
frequency: TaskFrequency(id: 6, name: "once", days: nil, displayOrder: 0),
|
frequency: TaskFrequency(id: 6, name: "once", days: nil, displayOrder: 0),
|
||||||
|
customIntervalDays: nil,
|
||||||
dueDate: "2024-11-01",
|
dueDate: "2024-11-01",
|
||||||
nextDueDate: nil,
|
nextDueDate: nil,
|
||||||
estimatedCost: 200.00,
|
estimatedCost: 200.00,
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ struct TaskFormView: View {
|
|||||||
formatter.dateFormat = "yyyy-MM-dd"
|
formatter.dateFormat = "yyyy-MM-dd"
|
||||||
_dueDate = State(initialValue: formatter.date(from: task.effectiveDueDate ?? "") ?? Date())
|
_dueDate = State(initialValue: formatter.date(from: task.effectiveDueDate ?? "") ?? Date())
|
||||||
|
|
||||||
_intervalDays = State(initialValue: "") // No longer in API
|
_intervalDays = State(initialValue: task.customIntervalDays != nil ? String(task.customIntervalDays!.int32Value) : "")
|
||||||
_estimatedCost = State(initialValue: task.estimatedCost != nil ? String(task.estimatedCost!.doubleValue) : "")
|
_estimatedCost = State(initialValue: task.estimatedCost != nil ? String(task.estimatedCost!.doubleValue) : "")
|
||||||
} else {
|
} else {
|
||||||
_title = State(initialValue: "")
|
_title = State(initialValue: "")
|
||||||
@@ -220,8 +220,15 @@ struct TaskFormView: View {
|
|||||||
Text(frequency.displayName).tag(frequency as TaskFrequency?)
|
Text(frequency.displayName).tag(frequency as TaskFrequency?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onChange(of: selectedFrequency) { newFrequency in
|
||||||
|
// Clear interval days if not Custom frequency
|
||||||
|
if newFrequency?.name.lowercased() != "custom" {
|
||||||
|
intervalDays = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if selectedFrequency?.name != "once" {
|
// Show custom interval field only for "Custom" frequency
|
||||||
|
if selectedFrequency?.name.lowercased() == "custom" {
|
||||||
TextField(L10n.Tasks.customInterval, text: $intervalDays)
|
TextField(L10n.Tasks.customInterval, text: $intervalDays)
|
||||||
.keyboardType(.numberPad)
|
.keyboardType(.numberPad)
|
||||||
.focused($focusedField, equals: .intervalDays)
|
.focused($focusedField, equals: .intervalDays)
|
||||||
@@ -231,9 +238,15 @@ struct TaskFormView: View {
|
|||||||
} header: {
|
} header: {
|
||||||
Text(L10n.Tasks.scheduling)
|
Text(L10n.Tasks.scheduling)
|
||||||
} footer: {
|
} footer: {
|
||||||
Text(L10n.Tasks.required)
|
if selectedFrequency?.name.lowercased() == "custom" {
|
||||||
.font(.caption)
|
Text("Enter the number of days between each occurrence")
|
||||||
.foregroundColor(Color.appError)
|
.font(.caption)
|
||||||
|
.foregroundColor(Color.appTextSecondary)
|
||||||
|
} else {
|
||||||
|
Text(L10n.Tasks.required)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(Color.appError)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.listRowBackground(Color.appBackgroundSecondary)
|
.listRowBackground(Color.appBackgroundSecondary)
|
||||||
|
|
||||||
@@ -460,6 +473,11 @@ struct TaskFormView: View {
|
|||||||
|
|
||||||
if isEditMode, let task = existingTask {
|
if isEditMode, let task = existingTask {
|
||||||
// UPDATE existing task
|
// UPDATE existing task
|
||||||
|
// Include customIntervalDays only for "Custom" frequency
|
||||||
|
let customInterval: KotlinInt? = frequency.name.lowercased() == "custom" && !intervalDays.isEmpty
|
||||||
|
? KotlinInt(int: Int32(intervalDays) ?? 0)
|
||||||
|
: nil
|
||||||
|
|
||||||
let request = TaskCreateRequest(
|
let request = TaskCreateRequest(
|
||||||
residenceId: task.residenceId,
|
residenceId: task.residenceId,
|
||||||
title: title,
|
title: title,
|
||||||
@@ -468,6 +486,7 @@ struct TaskFormView: View {
|
|||||||
priorityId: KotlinInt(int: Int32(priority.id)),
|
priorityId: KotlinInt(int: Int32(priority.id)),
|
||||||
inProgress: inProgress,
|
inProgress: inProgress,
|
||||||
frequencyId: KotlinInt(int: Int32(frequency.id)),
|
frequencyId: KotlinInt(int: Int32(frequency.id)),
|
||||||
|
customIntervalDays: customInterval,
|
||||||
assignedToId: nil,
|
assignedToId: nil,
|
||||||
dueDate: dueDateString,
|
dueDate: dueDateString,
|
||||||
estimatedCost: estimatedCost.isEmpty ? nil : KotlinDouble(double: Double(estimatedCost) ?? 0.0),
|
estimatedCost: estimatedCost.isEmpty ? nil : KotlinDouble(double: Double(estimatedCost) ?? 0.0),
|
||||||
@@ -491,6 +510,11 @@ struct TaskFormView: View {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Include customIntervalDays only for "Custom" frequency
|
||||||
|
let customIntervalCreate: KotlinInt? = frequency.name.lowercased() == "custom" && !intervalDays.isEmpty
|
||||||
|
? KotlinInt(int: Int32(intervalDays) ?? 0)
|
||||||
|
: nil
|
||||||
|
|
||||||
let request = TaskCreateRequest(
|
let request = TaskCreateRequest(
|
||||||
residenceId: actualResidenceId,
|
residenceId: actualResidenceId,
|
||||||
title: title,
|
title: title,
|
||||||
@@ -499,6 +523,7 @@ struct TaskFormView: View {
|
|||||||
priorityId: KotlinInt(int: Int32(priority.id)),
|
priorityId: KotlinInt(int: Int32(priority.id)),
|
||||||
inProgress: inProgress,
|
inProgress: inProgress,
|
||||||
frequencyId: KotlinInt(int: Int32(frequency.id)),
|
frequencyId: KotlinInt(int: Int32(frequency.id)),
|
||||||
|
customIntervalDays: customIntervalCreate,
|
||||||
assignedToId: nil,
|
assignedToId: nil,
|
||||||
dueDate: dueDateString,
|
dueDate: dueDateString,
|
||||||
estimatedCost: estimatedCost.isEmpty ? nil : KotlinDouble(double: Double(estimatedCost) ?? 0.0),
|
estimatedCost: estimatedCost.isEmpty ? nil : KotlinDouble(double: Double(estimatedCost) ?? 0.0),
|
||||||
|
|||||||
Reference in New Issue
Block a user