diff --git a/opal-web/src/lib/api/endpoints.js b/opal-web/src/lib/api/endpoints.js index c79ea9d..6780afb 100644 --- a/opal-web/src/lib/api/endpoints.js +++ b/opal-web/src/lib/api/endpoints.js @@ -98,6 +98,28 @@ export const tasks = { */ async stop(uuid) { return apiRequest(`/tasks/${uuid}/stop`, { method: 'POST' }); + }, + + /** + * Parse CLI input and create task + * @param {string} input - Raw opal CLI syntax + * @returns {Promise} + */ + async parse(input) { + return apiRequest('/tasks/parse', { + method: 'POST', + body: JSON.stringify({ input }) + }); + }, + + /** + * List tasks by report name + * @param {string} reportName + * @returns {Promise} + */ + async listByReport(reportName) { + const result = await apiRequest(`/tasks?report=${encodeURIComponent(reportName)}`); + return result.tasks ?? result; } }; diff --git a/opal-web/src/lib/stores/tasks.js b/opal-web/src/lib/stores/tasks.js index 46a3bd7..322a6d7 100644 --- a/opal-web/src/lib/stores/tasks.js +++ b/opal-web/src/lib/stores/tasks.js @@ -1,26 +1,142 @@ import { writable, derived } from 'svelte/store'; import { tasks as tasksAPI } from '$lib/api/endpoints.js'; import { queueChange } from '$lib/utils/sync-queue.js'; +import { generateUUID } from '$lib/utils/uuid.js'; /** * @typedef {import('$lib/api/types.js').Task} Task * @typedef {import('$lib/api/types.js').TaskFilters} TaskFilters */ +const MOCK_MODE = import.meta.env.VITE_MOCK_MODE === 'true'; + +/** Report names that map to pending tasks in mock mode */ +const PENDING_REPORTS = new Set(['list', 'next', 'active', 'ready', 'overdue', 'waiting', 'newest', 'oldest']); + /** * Create tasks store */ function createTasksStore() { const { subscribe, set, update } = writable(/** @type {Task[]} */ ([])); - + + /** @type {Task[]} */ + let mockData = []; + + /** Ensure mock data is loaded */ + async function ensureMockData() { + if (mockData.length === 0) { + const { mockTasks } = await import('$lib/mock/tasks.js'); + mockData = [...mockTasks]; + } + } + return { subscribe, - + /** - * Load all tasks from API + * Load tasks by report name + * @param {string} reportName - Backend report name (e.g. 'list', 'next', 'completed') + */ + async loadReport(reportName) { + if (MOCK_MODE) { + await ensureMockData(); + if (reportName === 'completed') { + set(mockData.filter(t => t.status === 'C')); + } else if (PENDING_REPORTS.has(reportName)) { + set(mockData.filter(t => t.status === 'P')); + } else { + set(mockData.filter(t => t.status === 'P')); + } + return; + } + + try { + const tasks = await tasksAPI.listByReport(reportName); + set(tasks); + } catch (error) { + console.error('Failed to load report:', error); + throw error; + } + }, + + /** + * Parse CLI input and create a task + * @param {string} input - Raw opal CLI syntax + * @returns {Promise} + */ + async parseAndCreate(input) { + if (MOCK_MODE) { + await ensureMockData(); + // Naive parse: non-modifier words become description + const words = input.split(/\s+/); + const descWords = []; + const task = /** @type {Task} */ ({ + uuid: generateUUID(), + id: mockData.length + 1, + status: 'P', + description: '', + project: null, + priority: 1, + created: Date.now() / 1000, + modified: Date.now() / 1000, + start: null, + end: null, + due: null, + scheduled: null, + wait: null, + until: null, + recurrence_duration: null, + parent_uuid: null, + tags: [] + }); + + for (let i = 0; i < words.length; i++) { + const w = words[i]; + if (w.startsWith('project:')) { + task.project = w.slice(8); + } else if (w.startsWith('priority:') || w.startsWith('pri:')) { + const val = w.includes(':') ? w.split(':')[1] : '1'; + /** @type {Record} */ + const map = { H: 3, M: 2, L: 0, h: 3, m: 2, l: 0 }; + task.priority = /** @type {import('$lib/api/types.js').TaskPriority} */ ((map[val] ?? parseInt(val, 10)) || 1); + } else if (w.startsWith('+')) { + task.tags = [...(task.tags || []), w.slice(1)]; + } else { + descWords.push(w); + } + } + task.description = descWords.join(' ') || 'New task'; + + mockData = [task, ...mockData]; + update(tasks => [task, ...tasks]); + return task; + } + + try { + const created = await tasksAPI.parse(input); + update(tasks => [created, ...tasks]); + return created; + } catch (error) { + console.error('Failed to parse and create task:', error); + throw error; + } + }, + + /** + * Load all tasks from API (or mock data in dev) * @param {TaskFilters} [filters] */ async load(filters = {}) { + if (MOCK_MODE) { + await ensureMockData(); + let filtered = mockData; + if (filters.status) { + filtered = filtered.filter(t => t.status === filters.status); + } + set(filtered); + return; + } + try { const tasks = await tasksAPI.list(filters); set(tasks); @@ -29,12 +145,19 @@ function createTasksStore() { throw error; } }, - + /** * Add new task (optimistic update) * @param {Partial} task */ async add(task) { + if (MOCK_MODE) { + const fullTask = /** @type {Task} */ ({ ...task, id: mockData.length + 1 }); + mockData = [...mockData, fullTask]; + update(tasks => [...tasks, fullTask]); + return fullTask; + } + try { const created = await tasksAPI.create(task); update(tasks => [...tasks, created]); @@ -46,13 +169,13 @@ function createTasksStore() { task_uuid: task.uuid, data: task }); - + // Still update UI optimistically update(tasks => [...tasks, task]); throw error; } }, - + /** * Update task (optimistic update) * @param {string} uuid @@ -67,7 +190,15 @@ function createTasksStore() { } return tasks; }); - + + if (MOCK_MODE) { + const index = mockData.findIndex(t => t.uuid === uuid); + if (index >= 0) { + mockData[index] = { ...mockData[index], ...updates, modified: Date.now() / 1000 }; + } + return; + } + try { const updated = await tasksAPI.update(uuid, updates); // Sync with server response @@ -88,7 +219,7 @@ function createTasksStore() { throw error; } }, - + /** * Delete task * @param {string} uuid @@ -96,7 +227,12 @@ function createTasksStore() { async deleteTask(uuid) { // Optimistic removal update(tasks => tasks.filter(t => t.uuid !== uuid)); - + + if (MOCK_MODE) { + mockData = mockData.filter(t => t.uuid !== uuid); + return; + } + try { await tasksAPI.delete(uuid); } catch (error) { @@ -108,12 +244,39 @@ function createTasksStore() { throw error; } }, - + /** * Complete task * @param {string} uuid */ async complete(uuid) { + if (MOCK_MODE) { + update(tasks => { + const index = tasks.findIndex(t => t.uuid === uuid); + if (index >= 0) { + const newStatus = tasks[index].status === 'C' ? 'P' : 'C'; + tasks[index] = { + ...tasks[index], + status: /** @type {'P'|'C'} */ (newStatus), + end: newStatus === 'C' ? Date.now() / 1000 : null, + modified: Date.now() / 1000 + }; + } + return [...tasks]; + }); + const mi = mockData.findIndex(t => t.uuid === uuid); + if (mi >= 0) { + const newStatus = mockData[mi].status === 'C' ? 'P' : 'C'; + mockData[mi] = { + ...mockData[mi], + status: /** @type {'P'|'C'} */ (newStatus), + end: newStatus === 'C' ? Date.now() / 1000 : null, + modified: Date.now() / 1000 + }; + } + return; + } + try { const completed = await tasksAPI.complete(uuid); update(tasks => {