dashboard
This commit is contained in:
1 parent
9c65fc05f4
commit
baf0fa186e
3 files changed
+235
-3
No files matched your search
@@ -12,6 +12,7 @@ export interface Issue {
|
||||
reporterId: string
|
||||
assignee?: User
|
||||
reporter?: User
|
||||
dueDate: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
<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 #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"></style>
|
||||
<style scoped lang="scss">
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
.dash_grid {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user