popilot 0.6.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 (112) hide show
  1. package/bin/cli.mjs +204 -2
  2. package/lib/doctor.mjs +38 -1
  3. package/lib/hydrate.mjs +15 -0
  4. package/lib/scaffold.mjs +5 -0
  5. package/lib/setup-wizard.mjs +35 -2
  6. package/package.json +1 -1
  7. package/scaffold/.context/project.yaml.example +19 -0
  8. package/scaffold/mcp-pm/package.json +19 -0
  9. package/scaffold/mcp-pm/src/api-client.ts +69 -0
  10. package/scaffold/mcp-pm/src/index.ts +660 -0
  11. package/scaffold/mcp-pm/tsconfig.json +14 -0
  12. package/scaffold/pm-api/package.json +21 -0
  13. package/scaffold/pm-api/sql/schema-core.sql +331 -0
  14. package/scaffold/pm-api/sql/schema-docs.sql +25 -0
  15. package/scaffold/pm-api/sql/schema-meetings.sql +17 -0
  16. package/scaffold/pm-api/sql/schema-rewards.sql +16 -0
  17. package/scaffold/pm-api/src/auth.ts +28 -0
  18. package/scaffold/pm-api/src/blockchain/adapter.ts +20 -0
  19. package/scaffold/pm-api/src/blockchain/tron.ts +62 -0
  20. package/scaffold/pm-api/src/db/adapter.ts +36 -0
  21. package/scaffold/pm-api/src/db/turso.ts +147 -0
  22. package/scaffold/pm-api/src/index.ts +114 -0
  23. package/scaffold/pm-api/src/mcp-tools/dashboard.ts +40 -0
  24. package/scaffold/pm-api/src/mcp-tools/epic.ts +67 -0
  25. package/scaffold/pm-api/src/mcp-tools/event.ts +89 -0
  26. package/scaffold/pm-api/src/mcp-tools/index.ts +11 -0
  27. package/scaffold/pm-api/src/mcp-tools/initiative.ts +51 -0
  28. package/scaffold/pm-api/src/mcp-tools/memo.ts +164 -0
  29. package/scaffold/pm-api/src/mcp-tools/notification.ts +37 -0
  30. package/scaffold/pm-api/src/mcp-tools/retro.ts +183 -0
  31. package/scaffold/pm-api/src/mcp-tools/sprint.ts +204 -0
  32. package/scaffold/pm-api/src/mcp-tools/standup.ts +136 -0
  33. package/scaffold/pm-api/src/mcp-tools/story.ts +230 -0
  34. package/scaffold/pm-api/src/mcp-tools/task.ts +187 -0
  35. package/scaffold/pm-api/src/mcp-tools/utils.ts +83 -0
  36. package/scaffold/pm-api/src/mcp.ts +871 -0
  37. package/scaffold/pm-api/src/nudge.ts +283 -0
  38. package/scaffold/pm-api/src/routes/auth.ts +32 -0
  39. package/scaffold/pm-api/src/routes/v2-activity.ts +27 -0
  40. package/scaffold/pm-api/src/routes/v2-admin.ts +165 -0
  41. package/scaffold/pm-api/src/routes/v2-dashboard.ts +189 -0
  42. package/scaffold/pm-api/src/routes/v2-docs.ts +34 -0
  43. package/scaffold/pm-api/src/routes/v2-initiatives.ts +118 -0
  44. package/scaffold/pm-api/src/routes/v2-kickoff.ts +265 -0
  45. package/scaffold/pm-api/src/routes/v2-meetings.ts +324 -0
  46. package/scaffold/pm-api/src/routes/v2-memos.ts +257 -0
  47. package/scaffold/pm-api/src/routes/v2-nav.ts +260 -0
  48. package/scaffold/pm-api/src/routes/v2-notifications.ts +79 -0
  49. package/scaffold/pm-api/src/routes/v2-page-content.ts +35 -0
  50. package/scaffold/pm-api/src/routes/v2-pm.ts +380 -0
  51. package/scaffold/pm-api/src/routes/v2-policy.ts +58 -0
  52. package/scaffold/pm-api/src/routes/v2-retro.ts +221 -0
  53. package/scaffold/pm-api/src/routes/v2-rewards.ts +132 -0
  54. package/scaffold/pm-api/src/routes/v2-scenarios.ts +48 -0
  55. package/scaffold/pm-api/src/routes/v2-search.ts +32 -0
  56. package/scaffold/pm-api/src/routes/v2-standup.ts +127 -0
  57. package/scaffold/pm-api/src/routes/v2-user.ts +38 -0
  58. package/scaffold/pm-api/src/types.ts +11 -0
  59. package/scaffold/pm-api/src/utils/activity.ts +22 -0
  60. package/scaffold/pm-api/src/utils/admin.ts +9 -0
  61. package/scaffold/pm-api/src/utils/agent-notify.ts +62 -0
  62. package/scaffold/pm-api/src/utils/assignee.ts +69 -0
  63. package/scaffold/pm-api/src/utils/db.ts +45 -0
  64. package/scaffold/pm-api/src/utils/initiative.ts +23 -0
  65. package/scaffold/pm-api/src/utils/sprint-lifecycle.ts +96 -0
  66. package/scaffold/pm-api/tsconfig.json +15 -0
  67. package/scaffold/pm-api/wrangler.toml.hbs +11 -0
  68. package/scaffold/spec-site/package-lock.json +40 -0
  69. package/scaffold/spec-site/package.json +4 -1
  70. package/scaffold/spec-site/src/api/types.ts +6 -0
  71. package/scaffold/spec-site/src/components/AppHeader.vue +429 -55
  72. package/scaffold/spec-site/src/components/MemberSelect.vue +48 -0
  73. package/scaffold/spec-site/src/components/NotificationDropdown.vue +116 -0
  74. package/scaffold/spec-site/src/components/SearchModal.vue +102 -0
  75. package/scaffold/spec-site/src/components/VelocityChart.vue +77 -0
  76. package/scaffold/spec-site/src/composables/pmTypes.ts +15 -2
  77. package/scaffold/spec-site/src/composables/useDashboard.ts +221 -0
  78. package/scaffold/spec-site/src/composables/useMediaQuery.ts +28 -0
  79. package/scaffold/spec-site/src/composables/useNotification.ts +200 -0
  80. package/scaffold/spec-site/src/composables/usePmStore.ts +48 -1
  81. package/scaffold/spec-site/src/composables/useRetro.ts +6 -0
  82. package/scaffold/spec-site/src/composables/useStandup.ts +201 -0
  83. package/scaffold/spec-site/src/composables/useTheme.ts +37 -0
  84. package/scaffold/spec-site/src/composables/useUser.ts +19 -1
  85. package/scaffold/spec-site/src/features.ts +108 -0
  86. package/scaffold/spec-site/src/pages/AdminPage.vue +299 -0
  87. package/scaffold/spec-site/src/pages/DashboardPage.vue +650 -0
  88. package/scaffold/spec-site/src/pages/DocsHub.vue +157 -0
  89. package/scaffold/spec-site/src/pages/InboxPage.vue +156 -0
  90. package/scaffold/spec-site/src/pages/MeetingsPage.vue +294 -0
  91. package/scaffold/spec-site/src/pages/MyPage.vue +343 -0
  92. package/scaffold/spec-site/src/pages/RewardsPage.vue +266 -0
  93. package/scaffold/spec-site/src/pages/board/BoardAdmin.vue +422 -0
  94. package/scaffold/spec-site/src/pages/board/BoardEpicSection.vue +54 -0
  95. package/scaffold/spec-site/src/pages/board/BoardPage.vue +884 -0
  96. package/scaffold/spec-site/src/pages/board/BoardStoryCard.vue +67 -0
  97. package/scaffold/spec-site/src/pages/board/BoardTaskItem.vue +52 -0
  98. package/scaffold/spec-site/src/pages/board/MyTasksPage.vue +202 -0
  99. package/scaffold/spec-site/src/pages/board/SprintClose.vue +167 -0
  100. package/scaffold/spec-site/src/pages/board/SprintColumn.vue +49 -0
  101. package/scaffold/spec-site/src/pages/board/SprintKickoff.vue +389 -0
  102. package/scaffold/spec-site/src/pages/board/StatusBadge.vue +52 -0
  103. package/scaffold/spec-site/src/pages/board/StoryDetailPanel.vue +495 -0
  104. package/scaffold/spec-site/src/pages/board/TaskCard.vue +42 -0
  105. package/scaffold/spec-site/src/pages/retro/RetroCard.vue +36 -2
  106. package/scaffold/spec-site/src/pages/retro/RetroHeader.vue +82 -66
  107. package/scaffold/spec-site/src/pages/retro/RetroPage.vue +47 -18
  108. package/scaffold/spec-site/src/pages/standup/StandupEntryCard.vue +551 -0
  109. package/scaffold/spec-site/src/pages/standup/StandupForm.vue +68 -0
  110. package/scaffold/spec-site/src/pages/standup/StandupList.vue +71 -0
  111. package/scaffold/spec-site/src/pages/standup/StandupPage.vue +225 -0
  112. 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>