initial commit. Cloned timetracker repository
This commit is contained in:
250
bo/src/views/RepoChartView.vue
Normal file
250
bo/src/views/RepoChartView.vue
Normal file
@@ -0,0 +1,250 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted, type Ref } from 'vue'
|
||||
import { Bar } from 'vue-chartjs'
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
BarElement,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
} from 'chart.js'
|
||||
import { getRepos, getYears, getQuarters, getIssues, type QuarterData, type IssueTimeSummary } from '@/composable/useRepoApi'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { i18n } from '@/plugins/i18n'
|
||||
|
||||
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale)
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const repos = ref<number[]>([])
|
||||
const years = ref<number[]>([])
|
||||
const quarters = ref<QuarterData[]>([])
|
||||
const issues = ref<IssueTimeSummary[]>([])
|
||||
|
||||
const selectedRepo = ref<number | null>(null)
|
||||
const selectedYear = ref<number | null>(null)
|
||||
const selectedQuarter = ref<string | null>(null)
|
||||
|
||||
const page = ref(1)
|
||||
const totalItems = ref(0)
|
||||
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
async function loadData<T>(fetchFn: () => Promise<any>, target: Ref<T[]>, errorMsg: string) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const response = await fetchFn()
|
||||
target.value = response.items || response || []
|
||||
} catch (e: any) {
|
||||
error.value = e?.message || errorMsg
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => loadData(() => getRepos(), repos, i18n.t('repo_chart.failed_to_load_repositories')))
|
||||
|
||||
watch(selectedRepo, async (newRepo) => {
|
||||
selectedYear.value = null
|
||||
selectedQuarter.value = null
|
||||
quarters.value = []
|
||||
issues.value = []
|
||||
if (newRepo) {
|
||||
await loadData(() => getYears(newRepo), years, i18n.t('repo_chart.failed_to_load_years'))
|
||||
}
|
||||
})
|
||||
|
||||
watch(selectedYear, async (newYear) => {
|
||||
selectedQuarter.value = null
|
||||
issues.value = []
|
||||
if (newYear && selectedRepo.value) {
|
||||
await loadData(() => getQuarters(selectedRepo.value!, newYear), quarters, i18n.t('repo_chart.failed_to_load_quarters'))
|
||||
}
|
||||
})
|
||||
|
||||
watch(selectedQuarter, async (newQuarter) => {
|
||||
if (newQuarter && selectedRepo.value && selectedYear.value) {
|
||||
await loadIssues(selectedRepo.value, selectedYear.value, newQuarter)
|
||||
} else {
|
||||
issues.value = []
|
||||
}
|
||||
})
|
||||
|
||||
watch(page, () => {
|
||||
if (selectedRepo.value && selectedYear.value && selectedQuarter.value) {
|
||||
loadIssues(selectedRepo.value, selectedYear.value, selectedQuarter.value)
|
||||
}
|
||||
})
|
||||
|
||||
async function loadIssues(repoID: number, year: number, quarterStr: string) {
|
||||
const quarterPart = quarterStr.split('_Q')[1]
|
||||
if (!quarterPart) return
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const response = await getIssues(repoID, year, parseInt(quarterPart), page.value, 50)
|
||||
issues.value = response.items || []
|
||||
totalItems.value = response.items_count || 0
|
||||
} catch (e: any) {
|
||||
error.value = e?.message || i18n.t('repo_chart.failed_to_load_issues')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const chartData = computed(() => ({
|
||||
labels: quarters.value.map((q) => q.quarter),
|
||||
datasets: [
|
||||
{
|
||||
label: i18n.t('repo_chart.hours_worked'),
|
||||
backgroundColor: '#3b82f6',
|
||||
data: quarters.value.map((q) => q.time),
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
const chartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { position: 'top' as const },
|
||||
title: { display: true, text: i18n.t('repo_chart.work_by_quarter') },
|
||||
},
|
||||
scales: {
|
||||
y: { beginAtZero: true, title: { display: true, text: i18n.t('repo_chart.hours') } },
|
||||
},
|
||||
}
|
||||
|
||||
const hasData = computed(() => quarters.value.length > 0)
|
||||
const hasIssues = computed(() => issues.value.length > 0)
|
||||
|
||||
const items = computed(() => repos.value.map(r => ({ value: r, label: `Repo ${r}` })))
|
||||
|
||||
const yearItems = computed(() => [
|
||||
{ value: null, label: i18n.t('repo_chart.select_a_year') },
|
||||
...years.value.map(y => ({ value: y, label: String(y) }))
|
||||
])
|
||||
|
||||
const quarterItems = computed(() => [
|
||||
{ value: null, label: i18n.t('repo_chart.all_quarters') },
|
||||
...quarters.value.map(q => ({
|
||||
value: q.quarter,
|
||||
label: `${q.quarter} (${q.time.toFixed(1)}h)`
|
||||
}))
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="p-6 bg-white dark:bg-(--black) min-h-screen font-sans">
|
||||
<h1 class="text-2xl font-bold mb-6 text-black">{{ $t('repo_chart.repository_work_chart') }}</h1>
|
||||
|
||||
<div v-if="error" class="mb-4 p-3 bg-red-100 text-red-700 rounded">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="mb-4 p-3 bg-blue-100 text-blue-700 rounded">
|
||||
{{ $t('repo_chart.loading') }}...
|
||||
</div>
|
||||
|
||||
<div v-if="!authStore.isAuthenticated" class="mb-4 p-3 bg-yellow-100 text-yellow-700 rounded">
|
||||
<!-- Please log in to view repository work charts. -->
|
||||
{{ $t('repo_chart.login_to_view_charts') }}
|
||||
</div>
|
||||
|
||||
<div v-if="authStore.isAuthenticated" class="flex flex-wrap gap-4 mb-6">
|
||||
<div class="flex flex-col min-w-[192px]">
|
||||
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ $t('repo_chart.repository')
|
||||
}}</label>
|
||||
<USelect v-model="selectedRepo" :items="items" :disabled="loading"
|
||||
:placeholder="$t('repo_chart.select_a_repository')" class="dark:text-white text-black "/>
|
||||
<!-- Select a repository -->
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col min-w-[160px]">
|
||||
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ $t('repo_chart.year')
|
||||
}}</label>
|
||||
<USelect v-model="selectedYear" :items="yearItems"
|
||||
:disabled="loading || !selectedRepo || years.length === 0"
|
||||
:placeholder="$t('repo_chart.select_a_year')" class="dark:text-white text-black "/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col min-w-[192px]">
|
||||
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ $t('repo_chart.quarter')
|
||||
}}</label>
|
||||
<USelect v-model="selectedQuarter" :items="quarterItems"
|
||||
:disabled="loading || !selectedYear || quarters.length === 0"
|
||||
:placeholder="$t('repo_chart.all_quarters')" class="dark:text-white text-black "/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hasData && authStore.isAuthenticated"
|
||||
class="mb-6 p-4 border border-(--border-light) dark:border-(--border-dark) rounded">
|
||||
<h2 class="text-xl font-medium mb-4 text-black dark:text-white">{{
|
||||
$t('repo_chart.work_done_by_quarter') }}</h2>
|
||||
<div class="h-80">
|
||||
<Bar :data="chartData" :options="chartOptions" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hasIssues && authStore.isAuthenticated"
|
||||
class="p-4 border border-(--border-light) dark:border-(--border-dark) rounded">
|
||||
<h2 class="text-xl font-medium mb-4 text-black dark:text-white">{{ $t('repo_chart.issues_for') }} {{
|
||||
selectedQuarter }}
|
||||
</h2>
|
||||
|
||||
<table class="w-full border border-(--border-light) dark:border-(--border-dark)">
|
||||
<thead>
|
||||
<tr class="bg-gray-100 dark:bg-(--black)">
|
||||
<th
|
||||
class="p-3 text-left text-xs font-bold text-gray-600 dark:text-white uppercase border border-(--border-light) dark:border-(--border-dark)">
|
||||
ID</th>
|
||||
<th
|
||||
class="p-3 text-left text-xs font-bold text-gray-600 dark:text-white uppercase border border-(--border-light) dark:border-(--border-dark)">
|
||||
{{ $t('repo_chart.issue_name') }}</th>
|
||||
<th
|
||||
class="p-3 text-left text-xs font-bold text-gray-600 dark:text-white uppercase border border-(--border-light) dark:border-(--border-dark)">
|
||||
{{ $t('repo_chart.user_initials') }}</th>
|
||||
<th
|
||||
class="p-3 text-left text-xs font-bold text-gray-600 dark:text-white uppercase border border-(--border-light) dark:border-(--border-dark)">
|
||||
{{ $t('repo_chart.created_on') }}</th>
|
||||
<th
|
||||
class="p-3 text-left text-xs font-bold text-gray-600 dark:text-white uppercase border border-(--border-light) dark:border-(--border-dark)">
|
||||
{{ $t('repo_chart.hours_spent') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="issue in issues" :key="issue.IssueID"
|
||||
class=" border-b border-(--border-light) dark:border-(--border-dark)">
|
||||
<td class="p-3 text-black dark:text-white">{{ issue.IssueID }}</td>
|
||||
<td class="p-3 text-black dark:text-white">{{ issue.IssueName }}</td>
|
||||
<td class="p-3 text-black dark:text-white">{{ issue.Initials }}</td>
|
||||
<td class="p-3 text-black dark:text-white">{{ issue.CreatedDate }}</td>
|
||||
<td class="p-3 text-black dark:text-white">{{ issue.TotalHoursSpent }}h</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="pt-4 flex justify-center items-center dark:text-white!">
|
||||
<UPagination v-model:page="page" :total="totalItems" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="selectedQuarter && !loading && authStore.isAuthenticated && !hasIssues"
|
||||
class="mt-4 p-3 dark:bg-(--black) bg-white border border-(--border-light) dark:border-(--border-dark) dark:text-white text-black rounded">
|
||||
{{ $t('validate_error.no_issues_for_quarter') }}.
|
||||
</div>
|
||||
|
||||
<div v-else-if="!loading && authStore.isAuthenticated" class="p-3 dark:bg-(--black) bg-white border border-(--border-light) dark:border-(--border-dark) rounded">
|
||||
<span v-if="!selectedRepo">{{ $t('repo_chart.select_repo_to_view_data') }}</span>
|
||||
<span v-else-if="!selectedYear">{{ $t('repo_chart.select_year_to_view_data') }}</span>
|
||||
<span v-else-if="!selectedQuarter">{{ $t('repo_chart.select_quarter_to_view_issues') }}</span>
|
||||
<span v-else-if="quarters.length === 0">{{ $t('repo_chart.no_work_data_available') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user