diff --git a/frontend/src/api/systems.ts b/frontend/src/api/systems.ts new file mode 100644 index 0000000..d5d8012 --- /dev/null +++ b/frontend/src/api/systems.ts @@ -0,0 +1,44 @@ +import { apiGet, apiPost, apiPatch, apiDelete } from "@/api/client"; + +export interface System { + id: number; + project_id: number; + name: string; + description: string; + color: string | null; + status: "active" | "archived"; + order_index: number; + open_issue_count: number; + created_at: string | null; + updated_at: string | null; +} + +export async function listSystems(projectId: number): Promise { + const data = await apiGet<{ systems: System[] }>(`/api/projects/${projectId}/systems`); + return data.systems; +} + +export async function createSystem( + projectId: number, + data: { name: string; description?: string; color?: string }, +): Promise { + return apiPost(`/api/projects/${projectId}/systems`, data); +} + +export async function updateSystem( + projectId: number, + systemId: number, + data: Partial<{ + name: string; + description: string; + color: string; + status: "active" | "archived"; + order_index: number; + }>, +): Promise { + return apiPatch(`/api/projects/${projectId}/systems/${systemId}`, data); +} + +export async function deleteSystem(projectId: number, systemId: number): Promise { + return apiDelete(`/api/projects/${projectId}/systems/${systemId}`); +} diff --git a/frontend/src/components/SystemsSection.vue b/frontend/src/components/SystemsSection.vue new file mode 100644 index 0000000..75ce89e --- /dev/null +++ b/frontend/src/components/SystemsSection.vue @@ -0,0 +1,525 @@ + + + + + diff --git a/frontend/src/stores/systems.ts b/frontend/src/stores/systems.ts new file mode 100644 index 0000000..0fbb2d5 --- /dev/null +++ b/frontend/src/stores/systems.ts @@ -0,0 +1,73 @@ +import { ref } from "vue"; +import { defineStore } from "pinia"; +import * as api from "@/api/systems"; +import type { System } from "@/api/systems"; +import { useToastStore } from "@/stores/toast"; + +export const useSystemsStore = defineStore("systems", () => { + const systemsByProject = ref>({}); + const loading = ref(false); + + async function fetchSystems(projectId: number) { + loading.value = true; + try { + systemsByProject.value[projectId] = await api.listSystems(projectId); + } catch (e) { + useToastStore().show("Failed to load systems", "error"); + throw e; + } finally { + loading.value = false; + } + } + + async function createSystem( + projectId: number, + data: { name: string; description?: string; color?: string }, + ) { + const system = await api.createSystem(projectId, data); + if (!systemsByProject.value[projectId]) systemsByProject.value[projectId] = []; + systemsByProject.value[projectId].push(system); + return system; + } + + async function updateSystem( + projectId: number, + systemId: number, + data: Partial>, + ) { + const system = await api.updateSystem(projectId, systemId, data); + const list = systemsByProject.value[projectId]; + if (list) { + const idx = list.findIndex((s) => s.id === systemId); + if (idx >= 0) list[idx] = system; + } + return system; + } + + async function archiveSystem(projectId: number, systemId: number) { + return updateSystem(projectId, systemId, { status: "archived" }); + } + + async function unarchiveSystem(projectId: number, systemId: number) { + return updateSystem(projectId, systemId, { status: "active" }); + } + + async function deleteSystem(projectId: number, systemId: number) { + await api.deleteSystem(projectId, systemId); + const list = systemsByProject.value[projectId]; + if (list) { + systemsByProject.value[projectId] = list.filter((s) => s.id !== systemId); + } + } + + return { + systemsByProject, + loading, + fetchSystems, + createSystem, + updateSystem, + archiveSystem, + unarchiveSystem, + deleteSystem, + }; +}); diff --git a/frontend/src/views/ProjectView.vue b/frontend/src/views/ProjectView.vue index cf9e182..5290cce 100644 --- a/frontend/src/views/ProjectView.vue +++ b/frontend/src/views/ProjectView.vue @@ -7,6 +7,7 @@ import { useTasksStore } from "@/stores/tasks"; import { relativeTime } from "@/composables/useRelativeTime"; import ShareDialog from "@/components/ShareDialog.vue"; import ProjectRulesTab from "@/components/rules/ProjectRulesTab.vue"; +import SystemsSection from "@/components/SystemsSection.vue"; import { LayoutGrid, Clock, @@ -87,7 +88,7 @@ async function confirmStartPlanning() { const saving = ref(false); const error = ref(null); -const activeTab = ref<"tasks" | "notes" | "rules">("tasks"); +const activeTab = ref<"tasks" | "notes" | "systems" | "rules">("tasks"); const tasks = ref([]); const notes = ref([]); @@ -479,6 +480,9 @@ async function confirmDelete() { Notes {{ project.summary.note_count }} + @@ -663,6 +667,9 @@ async function confirmDelete() { + + +