popilot 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/hydrate.mjs +6 -1
- package/lib/setup-wizard.mjs +29 -3
- package/package.json +1 -1
- package/scaffold/.claude/commands/_domain.md.hbs +33 -0
- package/scaffold/.claude/commands/analytics.md.hbs +55 -0
- package/scaffold/.claude/commands/daily.md.hbs +301 -0
- package/scaffold/.claude/commands/dev.md.hbs +62 -0
- package/scaffold/.claude/commands/gtm.md +82 -0
- package/scaffold/.claude/commands/handoff.md +259 -0
- package/scaffold/.claude/commands/market.md +120 -0
- package/scaffold/.claude/commands/metrics.md +123 -0
- package/scaffold/.claude/commands/oscar-loop.md +436 -0
- package/scaffold/.claude/commands/party.md +85 -0
- package/scaffold/.claude/commands/plan.md +43 -0
- package/scaffold/.claude/commands/poc.md +69 -0
- package/scaffold/.claude/commands/research.md +203 -0
- package/scaffold/.claude/commands/retro.md +68 -0
- package/scaffold/.claude/commands/save.md +440 -0
- package/scaffold/.claude/commands/sessions.md +139 -0
- package/scaffold/.claude/commands/sprint.md +106 -0
- package/scaffold/.claude/commands/start.md +396 -0
- package/scaffold/.claude/commands/strategy.md +41 -0
- package/scaffold/.claude/commands/task.md +220 -0
- package/scaffold/.claude/commands/tracking.md +116 -0
- package/scaffold/.claude/commands/validate.md +58 -0
- package/scaffold/.context/WORKFLOW.md.hbs +58 -26
- package/scaffold/.context/agents/planner.md.hbs +35 -7
- package/scaffold/.context/integrations/_registry.yaml +6 -0
- package/scaffold/.context/integrations/providers/sqlite_lambda.yaml +24 -0
- package/scaffold/.context/integrations/providers/supabase.yaml +34 -0
- package/scaffold/.context/integrations/providers/turso_cf.yaml +34 -0
- package/scaffold/.context/poc/_skills/build.md +79 -0
- package/scaffold/.context/poc/_skills/scope.md +50 -0
- package/scaffold/.context/poc/_skills/spec.md +80 -0
- package/scaffold/.context/poc/_skills/verify.md +60 -0
- package/scaffold/CLAUDE.md.hbs +210 -0
- package/scaffold/spec-site/.env.example +11 -0
- package/scaffold/spec-site/index.html +2 -2
- package/scaffold/spec-site/sql/schema.sql +224 -0
- package/scaffold/spec-site/src/api/client.ts +131 -0
- package/scaffold/spec-site/src/api/types.ts +177 -0
- package/scaffold/spec-site/src/components/Accordion.vue +1 -1
- package/scaffold/spec-site/src/components/AppHeader.vue +5 -4
- package/scaffold/spec-site/src/components/CoachingCard.vue +1 -1
- package/scaffold/spec-site/src/components/ScenarioSwitcher.vue +1 -1
- package/scaffold/spec-site/src/composables/navTypes.ts +39 -0
- package/scaffold/spec-site/src/composables/pmTypes.ts +134 -0
- package/scaffold/spec-site/src/composables/useAuth.ts +139 -0
- package/scaffold/spec-site/src/composables/useMemo.ts +51 -40
- package/scaffold/spec-site/src/composables/useNavStore.ts +202 -0
- package/scaffold/spec-site/src/composables/usePageContent.ts +208 -0
- package/scaffold/spec-site/src/composables/usePmStore.ts +224 -0
- package/scaffold/spec-site/src/composables/useRetro.ts +181 -95
- package/scaffold/spec-site/src/composables/useScenarioStore.ts +74 -30
- package/scaffold/spec-site/src/composables/useUser.ts +12 -6
- package/scaffold/spec-site/src/data/navigation.ts +7 -42
- package/scaffold/spec-site/src/data/types.ts +13 -43
- package/scaffold/spec-site/src/main.ts +7 -0
- package/scaffold/spec-site/src/pages/PolicyDetail.vue +30 -11
- package/scaffold/spec-site/src/pages/PolicyIndex.vue +22 -7
- package/scaffold/spec-site/src/pages/retro/RetroActions.vue +3 -3
- package/scaffold/spec-site/src/pages/retro/RetroBoard.vue +2 -2
- package/scaffold/spec-site/src/pages/retro/RetroHeader.vue +5 -7
- package/scaffold/spec-site/src/pages/retro/RetroPage.vue +2 -2
- package/scaffold/spec-site/src/pages/shared/NoContentPlaceholder.vue +2 -2
- package/scaffold/spec-site/src/pages/shared/PolicyFallback.vue +25 -13
- package/scaffold/spec-site/src/router.ts +11 -7
- package/scaffold/spec-site/src/styles/base.css +2 -2
- package/scaffold/spec-site/src/styles/split-pane.css +1 -1
- package/scaffold/spec-site/src/styles/variables.css +7 -7
- package/scaffold/spec-site/src/assets/icons/menu/ic_ads.svg +0 -10
- package/scaffold/spec-site/src/assets/icons/menu/ic_ads_on.svg +0 -10
- package/scaffold/spec-site/src/assets/icons/menu/ic_board.svg +0 -14
- package/scaffold/spec-site/src/assets/icons/menu/ic_board_on.svg +0 -14
- package/scaffold/spec-site/src/assets/icons/menu/ic_dashboard.svg +0 -21
- package/scaffold/spec-site/src/assets/icons/menu/ic_dashboard_on.svg +0 -21
- package/scaffold/spec-site/src/assets/icons/menu/ic_pricing.svg +0 -20
- package/scaffold/spec-site/src/assets/icons/menu/ic_pricing_on.svg +0 -20
- package/scaffold/spec-site/src/assets/icons/menu/ic_store.svg +0 -11
- package/scaffold/spec-site/src/assets/icons/menu/ic_store_on.svg +0 -11
- package/scaffold/spec-site/src/composables/useTurso.ts +0 -160
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usePageContent — Load page content (rules, scenarios, specAreas, versions, meta)
|
|
3
|
+
*
|
|
4
|
+
* Stale-while-revalidate pattern:
|
|
5
|
+
* - localStorage cache for instant render + background API fetch
|
|
6
|
+
* - API failure gracefully falls back to cache
|
|
7
|
+
*
|
|
8
|
+
* In static mode, returns empty content (pages use local data files instead).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { ref, type Ref } from 'vue'
|
|
12
|
+
import { apiGet, isStaticMode } from '@/api/client'
|
|
13
|
+
import type { PageContentApiResponse } from '@/api/types'
|
|
14
|
+
import type { Rule, Scenario, SpecArea, PageVersion, VersionChange } from '@/data/types'
|
|
15
|
+
|
|
16
|
+
// ── Row → Domain mappers ──
|
|
17
|
+
|
|
18
|
+
function rowToRule(row: PageContentApiResponse['rules'][number]): Rule {
|
|
19
|
+
return {
|
|
20
|
+
id: row.id,
|
|
21
|
+
category: row.category,
|
|
22
|
+
name: row.name,
|
|
23
|
+
condition: row.condition,
|
|
24
|
+
severity: row.severity,
|
|
25
|
+
homeMessage: row.home_message,
|
|
26
|
+
action: row.action,
|
|
27
|
+
dataSource: row.data_source,
|
|
28
|
+
implStatus: row.impl_status,
|
|
29
|
+
implNote: row.impl_note ?? undefined,
|
|
30
|
+
actionRoute: row.action_route ?? undefined,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── Cache helpers ──
|
|
35
|
+
|
|
36
|
+
function cacheKey(pageId: string, sprint: string) {
|
|
37
|
+
return `spec_content_${pageId}_${sprint}`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface CachedContent {
|
|
41
|
+
rules: Record<string, Rule[]>
|
|
42
|
+
scenarios: Scenario<any>[]
|
|
43
|
+
specAreas: SpecArea[]
|
|
44
|
+
version: PageVersion | null
|
|
45
|
+
wireframeMeta: { specTitle: string; routeTitle: string; defaultScenarioId: string } | null
|
|
46
|
+
ts: number
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function loadCache(pageId: string, sprint: string): CachedContent | null {
|
|
50
|
+
try {
|
|
51
|
+
const raw = localStorage.getItem(cacheKey(pageId, sprint))
|
|
52
|
+
if (!raw) return null
|
|
53
|
+
return JSON.parse(raw) as CachedContent
|
|
54
|
+
} catch {
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function saveCache(pageId: string, sprint: string, content: CachedContent) {
|
|
60
|
+
try {
|
|
61
|
+
localStorage.setItem(cacheKey(pageId, sprint), JSON.stringify(content))
|
|
62
|
+
} catch {
|
|
63
|
+
// localStorage full — silently fail
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Public API ──
|
|
68
|
+
|
|
69
|
+
export interface PageContent {
|
|
70
|
+
rules: Ref<Record<string, Rule[]>>
|
|
71
|
+
scenarios: Ref<Scenario<any>[]>
|
|
72
|
+
specAreas: Ref<SpecArea[]>
|
|
73
|
+
version: Ref<PageVersion | null>
|
|
74
|
+
wireframeMeta: Ref<{ specTitle: string; routeTitle: string; defaultScenarioId: string } | null>
|
|
75
|
+
loading: Ref<boolean>
|
|
76
|
+
error: Ref<string | null>
|
|
77
|
+
load: () => Promise<void>
|
|
78
|
+
getRuleGroup: (group: string) => Rule[]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function usePageContent(pageId: string, sprint: string): PageContent {
|
|
82
|
+
const rules = ref<Record<string, Rule[]>>({})
|
|
83
|
+
const scenarios = ref<Scenario<any>[]>([])
|
|
84
|
+
const specAreas = ref<SpecArea[]>([])
|
|
85
|
+
const version = ref<PageVersion | null>(null)
|
|
86
|
+
const wireframeMeta = ref<{ specTitle: string; routeTitle: string; defaultScenarioId: string } | null>(null)
|
|
87
|
+
const loading = ref(true)
|
|
88
|
+
const error = ref<string | null>(null)
|
|
89
|
+
|
|
90
|
+
function applyContent(content: CachedContent) {
|
|
91
|
+
rules.value = content.rules
|
|
92
|
+
scenarios.value = content.scenarios
|
|
93
|
+
specAreas.value = content.specAreas
|
|
94
|
+
version.value = content.version
|
|
95
|
+
wireframeMeta.value = content.wireframeMeta
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function transformResponse(data: PageContentApiResponse): CachedContent {
|
|
99
|
+
// Transform rules: group by rule_group
|
|
100
|
+
const rulesByGroup: Record<string, Rule[]> = {}
|
|
101
|
+
for (const row of data.rules) {
|
|
102
|
+
const group = row.rule_group
|
|
103
|
+
if (!rulesByGroup[group]) rulesByGroup[group] = []
|
|
104
|
+
rulesByGroup[group].push(rowToRule(row))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Transform scenarios
|
|
108
|
+
const scenarioList: Scenario<any>[] = data.scenarios.map(row => ({
|
|
109
|
+
id: row.scenario_id,
|
|
110
|
+
label: row.label,
|
|
111
|
+
data: JSON.parse(row.data_json),
|
|
112
|
+
}))
|
|
113
|
+
|
|
114
|
+
// Transform spec areas
|
|
115
|
+
const areaList: SpecArea[] = data.areas.map(row => ({
|
|
116
|
+
id: row.area_id,
|
|
117
|
+
label: row.label,
|
|
118
|
+
shortLabel: row.short_label,
|
|
119
|
+
ruleCount: row.rule_count,
|
|
120
|
+
}))
|
|
121
|
+
|
|
122
|
+
// Transform version
|
|
123
|
+
const vRow = data.versions[0]
|
|
124
|
+
const pageVersion: PageVersion | null = vRow
|
|
125
|
+
? {
|
|
126
|
+
page: vRow.page_id,
|
|
127
|
+
version: vRow.version,
|
|
128
|
+
lastUpdated: vRow.last_updated,
|
|
129
|
+
sprint: vRow.sprint,
|
|
130
|
+
status: vRow.status as PageVersion['status'],
|
|
131
|
+
changelog: JSON.parse(vRow.changelog) as VersionChange[],
|
|
132
|
+
}
|
|
133
|
+
: null
|
|
134
|
+
|
|
135
|
+
// Transform wireframe meta
|
|
136
|
+
const mRow = data.meta[0]
|
|
137
|
+
const meta = mRow
|
|
138
|
+
? {
|
|
139
|
+
specTitle: mRow.spec_title,
|
|
140
|
+
routeTitle: mRow.route_title,
|
|
141
|
+
defaultScenarioId: mRow.default_scenario_id,
|
|
142
|
+
}
|
|
143
|
+
: null
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
rules: rulesByGroup,
|
|
147
|
+
scenarios: scenarioList,
|
|
148
|
+
specAreas: areaList,
|
|
149
|
+
version: pageVersion,
|
|
150
|
+
wireframeMeta: meta,
|
|
151
|
+
ts: Date.now(),
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function load() {
|
|
156
|
+
if (isStaticMode()) {
|
|
157
|
+
loading.value = false
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
loading.value = true
|
|
162
|
+
error.value = null
|
|
163
|
+
|
|
164
|
+
// 1. Try cache first (stale-while-revalidate)
|
|
165
|
+
const cached = loadCache(pageId, sprint)
|
|
166
|
+
if (cached) {
|
|
167
|
+
applyContent(cached)
|
|
168
|
+
loading.value = false
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 2. Fetch from API (background if cache existed)
|
|
172
|
+
try {
|
|
173
|
+
const { data, error: apiError } = await apiGet<PageContentApiResponse>(
|
|
174
|
+
`/api/v2/page-content/${encodeURIComponent(pageId)}/${encodeURIComponent(sprint)}`,
|
|
175
|
+
)
|
|
176
|
+
if (apiError || !data) throw new Error(apiError ?? 'Unknown error')
|
|
177
|
+
|
|
178
|
+
const fresh = transformResponse(data)
|
|
179
|
+
applyContent(fresh)
|
|
180
|
+
saveCache(pageId, sprint, fresh)
|
|
181
|
+
error.value = null
|
|
182
|
+
} catch (err) {
|
|
183
|
+
const msg = err instanceof Error ? err.message : 'Unknown error'
|
|
184
|
+
if (!cached) {
|
|
185
|
+
error.value = msg
|
|
186
|
+
}
|
|
187
|
+
console.warn(`[usePageContent] API fetch failed for ${pageId}/${sprint}: ${msg}`)
|
|
188
|
+
} finally {
|
|
189
|
+
loading.value = false
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function getRuleGroup(group: string): Rule[] {
|
|
194
|
+
return rules.value[group] ?? []
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
rules,
|
|
199
|
+
scenarios,
|
|
200
|
+
specAreas,
|
|
201
|
+
version,
|
|
202
|
+
wireframeMeta,
|
|
203
|
+
loading,
|
|
204
|
+
error,
|
|
205
|
+
load,
|
|
206
|
+
getRuleGroup,
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PM Store — API-backed epics, stories & tasks
|
|
3
|
+
*
|
|
4
|
+
* Singleton pattern: refs are module-level, shared across all consumers.
|
|
5
|
+
* In static mode, all data is empty and CRUD operations are disabled.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ref } from 'vue'
|
|
9
|
+
import { apiGet, apiPost, apiPatch, apiDelete, isStaticMode } from '@/api/client'
|
|
10
|
+
import {
|
|
11
|
+
type PmEpic, type PmStory, type PmTask,
|
|
12
|
+
type StoryStatus, type TaskStatus, type Priority, type EpicStatus,
|
|
13
|
+
mapEpic, mapStory, mapTask,
|
|
14
|
+
} from './pmTypes'
|
|
15
|
+
import type { PmEpicRow, PmStoryRow, PmTaskRow } from '@/api/types'
|
|
16
|
+
|
|
17
|
+
export type { StoryStatus, TaskStatus, Priority, EpicStatus, PmEpic, PmStory, PmTask } from './pmTypes'
|
|
18
|
+
export {
|
|
19
|
+
STORY_STATUSES, TASK_STATUSES, PRIORITIES, AREAS, EPIC_STATUSES,
|
|
20
|
+
STORY_STATUS_LABELS, TASK_STATUS_LABELS, PRIORITY_LABELS, EPIC_STATUS_LABELS,
|
|
21
|
+
} from './pmTypes'
|
|
22
|
+
|
|
23
|
+
// ── Singleton state ──
|
|
24
|
+
|
|
25
|
+
export const pmEpics = ref<PmEpic[]>([])
|
|
26
|
+
export const stories = ref<PmStory[]>([])
|
|
27
|
+
export const tasks = ref<PmTask[]>([])
|
|
28
|
+
export const pmLoaded = ref(false)
|
|
29
|
+
|
|
30
|
+
// ── Load ──
|
|
31
|
+
|
|
32
|
+
export async function loadEpics(): Promise<void> {
|
|
33
|
+
if (isStaticMode()) return
|
|
34
|
+
const { data, error } = await apiGet<{ epics: PmEpicRow[] }>('/api/v2/pm/epics')
|
|
35
|
+
if (!error && data) {
|
|
36
|
+
pmEpics.value = data.epics.map(mapEpic)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function loadPmData(sprint?: string): Promise<void> {
|
|
41
|
+
if (isStaticMode()) { pmLoaded.value = true; return }
|
|
42
|
+
const params: Record<string, string> = {}
|
|
43
|
+
if (sprint) params.sprint = sprint
|
|
44
|
+
|
|
45
|
+
const { data, error } = await apiGet<{ stories: PmStoryRow[]; tasks: PmTaskRow[] }>(
|
|
46
|
+
'/api/v2/pm/data', params,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if (!error && data) {
|
|
50
|
+
stories.value = data.stories.map(mapStory)
|
|
51
|
+
tasks.value = data.tasks.map(mapTask)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
pmLoaded.value = true
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Epic CRUD ──
|
|
58
|
+
|
|
59
|
+
export async function addEpic(data: {
|
|
60
|
+
title: string
|
|
61
|
+
description?: string | null
|
|
62
|
+
status?: EpicStatus
|
|
63
|
+
owner?: string | null
|
|
64
|
+
}): Promise<{ id?: number; error?: string }> {
|
|
65
|
+
if (isStaticMode()) return { error: 'CRUD not available in static mode' }
|
|
66
|
+
const { data: resp, error } = await apiPost<{ id?: number }>('/api/v2/pm/epics', {
|
|
67
|
+
title: data.title,
|
|
68
|
+
description: data.description ?? null,
|
|
69
|
+
status: data.status ?? 'active',
|
|
70
|
+
owner: data.owner ?? null,
|
|
71
|
+
})
|
|
72
|
+
if (error) return { error }
|
|
73
|
+
await loadEpics()
|
|
74
|
+
return { id: resp?.id }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function updateEpic(id: number, data: {
|
|
78
|
+
title?: string
|
|
79
|
+
description?: string | null
|
|
80
|
+
status?: EpicStatus
|
|
81
|
+
owner?: string | null
|
|
82
|
+
}): Promise<{ error?: string }> {
|
|
83
|
+
if (isStaticMode()) return { error: 'CRUD not available in static mode' }
|
|
84
|
+
const { error } = await apiPatch(`/api/v2/pm/epics/${id}`, data as Record<string, unknown>)
|
|
85
|
+
if (error) return { error }
|
|
86
|
+
await loadEpics()
|
|
87
|
+
return {}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function deleteEpic(id: number): Promise<{ error?: string }> {
|
|
91
|
+
if (isStaticMode()) return { error: 'CRUD not available in static mode' }
|
|
92
|
+
const { error } = await apiDelete(`/api/v2/pm/epics/${id}`)
|
|
93
|
+
if (error) return { error }
|
|
94
|
+
await loadEpics()
|
|
95
|
+
return {}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── Story CRUD ──
|
|
99
|
+
|
|
100
|
+
export async function addStory(data: {
|
|
101
|
+
epicId: number | null
|
|
102
|
+
sprint: string
|
|
103
|
+
title: string
|
|
104
|
+
description?: string | null
|
|
105
|
+
acceptanceCriteria?: string | null
|
|
106
|
+
assignee?: string | null
|
|
107
|
+
status?: StoryStatus
|
|
108
|
+
priority?: Priority
|
|
109
|
+
area?: string
|
|
110
|
+
storyPoints?: number | null
|
|
111
|
+
}): Promise<{ id?: number; error?: string }> {
|
|
112
|
+
if (isStaticMode()) return { error: 'CRUD not available in static mode' }
|
|
113
|
+
const maxOrder = stories.value
|
|
114
|
+
.filter(s => s.sprint === data.sprint)
|
|
115
|
+
.reduce((max, s) => Math.max(max, s.sortOrder), -1)
|
|
116
|
+
|
|
117
|
+
const { data: resp, error } = await apiPost<{ id?: number }>('/api/v2/pm/stories', {
|
|
118
|
+
epicId: data.epicId,
|
|
119
|
+
sprint: data.sprint,
|
|
120
|
+
title: data.title,
|
|
121
|
+
description: data.description ?? null,
|
|
122
|
+
acceptanceCriteria: data.acceptanceCriteria ?? null,
|
|
123
|
+
assignee: data.assignee ?? null,
|
|
124
|
+
status: data.status ?? 'draft',
|
|
125
|
+
priority: data.priority ?? 'medium',
|
|
126
|
+
area: data.area ?? 'FE',
|
|
127
|
+
storyPoints: data.storyPoints ?? null,
|
|
128
|
+
sortOrder: maxOrder + 1,
|
|
129
|
+
})
|
|
130
|
+
if (error) return { error }
|
|
131
|
+
await loadPmData(data.sprint)
|
|
132
|
+
return { id: resp?.id }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function updateStory(id: number, data: {
|
|
136
|
+
title?: string
|
|
137
|
+
description?: string | null
|
|
138
|
+
acceptanceCriteria?: string | null
|
|
139
|
+
assignee?: string | null
|
|
140
|
+
status?: StoryStatus
|
|
141
|
+
priority?: Priority
|
|
142
|
+
area?: string
|
|
143
|
+
storyPoints?: number | null
|
|
144
|
+
epicId?: number | null
|
|
145
|
+
sprint?: string
|
|
146
|
+
}): Promise<{ error?: string }> {
|
|
147
|
+
if (isStaticMode()) return { error: 'CRUD not available in static mode' }
|
|
148
|
+
const { error } = await apiPatch(`/api/v2/pm/stories/${id}`, data as Record<string, unknown>)
|
|
149
|
+
if (error) return { error }
|
|
150
|
+
await loadPmData()
|
|
151
|
+
return {}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function deleteStory(id: number): Promise<{ error?: string }> {
|
|
155
|
+
if (isStaticMode()) return { error: 'CRUD not available in static mode' }
|
|
156
|
+
const { error } = await apiDelete(`/api/v2/pm/stories/${id}`)
|
|
157
|
+
if (error) return { error }
|
|
158
|
+
await loadPmData()
|
|
159
|
+
return {}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── Task CRUD ──
|
|
163
|
+
|
|
164
|
+
export async function addTask(data: {
|
|
165
|
+
storyId: number
|
|
166
|
+
title: string
|
|
167
|
+
assignee?: string | null
|
|
168
|
+
description?: string | null
|
|
169
|
+
}): Promise<{ id?: number; error?: string }> {
|
|
170
|
+
if (isStaticMode()) return { error: 'CRUD not available in static mode' }
|
|
171
|
+
const maxOrder = tasks.value
|
|
172
|
+
.filter(t => t.storyId === data.storyId)
|
|
173
|
+
.reduce((max, t) => Math.max(max, t.sortOrder), -1)
|
|
174
|
+
|
|
175
|
+
const { data: resp, error } = await apiPost<{ id?: number }>('/api/v2/pm/tasks', {
|
|
176
|
+
storyId: data.storyId,
|
|
177
|
+
title: data.title,
|
|
178
|
+
assignee: data.assignee ?? null,
|
|
179
|
+
description: data.description ?? null,
|
|
180
|
+
sortOrder: maxOrder + 1,
|
|
181
|
+
})
|
|
182
|
+
if (error) return { error }
|
|
183
|
+
await loadPmData()
|
|
184
|
+
return { id: resp?.id }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function updateTask(id: number, data: {
|
|
188
|
+
title?: string
|
|
189
|
+
assignee?: string | null
|
|
190
|
+
status?: TaskStatus
|
|
191
|
+
description?: string | null
|
|
192
|
+
}): Promise<{ error?: string }> {
|
|
193
|
+
if (isStaticMode()) return { error: 'CRUD not available in static mode' }
|
|
194
|
+
const { error } = await apiPatch(`/api/v2/pm/tasks/${id}`, data as Record<string, unknown>)
|
|
195
|
+
if (error) return { error }
|
|
196
|
+
await loadPmData()
|
|
197
|
+
return {}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function deleteTask(id: number): Promise<{ error?: string }> {
|
|
201
|
+
if (isStaticMode()) return { error: 'CRUD not available in static mode' }
|
|
202
|
+
const { error } = await apiDelete(`/api/v2/pm/tasks/${id}`)
|
|
203
|
+
if (error) return { error }
|
|
204
|
+
await loadPmData()
|
|
205
|
+
return {}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ── Helpers ──
|
|
209
|
+
|
|
210
|
+
export function getStoriesForEpic(epicId: number): PmStory[] {
|
|
211
|
+
return stories.value.filter(s => s.epicId === epicId)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function getStoriesForSprint(sprint: string): PmStory[] {
|
|
215
|
+
return stories.value.filter(s => s.sprint === sprint)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function getEpicById(id: number): PmEpic | undefined {
|
|
219
|
+
return pmEpics.value.find(e => e.id === id)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function getTasksForStory(storyId: number): PmTask[] {
|
|
223
|
+
return tasks.value.filter(t => t.storyId === storyId)
|
|
224
|
+
}
|