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.
- package/bin/cli.mjs +204 -2
- package/lib/doctor.mjs +38 -1
- package/lib/hydrate.mjs +15 -0
- package/lib/scaffold.mjs +5 -0
- package/lib/setup-wizard.mjs +35 -2
- package/package.json +1 -1
- package/scaffold/.context/project.yaml.example +19 -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,389 @@
|
|
|
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
|
+
import { useUser } from '@/composables/useUser'
|
|
6
|
+
|
|
7
|
+
const route = useRoute()
|
|
8
|
+
const router = useRouter()
|
|
9
|
+
const { dynamicMembers, loadMembers } = useUser()
|
|
10
|
+
|
|
11
|
+
const sprintId = computed(() => route.params.sprintId as string)
|
|
12
|
+
|
|
13
|
+
// ── State ──
|
|
14
|
+
const step = ref<'create' | 'checkin' | 'plan' | 'done'>('create')
|
|
15
|
+
const loading = ref(false)
|
|
16
|
+
const error = ref('')
|
|
17
|
+
const retroActions = ref<Array<{ id: number; content: string; assignee: string | null }>>([])
|
|
18
|
+
|
|
19
|
+
// Step 1: Create
|
|
20
|
+
const newSprint = ref({ id: '', label: '', theme: '', startDate: '', endDate: '' })
|
|
21
|
+
const totalWorkingDays = ref(0)
|
|
22
|
+
|
|
23
|
+
// Step 2: Checkin
|
|
24
|
+
interface MemberInfo { id: number; display_name: string; checked: boolean }
|
|
25
|
+
const allMembers = ref<MemberInfo[]>([])
|
|
26
|
+
const absences = ref<Record<number, string[]>>({})
|
|
27
|
+
const absenceInput = ref<Record<number, string>>({})
|
|
28
|
+
|
|
29
|
+
// Step 3: Plan
|
|
30
|
+
const planData = ref<{
|
|
31
|
+
sprint: { velocity: number | null; start_date: string; end_date: string; status: string }
|
|
32
|
+
members: Array<{ member_id: number; display_name: string; working_days: number }>
|
|
33
|
+
absences: Array<{ member_id: number; absence_date: string; reason: string | null }>
|
|
34
|
+
stories: Array<{ id: number; title: string; story_points: number | null; assignee: string | null }>
|
|
35
|
+
totalSP: number
|
|
36
|
+
velocity: number
|
|
37
|
+
totalWorkingDays: number
|
|
38
|
+
} | null>(null)
|
|
39
|
+
|
|
40
|
+
// Backlog stories for selection
|
|
41
|
+
const backlogStories = ref<Array<{ id: number; title: string; story_points: number | null }>>([])
|
|
42
|
+
const selectedStoryIds = ref<Set<number>>(new Set())
|
|
43
|
+
|
|
44
|
+
const selectedSP = computed(() => {
|
|
45
|
+
return backlogStories.value
|
|
46
|
+
.filter(s => selectedStoryIds.value.has(s.id))
|
|
47
|
+
.reduce((sum, s) => sum + (s.story_points ?? 0), 0)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// ── Actions ──
|
|
51
|
+
async function createSprint() {
|
|
52
|
+
if (!newSprint.value.id || !newSprint.value.startDate || !newSprint.value.endDate) {
|
|
53
|
+
error.value = 'ID, start date, and end date are required'; return
|
|
54
|
+
}
|
|
55
|
+
loading.value = true; error.value = ''
|
|
56
|
+
const { data, error: e } = await apiPost('/api/v2/kickoff/create', newSprint.value)
|
|
57
|
+
loading.value = false
|
|
58
|
+
if (e) { error.value = e; return }
|
|
59
|
+
totalWorkingDays.value = (data as { totalWorkingDays: number }).totalWorkingDays
|
|
60
|
+
step.value = 'checkin'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function loadMemberList() {
|
|
64
|
+
const { data } = await apiGet<{ members: Array<{ id: number; display_name: string }> }>('/api/v2/admin/members')
|
|
65
|
+
if (data?.members) {
|
|
66
|
+
allMembers.value = data.members.map(m => ({ ...m, checked: false }))
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function addAbsence(memberId: number) {
|
|
71
|
+
const date = absenceInput.value[memberId]
|
|
72
|
+
if (!date) return
|
|
73
|
+
loading.value = true
|
|
74
|
+
await apiPost(`/api/v2/kickoff/${newSprint.value.id || sprintId.value}/absence`, {
|
|
75
|
+
memberId, dates: [date]
|
|
76
|
+
})
|
|
77
|
+
if (!absences.value[memberId]) absences.value[memberId] = []
|
|
78
|
+
absences.value[memberId].push(date)
|
|
79
|
+
absenceInput.value[memberId] = ''
|
|
80
|
+
loading.value = false
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function submitCheckin() {
|
|
84
|
+
const checkedIds = allMembers.value.filter(m => m.checked).map(m => m.id)
|
|
85
|
+
if (!checkedIds.length) { error.value = 'Please select participants'; return }
|
|
86
|
+
loading.value = true; error.value = ''
|
|
87
|
+
const sid = newSprint.value.id || sprintId.value
|
|
88
|
+
const { error: e } = await apiPost(`/api/v2/kickoff/${sid}/checkin`, { memberIds: checkedIds })
|
|
89
|
+
loading.value = false
|
|
90
|
+
if (e) { error.value = e; return }
|
|
91
|
+
await loadPlan()
|
|
92
|
+
step.value = 'plan'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function loadPlan() {
|
|
96
|
+
const sid = newSprint.value.id || sprintId.value
|
|
97
|
+
const { data } = await apiGet<typeof planData.value>(`/api/v2/kickoff/${sid}/plan`)
|
|
98
|
+
if (data) planData.value = data
|
|
99
|
+
|
|
100
|
+
// Load backlog stories
|
|
101
|
+
const { data: blData } = await apiGet<{ stories: Array<{ id: number; title: string; story_points: number | null }> }>('/api/v2/pm/data?sprint=backlog')
|
|
102
|
+
if (blData?.stories) backlogStories.value = blData.stories
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function toggleStory(id: number) {
|
|
106
|
+
if (selectedStoryIds.value.has(id)) selectedStoryIds.value.delete(id)
|
|
107
|
+
else selectedStoryIds.value.add(id)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function doKickoff() {
|
|
111
|
+
const sid = newSprint.value.id || sprintId.value
|
|
112
|
+
loading.value = true; error.value = ''
|
|
113
|
+
const { error: e } = await apiPost(`/api/v2/nav/sprints/${sid}/kickoff`, {
|
|
114
|
+
storyIds: [...selectedStoryIds.value],
|
|
115
|
+
velocity: planData.value?.velocity,
|
|
116
|
+
})
|
|
117
|
+
loading.value = false
|
|
118
|
+
if (e) { error.value = e; return }
|
|
119
|
+
step.value = 'done'
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
onMounted(async () => {
|
|
123
|
+
await loadMembers()
|
|
124
|
+
await loadMemberList()
|
|
125
|
+
|
|
126
|
+
// Load retro action items from the most recently closed sprint
|
|
127
|
+
try {
|
|
128
|
+
const { data: navData } = await apiGet<{ sprints: Array<{ id: string; status: string }> }>('/api/v2/nav')
|
|
129
|
+
const lastClosed = navData?.sprints?.filter((s: { status: string }) => s.status === 'closed')?.[0]
|
|
130
|
+
if (lastClosed) {
|
|
131
|
+
const { data: actData } = await apiGet<{ actions: Array<{ id: number; content: string; assignee: string | null }> }>(`/api/v2/retro/${lastClosed.id}/actions`)
|
|
132
|
+
if (actData?.actions) retroActions.value = actData.actions
|
|
133
|
+
}
|
|
134
|
+
} catch (_) { /* ignore if no retro data */ }
|
|
135
|
+
|
|
136
|
+
// If sprintId in URL, check existing sprint status
|
|
137
|
+
if (sprintId.value && sprintId.value !== 'new') {
|
|
138
|
+
newSprint.value.id = sprintId.value
|
|
139
|
+
const { data } = await apiGet<{ sprint: { status: string } }>(`/api/v2/kickoff/${sprintId.value}/plan`)
|
|
140
|
+
if (data?.sprint) {
|
|
141
|
+
if (data.sprint.status === 'planning') {
|
|
142
|
+
await loadPlan()
|
|
143
|
+
step.value = 'plan'
|
|
144
|
+
} else if (data.sprint.status === 'active') {
|
|
145
|
+
error.value = `${sprintId.value} is already an active sprint. Navigate to /kickoff/new to create a new sprint.`
|
|
146
|
+
} else {
|
|
147
|
+
error.value = `${sprintId.value} is already a closed sprint.`
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
</script>
|
|
153
|
+
|
|
154
|
+
<template>
|
|
155
|
+
<div class="kickoff">
|
|
156
|
+
<!-- Step indicator -->
|
|
157
|
+
<div class="steps">
|
|
158
|
+
<div class="step-item" :class="{ active: step === 'create', done: step !== 'create' }">
|
|
159
|
+
<span class="step-num">1</span> Create Sprint
|
|
160
|
+
</div>
|
|
161
|
+
<div class="step-line" />
|
|
162
|
+
<div class="step-item" :class="{ active: step === 'checkin', done: step === 'plan' || step === 'done' }">
|
|
163
|
+
<span class="step-num">2</span> Team Check-in
|
|
164
|
+
</div>
|
|
165
|
+
<div class="step-line" />
|
|
166
|
+
<div class="step-item" :class="{ active: step === 'plan', done: step === 'done' }">
|
|
167
|
+
<span class="step-num">3</span> Story Selection + Kickoff
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<p v-if="error" class="error-msg">{{ error }}</p>
|
|
172
|
+
|
|
173
|
+
<!-- Previous retro action items -->
|
|
174
|
+
<div v-if="retroActions.length && step === 'create'" class="retro-actions-info">
|
|
175
|
+
<h3>Previous Retro Action Items (added to backlog)</h3>
|
|
176
|
+
<div v-for="a in retroActions" :key="a.id" class="retro-action-item">
|
|
177
|
+
{{ a.content }}
|
|
178
|
+
<span v-if="a.assignee" class="retro-assignee">{{ a.assignee }}</span>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<!-- Step 1: Create Sprint -->
|
|
183
|
+
<section v-if="step === 'create' && !error" class="kickoff-section">
|
|
184
|
+
<h2>Create Sprint</h2>
|
|
185
|
+
<div class="form-grid">
|
|
186
|
+
<div class="form-field">
|
|
187
|
+
<label>Sprint ID</label>
|
|
188
|
+
<input v-model="newSprint.id" class="input" placeholder="s56" />
|
|
189
|
+
</div>
|
|
190
|
+
<div class="form-field">
|
|
191
|
+
<label>Label</label>
|
|
192
|
+
<input v-model="newSprint.label" class="input" placeholder="S56" />
|
|
193
|
+
</div>
|
|
194
|
+
<div class="form-field full">
|
|
195
|
+
<label>Theme</label>
|
|
196
|
+
<input v-model="newSprint.theme" class="input" placeholder="Sprint goal / theme" />
|
|
197
|
+
</div>
|
|
198
|
+
<div class="form-field">
|
|
199
|
+
<label>Start Date</label>
|
|
200
|
+
<input v-model="newSprint.startDate" type="date" class="input" />
|
|
201
|
+
</div>
|
|
202
|
+
<div class="form-field">
|
|
203
|
+
<label>End Date</label>
|
|
204
|
+
<input v-model="newSprint.endDate" type="date" class="input" />
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
<button class="btn btn--primary" :disabled="loading" @click="createSprint">
|
|
208
|
+
Next →
|
|
209
|
+
</button>
|
|
210
|
+
</section>
|
|
211
|
+
|
|
212
|
+
<!-- Step 2: Team Check-in -->
|
|
213
|
+
<section v-if="step === 'checkin'" class="kickoff-section">
|
|
214
|
+
<h2>Team Check-in</h2>
|
|
215
|
+
<p class="section-desc">Select team members participating in this sprint and enter any planned absences.</p>
|
|
216
|
+
<p class="info-badge">Total working days: <strong>{{ totalWorkingDays }}</strong></p>
|
|
217
|
+
|
|
218
|
+
<div class="member-list">
|
|
219
|
+
<div v-for="m in allMembers" :key="m.id" class="member-card" :class="{ checked: m.checked }">
|
|
220
|
+
<div class="member-header">
|
|
221
|
+
<label class="member-check">
|
|
222
|
+
<input type="checkbox" v-model="m.checked" />
|
|
223
|
+
<span class="member-name">{{ m.display_name }}</span>
|
|
224
|
+
</label>
|
|
225
|
+
</div>
|
|
226
|
+
<!-- Absence input (checked members only) -->
|
|
227
|
+
<div v-if="m.checked" class="absence-section">
|
|
228
|
+
<div class="absence-tags">
|
|
229
|
+
<span v-for="(d, i) in (absences[m.id] || [])" :key="i" class="absence-tag">
|
|
230
|
+
{{ d }} ×
|
|
231
|
+
</span>
|
|
232
|
+
</div>
|
|
233
|
+
<div class="absence-input-row">
|
|
234
|
+
<input v-model="absenceInput[m.id]" type="date" class="input input--sm" placeholder="Absence date" />
|
|
235
|
+
<button class="btn btn--sm" :disabled="!absenceInput[m.id]" @click="addAbsence(m.id)">Add</button>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
<button class="btn btn--primary" :disabled="loading || !allMembers.some(m => m.checked)" @click="submitCheckin">
|
|
242
|
+
Next →
|
|
243
|
+
</button>
|
|
244
|
+
</section>
|
|
245
|
+
|
|
246
|
+
<!-- Step 3: Plan + Kickoff -->
|
|
247
|
+
<section v-if="step === 'plan' && planData" class="kickoff-section">
|
|
248
|
+
<h2>Story Selection + Kickoff</h2>
|
|
249
|
+
|
|
250
|
+
<!-- Velocity summary -->
|
|
251
|
+
<div class="velocity-summary">
|
|
252
|
+
<div class="velocity-card">
|
|
253
|
+
<div class="velocity-label">Team Velocity</div>
|
|
254
|
+
<div class="velocity-value">{{ planData.velocity }} WD</div>
|
|
255
|
+
<div class="velocity-sub">{{ planData.members.length }} members × {{ planData.totalWorkingDays }} working days</div>
|
|
256
|
+
</div>
|
|
257
|
+
<div class="velocity-card">
|
|
258
|
+
<div class="velocity-label">Selected SP</div>
|
|
259
|
+
<div class="velocity-value" :class="{ over: selectedSP > planData.velocity }">{{ selectedSP }}</div>
|
|
260
|
+
<div v-if="selectedSP > planData.velocity" class="velocity-warn">Exceeds velocity!</div>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
<!-- Per-member working days -->
|
|
265
|
+
<div class="wd-grid">
|
|
266
|
+
<div v-for="m in planData.members" :key="m.member_id" class="wd-chip">
|
|
267
|
+
{{ m.display_name }}: <strong>{{ m.working_days }} days</strong>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
|
|
271
|
+
<!-- Backlog story selection -->
|
|
272
|
+
<h3>Select from Backlog</h3>
|
|
273
|
+
<div v-if="!backlogStories.length" class="empty">Backlog is empty</div>
|
|
274
|
+
<div v-else class="story-list">
|
|
275
|
+
<div v-for="s in backlogStories" :key="s.id"
|
|
276
|
+
class="story-card" :class="{ selected: selectedStoryIds.has(s.id) }"
|
|
277
|
+
@click="toggleStory(s.id)">
|
|
278
|
+
<input type="checkbox" :checked="selectedStoryIds.has(s.id)" @click.stop="toggleStory(s.id)" />
|
|
279
|
+
<span class="story-title">{{ s.title }}</span>
|
|
280
|
+
<span class="story-sp">{{ s.story_points ?? '-' }} SP</span>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<!-- Already assigned stories -->
|
|
285
|
+
<div v-if="planData.stories.length" class="assigned-stories">
|
|
286
|
+
<h3>Already Assigned ({{ planData.stories.length }} stories, {{ planData.totalSP }} SP)</h3>
|
|
287
|
+
<div v-for="s in planData.stories" :key="s.id" class="story-card assigned">
|
|
288
|
+
<span class="story-title">{{ s.title }}</span>
|
|
289
|
+
<span class="story-sp">{{ s.story_points ?? '-' }} SP</span>
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
<button class="btn btn--primary btn--lg" :disabled="loading" @click="doKickoff">
|
|
294
|
+
Kickoff! ({{ selectedSP + planData.totalSP }} SP)
|
|
295
|
+
</button>
|
|
296
|
+
</section>
|
|
297
|
+
|
|
298
|
+
<!-- Step 4: Done -->
|
|
299
|
+
<section v-if="step === 'done'" class="kickoff-section done-section">
|
|
300
|
+
<h2>Kickoff Complete!</h2>
|
|
301
|
+
<p>The sprint is now active.</p>
|
|
302
|
+
<button class="btn btn--primary" @click="router.push('/board/' + (newSprint.id || sprintId))">Go to Board →</button>
|
|
303
|
+
</section>
|
|
304
|
+
</div>
|
|
305
|
+
</template>
|
|
306
|
+
|
|
307
|
+
<style scoped>
|
|
308
|
+
.kickoff { max-width: 720px; margin: 0 auto; padding: 24px; }
|
|
309
|
+
|
|
310
|
+
/* Steps */
|
|
311
|
+
.steps { display: flex; align-items: center; gap: 0; margin-bottom: 32px; }
|
|
312
|
+
.step-item { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--text-muted); font-weight: 500; }
|
|
313
|
+
.step-item.active { color: #3b82f6; font-weight: 700; }
|
|
314
|
+
.step-item.done { color: #16a34a; }
|
|
315
|
+
.step-num { width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; background: rgba(0,0,0,0.06); color: var(--text-secondary); }
|
|
316
|
+
.step-item.active .step-num { background: #3b82f6; color: white; }
|
|
317
|
+
.step-item.done .step-num { background: #16a34a; color: white; }
|
|
318
|
+
.step-line { flex: 1; height: 2px; background: rgba(0,0,0,0.06); margin: 0 8px; }
|
|
319
|
+
|
|
320
|
+
.kickoff-section { margin-bottom: 32px; }
|
|
321
|
+
.kickoff-section h2 { margin-bottom: 12px; font-size: 18px; }
|
|
322
|
+
.section-desc { color: var(--text-secondary); font-size: 14px; margin-bottom: 16px; }
|
|
323
|
+
.info-badge { background: #eff6ff; color: #2563eb; padding: 8px 12px; border-radius: 6px; font-size: 13px; margin-bottom: 16px; display: inline-block; }
|
|
324
|
+
|
|
325
|
+
/* Form */
|
|
326
|
+
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 20px; }
|
|
327
|
+
.form-field { display: flex; flex-direction: column; gap: 4px; }
|
|
328
|
+
.form-field.full { grid-column: 1 / -1; }
|
|
329
|
+
.form-field label { font-size: 12px; font-weight: 600; color: var(--text-secondary); }
|
|
330
|
+
.input { padding: 8px 12px; border: 1px solid rgba(0,0,0,0.06); border-radius: 6px; font-size: 14px; background: rgba(255,255,255,0.25); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); }
|
|
331
|
+
.input:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 2px rgba(59,130,246,0.15); }
|
|
332
|
+
.input--sm { padding: 4px 8px; font-size: 12px; }
|
|
333
|
+
|
|
334
|
+
/* Members */
|
|
335
|
+
.member-list { display: flex; flex-direction: column; gap: 8px; margin-bottom: 20px; }
|
|
336
|
+
.member-card { border: 1px solid rgba(0,0,0,0.06); border-radius: 8px; padding: 12px; background: rgba(255,255,255,0.25); backdrop-filter: blur(40px) saturate(1.8); -webkit-backdrop-filter: blur(40px) saturate(1.8); }
|
|
337
|
+
.member-card.checked { border-color: #3b82f6; background: rgba(59,130,246,0.06); }
|
|
338
|
+
.member-header { display: flex; align-items: center; }
|
|
339
|
+
.member-check { display: flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; font-weight: 500; }
|
|
340
|
+
.absence-section { margin-top: 8px; padding-top: 8px; border-top: 1px solid rgba(0,0,0,0.04); }
|
|
341
|
+
.absence-tags { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 6px; }
|
|
342
|
+
.absence-tag { background: #fef3c7; color: #92400e; padding: 2px 8px; border-radius: 4px; font-size: 11px; }
|
|
343
|
+
.absence-input-row { display: flex; gap: 6px; align-items: center; }
|
|
344
|
+
|
|
345
|
+
/* Velocity */
|
|
346
|
+
.velocity-summary { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 20px; }
|
|
347
|
+
.velocity-card { background: rgba(255,255,255,0.25); backdrop-filter: blur(40px) saturate(1.8); -webkit-backdrop-filter: blur(40px) saturate(1.8); border: 1px solid rgba(255,255,255,0.45); box-shadow: 0 2px 12px rgba(0,0,0,0.03), inset 0 1px 0 rgba(255,255,255,0.7); border-radius: 8px; padding: 16px; text-align: center; }
|
|
348
|
+
.velocity-label { font-size: 12px; color: var(--text-secondary); font-weight: 600; }
|
|
349
|
+
.velocity-value { font-size: 28px; font-weight: 800; color: var(--text-primary); margin: 4px 0; }
|
|
350
|
+
.velocity-value.over { color: #dc2626; }
|
|
351
|
+
.velocity-sub { font-size: 11px; color: var(--text-muted); }
|
|
352
|
+
.velocity-warn { font-size: 12px; color: #dc2626; font-weight: 600; }
|
|
353
|
+
|
|
354
|
+
.wd-grid { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 20px; }
|
|
355
|
+
.wd-chip { background: rgba(0,0,0,0.04); padding: 4px 10px; border-radius: 4px; font-size: 12px; }
|
|
356
|
+
|
|
357
|
+
/* Stories */
|
|
358
|
+
.story-list { display: flex; flex-direction: column; gap: 4px; margin-bottom: 20px; }
|
|
359
|
+
.story-card { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border: 1px solid rgba(0,0,0,0.06); border-radius: 6px; cursor: pointer; font-size: 13px; background: rgba(255,255,255,0.25); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); }
|
|
360
|
+
.story-card.selected { background: rgba(59,130,246,0.08); border-color: #3b82f6; }
|
|
361
|
+
.story-card.assigned { background: rgba(34,197,94,0.06); border-color: rgba(34,197,94,0.2); cursor: default; }
|
|
362
|
+
.story-title { flex: 1; }
|
|
363
|
+
.story-sp { font-weight: 600; min-width: 4rem; text-align: right; }
|
|
364
|
+
|
|
365
|
+
.assigned-stories { margin-bottom: 20px; }
|
|
366
|
+
.assigned-stories h3 { font-size: 14px; color: var(--text-secondary); margin-bottom: 8px; }
|
|
367
|
+
|
|
368
|
+
/* Buttons */
|
|
369
|
+
.btn { padding: 8px 16px; border-radius: 6px; border: none; font-size: 14px; font-weight: 600; cursor: pointer; }
|
|
370
|
+
.btn--sm { padding: 4px 8px; font-size: 12px; }
|
|
371
|
+
.btn--lg { padding: 12px 24px; font-size: 16px; width: 100%; }
|
|
372
|
+
.btn--primary { background: #3b82f6; color: white; }
|
|
373
|
+
.btn--primary:hover { background: #2563eb; }
|
|
374
|
+
.btn--primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
375
|
+
|
|
376
|
+
.done-section { text-align: center; padding: 48px 0; }
|
|
377
|
+
.error-msg { color: #dc2626; background: #fef2f2; padding: 8px 12px; border-radius: 6px; font-size: 13px; margin-bottom: 16px; }
|
|
378
|
+
.empty { color: var(--text-muted); padding: 16px; text-align: center; }
|
|
379
|
+
.retro-actions-info { background: rgba(59,130,246,0.06); border: 1px solid rgba(59,130,246,0.15); border-radius: 12px; padding: 16px; margin-bottom: 16px; }
|
|
380
|
+
.retro-actions-info h3 { font-size: 13px; font-weight: 600; color: var(--text-secondary); margin-bottom: 8px; }
|
|
381
|
+
.retro-action-item { font-size: 13px; padding: 4px 0; display: flex; justify-content: space-between; }
|
|
382
|
+
.retro-assignee { font-size: 11px; color: var(--text-muted); }
|
|
383
|
+
|
|
384
|
+
@media (max-width: 640px) {
|
|
385
|
+
.form-grid { grid-template-columns: 1fr; }
|
|
386
|
+
.velocity-summary { grid-template-columns: 1fr; }
|
|
387
|
+
.steps { flex-wrap: wrap; gap: 4px; }
|
|
388
|
+
}
|
|
389
|
+
</style>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineProps<{
|
|
3
|
+
label: string
|
|
4
|
+
type?: 'status' | 'priority' | 'area'
|
|
5
|
+
value: string
|
|
6
|
+
}>()
|
|
7
|
+
|
|
8
|
+
const statusColors: Record<string, string> = {
|
|
9
|
+
'draft': '#94a3b8',
|
|
10
|
+
'backlog': '#a78bfa',
|
|
11
|
+
'ready': '#3b82f6',
|
|
12
|
+
'in-progress': '#f59e0b',
|
|
13
|
+
'review': '#8b5cf6',
|
|
14
|
+
'done': '#22c55e',
|
|
15
|
+
'todo': '#94a3b8',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const priorityColors: Record<string, string> = {
|
|
19
|
+
'low': '#94a3b8',
|
|
20
|
+
'medium': '#3b82f6',
|
|
21
|
+
'high': '#f59e0b',
|
|
22
|
+
'critical': '#ef4444',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getColor(type: string, value: string): string {
|
|
26
|
+
if (type === 'priority') return priorityColors[value] ?? '#94a3b8'
|
|
27
|
+
return statusColors[value] ?? '#94a3b8'
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<span
|
|
33
|
+
class="status-badge"
|
|
34
|
+
:style="{
|
|
35
|
+
'--badge-color': getColor(type ?? 'status', value),
|
|
36
|
+
}"
|
|
37
|
+
>{{ label }}</span>
|
|
38
|
+
</template>
|
|
39
|
+
|
|
40
|
+
<style scoped>
|
|
41
|
+
.status-badge {
|
|
42
|
+
display: inline-block;
|
|
43
|
+
padding: 2px 8px;
|
|
44
|
+
border-radius: 10px;
|
|
45
|
+
font-size: 10px;
|
|
46
|
+
font-weight: 600;
|
|
47
|
+
background: color-mix(in srgb, var(--badge-color) 15%, transparent);
|
|
48
|
+
color: var(--badge-color);
|
|
49
|
+
white-space: nowrap;
|
|
50
|
+
line-height: 1.6;
|
|
51
|
+
}
|
|
52
|
+
</style>
|