22a3a3c1d1
Vue 3 + TypeScript frontend with Pinia stores, markdown rendering (marked + DOMPurify), wikilink/tag linkification, and autocomplete. Quart async backend with SQLAlchemy 2.0, PostgreSQL ARRAY columns, task-note companion linking, backlinks, and note-to-task conversion. Docker Compose setup with PostgreSQL 16 and Ollama. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
181 lines
4.5 KiB
TypeScript
181 lines
4.5 KiB
TypeScript
import { ref } from "vue";
|
|
import { defineStore } from "pinia";
|
|
import { apiGet, apiPost, apiPut, apiPatch, apiDelete } from "@/api/client";
|
|
import type { Task, TaskListResponse, TaskStatus, TaskPriority } from "@/types/task";
|
|
|
|
export const useTasksStore = defineStore("tasks", () => {
|
|
const tasks = ref<Task[]>([]);
|
|
const currentTask = ref<Task | null>(null);
|
|
const total = ref(0);
|
|
const loading = ref(false);
|
|
|
|
// Filter / pagination / sort state
|
|
const activeTagFilters = ref<string[]>([]);
|
|
const statusFilter = ref<TaskStatus | "">("");
|
|
const priorityFilter = ref<TaskPriority | "">("");
|
|
const limit = ref(20);
|
|
const offset = ref(0);
|
|
const sortField = ref("updated_at");
|
|
const sortOrder = ref<"asc" | "desc">("desc");
|
|
const searchQuery = ref("");
|
|
|
|
async function refresh() {
|
|
loading.value = true;
|
|
try {
|
|
const searchParams = new URLSearchParams();
|
|
if (searchQuery.value) searchParams.set("q", searchQuery.value);
|
|
for (const t of activeTagFilters.value) {
|
|
searchParams.append("tag", t);
|
|
}
|
|
if (statusFilter.value) searchParams.set("status", statusFilter.value);
|
|
if (priorityFilter.value)
|
|
searchParams.set("priority", priorityFilter.value);
|
|
searchParams.set("sort", sortField.value);
|
|
searchParams.set("order", sortOrder.value);
|
|
searchParams.set("limit", String(limit.value));
|
|
searchParams.set("offset", String(offset.value));
|
|
|
|
const qs = searchParams.toString();
|
|
const data = await apiGet<TaskListResponse>(
|
|
`/api/tasks${qs ? `?${qs}` : ""}`
|
|
);
|
|
tasks.value = data.tasks;
|
|
total.value = data.total;
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
async function fetchTask(id: number) {
|
|
loading.value = true;
|
|
try {
|
|
currentTask.value = await apiGet<Task>(`/api/tasks/${id}`);
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
async function createTask(data: {
|
|
title: string;
|
|
description: string;
|
|
status?: TaskStatus;
|
|
priority?: TaskPriority;
|
|
due_date?: string | null;
|
|
}): Promise<Task> {
|
|
return await apiPost<Task>("/api/tasks", data);
|
|
}
|
|
|
|
async function updateTask(
|
|
id: number,
|
|
data: Partial<
|
|
Pick<Task, "title" | "description" | "status" | "priority" | "due_date">
|
|
>
|
|
): Promise<Task> {
|
|
const task = await apiPut<Task>(`/api/tasks/${id}`, data);
|
|
if (currentTask.value?.id === id) {
|
|
currentTask.value = task;
|
|
}
|
|
return task;
|
|
}
|
|
|
|
async function patchStatus(id: number, status: TaskStatus): Promise<Task> {
|
|
const task = await apiPatch<Task>(`/api/tasks/${id}/status`, { status });
|
|
// Update in list if present
|
|
const idx = tasks.value.findIndex((t) => t.id === id);
|
|
if (idx !== -1) {
|
|
tasks.value[idx] = task;
|
|
}
|
|
if (currentTask.value?.id === id) {
|
|
currentTask.value = task;
|
|
}
|
|
return task;
|
|
}
|
|
|
|
async function deleteTask(id: number) {
|
|
await apiDelete(`/api/tasks/${id}`);
|
|
tasks.value = tasks.value.filter((t) => t.id !== id);
|
|
if (currentTask.value?.id === id) {
|
|
currentTask.value = null;
|
|
}
|
|
}
|
|
|
|
function setStatusFilter(status: TaskStatus | "") {
|
|
statusFilter.value = status;
|
|
offset.value = 0;
|
|
refresh();
|
|
}
|
|
|
|
function setPriorityFilter(priority: TaskPriority | "") {
|
|
priorityFilter.value = priority;
|
|
offset.value = 0;
|
|
refresh();
|
|
}
|
|
|
|
function addTagFilter(tag: string) {
|
|
if (!activeTagFilters.value.includes(tag)) {
|
|
activeTagFilters.value.push(tag);
|
|
offset.value = 0;
|
|
refresh();
|
|
}
|
|
}
|
|
|
|
function removeTagFilter(tag: string) {
|
|
activeTagFilters.value = activeTagFilters.value.filter((t) => t !== tag);
|
|
offset.value = 0;
|
|
refresh();
|
|
}
|
|
|
|
function clearTagFilters() {
|
|
activeTagFilters.value = [];
|
|
offset.value = 0;
|
|
refresh();
|
|
}
|
|
|
|
function setSort(field: string, order: "asc" | "desc") {
|
|
sortField.value = field;
|
|
sortOrder.value = order;
|
|
offset.value = 0;
|
|
refresh();
|
|
}
|
|
|
|
function setOffset(newOffset: number) {
|
|
offset.value = newOffset;
|
|
refresh();
|
|
}
|
|
|
|
function setSearch(q: string) {
|
|
searchQuery.value = q;
|
|
offset.value = 0;
|
|
refresh();
|
|
}
|
|
|
|
return {
|
|
tasks,
|
|
currentTask,
|
|
total,
|
|
loading,
|
|
activeTagFilters,
|
|
statusFilter,
|
|
priorityFilter,
|
|
limit,
|
|
offset,
|
|
sortField,
|
|
sortOrder,
|
|
searchQuery,
|
|
refresh,
|
|
fetchTask,
|
|
createTask,
|
|
updateTask,
|
|
patchStatus,
|
|
deleteTask,
|
|
setStatusFilter,
|
|
setPriorityFilter,
|
|
addTagFilter,
|
|
removeTagFilter,
|
|
clearTagFilters,
|
|
setSort,
|
|
setOffset,
|
|
setSearch,
|
|
};
|
|
});
|