Добавлен график по закрытию спринтов и изменена сортировка

master
an.nechaev 2026-06-19 18:57:22 +04:00
parent 8c246f86eb
commit 6fa3483845
6 changed files with 196 additions and 5 deletions

View File

@ -8,6 +8,7 @@
"name": "reports-frontend",
"version": "0.0.0",
"dependencies": {
"chart.js": "^4.5.1",
"vue": "^3.5.13"
},
"devDependencies": {
@ -511,6 +512,12 @@
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"license": "MIT"
},
"node_modules/@kurkle/color": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
"license": "MIT"
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.62.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.1.tgz",
@ -1110,6 +1117,18 @@
"balanced-match": "^1.0.0"
}
},
"node_modules/chart.js": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
"license": "MIT",
"dependencies": {
"@kurkle/color": "^0.3.0"
},
"engines": {
"pnpm": ">=8"
}
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",

View File

@ -9,6 +9,7 @@
"preview": "vite preview"
},
"dependencies": {
"chart.js": "^4.5.1",
"vue": "^3.5.13"
},
"devDependencies": {

View File

@ -54,7 +54,7 @@ const selectedTeam = computed(
<h2 class="main__title">
{{ selectedTeam?.name ?? 'Сводка по спринтам' }}
</h2>
<p class="main__subtitle">Спринты отсортированы по дате начала</p>
<p class="main__subtitle">Сначала — самые последние спринты</p>
</header>
<SprintTable v-if="selectedTeamId !== null" :team-id="selectedTeamId" />

View File

@ -0,0 +1,166 @@
<script setup lang="ts">
import {
CategoryScale,
Chart,
Filler,
Legend,
LineController,
LineElement,
LinearScale,
PointElement,
Tooltip,
} from 'chart.js'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import type { Sprint } from '../types'
Chart.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
LineController,
Tooltip,
Legend,
Filler,
)
const props = defineProps<{
sprints: Sprint[]
}>()
const canvasRef = ref<HTMLCanvasElement | null>(null)
let chart: Chart | null = null
const chartSprints = computed(() =>
[...props.sprints].sort(
(left, right) =>
new Date(left.sprint_activate_datetime).getTime() -
new Date(right.sprint_activate_datetime).getTime(),
),
)
function shortSprintLabel(name: string): string {
const match = name.match(/(\d{4}_\d+)/)
return match?.[1] ?? name
}
function renderChart() {
if (!canvasRef.value) {
return
}
chart?.destroy()
const labels = chartSprints.value.map((sprint) => shortSprintLabel(sprint.sprint_name))
const data = chartSprints.value.map((sprint) => sprint.sprint_completion_percentage)
chart = new Chart(canvasRef.value, {
type: 'line',
data: {
labels,
datasets: [
{
label: 'Выполнение спринта',
data,
borderColor: '#29a5ff',
backgroundColor: 'rgba(41, 165, 255, 0.12)',
pointBackgroundColor: '#29a5ff',
pointBorderColor: '#29a5ff',
pointRadius: 4,
pointHoverRadius: 6,
borderWidth: 2,
fill: true,
tension: 0.3,
spanGaps: true,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
tooltip: {
backgroundColor: '#1e2226',
titleColor: '#f1f4f7',
bodyColor: '#f1f4f7',
borderColor: 'rgba(255, 255, 255, 0.08)',
borderWidth: 1,
callbacks: {
title: (items) => {
const index = items[0]?.dataIndex ?? 0
return chartSprints.value[index]?.sprint_name ?? ''
},
label: (item) => {
if (item.parsed.y === null) {
return 'Нет данных'
}
return `Выполнение: ${item.parsed.y.toFixed(1)}%`
},
},
},
},
scales: {
x: {
ticks: {
color: 'rgba(241, 244, 247, 0.55)',
maxRotation: 45,
minRotation: 0,
},
grid: {
color: 'rgba(255, 255, 255, 0.06)',
},
},
y: {
min: 0,
max: 100,
ticks: {
color: 'rgba(241, 244, 247, 0.55)',
callback: (value) => `${value}%`,
},
grid: {
color: 'rgba(255, 255, 255, 0.06)',
},
},
},
},
})
}
onMounted(renderChart)
watch(chartSprints, renderChart, { deep: true })
onUnmounted(() => {
chart?.destroy()
})
</script>
<template>
<div class="completion-chart">
<h3 class="completion-chart__title">Процент закрытия спринтов</h3>
<div class="completion-chart__canvas-wrap">
<canvas ref="canvasRef" />
</div>
</div>
</template>
<style scoped>
.completion-chart {
margin-bottom: 28px;
}
.completion-chart__title {
margin: 0 0 12px;
font-size: 0.875rem;
font-weight: 600;
color: rgba(241, 244, 247, 0.7);
}
.completion-chart__canvas-wrap {
height: 240px;
}
</style>

View File

@ -2,6 +2,7 @@
import { computed, ref, watch } from 'vue'
import { fetchTeamSprints } from '../api'
import type { Sprint } from '../types'
import SprintCompletionChart from './SprintCompletionChart.vue'
const props = defineProps<{
teamId: number
@ -60,8 +61,8 @@ function formatEfficiency(value: number | null): string {
const sortedSprints = computed(() =>
[...sprints.value].sort(
(left, right) =>
new Date(left.sprint_activate_datetime).getTime() -
new Date(right.sprint_activate_datetime).getTime(),
new Date(right.sprint_activate_datetime).getTime() -
new Date(left.sprint_activate_datetime).getTime(),
),
)
@ -96,7 +97,10 @@ watch(
Для этой команды пока нет данных по спринтам
</div>
<div v-else class="sprint-table__wrapper">
<div v-else>
<SprintCompletionChart :sprints="sprints" />
<div class="sprint-table__wrapper">
<table class="sprint-table__table">
<thead>
<tr>
@ -171,6 +175,7 @@ watch(
</table>
</div>
</div>
</div>
</template>
<style scoped>

View File

@ -1 +1 @@
{"root":["./src/api.ts","./src/main.ts","./src/types.ts","./src/vite-env.d.ts","./src/app.vue","./src/components/sprinttable.vue","./src/components/teamsidebar.vue"],"version":"5.7.3"}
{"root":["./src/api.ts","./src/main.ts","./src/types.ts","./src/vite-env.d.ts","./src/app.vue","./src/components/sprintcompletionchart.vue","./src/components/sprinttable.vue","./src/components/teamsidebar.vue"],"version":"5.7.3"}