popilot 0.5.0 → 0.7.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.
Files changed (171) hide show
  1. package/adapters/codex/.codex/commands/_domain.md.hbs +33 -0
  2. package/adapters/codex/.codex/commands/analytics.md.hbs +55 -0
  3. package/adapters/codex/.codex/commands/daily.md.hbs +301 -0
  4. package/adapters/codex/.codex/commands/dev.md.hbs +62 -0
  5. package/adapters/codex/.codex/commands/gtm.md +82 -0
  6. package/adapters/codex/.codex/commands/handoff.md +259 -0
  7. package/adapters/codex/.codex/commands/market.md +120 -0
  8. package/adapters/codex/.codex/commands/metrics.md +123 -0
  9. package/adapters/codex/.codex/commands/oscar-loop.md +436 -0
  10. package/adapters/codex/.codex/commands/party.md +85 -0
  11. package/adapters/codex/.codex/commands/plan.md +43 -0
  12. package/adapters/codex/.codex/commands/research.md +203 -0
  13. package/adapters/codex/.codex/commands/retro.md +68 -0
  14. package/adapters/codex/.codex/commands/save.md +440 -0
  15. package/adapters/codex/.codex/commands/sessions.md +139 -0
  16. package/adapters/codex/.codex/commands/sprint.md +106 -0
  17. package/adapters/codex/.codex/commands/start.md +396 -0
  18. package/adapters/codex/.codex/commands/strategy.md +41 -0
  19. package/adapters/codex/.codex/commands/task.md +220 -0
  20. package/adapters/codex/.codex/commands/tracking.md +116 -0
  21. package/adapters/codex/.codex/commands/validate.md +58 -0
  22. package/adapters/codex/AGENTS.md.hbs +210 -0
  23. package/adapters/codex/manifest.yaml +36 -0
  24. package/adapters/gemini/.gemini/commands/_domain.md.hbs +33 -0
  25. package/adapters/gemini/.gemini/commands/analytics.md.hbs +55 -0
  26. package/adapters/gemini/.gemini/commands/daily.md.hbs +301 -0
  27. package/adapters/gemini/.gemini/commands/dev.md.hbs +62 -0
  28. package/adapters/gemini/.gemini/commands/gtm.md +82 -0
  29. package/adapters/gemini/.gemini/commands/handoff.md +259 -0
  30. package/adapters/gemini/.gemini/commands/market.md +120 -0
  31. package/adapters/gemini/.gemini/commands/metrics.md +123 -0
  32. package/adapters/gemini/.gemini/commands/oscar-loop.md +436 -0
  33. package/adapters/gemini/.gemini/commands/party.md +85 -0
  34. package/adapters/gemini/.gemini/commands/plan.md +43 -0
  35. package/adapters/gemini/.gemini/commands/research.md +203 -0
  36. package/adapters/gemini/.gemini/commands/retro.md +68 -0
  37. package/adapters/gemini/.gemini/commands/save.md +440 -0
  38. package/adapters/gemini/.gemini/commands/sessions.md +139 -0
  39. package/adapters/gemini/.gemini/commands/sprint.md +106 -0
  40. package/adapters/gemini/.gemini/commands/start.md +396 -0
  41. package/adapters/gemini/.gemini/commands/strategy.md +41 -0
  42. package/adapters/gemini/.gemini/commands/task.md +220 -0
  43. package/adapters/gemini/.gemini/commands/tracking.md +116 -0
  44. package/adapters/gemini/.gemini/commands/validate.md +58 -0
  45. package/adapters/gemini/GEMINI.md.hbs +210 -0
  46. package/adapters/gemini/manifest.yaml +36 -0
  47. package/bin/cli.mjs +215 -4
  48. package/lib/doctor.mjs +38 -1
  49. package/lib/hydrate.mjs +15 -0
  50. package/lib/industry-presets.mjs +135 -0
  51. package/lib/scaffold.mjs +5 -0
  52. package/lib/setup-wizard.mjs +71 -2
  53. package/package.json +1 -1
  54. package/scaffold/.context/agents/TEMPLATE.md +14 -0
  55. package/scaffold/.context/agents/analyst.md.hbs +3 -3
  56. package/scaffold/.context/agents/developer.md.hbs +5 -5
  57. package/scaffold/.context/agents/gtm-strategist.md.hbs +3 -3
  58. package/scaffold/.context/agents/handoff-specialist.md.hbs +18 -18
  59. package/scaffold/.context/agents/market-researcher.md.hbs +6 -6
  60. package/scaffold/.context/agents/orchestrator.md.hbs +8 -8
  61. package/scaffold/.context/agents/planner.md.hbs +6 -6
  62. package/scaffold/.context/agents/qa.md.hbs +5 -5
  63. package/scaffold/.context/agents/researcher.md.hbs +33 -6
  64. package/scaffold/.context/agents/strategist.md.hbs +8 -8
  65. package/scaffold/.context/agents/tracking-governor.md.hbs +2 -2
  66. package/scaffold/.context/project.yaml.example +25 -0
  67. package/scaffold/mcp-pm/package.json +19 -0
  68. package/scaffold/mcp-pm/src/api-client.ts +69 -0
  69. package/scaffold/mcp-pm/src/index.ts +660 -0
  70. package/scaffold/mcp-pm/tsconfig.json +14 -0
  71. package/scaffold/pm-api/package.json +21 -0
  72. package/scaffold/pm-api/sql/schema-core.sql +331 -0
  73. package/scaffold/pm-api/sql/schema-docs.sql +25 -0
  74. package/scaffold/pm-api/sql/schema-meetings.sql +17 -0
  75. package/scaffold/pm-api/sql/schema-rewards.sql +16 -0
  76. package/scaffold/pm-api/src/auth.ts +28 -0
  77. package/scaffold/pm-api/src/blockchain/adapter.ts +20 -0
  78. package/scaffold/pm-api/src/blockchain/tron.ts +62 -0
  79. package/scaffold/pm-api/src/db/adapter.ts +36 -0
  80. package/scaffold/pm-api/src/db/turso.ts +147 -0
  81. package/scaffold/pm-api/src/index.ts +114 -0
  82. package/scaffold/pm-api/src/mcp-tools/dashboard.ts +40 -0
  83. package/scaffold/pm-api/src/mcp-tools/epic.ts +67 -0
  84. package/scaffold/pm-api/src/mcp-tools/event.ts +89 -0
  85. package/scaffold/pm-api/src/mcp-tools/index.ts +11 -0
  86. package/scaffold/pm-api/src/mcp-tools/initiative.ts +51 -0
  87. package/scaffold/pm-api/src/mcp-tools/memo.ts +164 -0
  88. package/scaffold/pm-api/src/mcp-tools/notification.ts +37 -0
  89. package/scaffold/pm-api/src/mcp-tools/retro.ts +183 -0
  90. package/scaffold/pm-api/src/mcp-tools/sprint.ts +204 -0
  91. package/scaffold/pm-api/src/mcp-tools/standup.ts +136 -0
  92. package/scaffold/pm-api/src/mcp-tools/story.ts +230 -0
  93. package/scaffold/pm-api/src/mcp-tools/task.ts +187 -0
  94. package/scaffold/pm-api/src/mcp-tools/utils.ts +83 -0
  95. package/scaffold/pm-api/src/mcp.ts +871 -0
  96. package/scaffold/pm-api/src/nudge.ts +283 -0
  97. package/scaffold/pm-api/src/routes/auth.ts +32 -0
  98. package/scaffold/pm-api/src/routes/v2-activity.ts +27 -0
  99. package/scaffold/pm-api/src/routes/v2-admin.ts +165 -0
  100. package/scaffold/pm-api/src/routes/v2-dashboard.ts +189 -0
  101. package/scaffold/pm-api/src/routes/v2-docs.ts +34 -0
  102. package/scaffold/pm-api/src/routes/v2-initiatives.ts +118 -0
  103. package/scaffold/pm-api/src/routes/v2-kickoff.ts +265 -0
  104. package/scaffold/pm-api/src/routes/v2-meetings.ts +324 -0
  105. package/scaffold/pm-api/src/routes/v2-memos.ts +257 -0
  106. package/scaffold/pm-api/src/routes/v2-nav.ts +260 -0
  107. package/scaffold/pm-api/src/routes/v2-notifications.ts +79 -0
  108. package/scaffold/pm-api/src/routes/v2-page-content.ts +35 -0
  109. package/scaffold/pm-api/src/routes/v2-pm.ts +380 -0
  110. package/scaffold/pm-api/src/routes/v2-policy.ts +58 -0
  111. package/scaffold/pm-api/src/routes/v2-retro.ts +221 -0
  112. package/scaffold/pm-api/src/routes/v2-rewards.ts +132 -0
  113. package/scaffold/pm-api/src/routes/v2-scenarios.ts +48 -0
  114. package/scaffold/pm-api/src/routes/v2-search.ts +32 -0
  115. package/scaffold/pm-api/src/routes/v2-standup.ts +127 -0
  116. package/scaffold/pm-api/src/routes/v2-user.ts +38 -0
  117. package/scaffold/pm-api/src/types.ts +11 -0
  118. package/scaffold/pm-api/src/utils/activity.ts +22 -0
  119. package/scaffold/pm-api/src/utils/admin.ts +9 -0
  120. package/scaffold/pm-api/src/utils/agent-notify.ts +62 -0
  121. package/scaffold/pm-api/src/utils/assignee.ts +69 -0
  122. package/scaffold/pm-api/src/utils/db.ts +45 -0
  123. package/scaffold/pm-api/src/utils/initiative.ts +23 -0
  124. package/scaffold/pm-api/src/utils/sprint-lifecycle.ts +96 -0
  125. package/scaffold/pm-api/tsconfig.json +15 -0
  126. package/scaffold/pm-api/wrangler.toml.hbs +11 -0
  127. package/scaffold/spec-site/package-lock.json +40 -0
  128. package/scaffold/spec-site/package.json +4 -1
  129. package/scaffold/spec-site/src/api/types.ts +6 -0
  130. package/scaffold/spec-site/src/components/AppHeader.vue +429 -55
  131. package/scaffold/spec-site/src/components/MemberSelect.vue +48 -0
  132. package/scaffold/spec-site/src/components/NotificationDropdown.vue +116 -0
  133. package/scaffold/spec-site/src/components/SearchModal.vue +102 -0
  134. package/scaffold/spec-site/src/components/VelocityChart.vue +77 -0
  135. package/scaffold/spec-site/src/composables/pmTypes.ts +15 -2
  136. package/scaffold/spec-site/src/composables/useDashboard.ts +221 -0
  137. package/scaffold/spec-site/src/composables/useMediaQuery.ts +28 -0
  138. package/scaffold/spec-site/src/composables/useNotification.ts +200 -0
  139. package/scaffold/spec-site/src/composables/usePmStore.ts +48 -1
  140. package/scaffold/spec-site/src/composables/useRetro.ts +6 -0
  141. package/scaffold/spec-site/src/composables/useStandup.ts +201 -0
  142. package/scaffold/spec-site/src/composables/useTheme.ts +37 -0
  143. package/scaffold/spec-site/src/composables/useUser.ts +19 -1
  144. package/scaffold/spec-site/src/features.ts +108 -0
  145. package/scaffold/spec-site/src/pages/AdminPage.vue +299 -0
  146. package/scaffold/spec-site/src/pages/DashboardPage.vue +650 -0
  147. package/scaffold/spec-site/src/pages/DocsHub.vue +157 -0
  148. package/scaffold/spec-site/src/pages/InboxPage.vue +156 -0
  149. package/scaffold/spec-site/src/pages/MeetingsPage.vue +294 -0
  150. package/scaffold/spec-site/src/pages/MyPage.vue +343 -0
  151. package/scaffold/spec-site/src/pages/RewardsPage.vue +266 -0
  152. package/scaffold/spec-site/src/pages/board/BoardAdmin.vue +422 -0
  153. package/scaffold/spec-site/src/pages/board/BoardEpicSection.vue +54 -0
  154. package/scaffold/spec-site/src/pages/board/BoardPage.vue +884 -0
  155. package/scaffold/spec-site/src/pages/board/BoardStoryCard.vue +67 -0
  156. package/scaffold/spec-site/src/pages/board/BoardTaskItem.vue +52 -0
  157. package/scaffold/spec-site/src/pages/board/MyTasksPage.vue +202 -0
  158. package/scaffold/spec-site/src/pages/board/SprintClose.vue +167 -0
  159. package/scaffold/spec-site/src/pages/board/SprintColumn.vue +49 -0
  160. package/scaffold/spec-site/src/pages/board/SprintKickoff.vue +389 -0
  161. package/scaffold/spec-site/src/pages/board/StatusBadge.vue +52 -0
  162. package/scaffold/spec-site/src/pages/board/StoryDetailPanel.vue +495 -0
  163. package/scaffold/spec-site/src/pages/board/TaskCard.vue +42 -0
  164. package/scaffold/spec-site/src/pages/retro/RetroCard.vue +36 -2
  165. package/scaffold/spec-site/src/pages/retro/RetroHeader.vue +82 -66
  166. package/scaffold/spec-site/src/pages/retro/RetroPage.vue +47 -18
  167. package/scaffold/spec-site/src/pages/standup/StandupEntryCard.vue +551 -0
  168. package/scaffold/spec-site/src/pages/standup/StandupForm.vue +68 -0
  169. package/scaffold/spec-site/src/pages/standup/StandupList.vue +71 -0
  170. package/scaffold/spec-site/src/pages/standup/StandupPage.vue +225 -0
  171. package/scaffold/spec-site/src/router.ts +141 -0
@@ -0,0 +1,422 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, onMounted } from 'vue'
3
+ import { useRouter } from 'vue-router'
4
+ import { sprints, loaded, loadNavData } from '@/composables/useNavStore'
5
+ import {
6
+ pmEpics, stories, tasks, pmLoaded, loadEpics, loadPmData,
7
+ addEpic as addPmEpic, updateEpic as updatePmEpic, deleteEpic as deletePmEpic,
8
+ addStory, updateStory, deleteStory,
9
+ addTask, updateTask, deleteTask,
10
+ getStoriesForSprint, getStoriesForEpic, getTasksForStory, getEpicById,
11
+ STORY_STATUSES, TASK_STATUSES, PRIORITIES, AREAS, EPIC_STATUSES,
12
+ STORY_STATUS_LABELS, TASK_STATUS_LABELS, PRIORITY_LABELS, EPIC_STATUS_LABELS,
13
+ type PmEpic, type PmStory, type PmTask, type StoryStatus, type TaskStatus, type Priority, type EpicStatus,
14
+ } from '@/composables/usePmStore'
15
+
16
+ const router = useRouter()
17
+ const loading = ref(true)
18
+ const statusMsg = ref('')
19
+ const selectedSprint = ref('')
20
+ const activeTab = ref<'stories' | 'epics'>('stories')
21
+
22
+ function clearStatus() { setTimeout(() => { statusMsg.value = '' }, 3000) }
23
+
24
+ // Epic management
25
+ const epicForm = ref({ title: '', description: '', status: 'active' as EpicStatus, owner: '' })
26
+ const showEpicForm = ref(false)
27
+ const editingEpic = ref<number | null>(null)
28
+ const editEpicData = ref({ title: '', description: '', status: 'active' as EpicStatus, owner: '' })
29
+
30
+ async function handleAddEpic() {
31
+ if (!epicForm.value.title.trim()) return
32
+ const f = epicForm.value
33
+ const r = await addPmEpic({ title: f.title.trim(), description: f.description.trim() || null, status: f.status, owner: f.owner || null })
34
+ if (r.error) { statusMsg.value = `Error: ${r.error}` }
35
+ else { statusMsg.value = 'Epic added'; showEpicForm.value = false; epicForm.value = { title: '', description: '', status: 'active', owner: '' } }
36
+ clearStatus()
37
+ }
38
+
39
+ function startEditEpic(e: PmEpic) {
40
+ editingEpic.value = e.id
41
+ editEpicData.value = { title: e.title, description: e.description ?? '', status: e.status, owner: e.owner ?? '' }
42
+ }
43
+
44
+ async function saveEditEpic(id: number) {
45
+ const d = editEpicData.value
46
+ const r = await updatePmEpic(id, { title: d.title, description: d.description || null, status: d.status, owner: d.owner || null })
47
+ if (r.error) { statusMsg.value = `Error: ${r.error}` }
48
+ else { statusMsg.value = 'Epic updated'; editingEpic.value = null }
49
+ clearStatus()
50
+ }
51
+
52
+ async function handleDeleteEpic(id: number, title: string) {
53
+ if (!confirm(`Delete "${title}" epic? Child stories will become unassigned.`)) return
54
+ const r = await deletePmEpic(id)
55
+ if (r.error) { statusMsg.value = `Error: ${r.error}` }
56
+ else { statusMsg.value = 'Epic deleted' }
57
+ clearStatus()
58
+ }
59
+
60
+ function epicStoryCount(epicId: number): { total: number; done: number } {
61
+ const s = stories.value.filter(st => st.epicId === epicId)
62
+ return { total: s.length, done: s.filter(st => st.status === 'done').length }
63
+ }
64
+
65
+ // Sprint stories
66
+ const sprintStories = computed(() => getStoriesForSprint(selectedSprint.value))
67
+
68
+ const epicGroupedStories = computed(() => {
69
+ const groups = new Map<number | null, PmStory[]>()
70
+ for (const s of sprintStories.value) {
71
+ if (!groups.has(s.epicId)) groups.set(s.epicId, [])
72
+ groups.get(s.epicId)!.push(s)
73
+ }
74
+ return [...groups.entries()].sort(([a], [b]) => { if (a === null) return 1; if (b === null) return -1; return a - b })
75
+ })
76
+
77
+ // Story form
78
+ const storyFormEpicId = ref<number | null | 'show'>(null)
79
+ const storyForm = ref({
80
+ title: '', description: '', acceptanceCriteria: '',
81
+ assignee: '', status: 'draft' as StoryStatus, priority: 'medium' as Priority,
82
+ area: 'FE', storyPoints: '' as string, epicId: null as number | null,
83
+ })
84
+
85
+ function showStoryForm() {
86
+ storyFormEpicId.value = 'show'
87
+ storyForm.value = { title: '', description: '', acceptanceCriteria: '', assignee: '', status: 'draft', priority: 'medium', area: 'FE', storyPoints: '', epicId: null }
88
+ }
89
+
90
+ async function handleAddStory() {
91
+ if (!storyForm.value.title.trim()) return
92
+ const f = storyForm.value
93
+ const r = await addStory({ epicId: f.epicId, sprint: selectedSprint.value, title: f.title.trim(), description: f.description.trim() || null, acceptanceCriteria: f.acceptanceCriteria.trim() || null, assignee: f.assignee || null, status: f.status, priority: f.priority, area: f.area, storyPoints: f.storyPoints ? Number(f.storyPoints) : null })
94
+ if (r.error) { statusMsg.value = `Error: ${r.error}` }
95
+ else { statusMsg.value = 'Story added'; storyFormEpicId.value = null }
96
+ clearStatus()
97
+ }
98
+
99
+ // Story edit
100
+ const editingStory = ref<number | null>(null)
101
+ const editStoryData = ref({ title: '', description: '', acceptanceCriteria: '', assignee: '', status: 'draft' as StoryStatus, priority: 'medium' as Priority, area: 'FE', storyPoints: '' as string, epicId: null as number | null, sprint: '' as string | undefined })
102
+
103
+ function startEditStory(s: PmStory) {
104
+ editingStory.value = s.id
105
+ editStoryData.value = { title: s.title, description: s.description ?? '', acceptanceCriteria: s.acceptanceCriteria ?? '', assignee: s.assignee ?? '', status: s.status, priority: s.priority, area: s.area, storyPoints: s.storyPoints?.toString() ?? '', epicId: s.epicId, sprint: s.sprint }
106
+ }
107
+
108
+ async function saveEditStory(id: number) {
109
+ const d = editStoryData.value
110
+ const r = await updateStory(id, { title: d.title, description: d.description || null, acceptanceCriteria: d.acceptanceCriteria || null, assignee: d.assignee || null, status: d.status, priority: d.priority, area: d.area, storyPoints: d.storyPoints ? Number(d.storyPoints) : null, epicId: d.epicId, sprint: d.sprint })
111
+ if (r.error) { statusMsg.value = `Error: ${r.error}` }
112
+ else { statusMsg.value = 'Story updated'; editingStory.value = null; await loadPmData(selectedSprint.value) }
113
+ clearStatus()
114
+ }
115
+
116
+ async function handleDeleteStory(id: number, title: string) {
117
+ if (!confirm(`Delete "${title}" story and all its tasks?`)) return
118
+ const r = await deleteStory(id)
119
+ if (r.error) { statusMsg.value = `Error: ${r.error}` }
120
+ else { statusMsg.value = 'Story deleted'; await loadPmData(selectedSprint.value) }
121
+ clearStatus()
122
+ }
123
+
124
+ // Task form
125
+ const taskFormStoryId = ref<number | null>(null)
126
+ const taskForm = ref({ title: '', assignee: '', description: '' })
127
+
128
+ function showTaskForm(storyId: number) { taskFormStoryId.value = storyId; taskForm.value = { title: '', assignee: '', description: '' } }
129
+
130
+ async function handleAddTask() {
131
+ if (!taskFormStoryId.value || !taskForm.value.title.trim()) return
132
+ const f = taskForm.value
133
+ const r = await addTask({ storyId: taskFormStoryId.value, title: f.title.trim(), assignee: f.assignee || null, description: f.description.trim() || null })
134
+ if (r.error) { statusMsg.value = `Error: ${r.error}` }
135
+ else { statusMsg.value = 'Task added'; taskFormStoryId.value = null }
136
+ clearStatus()
137
+ }
138
+
139
+ // Task edit
140
+ const editingTask = ref<number | null>(null)
141
+ const editTaskData = ref({ title: '', assignee: '', description: '', status: 'todo' as TaskStatus })
142
+
143
+ function startEditTask(t: PmTask) {
144
+ editingTask.value = t.id
145
+ editTaskData.value = { title: t.title, assignee: t.assignee ?? '', description: t.description ?? '', status: t.status }
146
+ }
147
+
148
+ async function saveEditTask(id: number) {
149
+ const d = editTaskData.value
150
+ const r = await updateTask(id, { title: d.title, assignee: d.assignee || null, description: d.description || null, status: d.status })
151
+ if (r.error) { statusMsg.value = `Error: ${r.error}` }
152
+ else { statusMsg.value = 'Task updated'; editingTask.value = null }
153
+ clearStatus()
154
+ }
155
+
156
+ async function handleDeleteTask(id: number) {
157
+ if (!confirm('Delete this task?')) return
158
+ const r = await deleteTask(id)
159
+ if (r.error) { statusMsg.value = `Error: ${r.error}` }
160
+ else { statusMsg.value = 'Task deleted' }
161
+ clearStatus()
162
+ }
163
+
164
+ async function onSprintChange() { await loadPmData(selectedSprint.value) }
165
+
166
+ onMounted(async () => {
167
+ if (!loaded.value) await loadNavData()
168
+ await loadEpics()
169
+ const active = sprints.value.find(s => s.active)
170
+ selectedSprint.value = active?.id ?? sprints.value[0]?.id ?? ''
171
+ await loadPmData(selectedSprint.value)
172
+ loading.value = false
173
+ })
174
+ </script>
175
+
176
+ <template>
177
+ <div class="admin">
178
+ <div class="admin-header">
179
+ <h1>Story &amp; Task Admin</h1>
180
+ <p class="admin-subtitle">Epic management + per-sprint story CRUD</p>
181
+ </div>
182
+
183
+ <Transition name="fade">
184
+ <div v-if="statusMsg" class="admin-status">{{ statusMsg }}</div>
185
+ </Transition>
186
+
187
+ <!-- Tab toggle -->
188
+ <div class="tab-bar">
189
+ <button class="tab-btn" :class="{ active: activeTab === 'stories' }" @click="activeTab = 'stories'">Stories/Tasks</button>
190
+ <button class="tab-btn" :class="{ active: activeTab === 'epics' }" @click="activeTab = 'epics'">Epic Admin</button>
191
+ <div class="tab-spacer" />
192
+ <button class="btn" @click="router.push(`/board/${selectedSprint}`)">Board</button>
193
+ </div>
194
+
195
+ <div v-if="loading" class="admin-loading">Loading...</div>
196
+
197
+ <!-- Epics tab -->
198
+ <div v-else-if="activeTab === 'epics'" class="epics-tab">
199
+ <div class="top-actions">
200
+ <button class="btn btn--primary" @click="showEpicForm = !showEpicForm">{{ showEpicForm ? 'Cancel' : '+ New Epic' }}</button>
201
+ </div>
202
+
203
+ <div v-if="showEpicForm" class="admin-card">
204
+ <h2>New Epic</h2>
205
+ <div class="epic-add-form">
206
+ <input v-model="epicForm.title" class="input" placeholder="Epic title" />
207
+ <textarea v-model="epicForm.description" class="input" rows="2" placeholder="Description" />
208
+ <div class="edit-form-row">
209
+ <select v-model="epicForm.status" class="input input--xs">
210
+ <option v-for="st in EPIC_STATUSES" :key="st" :value="st">{{ EPIC_STATUS_LABELS[st] }}</option>
211
+ </select>
212
+ <button class="btn btn--primary" @click="handleAddEpic" :disabled="!epicForm.title.trim()">Add</button>
213
+ </div>
214
+ </div>
215
+ </div>
216
+
217
+ <div class="epic-list">
218
+ <div v-for="e in pmEpics" :key="e.id" class="epic-card">
219
+ <template v-if="editingEpic === e.id">
220
+ <div class="epic-edit-form">
221
+ <input v-model="editEpicData.title" class="input" placeholder="Title" />
222
+ <textarea v-model="editEpicData.description" class="input" rows="2" placeholder="Description" />
223
+ <div class="edit-form-row">
224
+ <select v-model="editEpicData.status" class="input input--xs">
225
+ <option v-for="st in EPIC_STATUSES" :key="st" :value="st">{{ EPIC_STATUS_LABELS[st] }}</option>
226
+ </select>
227
+ <button class="btn btn--sm btn--primary" @click="saveEditEpic(e.id)">Save</button>
228
+ <button class="btn btn--sm" @click="editingEpic = null">Cancel</button>
229
+ </div>
230
+ </div>
231
+ </template>
232
+ <template v-else>
233
+ <div class="epic-card-header">
234
+ <span class="epic-title">{{ e.title }}</span>
235
+ <span class="epic-status-badge" :class="e.status">{{ EPIC_STATUS_LABELS[e.status] }}</span>
236
+ <span v-if="e.owner" class="epic-owner">{{ e.owner }}</span>
237
+ <span class="epic-progress">{{ epicStoryCount(e.id).done }}/{{ epicStoryCount(e.id).total }} stories</span>
238
+ <div class="epic-actions">
239
+ <button class="btn btn--sm" @click="startEditEpic(e)">Edit</button>
240
+ <button class="btn btn--sm btn--danger" @click="handleDeleteEpic(e.id, e.title)">Delete</button>
241
+ </div>
242
+ </div>
243
+ <div v-if="e.description" class="epic-desc">{{ e.description }}</div>
244
+ </template>
245
+ </div>
246
+ </div>
247
+ </div>
248
+
249
+ <!-- Stories/Tasks tab -->
250
+ <div v-else class="stories-tab">
251
+ <div class="top-actions">
252
+ <select v-model="selectedSprint" class="input" @change="onSprintChange">
253
+ <option v-for="s in sprints" :key="s.id" :value="s.id">{{ s.label }} -- {{ s.theme }}</option>
254
+ </select>
255
+ <button class="btn btn--primary btn--sm" @click="showStoryForm">+ Add Story</button>
256
+ </div>
257
+
258
+ <div v-if="storyFormEpicId === 'show'" class="add-story-form admin-card">
259
+ <h2>New Story</h2>
260
+ <input v-model="storyForm.title" class="input" placeholder="Story title" />
261
+ <div class="edit-form-row">
262
+ <select v-model="storyForm.epicId" class="input input--sm"><option :value="null">No epic</option><option v-for="e in pmEpics" :key="e.id" :value="e.id">{{ e.title }}</option></select>
263
+ <select v-model="storyForm.status" class="input input--xs"><option v-for="st in STORY_STATUSES" :key="st" :value="st">{{ STORY_STATUS_LABELS[st] }}</option></select>
264
+ <select v-model="storyForm.priority" class="input input--xs"><option v-for="p in PRIORITIES" :key="p" :value="p">{{ PRIORITY_LABELS[p] }}</option></select>
265
+ <select v-model="storyForm.area" class="input input--xs"><option v-for="a in AREAS" :key="a" :value="a">{{ a }}</option></select>
266
+ <input v-model="storyForm.storyPoints" class="input input--xs" type="number" placeholder="SP" />
267
+ </div>
268
+ <textarea v-model="storyForm.description" class="input" rows="2" placeholder="Description" />
269
+ <textarea v-model="storyForm.acceptanceCriteria" class="input" rows="2" placeholder="Acceptance criteria" />
270
+ <div class="edit-form-row">
271
+ <button class="btn btn--sm btn--primary" @click="handleAddStory" :disabled="!storyForm.title.trim()">Add</button>
272
+ <button class="btn btn--sm" @click="storyFormEpicId = null">Cancel</button>
273
+ </div>
274
+ </div>
275
+
276
+ <!-- Epic > Story > Task tree -->
277
+ <div class="epic-list">
278
+ <div v-for="[epicId, groupStories] in epicGroupedStories" :key="epicId ?? 'unassigned'" class="epic-card">
279
+ <div class="epic-card-header">
280
+ <span class="epic-label">{{ epicId !== null ? (getEpicById(epicId)?.title ?? `Epic #${epicId}`) : 'Unassigned' }}</span>
281
+ </div>
282
+ <div class="story-section">
283
+ <div v-for="s in groupStories" :key="s.id" class="story-block">
284
+ <div class="story-row">
285
+ <template v-if="editingStory === s.id">
286
+ <div class="edit-form">
287
+ <input v-model="editStoryData.title" class="input" placeholder="Title" />
288
+ <div class="edit-form-row">
289
+ <select v-model="editStoryData.epicId" class="input input--sm"><option :value="null">No epic</option><option v-for="e in pmEpics" :key="e.id" :value="e.id">{{ e.title }}</option></select>
290
+ <select v-model="editStoryData.sprint" class="input input--xs"><option v-for="sp in sprints" :key="sp.id" :value="sp.id">{{ sp.label }}</option></select>
291
+ <select v-model="editStoryData.status" class="input input--xs"><option v-for="st in STORY_STATUSES" :key="st" :value="st">{{ STORY_STATUS_LABELS[st] }}</option></select>
292
+ <select v-model="editStoryData.priority" class="input input--xs"><option v-for="p in PRIORITIES" :key="p" :value="p">{{ PRIORITY_LABELS[p] }}</option></select>
293
+ <select v-model="editStoryData.area" class="input input--xs"><option v-for="a in AREAS" :key="a" :value="a">{{ a }}</option></select>
294
+ <input v-model="editStoryData.storyPoints" class="input input--xs" type="number" placeholder="SP" />
295
+ </div>
296
+ <textarea v-model="editStoryData.description" class="input" rows="2" placeholder="Description" />
297
+ <textarea v-model="editStoryData.acceptanceCriteria" class="input" rows="2" placeholder="Acceptance criteria" />
298
+ <div class="edit-form-row">
299
+ <button class="btn btn--sm btn--primary" @click="saveEditStory(s.id)">Save</button>
300
+ <button class="btn btn--sm" @click="editingStory = null">Cancel</button>
301
+ </div>
302
+ </div>
303
+ </template>
304
+ <template v-else>
305
+ <div class="story-info">
306
+ <span class="story-status-dot" :class="s.status" />
307
+ <span class="story-title">{{ s.title }}</span>
308
+ <span v-if="s.assignee" class="story-assignee">{{ s.assignee }}</span>
309
+ <span class="story-meta">{{ s.area }} / {{ PRIORITY_LABELS[s.priority] }}</span>
310
+ </div>
311
+ <div class="story-actions">
312
+ <button class="btn btn--sm" @click="startEditStory(s)">Edit</button>
313
+ <button class="btn btn--sm btn--danger" @click="handleDeleteStory(s.id, s.title)">Delete</button>
314
+ <button class="btn btn--sm" @click="showTaskForm(s.id)">+ Task</button>
315
+ </div>
316
+ </template>
317
+ </div>
318
+ <div class="task-list">
319
+ <div v-for="t in getTasksForStory(s.id)" :key="t.id" class="task-row">
320
+ <template v-if="editingTask === t.id">
321
+ <div class="edit-form edit-form--inline">
322
+ <input v-model="editTaskData.title" class="input input--sm" placeholder="Title" />
323
+ <select v-model="editTaskData.status" class="input input--xs"><option v-for="st in TASK_STATUSES" :key="st" :value="st">{{ TASK_STATUS_LABELS[st] }}</option></select>
324
+ <button class="btn btn--sm btn--primary" @click="saveEditTask(t.id)">Save</button>
325
+ <button class="btn btn--sm" @click="editingTask = null">Cancel</button>
326
+ </div>
327
+ </template>
328
+ <template v-else>
329
+ <span class="task-status-dot" :class="t.status" />
330
+ <span class="task-title">{{ t.title }}</span>
331
+ <span v-if="t.assignee" class="task-assignee">{{ t.assignee }}</span>
332
+ <div class="task-actions">
333
+ <button class="btn btn--sm" @click="startEditTask(t)">Edit</button>
334
+ <button class="btn btn--sm btn--danger" @click="handleDeleteTask(t.id)">Delete</button>
335
+ </div>
336
+ </template>
337
+ </div>
338
+ <div v-if="taskFormStoryId === s.id" class="add-form">
339
+ <input v-model="taskForm.title" class="input input--sm" placeholder="Task title" />
340
+ <button class="btn btn--sm btn--primary" @click="handleAddTask" :disabled="!taskForm.title.trim()">Add</button>
341
+ <button class="btn btn--sm" @click="taskFormStoryId = null">Cancel</button>
342
+ </div>
343
+ </div>
344
+ </div>
345
+ </div>
346
+ </div>
347
+ </div>
348
+ </div>
349
+ </div>
350
+ </template>
351
+
352
+ <style scoped>
353
+ .admin { max-width: 1100px; margin: 0 auto; padding: 32px 24px; }
354
+ .admin-header { margin-bottom: 24px; }
355
+ .admin-header h1 { font-size: 24px; font-weight: 700; color: #1e293b; margin-bottom: 4px; }
356
+ .admin-subtitle { font-size: 13px; color: #94a3b8; }
357
+ .admin-status { background: #ecfdf5; border: 1px solid #a7f3d0; color: #065f46; padding: 10px 16px; border-radius: 8px; font-size: 13px; font-weight: 500; margin-bottom: 16px; }
358
+ .tab-bar { display: flex; gap: 4px; margin-bottom: 20px; align-items: center; border-bottom: 2px solid #e2e8f0; padding-bottom: 0; }
359
+ .tab-btn { padding: 8px 16px; border: none; background: none; font-size: 13px; font-weight: 600; color: #94a3b8; cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -2px; transition: all 0.15s; }
360
+ .tab-btn:hover { color: #475569; }
361
+ .tab-btn.active { color: #1e293b; border-bottom-color: #1e293b; }
362
+ .tab-spacer { flex: 1; }
363
+ .top-actions { display: flex; gap: 8px; margin-bottom: 20px; align-items: center; }
364
+ .admin-card { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; padding: 20px 24px; margin-bottom: 20px; }
365
+ .admin-card h2 { font-size: 16px; font-weight: 600; color: #1e293b; margin-bottom: 16px; }
366
+ .epic-add-form, .epic-edit-form { display: flex; flex-direction: column; gap: 8px; padding: 12px 16px; }
367
+ .epic-list { display: flex; flex-direction: column; gap: 12px; }
368
+ .epic-card { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; }
369
+ .epic-card-header { padding: 12px 20px; background: #f8fafc; border-bottom: 1px solid #f1f5f9; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
370
+ .epic-title { font-size: 14px; font-weight: 700; color: #1e293b; }
371
+ .epic-label { font-size: 14px; font-weight: 600; color: #1e293b; }
372
+ .epic-status-badge { font-size: 10px; font-weight: 600; padding: 2px 8px; border-radius: 10px; }
373
+ .epic-status-badge.active { background: #ecfdf5; color: #059669; }
374
+ .epic-status-badge.completed { background: #eff6ff; color: #3b82f6; }
375
+ .epic-status-badge.archived { background: #f1f5f9; color: #94a3b8; }
376
+ .epic-owner { font-size: 11px; color: #64748b; background: #f1f5f9; padding: 1px 6px; border-radius: 3px; }
377
+ .epic-progress { font-size: 11px; color: #94a3b8; margin-left: auto; }
378
+ .epic-actions { display: flex; gap: 4px; }
379
+ .epic-desc { padding: 8px 20px 12px; font-size: 12px; color: #64748b; }
380
+ .story-section { padding: 12px 20px 16px; }
381
+ .story-block { border: 1px solid #f1f5f9; border-radius: 8px; margin-bottom: 8px; overflow: hidden; }
382
+ .story-row { padding: 10px 12px; display: flex; align-items: center; justify-content: space-between; gap: 8px; }
383
+ .story-info { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; }
384
+ .story-title { font-size: 13px; font-weight: 600; color: #1e293b; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
385
+ .story-assignee { font-size: 11px; color: #64748b; background: #f1f5f9; padding: 1px 6px; border-radius: 3px; flex-shrink: 0; }
386
+ .story-meta { font-size: 10px; color: #94a3b8; flex-shrink: 0; }
387
+ .story-status-dot, .task-status-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
388
+ .story-status-dot.draft, .task-status-dot.todo { background: #94a3b8; }
389
+ .story-status-dot.backlog { background: #a78bfa; }
390
+ .story-status-dot.ready { background: #3b82f6; }
391
+ .story-status-dot.in-progress, .task-status-dot.in-progress { background: #f59e0b; }
392
+ .story-status-dot.review { background: #8b5cf6; }
393
+ .story-status-dot.done, .task-status-dot.done { background: #22c55e; }
394
+ .story-actions, .task-actions { display: flex; gap: 4px; flex-shrink: 0; }
395
+ .task-list { padding: 0 12px 8px; margin-left: 16px; border-left: 2px solid #f1f5f9; }
396
+ .task-row { display: flex; align-items: center; gap: 8px; padding: 4px 8px; font-size: 12px; }
397
+ .task-title { flex: 1; color: #475569; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
398
+ .task-assignee { font-size: 10px; color: #94a3b8; background: #f8fafc; padding: 1px 4px; border-radius: 2px; }
399
+ .add-form, .add-story-form { display: flex; flex-direction: column; gap: 6px; padding: 10px 12px; border-top: 1px solid #f1f5f9; margin-top: 4px; }
400
+ .add-form { flex-direction: row; flex-wrap: wrap; align-items: center; }
401
+ .edit-form { display: flex; flex-direction: column; gap: 6px; width: 100%; }
402
+ .edit-form--inline { flex-direction: row; flex-wrap: wrap; align-items: center; width: 100%; }
403
+ .edit-form-row { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
404
+ .input { padding: 6px 10px; border: 1px solid #e2e8f0; border-radius: 6px; font-size: 13px; flex: 1; min-width: 80px; }
405
+ .input--sm { max-width: 200px; }
406
+ .input--xs { max-width: 100px; flex: none; }
407
+ .input:focus { outline: none; border-color: #3b82f6; }
408
+ select.input { cursor: pointer; }
409
+ textarea.input { resize: vertical; min-height: 36px; }
410
+ .btn { padding: 6px 14px; border: 1px solid #e2e8f0; border-radius: 6px; font-size: 13px; font-weight: 500; cursor: pointer; background: #fff; color: #475569; white-space: nowrap; transition: all 0.15s; }
411
+ .btn:hover { background: #f1f5f9; }
412
+ .btn:disabled { opacity: 0.5; cursor: not-allowed; }
413
+ .btn--primary { background: #1e293b; color: #fff; border-color: #1e293b; }
414
+ .btn--primary:hover { background: #334155; }
415
+ .btn--sm { padding: 4px 10px; font-size: 11px; }
416
+ .btn--danger { color: #ef4444; border-color: #fca5a5; }
417
+ .btn--danger:hover { background: #fef2f2; }
418
+ .admin-loading { padding: 40px; text-align: center; color: #94a3b8; font-size: 14px; }
419
+ .fade-enter-active, .fade-leave-active { transition: opacity 0.3s; }
420
+ .fade-enter-from, .fade-leave-to { opacity: 0; }
421
+ @media (max-width: 767px) { .admin { padding: 16px; } .story-row { flex-direction: column; align-items: flex-start; } .story-actions { margin-top: 4px; } }
422
+ </style>
@@ -0,0 +1,54 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed } from 'vue'
3
+ import type { PmEpic, PmStory } from '@/composables/usePmStore'
4
+ import BoardStoryCard from './BoardStoryCard.vue'
5
+
6
+ const props = defineProps<{
7
+ epic: PmEpic | null
8
+ stories: PmStory[]
9
+ }>()
10
+
11
+ const emit = defineEmits<{ updated: []; selectStory: [story: PmStory] }>()
12
+
13
+ const collapsed = ref(false)
14
+
15
+ const doneStories = computed(() => props.stories.filter(s => s.status === 'done').length)
16
+ </script>
17
+
18
+ <template>
19
+ <div class="epic-section">
20
+ <div class="epic-header" @click="collapsed = !collapsed">
21
+ <span class="collapse-icon">{{ collapsed ? '&#9654;' : '&#9660;' }}</span>
22
+ <span class="epic-label">{{ epic ? epic.title : 'Unassigned' }}</span>
23
+ <span class="epic-story-count">{{ doneStories }}/{{ stories.length }} stories</span>
24
+ </div>
25
+
26
+ <div v-if="!collapsed" class="epic-stories">
27
+ <div v-if="stories.length === 0" class="no-stories">
28
+ No stories -- add them from the admin page
29
+ </div>
30
+ <div v-else class="stories-grid">
31
+ <BoardStoryCard
32
+ v-for="story in stories"
33
+ :key="story.id"
34
+ :story="story"
35
+ @select="(s) => emit('selectStory', s)"
36
+ @updated="emit('updated')"
37
+ />
38
+ </div>
39
+ </div>
40
+ </div>
41
+ </template>
42
+
43
+ <style scoped>
44
+ .epic-section { border: 1px solid #e2e8f0; border-radius: 10px; overflow: hidden; background: #fff; }
45
+ .epic-header { display: flex; align-items: center; gap: 8px; padding: 12px 16px; background: #f8fafc; cursor: pointer; user-select: none; transition: background 0.1s; }
46
+ .epic-header:hover { background: #f1f5f9; }
47
+ .collapse-icon { font-size: 10px; color: #94a3b8; width: 14px; text-align: center; }
48
+ .epic-label { font-size: 14px; font-weight: 600; color: #1e293b; }
49
+ .epic-story-count { font-size: 11px; color: #94a3b8; margin-left: auto; }
50
+ .epic-stories { padding: 12px 16px 16px; }
51
+ .no-stories { text-align: center; padding: 20px; color: #94a3b8; font-size: 13px; }
52
+ .stories-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 10px; }
53
+ @media (max-width: 767px) { .stories-grid { grid-template-columns: 1fr; } }
54
+ </style>