|
29 | 29 | <div class="card-meta"> |
30 | 30 | <span class="id">#{{ element.id?.slice(0, 6) }}</span> |
31 | 31 | <span class="points" v-if="element.storyPoints || element.estimatedHours"> |
32 | | - {{ element.storyPoints || element.estimatedHours }} pts |
| 32 | + {{ parseFloat(element.storyPoints) || parseFloat(element.estimatedHours) || 0 }} pts |
33 | 33 | </span> |
| 34 | + <span class="points no-points" v-else>0 pts</span> |
34 | 35 | </div> |
35 | 36 | </div> |
36 | 37 | </div> |
|
63 | 64 | class="issue-list cycle-list" |
64 | 65 | :animation="200" |
65 | 66 | ghost-class="ghost-card" |
| 67 | + @change="handleCycleChange" |
66 | 68 | > |
67 | 69 | <template #item="{ element }"> |
68 | 70 | <div class="planning-card"> |
|
71 | 73 | <div class="card-meta"> |
72 | 74 | <span class="id">#{{ element.id?.slice(0, 6) }}</span> |
73 | 75 | <span class="points" v-if="element.storyPoints || element.estimatedHours"> |
74 | | - {{ element.storyPoints || element.estimatedHours }} pts |
| 76 | + {{ parseFloat(element.storyPoints) || parseFloat(element.estimatedHours) || 0 }} pts |
75 | 77 | </span> |
| 78 | + <span class="points no-points" v-else>0 pts</span> |
76 | 79 | </div> |
77 | 80 | </div> |
78 | 81 | </div> |
@@ -120,41 +123,96 @@ export default { |
120 | 123 | const cycleIssues = ref([]); |
121 | 124 | const saving = ref(false); |
122 | 125 | const capacity = ref(20); // TODO: Fetch from settings or team capacity |
| 126 | + const updateTrigger = ref(0); // Force reactivity trigger |
123 | 127 |
|
124 | 128 | const totalPoints = computed(() => { |
125 | | - return cycleIssues.value.reduce((sum, issue) => { |
126 | | - return sum + (issue.storyPoints || issue.estimatedHours || 0); |
| 129 | + // Access updateTrigger to force recomputation |
| 130 | + updateTrigger.value; |
| 131 | + |
| 132 | + console.log('[SprintPlanning] Computing total points for', cycleIssues.value.length, 'issues'); |
| 133 | + const total = cycleIssues.value.reduce((sum, issue) => { |
| 134 | + const points = parseFloat(issue.storyPoints) || parseFloat(issue.estimatedHours) || 0; |
| 135 | + console.log('[SprintPlanning] Issue:', issue.title, 'Points:', points, 'storyPoints:', issue.storyPoints, 'estimatedHours:', issue.estimatedHours); |
| 136 | + return sum + points; |
127 | 137 | }, 0); |
| 138 | + console.log('[SprintPlanning] Total calculated:', total); |
| 139 | + return Math.round(total * 10) / 10; // Round to 1 decimal place |
128 | 140 | }); |
129 | | -
|
130 | | - // Fetch data when modal opens |
131 | | - watch(() => props.modelValue, async (val) => { |
132 | | - if (val && props.cycle) { |
133 | | - await fetchData(); |
134 | | - } |
135 | | - }); |
| 141 | + |
| 142 | + const handleCycleChange = (event) => { |
| 143 | + console.log('[SprintPlanning] Cycle changed:', event); |
| 144 | + console.log('[SprintPlanning] Current cycle issues:', cycleIssues.value.length); |
| 145 | + // Force recomputation of totalPoints |
| 146 | + updateTrigger.value++; |
| 147 | + }; |
| 148 | + |
| 149 | + // Watch for changes in cycleIssues |
| 150 | + watch(cycleIssues, (newVal) => { |
| 151 | + console.log('[SprintPlanning] Cycle issues changed:', newVal.length, 'issues'); |
| 152 | + console.log('[SprintPlanning] Issues:', newVal.map(i => ({ title: i.title, points: i.storyPoints || i.estimatedHours }))); |
| 153 | + }, { deep: true }); |
136 | 154 |
|
137 | 155 | const fetchData = async () => { |
138 | 156 | try { |
139 | | - // Fetch backlog issues (status=backlog or no cycle) |
140 | | - // Ideally we filter by no assigned cycle, but backend might not stick strictly. |
141 | | - // For now fetch all candidates. |
142 | | - const allIssues = await DevtelService.listIssues(props.projectId, { status: ['backlog', 'todo'] }); // Adjust filters as needed |
| 157 | + console.log('[SprintPlanning] Fetching issues for project:', props.projectId); |
| 158 | + |
| 159 | + // Fetch all issues from the project |
| 160 | + const response = await DevtelService.listIssues(props.projectId, {}); |
| 161 | + |
| 162 | + console.log('[SprintPlanning] Raw response:', response); |
| 163 | + |
| 164 | + // Handle different response formats |
| 165 | + let allIssues = []; |
| 166 | + if (Array.isArray(response)) { |
| 167 | + allIssues = response; |
| 168 | + } else if (response && response.rows) { |
| 169 | + allIssues = response.rows; |
| 170 | + } else if (response && response.data) { |
| 171 | + allIssues = Array.isArray(response.data) ? response.data : response.data.rows || []; |
| 172 | + } |
| 173 | + |
| 174 | + console.log('[SprintPlanning] All issues:', allIssues); |
| 175 | + console.log('[SprintPlanning] Sample issue:', allIssues[0]); |
143 | 176 | |
144 | 177 | // Separate into those already in this cycle and others |
145 | 178 | if (props.cycle) { |
146 | | - // Assuming issue object has cycleId property |
| 179 | + // Issues already assigned to this cycle |
147 | 180 | cycleIssues.value = allIssues.filter(i => i.cycleId === props.cycle.id); |
148 | | - backlogIssues.value = allIssues.filter(i => i.cycleId !== props.cycle.id); |
| 181 | + // Backlog issues: no cycle assigned or in backlog/todo status |
| 182 | + backlogIssues.value = allIssues.filter(i => |
| 183 | + !i.cycleId && (i.status === 'backlog' || i.status === 'todo') |
| 184 | + ); |
149 | 185 | } else { |
150 | | - backlogIssues.value = allIssues; |
| 186 | + backlogIssues.value = allIssues.filter(i => |
| 187 | + !i.cycleId && (i.status === 'backlog' || i.status === 'todo') |
| 188 | + ); |
151 | 189 | cycleIssues.value = []; |
152 | 190 | } |
| 191 | + |
| 192 | + console.log('[SprintPlanning] Backlog issues:', backlogIssues.value); |
| 193 | + console.log('[SprintPlanning] Cycle issues:', cycleIssues.value); |
153 | 194 | } catch (e) { |
154 | | - ElMessage.error('Failed to load issues for planning'); |
| 195 | + console.error('[SprintPlanning] Failed to load issues:', e); |
| 196 | + ElMessage.error('Failed to load issues for planning: ' + e.message); |
155 | 197 | } |
156 | 198 | }; |
157 | 199 |
|
| 200 | + // Fetch data when modal opens |
| 201 | + watch(() => props.modelValue, async (val) => { |
| 202 | + if (val && props.cycle) { |
| 203 | + console.log('[SprintPlanning] Modal opened for cycle:', props.cycle); |
| 204 | + await fetchData(); |
| 205 | + } |
| 206 | + }, { immediate: true }); |
| 207 | + |
| 208 | + // Also fetch on mount if already open |
| 209 | + onMounted(async () => { |
| 210 | + if (props.modelValue && props.cycle) { |
| 211 | + console.log('[SprintPlanning] Modal mounted, fetching data'); |
| 212 | + await fetchData(); |
| 213 | + } |
| 214 | + }); |
| 215 | +
|
158 | 216 | const handleClose = () => { |
159 | 217 | visible.value = false; |
160 | 218 | }; |
@@ -183,7 +241,8 @@ export default { |
183 | 241 | capacity, |
184 | 242 | totalPoints, |
185 | 243 | handleClose, |
186 | | - savePlan |
| 244 | + savePlan, |
| 245 | + handleCycleChange |
187 | 246 | }; |
188 | 247 | } |
189 | 248 | }; |
@@ -301,6 +360,11 @@ export default { |
301 | 360 | color: var(--el-text-color-secondary); |
302 | 361 | } |
303 | 362 |
|
| 363 | +.card-meta .no-points { |
| 364 | + color: var(--el-text-color-placeholder); |
| 365 | + font-style: italic; |
| 366 | +} |
| 367 | +
|
304 | 368 | .divider { |
305 | 369 | display: flex; |
306 | 370 | align-items: center; |
|
0 commit comments