New features: - Task comments with date-stamped entries and last-comment summary - Recurring tasks expanded on calendar (daily/weekly/monthly/yearly) - System tray mode replacing CMD window (Windows/macOS/Linux) - Ironpad logo as exe icon, tray icon, favicon, and header logo Technical changes: - Backend restructured for dual-mode: dev (API-only) / prod (tray + server) - tray-item crate for cross-platform tray, winresource for icon embedding - Calendar view refactored with CalendarEntry interface for recurring merging - Added CHANGELOG.md, build-local.ps1, version bumped to 0.2.0 Co-authored-by: Cursor <cursoragent@cursor.com>
274 lines
6.9 KiB
TypeScript
274 lines
6.9 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
import { ref, computed } from 'vue'
|
|
import type { Task, TaskWithContent } from '../types'
|
|
import { tasksApi } from '../api/client'
|
|
|
|
export const useTasksStore = defineStore('tasks', () => {
|
|
// State
|
|
const tasks = ref<Task[]>([])
|
|
const allTasks = ref<Task[]>([])
|
|
const currentProjectId = ref<string | null>(null)
|
|
const selectedTask = ref<TaskWithContent | null>(null)
|
|
const loading = ref(false)
|
|
const error = ref<string | null>(null)
|
|
|
|
// Getters
|
|
const activeTasks = computed(() =>
|
|
tasks.value.filter(t => !t.completed && t.section !== 'Backlog')
|
|
)
|
|
|
|
const completedTasks = computed(() =>
|
|
tasks.value.filter(t => t.completed || t.section === 'Completed')
|
|
)
|
|
|
|
const backlogTasks = computed(() =>
|
|
tasks.value.filter(t => !t.completed && t.section === 'Backlog')
|
|
)
|
|
|
|
const pendingTasks = computed(() =>
|
|
allTasks.value.filter(t => !t.completed)
|
|
)
|
|
|
|
const tasksByProject = computed(() => (projectId: string) =>
|
|
allTasks.value.filter(t => t.project_id === projectId)
|
|
)
|
|
|
|
/** All unique tags used across tasks in the current project */
|
|
const projectTags = computed(() => {
|
|
const tagSet = new Set<string>()
|
|
for (const task of tasks.value) {
|
|
if (task.tags) {
|
|
for (const tag of task.tags) {
|
|
tagSet.add(tag)
|
|
}
|
|
}
|
|
}
|
|
return [...tagSet].sort()
|
|
})
|
|
|
|
// Actions
|
|
async function loadAllTasks() {
|
|
try {
|
|
loading.value = true
|
|
error.value = null
|
|
allTasks.value = await tasksApi.listAll()
|
|
} catch (err) {
|
|
error.value = `Failed to load tasks: ${err}`
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function loadProjectTasks(projectId: string) {
|
|
try {
|
|
loading.value = true
|
|
error.value = null
|
|
currentProjectId.value = projectId
|
|
tasks.value = await tasksApi.list(projectId)
|
|
} catch (err) {
|
|
error.value = `Failed to load project tasks: ${err}`
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function loadTask(projectId: string, taskId: string) {
|
|
try {
|
|
error.value = null
|
|
selectedTask.value = await tasksApi.get(projectId, taskId)
|
|
} catch (err) {
|
|
error.value = `Failed to load task: ${err}`
|
|
selectedTask.value = null
|
|
}
|
|
}
|
|
|
|
async function createTask(projectId: string, title: string, section?: string, parentId?: string) {
|
|
try {
|
|
error.value = null
|
|
const task = await tasksApi.create(projectId, title, section, parentId)
|
|
|
|
// Refresh tasks list
|
|
if (currentProjectId.value === projectId) {
|
|
await loadProjectTasks(projectId)
|
|
}
|
|
|
|
return task
|
|
} catch (err) {
|
|
error.value = `Failed to create task: ${err}`
|
|
throw err
|
|
}
|
|
}
|
|
|
|
async function updateTaskContent(projectId: string, taskId: string, content: string) {
|
|
try {
|
|
error.value = null
|
|
const task = await tasksApi.updateContent(projectId, taskId, content)
|
|
selectedTask.value = task
|
|
|
|
// Refresh tasks list to update timestamps
|
|
if (currentProjectId.value === projectId) {
|
|
await loadProjectTasks(projectId)
|
|
}
|
|
|
|
return task
|
|
} catch (err) {
|
|
error.value = `Failed to update task: ${err}`
|
|
throw err
|
|
}
|
|
}
|
|
|
|
async function toggleTask(projectId: string, taskId: string) {
|
|
try {
|
|
error.value = null
|
|
await tasksApi.toggle(projectId, taskId)
|
|
|
|
// Refresh tasks
|
|
if (currentProjectId.value === projectId) {
|
|
await loadProjectTasks(projectId)
|
|
}
|
|
|
|
// Update selected task if it's the one being toggled
|
|
if (selectedTask.value?.id === taskId) {
|
|
await loadTask(projectId, taskId)
|
|
}
|
|
} catch (err) {
|
|
error.value = `Failed to toggle task: ${err}`
|
|
throw err
|
|
}
|
|
}
|
|
|
|
async function updateTaskMeta(
|
|
projectId: string,
|
|
taskId: string,
|
|
meta: { title?: string; section?: string; priority?: string; due_date?: string; is_active?: boolean; tags?: string[]; recurrence?: string; recurrence_interval?: number }
|
|
) {
|
|
try {
|
|
error.value = null
|
|
await tasksApi.updateMeta(projectId, taskId, meta)
|
|
|
|
// Refresh tasks
|
|
if (currentProjectId.value === projectId) {
|
|
await loadProjectTasks(projectId)
|
|
}
|
|
|
|
// Update selected task if it's the one being updated
|
|
if (selectedTask.value?.id === taskId) {
|
|
await loadTask(projectId, taskId)
|
|
}
|
|
} catch (err) {
|
|
error.value = `Failed to update task: ${err}`
|
|
throw err
|
|
}
|
|
}
|
|
|
|
async function deleteTask(projectId: string, taskId: string) {
|
|
try {
|
|
error.value = null
|
|
await tasksApi.delete(projectId, taskId)
|
|
|
|
// Clear selected task if it was deleted
|
|
if (selectedTask.value?.id === taskId) {
|
|
selectedTask.value = null
|
|
}
|
|
|
|
// Refresh tasks
|
|
if (currentProjectId.value === projectId) {
|
|
await loadProjectTasks(projectId)
|
|
}
|
|
} catch (err) {
|
|
error.value = `Failed to delete task: ${err}`
|
|
throw err
|
|
}
|
|
}
|
|
|
|
async function addComment(projectId: string, taskId: string, text: string) {
|
|
try {
|
|
error.value = null
|
|
const task = await tasksApi.addComment(projectId, taskId, text)
|
|
selectedTask.value = task
|
|
|
|
// Refresh task list so last_comment updates
|
|
if (currentProjectId.value === projectId) {
|
|
await loadProjectTasks(projectId)
|
|
}
|
|
|
|
return task
|
|
} catch (err) {
|
|
error.value = `Failed to add comment: ${err}`
|
|
throw err
|
|
}
|
|
}
|
|
|
|
async function deleteComment(projectId: string, taskId: string, commentIndex: number) {
|
|
try {
|
|
error.value = null
|
|
const task = await tasksApi.deleteComment(projectId, taskId, commentIndex)
|
|
selectedTask.value = task
|
|
|
|
// Refresh task list so last_comment updates
|
|
if (currentProjectId.value === projectId) {
|
|
await loadProjectTasks(projectId)
|
|
}
|
|
|
|
return task
|
|
} catch (err) {
|
|
error.value = `Failed to delete comment: ${err}`
|
|
throw err
|
|
}
|
|
}
|
|
|
|
function selectTask(task: Task | null) {
|
|
if (task && currentProjectId.value) {
|
|
loadTask(currentProjectId.value, task.id)
|
|
} else {
|
|
selectedTask.value = null
|
|
}
|
|
}
|
|
|
|
function clearSelectedTask() {
|
|
selectedTask.value = null
|
|
}
|
|
|
|
function clearProjectTasks() {
|
|
tasks.value = []
|
|
currentProjectId.value = null
|
|
selectedTask.value = null
|
|
}
|
|
|
|
function clearError() {
|
|
error.value = null
|
|
}
|
|
|
|
return {
|
|
// State
|
|
tasks,
|
|
allTasks,
|
|
currentProjectId,
|
|
selectedTask,
|
|
loading,
|
|
error,
|
|
// Getters
|
|
activeTasks,
|
|
completedTasks,
|
|
backlogTasks,
|
|
pendingTasks,
|
|
tasksByProject,
|
|
projectTags,
|
|
// Actions
|
|
loadAllTasks,
|
|
loadProjectTasks,
|
|
loadTask,
|
|
createTask,
|
|
updateTaskContent,
|
|
toggleTask,
|
|
updateTaskMeta,
|
|
deleteTask,
|
|
addComment,
|
|
deleteComment,
|
|
selectTask,
|
|
clearSelectedTask,
|
|
clearProjectTasks,
|
|
clearError
|
|
}
|
|
})
|