···17 - [x] `projects` - List all project names
18- [x] Tag management system
19 - [x] `tags` - List all tag names
20-- [ ] Status tracking - todo, in-progress, blocked, done, abandoned
21-- [ ] Priority system - High/medium/low or numeric scales
0000022- [ ] Due dates & scheduling - Including recurring tasks
23- [ ] Task dependencies - Task A blocks task B relationships
24
···17 - [x] `projects` - List all project names
18- [x] Tag management system
19 - [x] `tags` - List all tag names
20+- [x] Status tracking - todo, in-progress, blocked, done, abandoned
21+ - [x] Status indicators in task list view (colored unicode/non-emoji symbols)
22+- [x] Priority system - High/medium/low or numeric scales
23+ - [x] Priority indicators in task list view (โ โ โ visual + color coding)
24+- [x] Edit View
25+ - [x] Interactive status picker in task edit view
26+ - [x] Priority system toggle in task edit view (Text/Numeric/Legacy modes)
27- [ ] Due dates & scheduling - Including recurring tasks
28- [ ] Task dependencies - Task A blocks task B relationships
29
···23import (
4 "encoding/json"
05 "time"
000000000000000000000000000006)
78// Model defines the common interface that all domain models must implement
···169// HasPriority returns true if the task has a priority set
170func (t *Task) HasPriority() bool {
171 return t.Priority != ""
0000000000000000000000000000000000000000000000000000000000000000000000000000000000172}
173174// IsWatched returns true if the movie has been watched
···23import (
4 "encoding/json"
5+ "slices"
6 "time"
7+)
8+9+type TaskStatus string
10+type TaskPriority string
11+type TaskWeight int
12+13+// TODO: Use [TaskStatus]
14+const (
15+ StatusTodo = "todo"
16+ StatusInProgress = "in-progress"
17+ StatusBlocked = "blocked"
18+ StatusDone = "done"
19+ StatusAbandoned = "abandoned"
20+ StatusPending = "pending"
21+ StatusCompleted = "completed"
22+ StatusDeleted = "deleted"
23+)
24+25+// TODO: Use [TaskPriority]
26+const (
27+ PriorityHigh = "High"
28+ PriorityMedium = "Medium"
29+ PriorityLow = "Low"
30+)
31+32+// TODO: Use [TaskWeight]
33+const (
34+ PriorityNumericMin = 1
35+ PriorityNumericMax = 5
36)
3738// Model defines the common interface that all domain models must implement
···199// HasPriority returns true if the task has a priority set
200func (t *Task) HasPriority() bool {
201 return t.Priority != ""
202+}
203+204+// New status tracking methods
205+func (t *Task) IsTodo() bool {
206+ return t.Status == StatusTodo
207+}
208+209+func (t *Task) IsInProgress() bool {
210+ return t.Status == StatusInProgress
211+}
212+213+func (t *Task) IsBlocked() bool {
214+ return t.Status == StatusBlocked
215+}
216+217+func (t *Task) IsDone() bool {
218+ return t.Status == StatusDone
219+}
220+221+func (t *Task) IsAbandoned() bool {
222+ return t.Status == StatusAbandoned
223+}
224+225+// IsValidStatus returns true if the status is one of the defined valid statuses
226+func (t *Task) IsValidStatus() bool {
227+ validStatuses := []string{
228+ StatusTodo, StatusInProgress, StatusBlocked, StatusDone, StatusAbandoned,
229+ StatusPending, StatusCompleted, StatusDeleted, // legacy support
230+ }
231+ return slices.Contains(validStatuses, t.Status)
232+}
233+234+// IsValidPriority returns true if the priority is valid (text-based or numeric string)
235+func (t *Task) IsValidPriority() bool {
236+ if t.Priority == "" {
237+ return true
238+ }
239+240+ textPriorities := []string{PriorityHigh, PriorityMedium, PriorityLow}
241+ if slices.Contains(textPriorities, t.Priority) {
242+ return true
243+ }
244+245+ if len(t.Priority) == 1 && t.Priority >= "A" && t.Priority <= "Z" {
246+ return true
247+ }
248+249+ switch t.Priority {
250+ case "1", "2", "3", "4", "5":
251+ return true
252+ }
253+254+ return false
255+}
256+257+// GetPriorityWeight returns a numeric weight for sorting priorities
258+//
259+// Higher numbers = higher priority
260+func (t *Task) GetPriorityWeight() int {
261+ switch t.Priority {
262+ case PriorityHigh, "5":
263+ return 5
264+ case PriorityMedium, "4":
265+ return 4
266+ case PriorityLow, "3":
267+ return 3
268+ case "2":
269+ return 2
270+ case "1":
271+ return 1
272+ case "A":
273+ return 26
274+ case "B":
275+ return 25
276+ case "C":
277+ return 24
278+ default:
279+ if len(t.Priority) == 1 && t.Priority >= "A" && t.Priority <= "Z" {
280+ return int('Z' - t.Priority[0] + 1)
281+ }
282+ return 0
283+ }
284}
285286// IsWatched returns true if the movie has been watched
+167-3
internal/models/models_test.go
···72 }
73 })
7400000000000000000000000000000000000000000000000000000000000075 t.Run("Priority Methods", func(t *testing.T) {
76 task := &Task{}
77···83 if !task.HasPriority() {
84 t.Error("Task with priority should return true for HasPriority")
85 }
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000086 })
8788 t.Run("Tags Marshaling", func(t *testing.T) {
···866 t.Errorf("Model %d: ID not set correctly", i)
867 }
868869- // Test table name method
870 tableName := model.GetTableName()
871 if tableName == "" {
872 t.Errorf("Model %d: table name should not be empty", i)
873 }
874875- // Test timestamp methods
876 now := time.Now()
877 model.SetCreatedAt(now)
878 model.SetUpdatedAt(now)
879880- // Note: We don't test exact equality due to potential precision differences
881 if model.GetCreatedAt().IsZero() {
882 t.Errorf("Model %d: created at should not be zero", i)
883 }
···72 }
73 })
7475+ t.Run("New Status Tracking Methods", func(t *testing.T) {
76+ testCases := []struct {
77+ status string
78+ isTodo bool
79+ isInProgress bool
80+ isBlocked bool
81+ isDone bool
82+ isAbandoned bool
83+ }{
84+ {StatusTodo, true, false, false, false, false},
85+ {StatusInProgress, false, true, false, false, false},
86+ {StatusBlocked, false, false, true, false, false},
87+ {StatusDone, false, false, false, true, false},
88+ {StatusAbandoned, false, false, false, false, true},
89+ {"unknown", false, false, false, false, false},
90+ }
91+92+ for _, tc := range testCases {
93+ task := &Task{Status: tc.status}
94+95+ if task.IsTodo() != tc.isTodo {
96+ t.Errorf("Status %s: expected IsTodo %v, got %v", tc.status, tc.isTodo, task.IsTodo())
97+ }
98+ if task.IsInProgress() != tc.isInProgress {
99+ t.Errorf("Status %s: expected IsInProgress %v, got %v", tc.status, tc.isInProgress, task.IsInProgress())
100+ }
101+ if task.IsBlocked() != tc.isBlocked {
102+ t.Errorf("Status %s: expected IsBlocked %v, got %v", tc.status, tc.isBlocked, task.IsBlocked())
103+ }
104+ if task.IsDone() != tc.isDone {
105+ t.Errorf("Status %s: expected IsDone %v, got %v", tc.status, tc.isDone, task.IsDone())
106+ }
107+ if task.IsAbandoned() != tc.isAbandoned {
108+ t.Errorf("Status %s: expected IsAbandoned %v, got %v", tc.status, tc.isAbandoned, task.IsAbandoned())
109+ }
110+ }
111+ })
112+113+ t.Run("Status Validation", func(t *testing.T) {
114+ validStatuses := []string{
115+ StatusTodo, StatusInProgress, StatusBlocked, StatusDone, StatusAbandoned,
116+ StatusPending, StatusCompleted, StatusDeleted,
117+ }
118+119+ for _, status := range validStatuses {
120+ task := &Task{Status: status}
121+ if !task.IsValidStatus() {
122+ t.Errorf("Status %s should be valid", status)
123+ }
124+ }
125+126+ invalidStatuses := []string{"unknown", "invalid", ""}
127+ for _, status := range invalidStatuses {
128+ task := &Task{Status: status}
129+ if task.IsValidStatus() {
130+ t.Errorf("Status %s should be invalid", status)
131+ }
132+ }
133+ })
134+135 t.Run("Priority Methods", func(t *testing.T) {
136 task := &Task{}
137···143 if !task.HasPriority() {
144 t.Error("Task with priority should return true for HasPriority")
145 }
146+ })
147+148+ t.Run("Priority System", func(t *testing.T) {
149+ t.Run("Text-based Priority Validation", func(t *testing.T) {
150+ validTextPriorities := []string{
151+ PriorityHigh, PriorityMedium, PriorityLow,
152+ }
153+154+ for _, priority := range validTextPriorities {
155+ task := &Task{Priority: priority}
156+ if !task.IsValidPriority() {
157+ t.Errorf("Priority %s should be valid", priority)
158+ }
159+ }
160+ })
161+162+ t.Run("Numeric Priority Validation", func(t *testing.T) {
163+ validNumericPriorities := []string{"1", "2", "3", "4", "5"}
164+165+ for _, priority := range validNumericPriorities {
166+ task := &Task{Priority: priority}
167+ if !task.IsValidPriority() {
168+ t.Errorf("Numeric priority %s should be valid", priority)
169+ }
170+ }
171+172+ invalidNumericPriorities := []string{"0", "6", "10", "-1"}
173+ for _, priority := range invalidNumericPriorities {
174+ task := &Task{Priority: priority}
175+ if task.IsValidPriority() {
176+ t.Errorf("Numeric priority %s should be invalid", priority)
177+ }
178+ }
179+ })
180+181+ t.Run("Legacy A-Z Priority Validation", func(t *testing.T) {
182+ validLegacyPriorities := []string{"A", "B", "C", "D", "Z"}
183+184+ for _, priority := range validLegacyPriorities {
185+ task := &Task{Priority: priority}
186+ if !task.IsValidPriority() {
187+ t.Errorf("Legacy priority %s should be valid", priority)
188+ }
189+ }
190+191+ invalidLegacyPriorities := []string{"AA", "a", "1A", ""}
192+ for _, priority := range invalidLegacyPriorities {
193+ task := &Task{Priority: priority}
194+ if priority != "" && task.IsValidPriority() {
195+ t.Errorf("Legacy priority %s should be invalid", priority)
196+ }
197+ }
198+ })
199+200+ t.Run("Empty Priority Validation", func(t *testing.T) {
201+ task := &Task{Priority: ""}
202+ if !task.IsValidPriority() {
203+ t.Error("Empty priority should be valid")
204+ }
205+ })
206+207+ t.Run("Priority Weight Calculation", func(t *testing.T) {
208+ testCases := []struct {
209+ priority string
210+ weight int
211+ }{
212+ {PriorityHigh, 5},
213+ {PriorityMedium, 4},
214+ {PriorityLow, 3},
215+ {"5", 5},
216+ {"4", 4},
217+ {"3", 3},
218+ {"2", 2},
219+ {"1", 1},
220+ {"A", 26},
221+ {"B", 25},
222+ {"C", 24},
223+ {"Z", 1},
224+ {"", 0},
225+ {"invalid", 0},
226+ }
227+228+ for _, tc := range testCases {
229+ task := &Task{Priority: tc.priority}
230+ weight := task.GetPriorityWeight()
231+ if weight != tc.weight {
232+ t.Errorf("Priority %s: expected weight %d, got %d", tc.priority, tc.weight, weight)
233+ }
234+ }
235+ })
236+237+ t.Run("Priority Weight Ordering", func(t *testing.T) {
238+ priorities := []string{PriorityHigh, PriorityMedium, PriorityLow}
239+ weights := []int{}
240+241+ for _, priority := range priorities {
242+ task := &Task{Priority: priority}
243+ weights = append(weights, task.GetPriorityWeight())
244+ }
245+246+ for i := 1; i < len(weights); i++ {
247+ if weights[i-1] <= weights[i] {
248+ t.Errorf("Priority weights should be in descending order: %v", weights)
249+ }
250+ }
251+ })
252 })
253254 t.Run("Tags Marshaling", func(t *testing.T) {
···1032 t.Errorf("Model %d: ID not set correctly", i)
1033 }
103401035 tableName := model.GetTableName()
1036 if tableName == "" {
1037 t.Errorf("Model %d: table name should not be empty", i)
1038 }
103901040 now := time.Now()
1041 model.SetCreatedAt(now)
1042 model.SetUpdatedAt(now)
10431044+ // NOTE: We don't test exact equality due to potential precision differences
1045 if model.GetCreatedAt().IsZero() {
1046 t.Errorf("Model %d: created at should not be zero", i)
1047 }