b02c40f716
Add relative date formatting (today, tomorrow, in 3d, etc.) for list and detail views. Add structured feedback helpers for add/complete/delete operations showing display IDs and parsed modifiers. Change Complete() to return spawned recurring instance so callers can display recurrence info. Add AppendTask to working set for immediate display ID assignment. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
777 lines
18 KiB
Go
777 lines
18 KiB
Go
package engine
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type Status byte
|
|
|
|
const (
|
|
StatusPending Status = 'P'
|
|
StatusCompleted Status = 'C'
|
|
StatusDeleted Status = 'D'
|
|
StatusRecurring Status = 'R'
|
|
)
|
|
|
|
// MarshalJSON encodes Status as a single-character string (e.g. "P", "C").
|
|
func (s Status) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(string(s))
|
|
}
|
|
|
|
// UnmarshalJSON decodes a single-character string into a Status.
|
|
func (s *Status) UnmarshalJSON(data []byte) error {
|
|
var str string
|
|
if err := json.Unmarshal(data, &str); err != nil {
|
|
return err
|
|
}
|
|
if len(str) != 1 {
|
|
return fmt.Errorf("invalid status: %q", str)
|
|
}
|
|
*s = Status(str[0])
|
|
return nil
|
|
}
|
|
|
|
type Priority int
|
|
|
|
const (
|
|
PriorityLow Priority = 0
|
|
PriorityDefault Priority = 1
|
|
PriorityMedium Priority = 2
|
|
PriorityHigh Priority = 3
|
|
)
|
|
|
|
type Task struct {
|
|
// Identity
|
|
UUID uuid.UUID `json:"uuid"`
|
|
ID int `json:"id"`
|
|
|
|
// Core fields
|
|
Status Status `json:"status"`
|
|
Description string `json:"description"`
|
|
Project *string `json:"project"`
|
|
Priority Priority `json:"priority"`
|
|
|
|
// Timestamps
|
|
Created time.Time `json:"created"`
|
|
Modified time.Time `json:"modified"`
|
|
Start *time.Time `json:"start,omitempty"`
|
|
End *time.Time `json:"end,omitempty"`
|
|
Due *time.Time `json:"due,omitempty"`
|
|
Scheduled *time.Time `json:"scheduled,omitempty"`
|
|
Wait *time.Time `json:"wait,omitempty"`
|
|
Until *time.Time `json:"until,omitempty"`
|
|
|
|
// Recurrence (parent-child approach)
|
|
RecurrenceDuration *time.Duration `json:"recurrence_duration,omitempty"`
|
|
ParentUUID *uuid.UUID `json:"parent_uuid,omitempty"`
|
|
|
|
// Derived fields (not stored in DB)
|
|
Tags []string `json:"tags"`
|
|
Urgency float64 `json:"urgency"`
|
|
}
|
|
|
|
// MarshalJSON emits Task with unix timestamps (int64) instead of RFC3339 strings.
|
|
func (t Task) MarshalJSON() ([]byte, error) {
|
|
type taskJSON struct {
|
|
UUID uuid.UUID `json:"uuid"`
|
|
ID int `json:"id"`
|
|
Status Status `json:"status"`
|
|
Description string `json:"description"`
|
|
Project *string `json:"project"`
|
|
Priority Priority `json:"priority"`
|
|
Created int64 `json:"created"`
|
|
Modified int64 `json:"modified"`
|
|
Start *int64 `json:"start,omitempty"`
|
|
End *int64 `json:"end,omitempty"`
|
|
Due *int64 `json:"due,omitempty"`
|
|
Scheduled *int64 `json:"scheduled,omitempty"`
|
|
Wait *int64 `json:"wait,omitempty"`
|
|
Until *int64 `json:"until,omitempty"`
|
|
RecurrenceDuration *int64 `json:"recurrence_duration,omitempty"`
|
|
ParentUUID *uuid.UUID `json:"parent_uuid,omitempty"`
|
|
Tags []string `json:"tags"`
|
|
Urgency float64 `json:"urgency"`
|
|
}
|
|
|
|
toUnix := func(tp *time.Time) *int64 {
|
|
if tp == nil {
|
|
return nil
|
|
}
|
|
v := tp.Unix()
|
|
return &v
|
|
}
|
|
|
|
var recurDur *int64
|
|
if t.RecurrenceDuration != nil {
|
|
v := int64(*t.RecurrenceDuration / time.Second)
|
|
recurDur = &v
|
|
}
|
|
|
|
return json.Marshal(taskJSON{
|
|
UUID: t.UUID,
|
|
ID: t.ID,
|
|
Status: t.Status,
|
|
Description: t.Description,
|
|
Project: t.Project,
|
|
Priority: t.Priority,
|
|
Created: t.Created.Unix(),
|
|
Modified: t.Modified.Unix(),
|
|
Start: toUnix(t.Start),
|
|
End: toUnix(t.End),
|
|
Due: toUnix(t.Due),
|
|
Scheduled: toUnix(t.Scheduled),
|
|
Wait: toUnix(t.Wait),
|
|
Until: toUnix(t.Until),
|
|
RecurrenceDuration: recurDur,
|
|
ParentUUID: t.ParentUUID,
|
|
Tags: t.Tags,
|
|
Urgency: t.Urgency,
|
|
})
|
|
}
|
|
|
|
// UnmarshalJSON parses Task from JSON with unix timestamps (int64) and duration in seconds.
|
|
func (t *Task) UnmarshalJSON(data []byte) error {
|
|
type taskJSON struct {
|
|
UUID uuid.UUID `json:"uuid"`
|
|
ID int `json:"id"`
|
|
Status Status `json:"status"`
|
|
Description string `json:"description"`
|
|
Project *string `json:"project"`
|
|
Priority Priority `json:"priority"`
|
|
Created int64 `json:"created"`
|
|
Modified int64 `json:"modified"`
|
|
Start *int64 `json:"start,omitempty"`
|
|
End *int64 `json:"end,omitempty"`
|
|
Due *int64 `json:"due,omitempty"`
|
|
Scheduled *int64 `json:"scheduled,omitempty"`
|
|
Wait *int64 `json:"wait,omitempty"`
|
|
Until *int64 `json:"until,omitempty"`
|
|
RecurrenceDuration *int64 `json:"recurrence_duration,omitempty"`
|
|
ParentUUID *uuid.UUID `json:"parent_uuid,omitempty"`
|
|
Tags []string `json:"tags"`
|
|
Urgency float64 `json:"urgency"`
|
|
}
|
|
|
|
var raw taskJSON
|
|
if err := json.Unmarshal(data, &raw); err != nil {
|
|
return err
|
|
}
|
|
|
|
fromUnix := func(v *int64) *time.Time {
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
t := time.Unix(*v, 0)
|
|
return &t
|
|
}
|
|
|
|
t.UUID = raw.UUID
|
|
t.ID = raw.ID
|
|
t.Status = raw.Status
|
|
t.Description = raw.Description
|
|
t.Project = raw.Project
|
|
t.Priority = raw.Priority
|
|
t.Created = time.Unix(raw.Created, 0)
|
|
t.Modified = time.Unix(raw.Modified, 0)
|
|
t.Start = fromUnix(raw.Start)
|
|
t.End = fromUnix(raw.End)
|
|
t.Due = fromUnix(raw.Due)
|
|
t.Scheduled = fromUnix(raw.Scheduled)
|
|
t.Wait = fromUnix(raw.Wait)
|
|
t.Until = fromUnix(raw.Until)
|
|
t.ParentUUID = raw.ParentUUID
|
|
t.Tags = raw.Tags
|
|
t.Urgency = raw.Urgency
|
|
|
|
if raw.RecurrenceDuration != nil {
|
|
d := time.Duration(*raw.RecurrenceDuration) * time.Second
|
|
t.RecurrenceDuration = &d
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// timeNow returns current time (allows mocking in tests)
|
|
var timeNow = time.Now
|
|
|
|
// Helper functions for SQL time conversion
|
|
|
|
func timeToSQL(t *time.Time) interface{} {
|
|
if t == nil {
|
|
return nil
|
|
}
|
|
return t.Unix()
|
|
}
|
|
|
|
func sqlToTime(v interface{}) *time.Time {
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
unix, ok := v.(int64)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
t := time.Unix(unix, 0)
|
|
return &t
|
|
}
|
|
|
|
func durationToSQL(d *time.Duration) interface{} {
|
|
if d == nil {
|
|
return nil
|
|
}
|
|
return int64(*d)
|
|
}
|
|
|
|
func sqlToDuration(v interface{}) *time.Duration {
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
nanos, ok := v.(int64)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
d := time.Duration(nanos)
|
|
return &d
|
|
}
|
|
|
|
func stringPtrToSQL(s *string) interface{} {
|
|
if s == nil {
|
|
return nil
|
|
}
|
|
return *s
|
|
}
|
|
|
|
func sqlToStringPtr(v interface{}) *string {
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
str, ok := v.(string)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return &str
|
|
}
|
|
|
|
func uuidPtrToSQL(u *uuid.UUID) interface{} {
|
|
if u == nil {
|
|
return nil
|
|
}
|
|
return u.String()
|
|
}
|
|
|
|
func sqlToUUIDPtr(v interface{}) *uuid.UUID {
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
str, ok := v.(string)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
u, err := uuid.Parse(str)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return &u
|
|
}
|
|
|
|
// CreateTask creates a new task with the given description
|
|
func CreateTask(description string) (*Task, error) {
|
|
return CreateTaskWithModifier(description, nil)
|
|
}
|
|
|
|
// CreateTaskWithModifier creates a new task with the given description and applies modifiers
|
|
func CreateTaskWithModifier(description string, mod *Modifier) (*Task, error) {
|
|
now := timeNow()
|
|
task := &Task{
|
|
UUID: uuid.New(),
|
|
Status: StatusPending,
|
|
Description: description,
|
|
Priority: PriorityDefault,
|
|
Created: now,
|
|
Modified: now,
|
|
Tags: []string{},
|
|
}
|
|
|
|
// Apply modifiers before saving (for attributes)
|
|
if mod != nil {
|
|
if err := mod.ApplyToNew(task); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if err := task.Save(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Apply tags after saving (requires task.ID)
|
|
if mod != nil {
|
|
for _, tag := range mod.AddTags {
|
|
if err := task.AddTag(tag); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Update task to trigger change log with tags
|
|
if len(mod.AddTags) > 0 {
|
|
task.Modified = timeNow()
|
|
if err := task.Save(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
return task, nil
|
|
}
|
|
|
|
// GetTask retrieves a task by UUID
|
|
func GetTask(taskUUID uuid.UUID) (*Task, error) {
|
|
db := GetDB()
|
|
if db == nil {
|
|
return nil, fmt.Errorf("database not initialized")
|
|
}
|
|
|
|
query := `
|
|
SELECT id, uuid, status, description, project, priority,
|
|
created, modified, start, end, due, scheduled, wait, until_date,
|
|
recurrence_duration, parent_uuid
|
|
FROM tasks
|
|
WHERE uuid = ?
|
|
`
|
|
|
|
task := &Task{}
|
|
var (
|
|
uuidStr string
|
|
project interface{}
|
|
created int64
|
|
modified int64
|
|
start interface{}
|
|
end interface{}
|
|
due interface{}
|
|
scheduled interface{}
|
|
wait interface{}
|
|
until interface{}
|
|
recurDuration interface{}
|
|
parentUUIDStr interface{}
|
|
)
|
|
|
|
err := db.QueryRow(query, taskUUID.String()).Scan(
|
|
&task.ID,
|
|
&uuidStr,
|
|
&task.Status,
|
|
&task.Description,
|
|
&project,
|
|
&task.Priority,
|
|
&created,
|
|
&modified,
|
|
&start,
|
|
&end,
|
|
&due,
|
|
&scheduled,
|
|
&wait,
|
|
&until,
|
|
&recurDuration,
|
|
&parentUUIDStr,
|
|
)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get task: %w", err)
|
|
}
|
|
|
|
// Parse UUID
|
|
task.UUID, err = uuid.Parse(uuidStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse UUID: %w", err)
|
|
}
|
|
|
|
// Convert timestamps
|
|
task.Created = time.Unix(created, 0)
|
|
task.Modified = time.Unix(modified, 0)
|
|
|
|
// Convert nullable fields
|
|
task.Project = sqlToStringPtr(project)
|
|
task.Start = sqlToTime(start)
|
|
task.End = sqlToTime(end)
|
|
task.Due = sqlToTime(due)
|
|
task.Scheduled = sqlToTime(scheduled)
|
|
task.Wait = sqlToTime(wait)
|
|
task.Until = sqlToTime(until)
|
|
task.RecurrenceDuration = sqlToDuration(recurDuration)
|
|
task.ParentUUID = sqlToUUIDPtr(parentUUIDStr)
|
|
|
|
// Load tags
|
|
tags, err := task.GetTags()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load tags: %w", err)
|
|
}
|
|
task.Tags = tags
|
|
|
|
return task, nil
|
|
}
|
|
|
|
// GetTasks retrieves all tasks with optional filtering
|
|
func GetTasks(filter *Filter) ([]*Task, error) {
|
|
db := GetDB()
|
|
if db == nil {
|
|
return nil, fmt.Errorf("database not initialized")
|
|
}
|
|
|
|
// Build WHERE clause from filter
|
|
whereClause := "1=1"
|
|
var args []interface{}
|
|
if filter != nil {
|
|
whereClause, args = filter.ToSQL()
|
|
}
|
|
|
|
query := fmt.Sprintf(`
|
|
SELECT id, uuid, status, description, project, priority,
|
|
created, modified, start, end, due, scheduled, wait, until_date,
|
|
recurrence_duration, parent_uuid
|
|
FROM tasks
|
|
WHERE %s
|
|
ORDER BY
|
|
CASE WHEN due IS NULL THEN 1 ELSE 0 END,
|
|
due ASC,
|
|
priority DESC
|
|
`, whereClause)
|
|
|
|
rows, err := db.Query(query, args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query tasks: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
tasks := []*Task{}
|
|
|
|
for rows.Next() {
|
|
task := &Task{}
|
|
var (
|
|
uuidStr string
|
|
project interface{}
|
|
created int64
|
|
modified int64
|
|
start interface{}
|
|
end interface{}
|
|
due interface{}
|
|
scheduled interface{}
|
|
wait interface{}
|
|
until interface{}
|
|
recurDuration interface{}
|
|
parentUUIDStr interface{}
|
|
)
|
|
|
|
err := rows.Scan(
|
|
&task.ID,
|
|
&uuidStr,
|
|
&task.Status,
|
|
&task.Description,
|
|
&project,
|
|
&task.Priority,
|
|
&created,
|
|
&modified,
|
|
&start,
|
|
&end,
|
|
&due,
|
|
&scheduled,
|
|
&wait,
|
|
&until,
|
|
&recurDuration,
|
|
&parentUUIDStr,
|
|
)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan task: %w", err)
|
|
}
|
|
|
|
// Parse UUID
|
|
task.UUID, err = uuid.Parse(uuidStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse UUID: %w", err)
|
|
}
|
|
|
|
// Convert timestamps
|
|
task.Created = time.Unix(created, 0)
|
|
task.Modified = time.Unix(modified, 0)
|
|
|
|
// Convert nullable fields
|
|
task.Project = sqlToStringPtr(project)
|
|
task.Start = sqlToTime(start)
|
|
task.End = sqlToTime(end)
|
|
task.Due = sqlToTime(due)
|
|
task.Scheduled = sqlToTime(scheduled)
|
|
task.Wait = sqlToTime(wait)
|
|
task.Until = sqlToTime(until)
|
|
task.RecurrenceDuration = sqlToDuration(recurDuration)
|
|
task.ParentUUID = sqlToUUIDPtr(parentUUIDStr)
|
|
|
|
// Load tags
|
|
tags, err := task.GetTags()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load tags: %w", err)
|
|
}
|
|
task.Tags = tags
|
|
|
|
tasks = append(tasks, task)
|
|
}
|
|
|
|
return tasks, nil
|
|
}
|
|
|
|
// Save inserts or updates the task in the database
|
|
func (t *Task) Save() error {
|
|
db := GetDB()
|
|
if db == nil {
|
|
return fmt.Errorf("database not initialized")
|
|
}
|
|
|
|
t.Modified = timeNow()
|
|
|
|
// Check if task exists
|
|
var exists bool
|
|
err := db.QueryRow("SELECT EXISTS(SELECT 1 FROM tasks WHERE uuid = ?)", t.UUID.String()).Scan(&exists)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to check if task exists: %w", err)
|
|
}
|
|
|
|
if exists {
|
|
// Update existing task
|
|
query := `
|
|
UPDATE tasks SET
|
|
status = ?, description = ?, project = ?, priority = ?,
|
|
modified = ?, start = ?, end = ?, due = ?,
|
|
scheduled = ?, wait = ?, until_date = ?,
|
|
recurrence_duration = ?, parent_uuid = ?
|
|
WHERE uuid = ?
|
|
`
|
|
|
|
_, err = db.Exec(query,
|
|
t.Status,
|
|
t.Description,
|
|
stringPtrToSQL(t.Project),
|
|
t.Priority,
|
|
t.Modified.Unix(),
|
|
timeToSQL(t.Start),
|
|
timeToSQL(t.End),
|
|
timeToSQL(t.Due),
|
|
timeToSQL(t.Scheduled),
|
|
timeToSQL(t.Wait),
|
|
timeToSQL(t.Until),
|
|
durationToSQL(t.RecurrenceDuration),
|
|
uuidPtrToSQL(t.ParentUUID),
|
|
t.UUID.String(),
|
|
)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update task: %w", err)
|
|
}
|
|
} else {
|
|
// Insert new task
|
|
query := `
|
|
INSERT INTO tasks (
|
|
uuid, status, description, project, priority,
|
|
created, modified, start, end, due, scheduled, wait, until_date,
|
|
recurrence_duration, parent_uuid
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`
|
|
|
|
result, err := db.Exec(query,
|
|
t.UUID.String(),
|
|
t.Status,
|
|
t.Description,
|
|
stringPtrToSQL(t.Project),
|
|
t.Priority,
|
|
t.Created.Unix(),
|
|
t.Modified.Unix(),
|
|
timeToSQL(t.Start),
|
|
timeToSQL(t.End),
|
|
timeToSQL(t.Due),
|
|
timeToSQL(t.Scheduled),
|
|
timeToSQL(t.Wait),
|
|
timeToSQL(t.Until),
|
|
durationToSQL(t.RecurrenceDuration),
|
|
uuidPtrToSQL(t.ParentUUID),
|
|
)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to insert task: %w", err)
|
|
}
|
|
|
|
id, err := result.LastInsertId()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get last insert id: %w", err)
|
|
}
|
|
t.ID = int(id)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// AddTag adds a tag to the task
|
|
func (t *Task) AddTag(tag string) error {
|
|
db := GetDB()
|
|
if db == nil {
|
|
return fmt.Errorf("database not initialized")
|
|
}
|
|
|
|
// Ensure task has been saved and has an ID
|
|
if t.ID == 0 {
|
|
return fmt.Errorf("task must be saved before adding tags")
|
|
}
|
|
|
|
_, err := db.Exec("INSERT OR IGNORE INTO tags (task_id, tag) VALUES (?, ?)", t.ID, tag)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to add tag: %w", err)
|
|
}
|
|
|
|
// Update in-memory tags
|
|
for _, existingTag := range t.Tags {
|
|
if existingTag == tag {
|
|
return nil // Already exists
|
|
}
|
|
}
|
|
t.Tags = append(t.Tags, tag)
|
|
|
|
return nil
|
|
}
|
|
|
|
// RemoveTag removes a tag from the task
|
|
func (t *Task) RemoveTag(tag string) error {
|
|
db := GetDB()
|
|
if db == nil {
|
|
return fmt.Errorf("database not initialized")
|
|
}
|
|
|
|
if t.ID == 0 {
|
|
return fmt.Errorf("task must be saved before removing tags")
|
|
}
|
|
|
|
_, err := db.Exec("DELETE FROM tags WHERE task_id = ? AND tag = ?", t.ID, tag)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to remove tag: %w", err)
|
|
}
|
|
|
|
// Update in-memory tags
|
|
newTags := []string{}
|
|
for _, existingTag := range t.Tags {
|
|
if existingTag != tag {
|
|
newTags = append(newTags, existingTag)
|
|
}
|
|
}
|
|
t.Tags = newTags
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetTags retrieves all tags for the task
|
|
func (t *Task) GetTags() ([]string, error) {
|
|
db := GetDB()
|
|
if db == nil {
|
|
return nil, fmt.Errorf("database not initialized")
|
|
}
|
|
|
|
if t.ID == 0 {
|
|
return []string{}, nil
|
|
}
|
|
|
|
rows, err := db.Query("SELECT tag FROM tags WHERE task_id = ? ORDER BY tag", t.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get tags: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
tags := []string{}
|
|
for rows.Next() {
|
|
var tag string
|
|
if err := rows.Scan(&tag); err != nil {
|
|
return nil, fmt.Errorf("failed to scan tag: %w", err)
|
|
}
|
|
tags = append(tags, tag)
|
|
}
|
|
|
|
return tags, nil
|
|
}
|
|
|
|
// Complete marks a task as completed.
|
|
// Returns the next recurring instance if one was spawned, or nil.
|
|
func (t *Task) Complete() (*Task, error) {
|
|
t.Status = StatusCompleted
|
|
now := timeNow()
|
|
t.End = &now
|
|
|
|
if err := t.Save(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// If this is a recurring instance, spawn next instance
|
|
if t.ParentUUID != nil {
|
|
next, err := SpawnNextInstance(t)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("completed task but failed to spawn next instance: %w", err)
|
|
}
|
|
return next, nil
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// Delete marks a task as deleted (soft delete)
|
|
func (t *Task) Delete(permanent bool) error {
|
|
db := GetDB()
|
|
if db == nil {
|
|
return fmt.Errorf("database not initialized")
|
|
}
|
|
|
|
if permanent {
|
|
// Hard delete - remove from database
|
|
_, err := db.Exec("DELETE FROM tasks WHERE uuid = ?", t.UUID.String())
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete task: %w", err)
|
|
}
|
|
} else {
|
|
// Soft delete - mark as deleted
|
|
t.Status = StatusDeleted
|
|
now := timeNow()
|
|
t.End = &now
|
|
if err := t.Save(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// StartTask sets the start time for a task
|
|
func (t *Task) StartTask() error {
|
|
now := timeNow()
|
|
t.Start = &now
|
|
return t.Save()
|
|
}
|
|
|
|
// StopTask clears the start time for a task
|
|
func (t *Task) StopTask() error {
|
|
t.Start = nil
|
|
return t.Save()
|
|
}
|
|
|
|
// IsRecurringTemplate returns true if task is a recurring template
|
|
func (t *Task) IsRecurringTemplate() bool {
|
|
return t.Status == StatusRecurring && t.RecurrenceDuration != nil && t.ParentUUID == nil
|
|
}
|
|
|
|
// IsRecurringInstance returns true if task is a recurring instance
|
|
func (t *Task) IsRecurringInstance() bool {
|
|
return t.ParentUUID != nil
|
|
}
|
|
|
|
// PopulateUrgency computes and sets the Urgency field on the given tasks.
|
|
func PopulateUrgency(tasks ...*Task) {
|
|
cfg, _ := GetConfig()
|
|
coeffs := BuildUrgencyCoefficients(cfg)
|
|
for _, t := range tasks {
|
|
t.Urgency = t.CalculateUrgency(coeffs)
|
|
}
|
|
}
|