dashboard

This commit is contained in:
vadim committed 2026-09-28 09:09:23 +03:00
1 parent 9c65fc05f4
commit baf0fa186e
3 files changed
+235 -3

No files matched your search

+1
View File
@@ -12,6 +12,7 @@ export interface Issue {
reporterId: string reporterId: string
assignee?: User assignee?: User
reporter?: User reporter?: User
dueDate: string | null
createdAt: string createdAt: string
updatedAt: string updatedAt: string
} }
+20
View File
@@ -0,0 +1,20 @@
<script setup lang="ts">
interface Props {
label: string
value: number | string
alert?: boolean
}
defineProps<Props>()
</script>
<template>
<v-card :border="!alert" :variant="alert ? 'flat' : undefined" :color="alert ? 'error' : undefined">
<v-card-text>
<div class="text-label-large" :class="{ 'text-medium-emphasis': !alert }">{{ label }}</div>
<div class="text-headline-large">{{ value }}</div>
</v-card-text>
</v-card>
</template>
<style scoped lang="scss"></style>
+214 -3
View File
@@ -1,7 +1,218 @@
<script setup lang="ts"></script> <script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { useRouter } from 'vue-router'
import { useIssueStore } from '@/stores/issue.ts'
import { useProjectStore } from '@/stores/project.ts'
import { useStatusStore } from '@/stores/status.ts'
import { authClient } from '@/lib/auth-client.ts'
import type { Issue } from '@/api/issue.api.ts'
import type { Project } from '@/api/project.api.ts'
import DashboardCard from '@/components/DashboardCard.vue'
const ACTIVE_STATUSES: Issue['status'][] = ['OPEN', 'IN_PROGRESS']
const PRIORITY_ICONS: Record<number, string> = {
4: 'mdi-chevron-double-up',
3: 'mdi-chevron-up',
2: 'mdi-equal',
1: 'mdi-chevron-down',
0: 'mdi-chevron-double-down',
}
const router = useRouter()
const session = authClient.useSession()
const issueStore = useIssueStore()
const projectStore = useProjectStore()
const statusStore = useStatusStore()
const { stats } = storeToRefs(issueStore)
const { projects } = storeToRefs(projectStore)
const currentUserId = computed(() => session.value.data?.user.id ?? null)
const myIssues = ref<Issue[]>([])
const loadingMyIssues = ref(false)
async function loadMyIssues(userId: string) {
loadingMyIssues.value = true
try {
const res = await issueStore.list({ page: 1, limit: 50, assigneeId: userId })
myIssues.value = res.list.filter((issue) => ACTIVE_STATUSES.includes(issue.status))
} finally {
loadingMyIssues.value = false
}
}
watch(
currentUserId,
(id) => {
if (id) void loadMyIssues(id)
else myIssues.value = []
},
{ immediate: true },
)
function isOverdue(issue: Issue) {
return !!issue.dueDate && new Date(issue.dueDate).getTime() < Date.now()
}
const overdueCount = computed(() => myIssues.value.filter(isOverdue).length)
const sortedMyIssues = computed(() =>
[...myIssues.value].sort((a, b) => {
if (!a.dueDate && !b.dueDate) return 0
if (!a.dueDate) return 1
if (!b.dueDate) return -1
return new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime()
}),
)
function statCount(status: Issue['status']) {
return stats.value?.byStatus.find((el) => el.status === status)?.count ?? 0
}
function statusMeta(status: string) {
const found = statusStore.statuses?.find((el) => el.alias === status)
return { label: found?.name ?? status, color: found?.color ?? 'grey' }
}
function priorityIcon(priority: number) {
return PRIORITY_ICONS[priority] ?? PRIORITY_ICONS[2]
}
function priorityColor(priority: number) {
if (priority >= 3) return 'error'
if (priority === 2) return 'success'
return 'primary'
}
function goToIssue(issue: Issue) {
router.push(`/issues/${issue.key}`)
}
function openProject(project: Project) {
projectStore.select(project)
router.push('/issues')
}
</script>
<template> <template>
<div>Home</div> <div>
<!-- <v-alert v-if="!session.data" type="info" variant="tonal">-->
<!-- <div class="d-flex align-center justify-space-between ga-4">-->
<!-- <div>Войдите, чтобы видеть свои задачи и проекты</div>-->
<!-- <v-btn variant="flat" to="/sign-in">Sign In</v-btn>-->
<!-- </div>-->
<!-- </v-alert>-->
<v-banner
v-if="!session.data"
color="error"
icon="mdi-alert"
text="Войдите, чтобы видеть свои задачи и проекты"
lines="one"
>
<template v-slot:actions>
<v-btn variant="flat" to="/sign-in">Sign In</v-btn>
</template>
</v-banner>
<div v-else class="d-flex flex-column ga-2">
<div class="dash_grid">
<dashboard-card label="Всего задач" :value="stats?.total ?? 0" />
<dashboard-card label="Открыто" :value="statCount('OPEN')" />
<dashboard-card label="В работе" :value="statCount('IN_PROGRESS')" />
<dashboard-card label="Просрочено" :value="overdueCount" :alert="overdueCount > 0" />
</div>
<v-row density="compact">
<v-col cols="12" md="8">
<v-card>
<v-card-title class="d-flex align-center justify-space-between">
<span>Мои задачи</span>
<v-btn size="small" variant="text" to="/issues">Все задачи</v-btn>
</v-card-title>
<v-divider />
<v-list v-if="sortedMyIssues.length" density="compact">
<v-list-item
v-for="issue in sortedMyIssues"
:key="issue.id"
class="cursor-pointer"
@click="goToIssue(issue)"
>
<template #prepend>
<v-icon :icon="priorityIcon(issue.priority)" :color="priorityColor(issue.priority)" size="small" />
</template> </template>
<style scoped lang="scss"></style> <template #title>
<div class="d-flex ga-2 align-center">
<span class="text-medium-emphasis">{{ issue.key }}</span>
<span>{{ issue.title }}</span>
</div>
</template>
<template #append>
<div class="d-flex ga-2 align-center">
<v-chip
v-if="issue.dueDate"
density="compact"
variant="flat"
:color="isOverdue(issue) ? 'error' : undefined"
>
{{ new Date(issue.dueDate).toLocaleDateString('ru-RU') }}
</v-chip>
<v-chip density="compact" variant="flat" :color="statusMeta(issue.status).color">
{{ statusMeta(issue.status).label }}
</v-chip>
</div>
</template>
</v-list-item>
</v-list>
<v-card-text v-else class="text-medium-emphasis">
{{ loadingMyIssues ? 'Загрузка…' : 'Нет активных задач' }}
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="4">
<v-card>
<v-card-title class="d-flex align-center justify-space-between">
<span>Проекты</span>
<v-btn size="small" variant="text" to="/settings/projects">Все проекты</v-btn>
</v-card-title>
<v-divider />
<v-list v-if="projects.length" density="compact">
<v-list-item
v-for="project in projects.slice(0, 6)"
:key="project.id"
class="cursor-pointer"
@click="openProject(project)"
>
<template #prepend>
<v-chip size="small" color="primary" variant="flat" density="compact">{{ project.key }}</v-chip>
</template>
<template #title>
<span class="ml-2">{{ project.name }}</span>
</template>
</v-list-item>
</v-list>
<v-card-text v-else class="text-medium-emphasis">Нет проектов</v-card-text>
</v-card>
</v-col>
</v-row>
</div>
</div>
</template>
<style scoped lang="scss">
.cursor-pointer {
cursor: pointer;
}
.dash_grid {
display: grid;
gap: 0.5rem;
grid-template-columns: repeat(4, 1fr);
}
</style>