Files
ulitka-app/src/views/HomeView.vue
T
2026-09-29 13:20:30 +03:00

203 lines
6.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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 DashboardCountCard from '@/components/DashboardCountCard.vue'
import LabelText from '@/components/LabelText.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, currentProjectId } = 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 },
)
watch(
currentProjectId,
(id) => {
void issueStore.getStat(id)
},
{ 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')
}
function onClickCard(status?: string) {
router.push({ path: '/issues', query: { status } })
}
</script>
<template>
<div>
<div v-if="session.data" class="d-flex flex-column ga-2">
<div class="dashboard_grid">
<dashboard-count-card label="Всего задач" :value="stats?.total ?? 0" @click="onClickCard()" />
<dashboard-count-card label="Открыто" :value="statCount('OPEN')" @click="onClickCard('OPEN')" />
<dashboard-count-card label="В работе" :value="statCount('IN_PROGRESS')" @click="onClickCard('IN_PROGRESS')" />
<dashboard-count-card label="Просрочено" :value="overdueCount" :alert="overdueCount > 0" />
</div>
<v-row density="compact">
<v-col cols="12" md="8">
<v-card border>
<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 #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 border>
<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" @click="openProject(project)">
<label-text :label="project.key" :text="project.name" />
</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;
}
.dashboard_grid {
display: grid;
gap: 0.5rem;
grid-template-columns: repeat(4, 1fr);
}
</style>