From 1c0cfbb411727e025e16eb558a863bed0b9a8806 Mon Sep 17 00:00:00 2001 From: "an.nechaev" Date: Fri, 19 Jun 2026 18:29:46 +0400 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D0=B5=20=D0=BF?= =?UTF-8?q?=D0=BE=20Lead=20Time?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/SprintTable.vue | 73 +++++++++++++++++++++++-- frontend/src/types.ts | 5 ++ src/Features/Reports/ReportsApi.php | 42 ++++++++++---- 3 files changed, 104 insertions(+), 16 deletions(-) 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 );