feat(tasks): due dates with optional time; alarms at the right moment
Due dates can carry a time (YYYY-MM-DDTHH:MM). Alarms fire at that time,
or 09:00 for all-day tasks, minus the user's lead time, in TZ (default
Europe/Berlin, tzdata embedded). A new due date or reopening a task
re-arms the alarm. Recurrence keeps the time; Markdown export/import
carry it. The UI shows relative labels ("Heute, 14:30"), marks overdue
tasks to the minute and groups by local day instead of the UTC date.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
300fd6a7be
commit
344f5e4ab2
21 files changed
+391
-50
No files matched your search
@@ -867,6 +867,7 @@ func (s *Store) CreateThought(userID, content, priority, source, dueDate string)
|
||||
|
||||
func (s *Store) CreateThoughtWithDetails(userID, content, priority, source, dueDate, taskListID, kanbanStatus, notes string) (*domain.Thought, error) {
|
||||
content = strings.TrimSpace(content)
|
||||
dueDate = domain.NormalizeDue(dueDate)
|
||||
if content == "" {
|
||||
return nil, errors.New("content cannot be empty")
|
||||
}
|
||||
@@ -1098,8 +1099,9 @@ func (s *Store) ReorderTasks(taskListID string, items []domain.ThoughtReorderIte
|
||||
}
|
||||
}
|
||||
_, err = tx.Exec(
|
||||
`UPDATE thoughts SET sort_order = ?, kanban_status = ?, is_completed = ? WHERE id = ?`,
|
||||
it.SortOrder, *it.KanbanStatus, isCompleted, it.ID,
|
||||
`UPDATE thoughts SET sort_order = ?, kanban_status = ?, is_completed = ?,
|
||||
alarm_sent = CASE WHEN ? = 0 THEN 0 ELSE alarm_sent END WHERE id = ?`,
|
||||
it.SortOrder, *it.KanbanStatus, isCompleted, isCompleted, it.ID,
|
||||
)
|
||||
} else {
|
||||
_, err = tx.Exec(
|
||||
@@ -1132,6 +1134,7 @@ func (s *Store) UpdateThoughtTriage(userID, thoughtID, action, convertTo, target
|
||||
newStatus := domain.StatusKept
|
||||
convertedType := thought.ConvertedType
|
||||
convertedRefID := thought.ConvertedRefID
|
||||
dueDate = domain.NormalizeDue(dueDate)
|
||||
if dueDate == "" {
|
||||
dueDate = thought.DueDate
|
||||
}
|
||||
@@ -1194,7 +1197,7 @@ func (s *Store) UpdateThoughtTriage(userID, thoughtID, action, convertTo, target
|
||||
}
|
||||
var metaItems []string
|
||||
if thought.DueDate != "" {
|
||||
metaItems = append(metaItems, "Fällig: "+thought.DueDate)
|
||||
metaItems = append(metaItems, "Fällig: "+domain.HumanDue(thought.DueDate))
|
||||
}
|
||||
if thought.Priority != "" && thought.Priority != "normal" {
|
||||
metaItems = append(metaItems, "Priorität: "+thought.Priority)
|
||||
@@ -1233,9 +1236,10 @@ func (s *Store) UpdateThoughtTriage(userID, thoughtID, action, convertTo, target
|
||||
|
||||
_, err = s.db.Exec(
|
||||
`UPDATE thoughts
|
||||
SET status = ?, converted_type = ?, converted_ref_id = ?, due_date = ?, task_list_id = ?, triaged_at = ?
|
||||
SET status = ?, converted_type = ?, converted_ref_id = ?, due_date = ?, task_list_id = ?, triaged_at = ?,
|
||||
alarm_sent = CASE WHEN due_date != ? THEN 0 ELSE alarm_sent END
|
||||
WHERE id = ? AND user_id = ?`,
|
||||
string(newStatus), convertedType, convertedRefID, dueDate, taskListID, now, thoughtID, userID,
|
||||
string(newStatus), convertedType, convertedRefID, dueDate, taskListID, now, dueDate, thoughtID, userID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1253,6 +1257,7 @@ func (s *Store) UpdateThoughtDetails(userID, thoughtID string, content *string,
|
||||
return nil, err
|
||||
}
|
||||
wasCompleted := thought.IsCompleted
|
||||
oldDue := thought.DueDate
|
||||
if err := s.ensureTaskListWritable(userID, thought.TaskListID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1278,7 +1283,7 @@ func (s *Store) UpdateThoughtDetails(userID, thoughtID string, content *string,
|
||||
}
|
||||
}
|
||||
if dueDate != nil {
|
||||
thought.DueDate = *dueDate
|
||||
thought.DueDate = domain.NormalizeDue(*dueDate)
|
||||
}
|
||||
if priority != nil {
|
||||
p := strings.ToLower(strings.TrimSpace(*priority))
|
||||
@@ -1321,6 +1326,11 @@ func (s *Store) UpdateThoughtDetails(userID, thoughtID string, content *string,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// A new due date or a reopened task gets its alarm again
|
||||
if thought.DueDate != oldDue || (wasCompleted && !thought.IsCompleted) {
|
||||
_, _ = s.db.Exec(`UPDATE thoughts SET alarm_sent = 0 WHERE id = ?`, thoughtID)
|
||||
thought.AlarmSent = false
|
||||
}
|
||||
if taskListID != nil {
|
||||
s.PruneTaskAssignees()
|
||||
thought.Assignees = s.getTaskAssignees(thoughtID)
|
||||
@@ -3967,7 +3977,8 @@ func (s *Store) SyncTaskFromBlockCheck(userID, taskID string, checked bool) {
|
||||
doneInt = 1
|
||||
kanban = "done"
|
||||
}
|
||||
res, _ := s.db.Exec(`UPDATE thoughts SET is_completed = ?, kanban_status = ? WHERE id = ? AND user_id = ? AND is_completed != ?`, doneInt, kanban, taskID, userID, doneInt)
|
||||
res, _ := s.db.Exec(`UPDATE thoughts SET is_completed = ?, kanban_status = ?, alarm_sent = CASE WHEN ? = 0 THEN 0 ELSE alarm_sent END
|
||||
WHERE id = ? AND user_id = ? AND is_completed != ?`, doneInt, kanban, doneInt, taskID, userID, doneInt)
|
||||
if checked && res != nil {
|
||||
if n, _ := res.RowsAffected(); n > 0 {
|
||||
s.spawnNextOccurrence(taskID)
|
||||
|
||||
Reference in new issue
Block a user