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,67 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import type { PmStory } from '@/composables/usePmStore'
4
+ import { getTasksForStory, STORY_STATUS_LABELS, PRIORITY_LABELS } from '@/composables/usePmStore'
5
+ import StatusBadge from './StatusBadge.vue'
6
+
7
+ const props = defineProps<{
8
+ story: PmStory
9
+ }>()
10
+
11
+ const emit = defineEmits<{ select: [story: PmStory]; updated: [] }>()
12
+
13
+ const storyTasks = computed(() => getTasksForStory(props.story.id))
14
+ const doneCount = computed(() => storyTasks.value.filter(t => t.status === 'done').length)
15
+ </script>
16
+
17
+ <template>
18
+ <div
19
+ class="story-card"
20
+ :class="{ 'story-done': story.status === 'done' }"
21
+ @click="emit('select', story)"
22
+ >
23
+ <div class="story-header">
24
+ <div class="story-top-row">
25
+ <StatusBadge :label="STORY_STATUS_LABELS[story.status]" type="status" :value="story.status" />
26
+ <StatusBadge :label="PRIORITY_LABELS[story.priority]" type="priority" :value="story.priority" />
27
+ <span class="story-area">{{ story.area }}</span>
28
+ <span v-if="story.storyPoints" class="story-points">{{ story.storyPoints }}pt</span>
29
+ </div>
30
+ <div class="story-title">{{ story.title }}</div>
31
+ <div class="story-bottom">
32
+ <span v-if="story.assignee" class="story-assignee">{{ story.assignee }}</span>
33
+ <span v-if="storyTasks.length > 0" class="story-task-count">{{ doneCount }}/{{ storyTasks.length }} tasks</span>
34
+ <span v-if="story.relatedPrs?.length" class="story-pr-badge" title="PRs linked">{{ story.relatedPrs.length }} PR</span>
35
+ </div>
36
+ </div>
37
+ </div>
38
+ </template>
39
+
40
+ <style scoped>
41
+ .story-card {
42
+ background: #fff;
43
+ border: 1px solid #e2e8f0;
44
+ border-radius: 8px;
45
+ padding: 12px;
46
+ cursor: pointer;
47
+ transition: box-shadow 0.15s, border-color 0.15s;
48
+ }
49
+ .story-card:hover {
50
+ box-shadow: 0 2px 8px rgba(0,0,0,0.06);
51
+ border-color: #cbd5e1;
52
+ }
53
+ .story-done { opacity: 0.6; }
54
+ .story-header { display: flex; flex-direction: column; gap: 6px; }
55
+ .story-top-row { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
56
+ .story-area { font-size: 10px; font-weight: 600; color: #64748b; background: #f1f5f9; padding: 1px 6px; border-radius: 3px; }
57
+ .story-points { font-size: 10px; font-weight: 600; color: #3b82f6; margin-left: auto; }
58
+ .story-title { font-size: 13px; font-weight: 600; color: #1e293b; line-height: 1.4; }
59
+ .story-bottom { display: flex; align-items: center; gap: 8px; }
60
+ .story-assignee { font-size: 11px; color: #64748b; }
61
+ .story-task-count { font-size: 10px; color: #94a3b8; margin-left: auto; }
62
+ .story-pr-badge { font-size: 11px; color: #3b82f6; }
63
+ @media (max-width: 767px) {
64
+ .story-card { padding: 8px 10px; }
65
+ .story-title { font-size: 12px; }
66
+ }
67
+ </style>
@@ -0,0 +1,52 @@
1
+ <script setup lang="ts">
2
+ import type { PmTask } from '@/composables/usePmStore'
3
+ import { TASK_STATUSES, TASK_STATUS_LABELS, updateTaskStatus, updateTask } from '@/composables/usePmStore'
4
+
5
+ const props = defineProps<{
6
+ task: PmTask
7
+ }>()
8
+
9
+ const emit = defineEmits<{
10
+ updated: []
11
+ }>()
12
+
13
+ async function updateTaskDate(value: string) {
14
+ await updateTask(props.task.id, { dueDate: value || null } as any)
15
+ emit('updated')
16
+ }
17
+
18
+ async function cycleStatus() {
19
+ const idx = TASK_STATUSES.indexOf(props.task.status)
20
+ const next = TASK_STATUSES[(idx + 1) % TASK_STATUSES.length]
21
+ await updateTaskStatus(props.task.id, next)
22
+ emit('updated')
23
+ }
24
+ </script>
25
+
26
+ <template>
27
+ <div class="task-item" :class="{ done: task.status === 'done' }">
28
+ <button class="task-check" @click="cycleStatus" :title="TASK_STATUS_LABELS[task.status]">
29
+ <span v-if="task.status === 'done'">&#10003;</span>
30
+ <span v-else-if="task.status === 'in-progress'" class="check-progress">&#9654;</span>
31
+ <span v-else class="check-empty">&#9675;</span>
32
+ </button>
33
+ <span class="task-title">{{ task.title }}</span>
34
+ <span v-if="task.storyPoints" class="task-sp">{{ task.storyPoints }}SP</span>
35
+ <span v-if="task.assignee" class="task-assignee">{{ task.assignee }}</span>
36
+ <input type="date" class="task-date" :value="task.dueDate ?? ''" @change="updateTaskDate(($event.target as HTMLInputElement).value)" title="Due date" />
37
+ </div>
38
+ </template>
39
+
40
+ <style scoped>
41
+ .task-item { display: flex; align-items: center; gap: 8px; padding: 4px 8px; border-radius: 4px; font-size: 12px; transition: background 0.1s; }
42
+ .task-item:hover { background: #f8fafc; }
43
+ .task-item.done .task-title { text-decoration: line-through; color: #94a3b8; }
44
+ .task-check { display: flex; align-items: center; justify-content: center; width: 20px; height: 20px; border: none; background: none; cursor: pointer; font-size: 14px; flex-shrink: 0; border-radius: 4px; transition: background 0.1s; }
45
+ .task-check:hover { background: #e2e8f0; }
46
+ .check-empty { color: #cbd5e1; }
47
+ .check-progress { color: #f59e0b; font-size: 10px; }
48
+ .task-title { flex: 1; color: #334155; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
49
+ .task-sp { font-size: 10px; font-weight: 700; color: #3b82f6; flex-shrink: 0; }
50
+ .task-date { border: 1px solid rgba(0,0,0,0.06); border-radius: 4px; padding: 1px 4px; font-size: 10px; background: transparent; color: var(--text-muted); flex-shrink: 0; width: 100px; }
51
+ .task-assignee { font-size: 11px; color: #94a3b8; flex-shrink: 0; background: #f1f5f9; padding: 1px 6px; border-radius: 3px; }
52
+ </style>
@@ -0,0 +1,202 @@
1
+ <script setup lang="ts">
2
+ import { computed, onMounted, ref } from 'vue'
3
+ import { useRoute } from 'vue-router'
4
+ import {
5
+ stories, tasks, pmLoaded, loadPmData,
6
+ getMyStories, getMyTasks,
7
+ updateStoryStatus, updateTaskStatus,
8
+ STORY_STATUSES, TASK_STATUSES,
9
+ STORY_STATUS_LABELS, TASK_STATUS_LABELS,
10
+ type PmStory, type PmTask,
11
+ } from '@/composables/usePmStore'
12
+ import { useUser } from '@/composables/useUser'
13
+ import StatusBadge from './StatusBadge.vue'
14
+
15
+ const route = useRoute()
16
+ const { currentUser } = useUser()
17
+ const loading = ref(true)
18
+
19
+ const sprint = computed(() => route.params.sprint as string)
20
+
21
+ const myStories = computed(() => {
22
+ if (!currentUser.value) return []
23
+ return getMyStories(currentUser.value).filter((s: PmStory) => s.sprint === sprint.value)
24
+ })
25
+
26
+ const myTasks = computed(() => {
27
+ if (!currentUser.value) return []
28
+ return getMyTasks(currentUser.value).filter((t: PmTask) => {
29
+ const story = stories.value.find((s: PmStory) => s.id === t.storyId)
30
+ return story && story.sprint === sprint.value
31
+ })
32
+ })
33
+
34
+ async function cycleStoryStatus(story: PmStory) {
35
+ const idx = STORY_STATUSES.indexOf(story.status)
36
+ const next = STORY_STATUSES[(idx + 1) % STORY_STATUSES.length]
37
+ await updateStoryStatus(story.id, next)
38
+ }
39
+
40
+ async function cycleTaskStatus(task: PmTask) {
41
+ const idx = TASK_STATUSES.indexOf(task.status)
42
+ const next = TASK_STATUSES[(idx + 1) % TASK_STATUSES.length]
43
+ await updateTaskStatus(task.id, next)
44
+ }
45
+
46
+ function getStoryTitle(storyId: number): string {
47
+ return stories.value.find(s => s.id === storyId)?.title ?? '?'
48
+ }
49
+
50
+ onMounted(async () => {
51
+ await loadPmData(sprint.value)
52
+ loading.value = false
53
+ })
54
+ </script>
55
+
56
+ <template>
57
+ <div class="my-tasks-page">
58
+ <h1>My Tasks — {{ sprint.toUpperCase() }}</h1>
59
+
60
+ <div v-if="!currentUser" class="empty">
61
+ User setup required. Please select your name on the retrospective page.
62
+ </div>
63
+
64
+ <div v-else-if="loading" class="loading">Loading...</div>
65
+
66
+ <template v-else>
67
+ <p class="user-label">{{ currentUser }}</p>
68
+
69
+ <!-- My stories -->
70
+ <section class="section">
71
+ <h2>Assigned Stories <span class="count">{{ myStories.length }}</span></h2>
72
+ <div v-if="myStories.length === 0" class="empty-section">No assigned stories</div>
73
+ <div v-else class="item-list">
74
+ <div
75
+ v-for="s in myStories"
76
+ :key="s.id"
77
+ class="item-row"
78
+ @click="cycleStoryStatus(s)"
79
+ >
80
+ <StatusBadge :label="STORY_STATUS_LABELS[s.status]" type="status" :value="s.status" />
81
+ <span class="item-title">{{ s.title }}</span>
82
+ <span v-if="s.storyPoints" class="item-points">{{ s.storyPoints }}pt</span>
83
+ </div>
84
+ </div>
85
+ </section>
86
+
87
+ <!-- My tasks -->
88
+ <section class="section">
89
+ <h2>Assigned Tasks <span class="count">{{ myTasks.length }}</span></h2>
90
+ <div v-if="myTasks.length === 0" class="empty-section">No assigned tasks</div>
91
+ <div v-else class="item-list">
92
+ <div
93
+ v-for="t in myTasks"
94
+ :key="t.id"
95
+ class="item-row"
96
+ @click="cycleTaskStatus(t)"
97
+ >
98
+ <StatusBadge :label="TASK_STATUS_LABELS[t.status]" type="status" :value="t.status" />
99
+ <span class="item-title">{{ t.title }}</span>
100
+ <span class="item-story">{{ getStoryTitle(t.storyId) }}</span>
101
+ </div>
102
+ </div>
103
+ </section>
104
+ </template>
105
+ </div>
106
+ </template>
107
+
108
+ <style scoped>
109
+ .my-tasks-page {
110
+ max-width: 800px;
111
+ margin: 0 auto;
112
+ padding: 24px;
113
+ height: 100%;
114
+ overflow-y: auto;
115
+ }
116
+
117
+ h1 {
118
+ font-size: 22px;
119
+ font-weight: 700;
120
+ color: #1e293b;
121
+ margin-bottom: 4px;
122
+ }
123
+
124
+ .user-label {
125
+ font-size: 14px;
126
+ color: #3b82f6;
127
+ font-weight: 600;
128
+ margin-bottom: 20px;
129
+ }
130
+
131
+ .section {
132
+ margin-bottom: 24px;
133
+ }
134
+ .section h2 {
135
+ font-size: 15px;
136
+ font-weight: 600;
137
+ color: #1e293b;
138
+ margin-bottom: 8px;
139
+ }
140
+ .count {
141
+ font-size: 12px;
142
+ color: #94a3b8;
143
+ font-weight: 400;
144
+ }
145
+
146
+ .item-list {
147
+ display: flex;
148
+ flex-direction: column;
149
+ gap: 4px;
150
+ }
151
+
152
+ .item-row {
153
+ display: flex;
154
+ align-items: center;
155
+ gap: 8px;
156
+ padding: 8px 12px;
157
+ border-radius: 6px;
158
+ cursor: pointer;
159
+ transition: background 0.1s;
160
+ }
161
+ .item-row:hover { background: #f8fafc; }
162
+
163
+ .item-title {
164
+ flex: 1;
165
+ font-size: 13px;
166
+ color: #334155;
167
+ font-weight: 500;
168
+ min-width: 0;
169
+ overflow: hidden;
170
+ text-overflow: ellipsis;
171
+ white-space: nowrap;
172
+ }
173
+
174
+ .item-points {
175
+ font-size: 11px;
176
+ color: #3b82f6;
177
+ font-weight: 600;
178
+ }
179
+
180
+ .item-story {
181
+ font-size: 11px;
182
+ color: #94a3b8;
183
+ max-width: 160px;
184
+ overflow: hidden;
185
+ text-overflow: ellipsis;
186
+ white-space: nowrap;
187
+ }
188
+
189
+ .loading, .empty {
190
+ text-align: center;
191
+ padding: 60px 20px;
192
+ color: #94a3b8;
193
+ font-size: 14px;
194
+ }
195
+
196
+ .empty-section {
197
+ padding: 16px;
198
+ text-align: center;
199
+ color: #cbd5e1;
200
+ font-size: 13px;
201
+ }
202
+ </style>
@@ -0,0 +1,167 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, onMounted } from 'vue'
3
+ import { useRoute, useRouter } from 'vue-router'
4
+ import { apiGet, apiPost } from '@/api/client'
5
+
6
+ const route = useRoute()
7
+ const router = useRouter()
8
+ const sprintId = computed(() => route.params.sprintId as string)
9
+
10
+ const loading = ref(false)
11
+ const error = ref('')
12
+ const planData = ref<{
13
+ sprint: { status: string; velocity: number | null }
14
+ summary: { completedCount: number; incompleteCount: number; totalStories: number; doneSP: number; totalSP?: number }
15
+ incompleteStories: Array<{ id: number; title: string; story_points: number | null; status: string; assignee: string | null }>
16
+ velocity?: Array<{ assignee: string; doneSP: number; totalSP: number }>
17
+ } | null>(null)
18
+
19
+ const result = ref<{
20
+ summary: {
21
+ completedCount: number; incompleteCount: number; totalStories: number
22
+ doneSP: number; totalSP: number; completionRate: number
23
+ movedToBacklog: Array<{ id: number; title: string }>
24
+ }
25
+ } | null>(null)
26
+
27
+ const incomplete = computed(() => planData.value?.incompleteStories ?? [])
28
+ const doneSP = computed(() => planData.value?.summary?.doneSP ?? 0)
29
+ const totalSP = computed(() => planData.value?.summary?.totalSP ?? 0)
30
+ const completionRate = computed(() => {
31
+ const total = planData.value?.summary?.totalStories ?? 0
32
+ const completed = planData.value?.summary?.completedCount ?? 0
33
+ return total > 0 ? Math.round((completed / total) * 100) : 0
34
+ })
35
+
36
+ async function closeSprint() {
37
+ loading.value = true
38
+ error.value = ''
39
+ const { data, error: e } = await apiPost(`/api/v2/kickoff/${sprintId.value}/close`, {})
40
+ loading.value = false
41
+ if (e) { error.value = e; return }
42
+ result.value = data as typeof result.value
43
+ }
44
+
45
+ onMounted(async () => {
46
+ const { data } = await apiGet<typeof planData.value>(`/api/v2/kickoff/${sprintId.value}/close-preview`)
47
+ if (data) planData.value = data
48
+ if (data?.sprint?.status !== 'active') {
49
+ error.value = `${sprintId.value} is not in active status (current: ${data?.sprint?.status})`
50
+ }
51
+ })
52
+ </script>
53
+
54
+ <template>
55
+ <div class="close-page">
56
+ <!-- Close complete -->
57
+ <div v-if="result" class="close-result">
58
+ <h1>{{ sprintId }} Closed</h1>
59
+ <div class="summary-cards">
60
+ <div class="summary-card">
61
+ <div class="summary-value">{{ result.summary.completedCount }}</div>
62
+ <div class="summary-label">Completed Stories</div>
63
+ </div>
64
+ <div class="summary-card">
65
+ <div class="summary-value">{{ result.summary.doneSP }}</div>
66
+ <div class="summary-label">Completed SP</div>
67
+ </div>
68
+ <div class="summary-card">
69
+ <div class="summary-value">{{ result.summary.completionRate }}%</div>
70
+ <div class="summary-label">Completion Rate</div>
71
+ </div>
72
+ </div>
73
+ <div v-if="result.summary.movedToBacklog.length" class="backlog-list">
74
+ <h3>Stories Moved to Backlog ({{ result.summary.incompleteCount }})</h3>
75
+ <div v-for="s in result.summary.movedToBacklog" :key="s.id" class="backlog-item">
76
+ S{{ s.id }}: {{ s.title }}
77
+ </div>
78
+ </div>
79
+ <div class="close-actions">
80
+ <button class="btn btn--primary" @click="router.push(`/retro/${sprintId}`)">Start Retrospective &rarr;</button>
81
+ <button class="btn" @click="router.push('/')">Dashboard</button>
82
+ </div>
83
+ </div>
84
+
85
+ <!-- Close confirmation -->
86
+ <div v-else>
87
+ <h1>Close {{ sprintId }}</h1>
88
+ <p v-if="error" class="error-msg">{{ error }}</p>
89
+
90
+ <div v-if="planData && !error" class="close-preview">
91
+ <div class="summary-cards">
92
+ <div class="summary-card">
93
+ <div class="summary-value">{{ planData?.summary?.completedCount ?? 0 }} / {{ planData?.summary?.totalStories ?? 0 }}</div>
94
+ <div class="summary-label">Completed Stories</div>
95
+ </div>
96
+ <div class="summary-card">
97
+ <div class="summary-value">{{ doneSP }} / {{ totalSP }}</div>
98
+ <div class="summary-label">Story Points</div>
99
+ </div>
100
+ <div class="summary-card">
101
+ <div class="summary-value">{{ completionRate }}%</div>
102
+ <div class="summary-label">Completion Rate</div>
103
+ </div>
104
+ </div>
105
+
106
+ <!-- Per-member velocity -->
107
+ <div v-if="planData?.velocity" class="velocity-section">
108
+ <h3>Team Member Performance</h3>
109
+ <div v-for="v in (planData as any).velocity" :key="v.assignee" class="velocity-row">
110
+ <span class="velocity-name">{{ v.assignee }}</span>
111
+ <span class="velocity-sp">{{ v.doneSP }} / {{ v.totalSP }} SP</span>
112
+ </div>
113
+ </div>
114
+
115
+ <div v-if="incomplete.length" class="incomplete-section">
116
+ <h3>Incomplete Stories ({{ incomplete.length }}) — will be moved to backlog</h3>
117
+ <div v-for="s in incomplete" :key="s.id" class="incomplete-item">
118
+ <span class="status-dot" />
119
+ S{{ s.id }}: {{ s.title }}
120
+ <span class="sp-badge">{{ s.story_points ?? '-' }} SP</span>
121
+ </div>
122
+ </div>
123
+
124
+ <button class="btn btn--danger btn--lg" :disabled="loading" @click="closeSprint">
125
+ {{ loading ? 'Closing...' : `Close ${sprintId}` }}
126
+ </button>
127
+ </div>
128
+ </div>
129
+ </div>
130
+ </template>
131
+
132
+ <style scoped>
133
+ .close-page { max-width: 720px; margin: 0 auto; padding: 32px 24px; }
134
+ .close-page h1 { font-size: 24px; font-weight: 700; margin-bottom: 20px; }
135
+ .summary-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 24px; }
136
+ .summary-card { background: #fff; border-radius: 12px; padding: 16px; text-align: center; }
137
+ .summary-value { font-size: 28px; font-weight: 800; color: var(--text-primary); }
138
+ .summary-label { font-size: 12px; color: var(--text-secondary); margin-top: 4px; }
139
+ .velocity-section { margin-bottom: 20px; }
140
+ .velocity-section h3 { font-size: 14px; color: var(--text-secondary); margin-bottom: 8px; }
141
+ .velocity-row { display: flex; justify-content: space-between; padding: 4px 0; font-size: 13px; }
142
+ .velocity-name { font-weight: 500; }
143
+ .velocity-sp { color: var(--text-secondary); }
144
+ .incomplete-section { margin-bottom: 24px; }
145
+ .incomplete-section h3 { font-size: 14px; color: var(--text-secondary); margin-bottom: 8px; }
146
+ .incomplete-item { padding: 6px 0; font-size: 13px; display: flex; align-items: center; gap: 6px; }
147
+ .status-dot { width: 6px; height: 6px; border-radius: 50%; background: #f59e0b; flex-shrink: 0; }
148
+ .sp-badge { font-size: 11px; color: var(--text-muted); margin-left: auto; }
149
+ .backlog-list { margin: 20px 0; }
150
+ .backlog-list h3 { font-size: 14px; color: var(--text-secondary); margin-bottom: 8px; }
151
+ .backlog-item { padding: 4px 0; font-size: 13px; color: var(--text-secondary); }
152
+ .close-actions { display: flex; gap: 12px; justify-content: center; margin-top: 24px; }
153
+ .close-result { text-align: center; }
154
+ .close-result h1 { margin-bottom: 24px; }
155
+ .btn { padding: 10px 20px; border-radius: 10px; border: 1px solid rgba(0,0,0,0.06); background: #fff; font-size: 14px; font-weight: 600; cursor: pointer; }
156
+ .btn--primary { background: var(--primary); color: #fff; border: none; }
157
+ .btn--danger { background: #ef4444; color: #fff; border: none; }
158
+ .btn--lg { width: 100%; padding: 14px; font-size: 16px; }
159
+ .btn:disabled { opacity: 0.5; cursor: not-allowed; }
160
+ .error-msg { color: #ef4444; background: rgba(239,68,68,0.08); padding: 8px 12px; border-radius: 6px; font-size: 13px; margin-bottom: 16px; }
161
+ @media (max-width: 640px) {
162
+ .close-page { padding: 16px; }
163
+ .summary-cards { grid-template-columns: 1fr; }
164
+ .summary-value { font-size: 22px; }
165
+ .close-page h1 { font-size: 20px; }
166
+ }
167
+ </style>
@@ -0,0 +1,49 @@
1
+ <script setup lang="ts">
2
+ import type { PmStory, StoryStatus } from '@/composables/usePmStore'
3
+ import TaskCard from './TaskCard.vue'
4
+
5
+ defineProps<{ status: StoryStatus; label: string; stories: PmStory[]; dragOver: boolean }>()
6
+ defineEmits<{
7
+ (e: 'drag-over', event: DragEvent): void
8
+ (e: 'drag-leave'): void
9
+ (e: 'drop', event: DragEvent): void
10
+ (e: 'drag-start', event: DragEvent, story: PmStory): void
11
+ (e: 'drag-end'): void
12
+ (e: 'select', story: PmStory): void
13
+ }>()
14
+
15
+ function statusDotColor(status: string): string {
16
+ const map: Record<string, string> = { draft: '#94a3b8', backlog: '#a78bfa', ready: '#3b82f6', 'ready-for-dev': '#3b82f6', 'in-progress': '#f59e0b', review: '#8b5cf6', qa: '#ec4899', done: '#22c55e' }
17
+ return map[status] || '#94a3b8'
18
+ }
19
+ </script>
20
+
21
+ <template>
22
+ <div class="kanban-col" :class="{ 'kanban-col--dragover': dragOver }"
23
+ @dragover="$emit('drag-over', $event)" @dragleave="$emit('drag-leave')" @drop="$emit('drop', $event)">
24
+ <div class="kanban-col-header">
25
+ <span class="kanban-col-dot" :style="{ background: statusDotColor(status) }" />
26
+ <span class="kanban-col-label">{{ label }}</span>
27
+ <span class="kanban-col-count">{{ stories.length }}</span>
28
+ </div>
29
+ <div class="kanban-col-body">
30
+ <div v-for="story in stories" :key="story.id" class="kanban-card-wrap" draggable="true"
31
+ @dragstart="$emit('drag-start', $event, story)" @dragend="$emit('drag-end')">
32
+ <TaskCard :story="story" @click="$emit('select', story)" />
33
+ </div>
34
+ <div v-if="stories.length === 0" class="kanban-empty">Drag to move</div>
35
+ </div>
36
+ </div>
37
+ </template>
38
+
39
+ <style scoped>
40
+ .kanban-col { min-width: 200px; flex: 1; display: flex; flex-direction: column; }
41
+ .kanban-col-header { display: flex; align-items: center; gap: 6px; padding: 8px 10px; font-size: 12px; font-weight: 700; color: var(--text-secondary); background: var(--card-bg, #fff); border: 1px solid var(--border-light, #e2e8f0); border-radius: 12px 12px 0 0; border-bottom: none; }
42
+ .kanban-col-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
43
+ .kanban-col-count { margin-left: auto; background: rgba(0,0,0,0.06); color: var(--text-secondary); font-size: 10px; font-weight: 700; min-width: 18px; height: 18px; border-radius: 9px; display: flex; align-items: center; justify-content: center; }
44
+ .kanban-col-body { flex: 1; display: flex; flex-direction: column; gap: 8px; padding: 10px; border: 1px solid rgba(0,0,0,0.06); border-radius: 0 0 8px 8px; background: rgba(0,0,0,0.02); min-height: 100px; }
45
+ .kanban-empty { text-align: center; padding: 20px 0; color: var(--text-muted); font-size: 13px; }
46
+ .kanban-card-wrap { cursor: grab; transition: opacity 0.15s, transform 0.15s; }
47
+ .kanban-card-wrap:active { cursor: grabbing; }
48
+ .kanban-col--dragover .kanban-col-body { background: rgba(59,130,246,0.06); border-color: rgba(59,130,246,0.3); border-style: dashed; }
49
+ </style>