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.
- package/adapters/codex/.codex/commands/_domain.md.hbs +33 -0
- package/adapters/codex/.codex/commands/analytics.md.hbs +55 -0
- package/adapters/codex/.codex/commands/daily.md.hbs +301 -0
- package/adapters/codex/.codex/commands/dev.md.hbs +62 -0
- package/adapters/codex/.codex/commands/gtm.md +82 -0
- package/adapters/codex/.codex/commands/handoff.md +259 -0
- package/adapters/codex/.codex/commands/market.md +120 -0
- package/adapters/codex/.codex/commands/metrics.md +123 -0
- package/adapters/codex/.codex/commands/oscar-loop.md +436 -0
- package/adapters/codex/.codex/commands/party.md +85 -0
- package/adapters/codex/.codex/commands/plan.md +43 -0
- package/adapters/codex/.codex/commands/research.md +203 -0
- package/adapters/codex/.codex/commands/retro.md +68 -0
- package/adapters/codex/.codex/commands/save.md +440 -0
- package/adapters/codex/.codex/commands/sessions.md +139 -0
- package/adapters/codex/.codex/commands/sprint.md +106 -0
- package/adapters/codex/.codex/commands/start.md +396 -0
- package/adapters/codex/.codex/commands/strategy.md +41 -0
- package/adapters/codex/.codex/commands/task.md +220 -0
- package/adapters/codex/.codex/commands/tracking.md +116 -0
- package/adapters/codex/.codex/commands/validate.md +58 -0
- package/adapters/codex/AGENTS.md.hbs +210 -0
- package/adapters/codex/manifest.yaml +36 -0
- package/adapters/gemini/.gemini/commands/_domain.md.hbs +33 -0
- package/adapters/gemini/.gemini/commands/analytics.md.hbs +55 -0
- package/adapters/gemini/.gemini/commands/daily.md.hbs +301 -0
- package/adapters/gemini/.gemini/commands/dev.md.hbs +62 -0
- package/adapters/gemini/.gemini/commands/gtm.md +82 -0
- package/adapters/gemini/.gemini/commands/handoff.md +259 -0
- package/adapters/gemini/.gemini/commands/market.md +120 -0
- package/adapters/gemini/.gemini/commands/metrics.md +123 -0
- package/adapters/gemini/.gemini/commands/oscar-loop.md +436 -0
- package/adapters/gemini/.gemini/commands/party.md +85 -0
- package/adapters/gemini/.gemini/commands/plan.md +43 -0
- package/adapters/gemini/.gemini/commands/research.md +203 -0
- package/adapters/gemini/.gemini/commands/retro.md +68 -0
- package/adapters/gemini/.gemini/commands/save.md +440 -0
- package/adapters/gemini/.gemini/commands/sessions.md +139 -0
- package/adapters/gemini/.gemini/commands/sprint.md +106 -0
- package/adapters/gemini/.gemini/commands/start.md +396 -0
- package/adapters/gemini/.gemini/commands/strategy.md +41 -0
- package/adapters/gemini/.gemini/commands/task.md +220 -0
- package/adapters/gemini/.gemini/commands/tracking.md +116 -0
- package/adapters/gemini/.gemini/commands/validate.md +58 -0
- package/adapters/gemini/GEMINI.md.hbs +210 -0
- package/adapters/gemini/manifest.yaml +36 -0
- package/bin/cli.mjs +215 -4
- package/lib/doctor.mjs +38 -1
- package/lib/hydrate.mjs +15 -0
- package/lib/industry-presets.mjs +135 -0
- package/lib/scaffold.mjs +5 -0
- package/lib/setup-wizard.mjs +71 -2
- package/package.json +1 -1
- package/scaffold/.context/agents/TEMPLATE.md +14 -0
- package/scaffold/.context/agents/analyst.md.hbs +3 -3
- package/scaffold/.context/agents/developer.md.hbs +5 -5
- package/scaffold/.context/agents/gtm-strategist.md.hbs +3 -3
- package/scaffold/.context/agents/handoff-specialist.md.hbs +18 -18
- package/scaffold/.context/agents/market-researcher.md.hbs +6 -6
- package/scaffold/.context/agents/orchestrator.md.hbs +8 -8
- package/scaffold/.context/agents/planner.md.hbs +6 -6
- package/scaffold/.context/agents/qa.md.hbs +5 -5
- package/scaffold/.context/agents/researcher.md.hbs +33 -6
- package/scaffold/.context/agents/strategist.md.hbs +8 -8
- package/scaffold/.context/agents/tracking-governor.md.hbs +2 -2
- package/scaffold/.context/project.yaml.example +25 -0
- package/scaffold/mcp-pm/package.json +19 -0
- package/scaffold/mcp-pm/src/api-client.ts +69 -0
- package/scaffold/mcp-pm/src/index.ts +660 -0
- package/scaffold/mcp-pm/tsconfig.json +14 -0
- package/scaffold/pm-api/package.json +21 -0
- package/scaffold/pm-api/sql/schema-core.sql +331 -0
- package/scaffold/pm-api/sql/schema-docs.sql +25 -0
- package/scaffold/pm-api/sql/schema-meetings.sql +17 -0
- package/scaffold/pm-api/sql/schema-rewards.sql +16 -0
- package/scaffold/pm-api/src/auth.ts +28 -0
- package/scaffold/pm-api/src/blockchain/adapter.ts +20 -0
- package/scaffold/pm-api/src/blockchain/tron.ts +62 -0
- package/scaffold/pm-api/src/db/adapter.ts +36 -0
- package/scaffold/pm-api/src/db/turso.ts +147 -0
- package/scaffold/pm-api/src/index.ts +114 -0
- package/scaffold/pm-api/src/mcp-tools/dashboard.ts +40 -0
- package/scaffold/pm-api/src/mcp-tools/epic.ts +67 -0
- package/scaffold/pm-api/src/mcp-tools/event.ts +89 -0
- package/scaffold/pm-api/src/mcp-tools/index.ts +11 -0
- package/scaffold/pm-api/src/mcp-tools/initiative.ts +51 -0
- package/scaffold/pm-api/src/mcp-tools/memo.ts +164 -0
- package/scaffold/pm-api/src/mcp-tools/notification.ts +37 -0
- package/scaffold/pm-api/src/mcp-tools/retro.ts +183 -0
- package/scaffold/pm-api/src/mcp-tools/sprint.ts +204 -0
- package/scaffold/pm-api/src/mcp-tools/standup.ts +136 -0
- package/scaffold/pm-api/src/mcp-tools/story.ts +230 -0
- package/scaffold/pm-api/src/mcp-tools/task.ts +187 -0
- package/scaffold/pm-api/src/mcp-tools/utils.ts +83 -0
- package/scaffold/pm-api/src/mcp.ts +871 -0
- package/scaffold/pm-api/src/nudge.ts +283 -0
- package/scaffold/pm-api/src/routes/auth.ts +32 -0
- package/scaffold/pm-api/src/routes/v2-activity.ts +27 -0
- package/scaffold/pm-api/src/routes/v2-admin.ts +165 -0
- package/scaffold/pm-api/src/routes/v2-dashboard.ts +189 -0
- package/scaffold/pm-api/src/routes/v2-docs.ts +34 -0
- package/scaffold/pm-api/src/routes/v2-initiatives.ts +118 -0
- package/scaffold/pm-api/src/routes/v2-kickoff.ts +265 -0
- package/scaffold/pm-api/src/routes/v2-meetings.ts +324 -0
- package/scaffold/pm-api/src/routes/v2-memos.ts +257 -0
- package/scaffold/pm-api/src/routes/v2-nav.ts +260 -0
- package/scaffold/pm-api/src/routes/v2-notifications.ts +79 -0
- package/scaffold/pm-api/src/routes/v2-page-content.ts +35 -0
- package/scaffold/pm-api/src/routes/v2-pm.ts +380 -0
- package/scaffold/pm-api/src/routes/v2-policy.ts +58 -0
- package/scaffold/pm-api/src/routes/v2-retro.ts +221 -0
- package/scaffold/pm-api/src/routes/v2-rewards.ts +132 -0
- package/scaffold/pm-api/src/routes/v2-scenarios.ts +48 -0
- package/scaffold/pm-api/src/routes/v2-search.ts +32 -0
- package/scaffold/pm-api/src/routes/v2-standup.ts +127 -0
- package/scaffold/pm-api/src/routes/v2-user.ts +38 -0
- package/scaffold/pm-api/src/types.ts +11 -0
- package/scaffold/pm-api/src/utils/activity.ts +22 -0
- package/scaffold/pm-api/src/utils/admin.ts +9 -0
- package/scaffold/pm-api/src/utils/agent-notify.ts +62 -0
- package/scaffold/pm-api/src/utils/assignee.ts +69 -0
- package/scaffold/pm-api/src/utils/db.ts +45 -0
- package/scaffold/pm-api/src/utils/initiative.ts +23 -0
- package/scaffold/pm-api/src/utils/sprint-lifecycle.ts +96 -0
- package/scaffold/pm-api/tsconfig.json +15 -0
- package/scaffold/pm-api/wrangler.toml.hbs +11 -0
- package/scaffold/spec-site/package-lock.json +40 -0
- package/scaffold/spec-site/package.json +4 -1
- package/scaffold/spec-site/src/api/types.ts +6 -0
- package/scaffold/spec-site/src/components/AppHeader.vue +429 -55
- package/scaffold/spec-site/src/components/MemberSelect.vue +48 -0
- package/scaffold/spec-site/src/components/NotificationDropdown.vue +116 -0
- package/scaffold/spec-site/src/components/SearchModal.vue +102 -0
- package/scaffold/spec-site/src/components/VelocityChart.vue +77 -0
- package/scaffold/spec-site/src/composables/pmTypes.ts +15 -2
- package/scaffold/spec-site/src/composables/useDashboard.ts +221 -0
- package/scaffold/spec-site/src/composables/useMediaQuery.ts +28 -0
- package/scaffold/spec-site/src/composables/useNotification.ts +200 -0
- package/scaffold/spec-site/src/composables/usePmStore.ts +48 -1
- package/scaffold/spec-site/src/composables/useRetro.ts +6 -0
- package/scaffold/spec-site/src/composables/useStandup.ts +201 -0
- package/scaffold/spec-site/src/composables/useTheme.ts +37 -0
- package/scaffold/spec-site/src/composables/useUser.ts +19 -1
- package/scaffold/spec-site/src/features.ts +108 -0
- package/scaffold/spec-site/src/pages/AdminPage.vue +299 -0
- package/scaffold/spec-site/src/pages/DashboardPage.vue +650 -0
- package/scaffold/spec-site/src/pages/DocsHub.vue +157 -0
- package/scaffold/spec-site/src/pages/InboxPage.vue +156 -0
- package/scaffold/spec-site/src/pages/MeetingsPage.vue +294 -0
- package/scaffold/spec-site/src/pages/MyPage.vue +343 -0
- package/scaffold/spec-site/src/pages/RewardsPage.vue +266 -0
- package/scaffold/spec-site/src/pages/board/BoardAdmin.vue +422 -0
- package/scaffold/spec-site/src/pages/board/BoardEpicSection.vue +54 -0
- package/scaffold/spec-site/src/pages/board/BoardPage.vue +884 -0
- package/scaffold/spec-site/src/pages/board/BoardStoryCard.vue +67 -0
- package/scaffold/spec-site/src/pages/board/BoardTaskItem.vue +52 -0
- package/scaffold/spec-site/src/pages/board/MyTasksPage.vue +202 -0
- package/scaffold/spec-site/src/pages/board/SprintClose.vue +167 -0
- package/scaffold/spec-site/src/pages/board/SprintColumn.vue +49 -0
- package/scaffold/spec-site/src/pages/board/SprintKickoff.vue +389 -0
- package/scaffold/spec-site/src/pages/board/StatusBadge.vue +52 -0
- package/scaffold/spec-site/src/pages/board/StoryDetailPanel.vue +495 -0
- package/scaffold/spec-site/src/pages/board/TaskCard.vue +42 -0
- package/scaffold/spec-site/src/pages/retro/RetroCard.vue +36 -2
- package/scaffold/spec-site/src/pages/retro/RetroHeader.vue +82 -66
- package/scaffold/spec-site/src/pages/retro/RetroPage.vue +47 -18
- package/scaffold/spec-site/src/pages/standup/StandupEntryCard.vue +551 -0
- package/scaffold/spec-site/src/pages/standup/StandupForm.vue +68 -0
- package/scaffold/spec-site/src/pages/standup/StandupList.vue +71 -0
- package/scaffold/spec-site/src/pages/standup/StandupPage.vue +225 -0
- 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'">✓</span>
|
|
30
|
+
<span v-else-if="task.status === 'in-progress'" class="check-progress">▶</span>
|
|
31
|
+
<span v-else class="check-empty">○</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 →</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>
|