|
| 1 | +package com.mapgie.dash.data.model |
| 2 | + |
| 3 | +import kotlinx.serialization.Serializable |
| 4 | +import java.time.Instant |
| 5 | +import java.time.LocalDate |
| 6 | +import java.time.temporal.ChronoUnit |
| 7 | + |
| 8 | +/** Draft key for the New chore / New task sheets, which have no item id yet. */ |
| 9 | +const val NEW_DRAFT_KEY = "new" |
| 10 | + |
| 11 | +/** The draft key for an Edit sheet: the item's id, or [NEW_DRAFT_KEY] while creating. */ |
| 12 | +fun draftKeyFor(itemId: String?): String = itemId ?: NEW_DRAFT_KEY |
| 13 | + |
| 14 | +/** |
| 15 | + * Everything the Edit chore sheet can change, as plain values so it can be |
| 16 | + * saved through rotation and process death and offered back on reopen. |
| 17 | + * Blank strings stand for "none" the way the sheet fields do. |
| 18 | + */ |
| 19 | +@Serializable |
| 20 | +data class ChoreDraft( |
| 21 | + val label: String = "", |
| 22 | + val category: String = "", |
| 23 | + val owner: String = "", |
| 24 | + val intervalDays: Int? = null, |
| 25 | + val tagId: String = "", |
| 26 | +) { |
| 27 | + /** True when any field differs from [opened], the values the sheet started with. */ |
| 28 | + fun differsFrom(opened: ChoreDraft): Boolean = |
| 29 | + label != opened.label || |
| 30 | + category != opened.category || |
| 31 | + owner != opened.owner || |
| 32 | + intervalDays != opened.intervalDays || |
| 33 | + tagId != opened.tagId |
| 34 | + |
| 35 | + /** The name to say when offering this draft back: the title typed so far, if any. */ |
| 36 | + fun displayName(): String? = label.trim().ifBlank { null } |
| 37 | + |
| 38 | + companion object { |
| 39 | + /** |
| 40 | + * The values the sheet opens with: [chore]'s own fields, or the New chore |
| 41 | + * defaults (General, [initialTagId] from an NFC scan) when [chore] is null. |
| 42 | + */ |
| 43 | + fun of(chore: Chore?, initialTagId: String = ""): ChoreDraft = |
| 44 | + if (chore == null) { |
| 45 | + ChoreDraft(category = GENERAL_CATEGORY, tagId = initialTagId) |
| 46 | + } else { |
| 47 | + ChoreDraft( |
| 48 | + label = chore.label, |
| 49 | + category = chore.category ?: "", |
| 50 | + owner = chore.owner ?: "", |
| 51 | + intervalDays = chore.intervalDays?.toInt(), |
| 52 | + tagId = chore.tagId, |
| 53 | + ) |
| 54 | + } |
| 55 | + } |
| 56 | +} |
| 57 | + |
| 58 | +/** The three shapes of a task's Due row, stored by name in a [TaskDraft]. */ |
| 59 | +object TaskDueType { |
| 60 | + const val NONE = "none" |
| 61 | + const val DATE = "date" |
| 62 | + const val PERIOD = "period" |
| 63 | +} |
| 64 | + |
| 65 | +/** |
| 66 | + * Everything the Edit task sheet can change, as plain values. Dates are epoch |
| 67 | + * days, the reminder is epoch millis at whole-minute precision and is null |
| 68 | + * whenever the reminder is off, so two drafts compare equal when the sheet |
| 69 | + * would show the same thing. |
| 70 | + */ |
| 71 | +@Serializable |
| 72 | +data class TaskDraft( |
| 73 | + val title: String = "", |
| 74 | + val notes: String = "", |
| 75 | + val category: String = "", |
| 76 | + val owner: String = "", |
| 77 | + val priority: String = TaskPriority.NORMAL.name, |
| 78 | + val dueType: String = TaskDueType.NONE, |
| 79 | + val dueDateEpochDay: Long? = null, |
| 80 | + val duePeriod: String = "today", |
| 81 | + val reminderEnabled: Boolean = false, |
| 82 | + val reminderAtEpochMillis: Long? = null, |
| 83 | +) { |
| 84 | + /** True when any field differs from [opened], the values the sheet started with. */ |
| 85 | + fun differsFrom(opened: TaskDraft): Boolean = this != opened |
| 86 | + |
| 87 | + /** The name to say when offering this draft back: the title typed so far, if any. */ |
| 88 | + fun displayName(): String? = title.trim().ifBlank { null } |
| 89 | + |
| 90 | + fun priorityEnum(): TaskPriority = |
| 91 | + runCatching { TaskPriority.valueOf(priority) }.getOrDefault(TaskPriority.NORMAL) |
| 92 | + |
| 93 | + fun dueDate(): LocalDate? = dueDateEpochDay?.let { LocalDate.ofEpochDay(it) } |
| 94 | + |
| 95 | + companion object { |
| 96 | + /** |
| 97 | + * The values the sheet opens with: [task]'s own fields, or the New task |
| 98 | + * defaults (General, normal priority, no due, no reminder) when null. |
| 99 | + */ |
| 100 | + fun of(task: TaskDto?): TaskDraft { |
| 101 | + if (task == null) return TaskDraft(category = GENERAL_CATEGORY) |
| 102 | + return TaskDraft( |
| 103 | + title = task.title, |
| 104 | + notes = task.notes ?: "", |
| 105 | + category = task.category ?: "", |
| 106 | + owner = task.owner ?: "", |
| 107 | + priority = task.priorityEnum().name, |
| 108 | + dueType = when { |
| 109 | + task.dueDate != null -> TaskDueType.DATE |
| 110 | + task.duePeriod != null -> TaskDueType.PERIOD |
| 111 | + else -> TaskDueType.NONE |
| 112 | + }, |
| 113 | + dueDateEpochDay = task.dueDate?.let { runCatching { LocalDate.parse(it).toEpochDay() }.getOrNull() }, |
| 114 | + duePeriod = task.duePeriod ?: "today", |
| 115 | + reminderEnabled = task.reminderAt != null, |
| 116 | + // Whole minutes, matching what the sheet resolves on save, so a stored |
| 117 | + // time with seconds does not look changed the moment the sheet opens. |
| 118 | + reminderAtEpochMillis = task.reminderAt?.let { |
| 119 | + runCatching { Instant.parse(it).truncatedTo(ChronoUnit.MINUTES).toEpochMilli() }.getOrNull() |
| 120 | + }, |
| 121 | + ) |
| 122 | + } |
| 123 | + } |
| 124 | +} |
0 commit comments