203 lines
6.6 KiB
Vue
203 lines
6.6 KiB
Vue
<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>
|