diff --git a/frontend/src/components/SprintTable.vue b/frontend/src/components/SprintTable.vue index 9cdbfa0..e534902 100644 --- a/frontend/src/components/SprintTable.vue +++ b/frontend/src/components/SprintTable.vue @@ -33,6 +33,22 @@ function formatCompletion(value: number | null): string { return `${value.toFixed(1)}%` } +function formatLeadTimeInt(value: number | null): string { + if (value === null) { + return '—' + } + + return String(value) +} + +function formatLeadTimePercentile(value: number | null): string { + if (value === null) { + return '—' + } + + return value.toFixed(1) +} + const sortedSprints = computed(() => [...sprints.value].sort( (left, right) => @@ -76,8 +92,20 @@ watch( - - + + + + + + + + + + + + + + @@ -100,6 +128,11 @@ watch( {{ formatCompletion(sprint.sprint_completion_percentage) }} + + + + +
СпринтВыполнениеСпринтВыполнениеLead Time
ГраницыПерсентиль
maxmin958050
{{ formatLeadTimeInt(sprint.lead_time_max) }}{{ formatLeadTimeInt(sprint.lead_time_min) }}{{ formatLeadTimePercentile(sprint.lead_time_p95) }}{{ formatLeadTimePercentile(sprint.lead_time_p80) }}{{ formatLeadTimePercentile(sprint.lead_time_p50) }}
@@ -131,16 +164,44 @@ watch( .sprint-table__table td { padding: 14px 18px; text-align: left; - border-bottom: 1px solid rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.08); background: transparent; } .sprint-table__table th { - color: rgba(241, 244, 247, 0.5); - font-size: 0.8rem; - font-weight: 600; + color: #f1f4f7; + font-size: 0.8125rem; + font-weight: 700; + text-align: center; + vertical-align: middle; +} + +.sprint-table__th-sprint, +.sprint-table__th-completion { + text-align: left; text-transform: uppercase; letter-spacing: 0.04em; + color: rgba(241, 244, 247, 0.5); + font-size: 0.8rem; +} + +.sprint-table__th-group { + font-size: 0.875rem; +} + +.sprint-table__th-subgroup { + font-size: 0.8125rem; +} + +.sprint-table__th-metric { + font-size: 0.8125rem; + font-weight: 700; +} + +.sprint-table__metric { + text-align: center; + font-variant-numeric: tabular-nums; + color: #f1f4f7; } .sprint-table__table tbody tr:last-child td { diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 001ea6a..bb8683f 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -10,4 +10,9 @@ export interface Sprint { sprint_activate_datetime: string sprint_complete_datetime: string sprint_completion_percentage: number | null + lead_time_max: number | null + lead_time_min: number | null + lead_time_p95: number | null + lead_time_p80: number | null + lead_time_p50: number | null } diff --git a/src/Features/Reports/ReportsApi.php b/src/Features/Reports/ReportsApi.php index 8f61c8a..9875bb8 100644 --- a/src/Features/Reports/ReportsApi.php +++ b/src/Features/Reports/ReportsApi.php @@ -36,20 +36,37 @@ final class ReportsApi { * sprint_name: string, * sprint_activate_datetime: string, * sprint_complete_datetime: string, - * sprint_completion_percentage: float|null + * sprint_completion_percentage: float|null, + * lead_time_max: int|null, + * lead_time_min: int|null, + * lead_time_p95: float|null, + * lead_time_p80: float|null, + * lead_time_p50: float|null * }> */ public function getSprintsByTeamId(int $teamId): array { $rows = $this->db->getRowset( - 'SELECT id, - jira_sprint_id, - sprint_name, - sprint_activate_datetime, - sprint_complete_datetime, - sprint_completion_percentage - FROM sprints - WHERE team_id = :team_id - ORDER BY sprint_activate_datetime ASC', + 'SELECT s.id, + s.jira_sprint_id, + s.sprint_name, + s.sprint_activate_datetime, + s.sprint_complete_datetime, + s.sprint_completion_percentage, + MAX(t.lead_time_days) AS lead_time_max, + MIN(t.lead_time_days) AS lead_time_min, + percentile_cont(0.95) WITHIN GROUP (ORDER BY t.lead_time_days) AS lead_time_p95, + percentile_cont(0.80) WITHIN GROUP (ORDER BY t.lead_time_days) AS lead_time_p80, + percentile_cont(0.50) WITHIN GROUP (ORDER BY t.lead_time_days) AS lead_time_p50 + FROM sprints s + LEFT JOIN tasks t ON t.sprint_id = s.id AND t.lead_time_days IS NOT NULL + WHERE s.team_id = :team_id + GROUP BY s.id, + s.jira_sprint_id, + s.sprint_name, + s.sprint_activate_datetime, + s.sprint_complete_datetime, + s.sprint_completion_percentage + ORDER BY s.sprint_activate_datetime ASC', ['team_id' => $teamId] ); @@ -63,6 +80,11 @@ final class ReportsApi { 'sprint_completion_percentage' => $row['sprint_completion_percentage'] !== null ? (float) $row['sprint_completion_percentage'] : null, + 'lead_time_max' => $row['lead_time_max'] !== null ? (int) $row['lead_time_max'] : null, + 'lead_time_min' => $row['lead_time_min'] !== null ? (int) $row['lead_time_min'] : null, + 'lead_time_p95' => $row['lead_time_p95'] !== null ? (float) $row['lead_time_p95'] : null, + 'lead_time_p80' => $row['lead_time_p80'] !== null ? (float) $row['lead_time_p80'] : null, + 'lead_time_p50' => $row['lead_time_p50'] !== null ? (float) $row['lead_time_p50'] : null, ], $rows );