checkvist-api/tasks.go

275 lines
7.5 KiB
Go
Raw Permalink Normal View History

2026-01-14 12:42:00 +01:00
package checkvist
import (
"context"
"fmt"
"time"
)
2026-01-14 12:42:00 +01:00
// tasks.go contains the TaskService for CRUD operations on tasks within a checklist.
// TaskService provides operations on tasks within a specific checklist.
type TaskService struct {
client *Client
checklistID int
}
// Tasks returns a TaskService for performing task operations on the specified checklist.
func (c *Client) Tasks(checklistID int) *TaskService {
return &TaskService{
client: c,
checklistID: checklistID,
}
}
// List returns all tasks in the checklist.
func (s *TaskService) List(ctx context.Context) ([]Task, error) {
path := fmt.Sprintf("/checklists/%d/tasks.json", s.checklistID)
var tasks []Task
if err := s.client.doGet(ctx, path, &tasks); err != nil {
return nil, err
}
// Parse due dates
for i := range tasks {
parseDueDate(&tasks[i])
}
return tasks, nil
}
// Get returns a single task by ID, including its parent hierarchy.
func (s *TaskService) Get(ctx context.Context, taskID int) (*Task, error) {
path := fmt.Sprintf("/checklists/%d/tasks/%d.json", s.checklistID, taskID)
var task Task
if err := s.client.doGet(ctx, path, &task); err != nil {
return nil, err
}
parseDueDate(&task)
return &task, nil
}
// CreateTaskRequest represents the request body for creating a task.
type CreateTaskRequest struct {
2026-01-14 21:24:46 +01:00
Content string `json:"content"`
ParentID int `json:"parent_id,omitempty"`
Position int `json:"position,omitempty"`
Due string `json:"due_date,omitempty"`
Priority int `json:"priority,omitempty"`
Tags string `json:"tags,omitempty"`
Repeat string `json:"repeat,omitempty"`
}
// createTaskWrapper wraps the task fields for the nested JSON format
// expected by the Checkvist API: {"task": {"content": "...", ...}}
type createTaskWrapper struct {
Task CreateTaskRequest `json:"task"`
}
// TaskBuilder provides a fluent interface for building task creation requests.
type TaskBuilder struct {
content string
parentID int
position int
due string
priority int
tags []string
repeat string
}
// NewTask creates a new TaskBuilder with the given content.
func NewTask(content string) *TaskBuilder {
return &TaskBuilder{content: content}
}
// WithParent sets the parent task ID for creating a subtask.
func (b *TaskBuilder) WithParent(parentID int) *TaskBuilder {
b.parentID = parentID
return b
}
// WithPosition sets the position of the task within its siblings.
func (b *TaskBuilder) WithPosition(position int) *TaskBuilder {
b.position = position
return b
}
// WithDueDate sets the due date using a DueDate value.
func (b *TaskBuilder) WithDueDate(due DueDate) *TaskBuilder {
b.due = due.String()
return b
}
// WithPriority sets the priority level (1 = highest, 2 = high, 0 = normal).
func (b *TaskBuilder) WithPriority(priority int) *TaskBuilder {
b.priority = priority
return b
}
// WithTags sets the tags for the task.
func (b *TaskBuilder) WithTags(tags ...string) *TaskBuilder {
b.tags = tags
return b
}
// WithRepeat sets the repeat pattern for the task using Checkvist's smart syntax.
// Common patterns include:
// - "daily" - repeats every day
// - "weekly" - repeats every week
// - "monthly" - repeats every month
// - "yearly" - repeats every year
// - "every 2 days" - repeats every 2 days
// - "every week on monday" - repeats weekly on Monday
// - "every month on 15" - repeats monthly on the 15th
// - "every 2 weeks on friday" - repeats every 2 weeks on Friday
func (b *TaskBuilder) WithRepeat(pattern string) *TaskBuilder {
b.repeat = pattern
return b
}
// build converts the TaskBuilder to a CreateTaskRequest.
func (b *TaskBuilder) build() CreateTaskRequest {
req := CreateTaskRequest{
Content: b.content,
ParentID: b.parentID,
Position: b.position,
Due: b.due,
Priority: b.priority,
Repeat: b.repeat,
}
if len(b.tags) > 0 {
for i, tag := range b.tags {
if i > 0 {
req.Tags += ", "
}
req.Tags += tag
}
}
return req
}
// Create creates a new task using a TaskBuilder.
func (s *TaskService) Create(ctx context.Context, builder *TaskBuilder) (*Task, error) {
path := fmt.Sprintf("/checklists/%d/tasks.json", s.checklistID)
body := createTaskWrapper{Task: builder.build()}
var task Task
if err := s.client.doPost(ctx, path, body, &task); err != nil {
return nil, err
}
parseDueDate(&task)
return &task, nil
}
// UpdateTaskRequest represents the request body for updating a task.
type UpdateTaskRequest struct {
Content *string `json:"content,omitempty"`
ParentID *int `json:"parent_id,omitempty"`
Position *int `json:"position,omitempty"`
Due *string `json:"due_date,omitempty"`
Priority *int `json:"priority,omitempty"`
Tags *string `json:"tags,omitempty"`
}
// updateTaskWrapper wraps the task fields for PUT requests
type updateTaskWrapper struct {
Task UpdateTaskRequest `json:"task"`
}
// Update updates an existing task.
func (s *TaskService) Update(ctx context.Context, taskID int, req UpdateTaskRequest) (*Task, error) {
path := fmt.Sprintf("/checklists/%d/tasks/%d.json", s.checklistID, taskID)
body := updateTaskWrapper{Task: req}
var task Task
if err := s.client.doPut(ctx, path, body, &task); err != nil {
return nil, err
}
parseDueDate(&task)
return &task, nil
}
// Delete permanently deletes a task.
func (s *TaskService) Delete(ctx context.Context, taskID int) error {
path := fmt.Sprintf("/checklists/%d/tasks/%d.json", s.checklistID, taskID)
return s.client.doDelete(ctx, path)
}
// Close marks a task as completed.
// The API returns an array containing the modified task and potentially its subtasks.
func (s *TaskService) Close(ctx context.Context, taskID int) (*Task, error) {
path := fmt.Sprintf("/checklists/%d/tasks/%d/close.json", s.checklistID, taskID)
var tasks []Task
if err := s.client.doPost(ctx, path, nil, &tasks); err != nil {
return nil, err
}
if len(tasks) == 0 {
return nil, fmt.Errorf("close task: unexpected empty response")
}
parseDueDate(&tasks[0])
return &tasks[0], nil
}
// Reopen reopens a closed or invalidated task.
// The API returns an array containing the modified task and potentially its subtasks.
func (s *TaskService) Reopen(ctx context.Context, taskID int) (*Task, error) {
path := fmt.Sprintf("/checklists/%d/tasks/%d/reopen.json", s.checklistID, taskID)
var tasks []Task
if err := s.client.doPost(ctx, path, nil, &tasks); err != nil {
return nil, err
}
if len(tasks) == 0 {
return nil, fmt.Errorf("reopen task: unexpected empty response")
}
parseDueDate(&tasks[0])
return &tasks[0], nil
}
// Invalidate marks a task as invalidated (strikethrough).
// The API returns an array containing the modified task and potentially its subtasks.
func (s *TaskService) Invalidate(ctx context.Context, taskID int) (*Task, error) {
path := fmt.Sprintf("/checklists/%d/tasks/%d/invalidate.json", s.checklistID, taskID)
var tasks []Task
if err := s.client.doPost(ctx, path, nil, &tasks); err != nil {
return nil, err
}
if len(tasks) == 0 {
return nil, fmt.Errorf("invalidate task: unexpected empty response")
}
parseDueDate(&tasks[0])
return &tasks[0], nil
}
// parseDueDate attempts to parse the DueDateRaw string into a time.Time.
// It supports the Checkvist API format (YYYY/MM/DD) and ISO 8601 format (YYYY-MM-DD).
func parseDueDate(task *Task) {
if task.DueDateRaw == "" {
return
}
// Try multiple date formats (API uses slashes, ISO uses dashes)
formats := []string{
"2006/01/02", // Checkvist API format
"2006-01-02", // ISO 8601 format
}
for _, format := range formats {
if t, err := time.Parse(format, task.DueDateRaw); err == nil {
task.DueDate = &t
return
}
}
}