Добавлен график по закрытию спринтов и изменена сортировка
parent
8c246f86eb
commit
6fa3483845
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"chart.js": "^4.5.1",
|
||||
"vue": "^3.5.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -169,6 +173,7 @@ watch(
|
|||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
Loading…
Reference in New Issue