Добавлен график по закрытию спринтов и изменена сортировка
parent
8c246f86eb
commit
6fa3483845
|
|
@ -8,6 +8,7 @@
|
||||||
"name": "reports-frontend",
|
"name": "reports-frontend",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"chart.js": "^4.5.1",
|
||||||
"vue": "^3.5.13"
|
"vue": "^3.5.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -511,6 +512,12 @@
|
||||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.62.1",
|
"version": "4.62.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.1.tgz",
|
"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"
|
"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": {
|
"node_modules/csstype": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"chart.js": "^4.5.1",
|
||||||
"vue": "^3.5.13"
|
"vue": "^3.5.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ const selectedTeam = computed(
|
||||||
<h2 class="main__title">
|
<h2 class="main__title">
|
||||||
{{ selectedTeam?.name ?? 'Сводка по спринтам' }}
|
{{ selectedTeam?.name ?? 'Сводка по спринтам' }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="main__subtitle">Спринты отсортированы по дате начала</p>
|
<p class="main__subtitle">Сначала — самые последние спринты</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<SprintTable v-if="selectedTeamId !== null" :team-id="selectedTeamId" />
|
<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 { computed, ref, watch } from 'vue'
|
||||||
import { fetchTeamSprints } from '../api'
|
import { fetchTeamSprints } from '../api'
|
||||||
import type { Sprint } from '../types'
|
import type { Sprint } from '../types'
|
||||||
|
import SprintCompletionChart from './SprintCompletionChart.vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
teamId: number
|
teamId: number
|
||||||
|
|
@ -60,8 +61,8 @@ function formatEfficiency(value: number | null): string {
|
||||||
const sortedSprints = computed(() =>
|
const sortedSprints = computed(() =>
|
||||||
[...sprints.value].sort(
|
[...sprints.value].sort(
|
||||||
(left, right) =>
|
(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>
|
||||||
|
|
||||||
<div v-else class="sprint-table__wrapper">
|
<div v-else>
|
||||||
|
<SprintCompletionChart :sprints="sprints" />
|
||||||
|
|
||||||
|
<div class="sprint-table__wrapper">
|
||||||
<table class="sprint-table__table">
|
<table class="sprint-table__table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -171,6 +175,7 @@ watch(
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
||||||
|
|
@ -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