From 4da29562bd7898903a248de48ccc123843fc0da2 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sat, 13 Jun 2026 23:42:40 -0400 Subject: [PATCH] =?UTF-8?q?feat(issues):=20S4a=20UI=20=E2=80=94=20Systems?= =?UTF-8?q?=20management=20section=20in=20project=20view?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Frontend foundation for Issues + Systems (spec #825, S4a). - frontend/src/api/systems.ts: typed client (System + list/create/update/delete) over /api/projects//systems, matching the rulebooks api style. - frontend/src/stores/systems.ts: Pinia store keyed by project (fetch/create/ update/archive/unarchive/delete), toast-on-error. - frontend/src/components/SystemsSection.vue: a Systems management section — cards (color swatch, name, description, 'N open' issue-count badge) with inline create/edit, archive (hidden behind a 'show archived' toggle), and a delete-confirm modal. v1 quality: loading skeleton, empty state, error toasts, keyboard a11y, focus rings; reuses existing CSS tokens (no new colors). - ProjectView.vue: new 'Systems' tab (between Notes and Rules), rendering , wired like the existing rules tab. S4b (next) adds issue-editor controls (kind=issue/system multi-select/arose-from), open-issues lists, and the dashboard surface. NEEDS operator browser verification (CI typechecks but can't render). Refs plan 825 (S4a). Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/api/systems.ts | 44 ++ frontend/src/components/SystemsSection.vue | 525 +++++++++++++++++++++ frontend/src/stores/systems.ts | 73 +++ frontend/src/views/ProjectView.vue | 9 +- 4 files changed, 650 insertions(+), 1 deletion(-) create mode 100644 frontend/src/api/systems.ts create mode 100644 frontend/src/components/SystemsSection.vue create mode 100644 frontend/src/stores/systems.ts 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() { + + +