Skip to main content

Tasks API

Manage academic and personal tasks with support for subtasks, recurring patterns, and Pomodoro tracking.

Store Location

src/stores/useTaskStore.ts

Data Types

Task

interface Task {
  id: string;                           // UUID
  title: string;                        // Task title (sanitized)
  description?: string;                 // Optional description
  status: TaskStatus;                   // 'pending' | 'in_progress' | 'completed'
  priority: TaskPriority;               // 'urgent' | 'high' | 'medium' | 'low'
  dueDate?: string;                     // ISO date string
  isRecurring: boolean;                 // Recurring task flag
  recurringPattern?: RecurringPattern;  // Recurrence configuration
  pomodorosEstimated?: number;          // Estimated pomodoros
  pomodorosCompleted: number;           // Completed pomodoros
  createdAt: string;                    // ISO timestamp
  completedAt?: string;                 // ISO timestamp when completed
  subtasks: Subtask[];                  // Array of subtasks
}

Subtask

interface Subtask {
  id: string;           // UUID
  title: string;        // Subtask title
  isCompleted: boolean; // Completion status
}

TaskStatus

type TaskStatus = 'pending' | 'in_progress' | 'completed';

TaskPriority

type TaskPriority = 'urgent' | 'high' | 'medium' | 'low';

RecurringPattern

interface RecurringPattern {
  frequency: 'daily' | 'weekly' | 'monthly';
  interval?: number;        // Every N days/weeks/months
  daysOfWeek?: number[];    // For weekly: [0-6] (Sunday=0)
  endDate?: string;         // ISO date string
}

Task Operations

Fetch Tasks

limit
number
Maximum tasks to fetch (default: 50)
import { useTaskStore } from '@/stores/useTaskStore';

// Fetch latest 50 tasks
await useTaskStore.getState().fetchTasks();

// Fetch specific limit
await useTaskStore.getState().fetchTasks(100);
Query:
SELECT tasks.*, subtasks.*
FROM tasks
LEFT JOIN subtasks ON subtasks.task_id = tasks.id
WHERE user_id = $1
ORDER BY created_at DESC
LIMIT $2;

Add Task

title
string
required
Task title (sanitized before saving)
options
object
Optional task configurationFields:
  • priority (TaskPriority): Default ‘medium’
  • dueDate (string): ISO date string
  • isRecurring (boolean): Default false
  • recurringPattern (RecurringPattern): Required if isRecurring=true
// Simple task
await useTaskStore.getState().addTask('Complete Math homework');

// Task with options
await useTaskStore.getState().addTask('Study for exam', {
  priority: 'urgent',
  dueDate: '2026-03-15',
  isRecurring: false
});

// Recurring task
await useTaskStore.getState().addTask('Daily review', {
  priority: 'medium',
  isRecurring: true,
  recurringPattern: {
    frequency: 'daily',
    interval: 1
  }
});
Process:
  1. Sanitizes title using sanitizarTexto()
  2. Generates UUID
  3. Creates task with defaults
  4. Optimistically updates store
  5. Syncs to Supabase

Update Task

id
string
required
Task UUID
updates
Partial<Task>
required
Fields to update
await useTaskStore.getState().updateTask('task-uuid', {
  title: 'Updated title',
  priority: 'high',
  dueDate: '2026-03-20'
});
Updatable Fields:
  • title (sanitized)
  • status
  • priority
  • dueDate

Delete Task

id
string
required
Task UUID
await useTaskStore.getState().deleteTask('task-uuid');
Cascade: Deletes all subtasks due to ON DELETE CASCADE in schema.

Toggle Task Status

id
string
required
Task UUID
Cycles through: pendingin_progresscompletedpending
await useTaskStore.getState().toggleTaskStatus('task-uuid');
Process:
  1. Finds task in store
  2. Calculates next status
  3. Sets completedAt timestamp when status becomes ‘completed’
  4. Optimistically updates store
  5. Syncs to database
  6. Triggers achievement check if completed
  7. Creates next recurring task if applicable
Recurring Task Logic:
if (nextStatus === 'completed' && task.isRecurring && task.recurringPattern) {
  const nextDate = calculateNextDueDate(baseDate, task.recurringPattern);
  // Creates new task with:
  // - Same title, priority, pattern
  // - Status: 'pending'
  // - New UUID
  // - Reset subtasks (all uncompleted)
  // - New dueDate
}

Subtask Operations

Add Subtask

taskId
string
required
Parent task UUID
title
string
required
Subtask title
await useTaskStore.getState().addSubtask('task-uuid', 'Read chapter 1');

Toggle Subtask

taskId
string
required
Parent task UUID
subtaskId
string
required
Subtask UUID
await useTaskStore.getState().toggleSubtask('task-uuid', 'subtask-uuid');

Delete Subtask

taskId
string
required
Parent task UUID
subtaskId
string
required
Subtask UUID
await useTaskStore.getState().deleteSubtask('task-uuid', 'subtask-uuid');

Filtering and Sorting

Set Filter

filter
TaskStatus | 'all'
required
Filter by status or show all
useTaskStore.getState().setFilter('pending');    // Show only pending
useTaskStore.getState().setFilter('completed');  // Show only completed
useTaskStore.getState().setFilter('all');        // Show all tasks

Set Sort Order

sortBy
'priority' | 'dueDate' | 'createdAt'
required
Sort criterion
useTaskStore.getState().setSortBy('priority');   // Sort by priority
useTaskStore.getState().setSortBy('dueDate');    // Sort by due date
useTaskStore.getState().setSortBy('createdAt');  // Sort by creation date
Priority Order:
const PRIORITY_ORDER = {
  urgent: 0,
  high: 1,
  medium: 2,
  low: 3
};

Get Filtered Tasks

Applies current filter and sort settings:
const filteredTasks = useTaskStore.getState().getFilteredTasks();

Store State

const { tasks, filter, sortBy, isLoading } = useTaskStore();

console.log(`Showing ${tasks.length} tasks`);
console.log(`Filter: ${filter}, Sort: ${sortBy}`);

Store Fields

tasks
Task[]
Array of all tasks (unfiltered)
filter
TaskStatus | 'all'
Current filter setting
sortBy
'priority' | 'dueDate' | 'createdAt'
Current sort criterion
isLoading
boolean
Loading state during fetch operations

Database Schema

From /home/daytona/workspace/source/supabase/schema.sql:155:
create table tasks (
  id uuid default gen_random_uuid() primary key,
  user_id uuid references profiles(id) on delete cascade not null,
  subject_id uuid references subjects(id) on delete set null,
  goal_id uuid references goals(id) on delete set null,
  
  title text not null,
  description text,
  status task_status default 'pending',
  priority task_priority default 'medium',
  due_date timestamp with time zone,
  
  is_recurring boolean default false,
  recurrence_pattern jsonb,
  
  pomodoros_estimated int default 0,
  pomodoros_completed int default 0,
  
  created_at timestamp with time zone default now(),
  updated_at timestamp with time zone default now(),
  completed_at timestamp with time zone
);

create table subtasks (
  id uuid default gen_random_uuid() primary key,
  task_id uuid references tasks(id) on delete cascade not null,
  title text not null,
  is_completed boolean default false
);

Row Level Security

-- Users can only access their own tasks
create policy "Usuarios gestionan sus tareas" 
  on tasks for all 
  using ( auth.uid() = user_id );

-- Subtasks inherit task permissions
create policy "Usuarios gestionan sus subtareas" 
  on subtasks for all 
  using ( 
    exists (
      select 1 from tasks 
      where id = subtasks.task_id 
        and user_id = auth.uid()
    )
  );

Pomodoro Integration

Tasks track Pomodoro sessions:
// Increment completed pomodoros
const task = useTaskStore.getState().tasks.find(t => t.id === 'task-uuid');
if (task) {
  await useTaskStore.getState().updateTask(task.id, {
    pomodorosCompleted: task.pomodorosCompleted + 1
  });
}
Alternatively, use the database function:
SELECT increment_task_pomodoros('task-uuid');

Achievements Integration

When a task is completed, achievement check is triggered:
if (nextStatus === 'completed') {
  const totalCompleted = tasks.filter(t => t.status === 'completed').length;
  
  // Dynamic import to avoid circular dependencies
  const { useAchievementsStore } = await import('./useAchievementsStore');
  useAchievementsStore.getState().checkAndUnlock('task_complete', { 
    count: totalCompleted 
  });
}

Persistence

Store is persisted to localStorage:
{
  name: 'focus-tasks-storage',
  storage: localStorage
}
Note: All fields are persisted. Tasks remain cached even offline.

Example: Task List Component

import { useTaskStore } from '@/stores/useTaskStore';
import { useEffect } from 'react';

function TaskList() {
  const { fetchTasks, getFilteredTasks, toggleTaskStatus, isLoading } = useTaskStore();

  useEffect(() => {
    fetchTasks();
  }, [fetchTasks]);

  const tasks = getFilteredTasks();

  if (isLoading) return <div>Loading tasks...</div>;

  return (
    <ul>
      {tasks.map(task => (
        <li key={task.id}>
          <button onClick={() => toggleTaskStatus(task.id)}>
            {task.status === 'completed' ? '✓' : '○'}
          </button>
          <span>{task.title}</span>
          <span>Priority: {task.priority}</span>
          <span>Subtasks: {task.subtasks.filter(st => st.isCompleted).length}/{task.subtasks.length}</span>
        </li>
      ))}
    </ul>
  );
}