Compare commits
7
Commits
4c6f21ef78
...
3157946186
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3157946186 | ||
|
|
67d30a6f32 | ||
|
|
3082794436 | ||
|
|
6f651eeaeb | ||
|
|
baf0fa186e | ||
|
|
9c65fc05f4 | ||
|
|
1b42c0d916 |
No files matched your search
+7
-3
@@ -1,13 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useEventSource } from '@vueuse/core'
|
import { useEventSource } from '@vueuse/core'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
import { computed, onMounted, watch } from 'vue'
|
import { computed, onMounted, watch } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { useStatusStore } from '@/stores/status.ts'
|
import { useStatusStore } from '@/stores/status.ts'
|
||||||
import { useIssueStore } from '@/stores/issue.ts'
|
import { useIssueStore } from '@/stores/issue.ts'
|
||||||
|
import { useProjectStore } from '@/stores/project.ts'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const issueStore = useIssueStore()
|
const issueStore = useIssueStore()
|
||||||
const statusStore = useStatusStore()
|
const statusStore = useStatusStore()
|
||||||
|
const projectStore = useProjectStore()
|
||||||
|
const { currentProjectId } = storeToRefs(projectStore)
|
||||||
|
|
||||||
interface IssueSseEvent {
|
interface IssueSseEvent {
|
||||||
type: string
|
type: string
|
||||||
@@ -25,7 +29,7 @@ const { data } = useEventSource<string[], IssueSseEvent>(
|
|||||||
console.log('Failed to connect EventSource after 3 retries')
|
console.log('Failed to connect EventSource after 3 retries')
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
autoConnect: true,
|
autoConnect: false,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,7 +39,7 @@ watch(data, () => {
|
|||||||
data.value?.type === 'issue.updated' ||
|
data.value?.type === 'issue.updated' ||
|
||||||
data.value?.type === 'issue.deleted'
|
data.value?.type === 'issue.deleted'
|
||||||
) {
|
) {
|
||||||
issueStore.getStat()
|
issueStore.getStat(currentProjectId.value)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -46,7 +50,7 @@ const layout = computed(() => {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await statusStore.list()
|
await statusStore.list()
|
||||||
await issueStore.getStat()
|
await issueStore.getStat(currentProjectId.value)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -83,6 +84,7 @@ export async function itemDelete(id: number) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function itemStat() {
|
export async function itemStat(projectId?: string) {
|
||||||
return apiClient.get<IssueStat>('/api/issues/stats')
|
const query = toQuery({ projectId: projectId })
|
||||||
|
return apiClient.get<IssueStat>(`/api/issues/stats${query}`)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
interface Props {
|
||||||
|
label: string
|
||||||
|
value: number | string
|
||||||
|
alert?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const { label, value, alert } = defineProps<Props>()
|
||||||
|
const emit = defineEmits<{ click: [] }>()
|
||||||
|
|
||||||
|
function onClick() {
|
||||||
|
emit('click')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-card :border="!alert" variant="flat" :color="alert ? 'error' : undefined" @click="onClick">
|
||||||
|
<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>
|
||||||
@@ -10,18 +10,19 @@ const emit = defineEmits<{ created: [issue: Issue] }>()
|
|||||||
|
|
||||||
const showForm = ref(false)
|
const showForm = ref(false)
|
||||||
const newIssue = reactive({
|
const newIssue = reactive({
|
||||||
key: 'type',
|
key: '',
|
||||||
title: '',
|
title: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
const onCreate = async () => {
|
const onCreate = async () => {
|
||||||
const projectId = projectStore.current?.id
|
const project = projectStore.current
|
||||||
if (!projectId) return
|
if (!project) return
|
||||||
|
|
||||||
const created = await issueStore.create({ ...newIssue, projectId })
|
const created = await issueStore.create({ ...newIssue, projectId: project.id, key: project.key })
|
||||||
if (!created) return
|
if (!created) return
|
||||||
|
|
||||||
emit('created', created)
|
emit('created', created)
|
||||||
|
newIssue.key = ''
|
||||||
newIssue.title = ''
|
newIssue.title = ''
|
||||||
showForm.value = false
|
showForm.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,19 @@
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import AssigneeFilter from '@/components/AssigneeFilter.vue'
|
import AssigneeFilter from '@/components/AssigneeFilter.vue'
|
||||||
import TextFilter from '@/components/TextFilter.vue'
|
import TextFilter from '@/components/TextFilter.vue'
|
||||||
|
import StatusFilter from '@/components/StatusFilter.vue'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
text?: string | null
|
text?: string | null
|
||||||
assigneeId?: string | null
|
assigneeId?: string | null
|
||||||
|
status?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), { text: null, assigneeId: null })
|
const props = withDefaults(defineProps<Props>(), { text: null, assigneeId: null })
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:text': [value: string | null]
|
'update:text': [value: string | null]
|
||||||
'update:assigneeId': [value: string | null]
|
'update:assigneeId': [value: string | null]
|
||||||
|
'update:status': [value: string | null]
|
||||||
'update:clear': []
|
'update:clear': []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
@@ -20,11 +23,16 @@ const selectedAssignees = computed(() => (props.assigneeId ? [props.assigneeId]
|
|||||||
function onAssigneeChanged(value: string[]) {
|
function onAssigneeChanged(value: string[]) {
|
||||||
emit('update:assigneeId', value.at(-1) ?? null)
|
emit('update:assigneeId', value.at(-1) ?? null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onStatusChanged(alias: string) {
|
||||||
|
emit('update:status', alias)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="filter-grid ga-4 align-center">
|
<div class="filter-grid ga-4 align-center">
|
||||||
<text-filter :model-value="text" @update:model-value="emit('update:text', $event)" />
|
<text-filter :model-value="text" @update:model-value="emit('update:text', $event)" />
|
||||||
|
<status-filter :status="status" @changed="onStatusChanged" />
|
||||||
<assignee-filter :selected="selectedAssignees" @changed="onAssigneeChanged" />
|
<assignee-filter :selected="selectedAssignees" @changed="onAssigneeChanged" />
|
||||||
<v-btn prepend-icon="mdi-close" variant="tonal" @click="emit('update:clear')">Clear Filters</v-btn>
|
<v-btn prepend-icon="mdi-close" variant="tonal" @click="emit('update:clear')">Clear Filters</v-btn>
|
||||||
</div>
|
</div>
|
||||||
@@ -33,6 +41,6 @@ function onAssigneeChanged(value: string[]) {
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.filter-grid {
|
.filter-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto auto;
|
grid-template-columns: 1fr auto auto auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -46,10 +46,9 @@ const onAssigneeChanged = async (assignee: string) => {
|
|||||||
v-if="[3, 4].includes(issue.priority) && ['OPEN', 'IN_PROGRESS'].includes(issue.status)"
|
v-if="[3, 4].includes(issue.priority) && ['OPEN', 'IN_PROGRESS'].includes(issue.status)"
|
||||||
icon="mdi-alert-box"
|
icon="mdi-alert-box"
|
||||||
color="error"
|
color="error"
|
||||||
></v-icon>
|
/>
|
||||||
<div :class="{ 'text-decoration-line-through': issue.status === 'DONE' }">{{ issue.title }}</div>
|
<div :class="{ 'text-decoration-line-through': issue.status === 'DONE' }">{{ issue.title }}</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div :class="{ 'text-decoration-line-through': issue.status === 'DONE' }">{{ issue.content }}</div>-->
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -61,7 +60,6 @@ const onAssigneeChanged = async (assignee: string) => {
|
|||||||
:model-value="selected"
|
:model-value="selected"
|
||||||
@update:model-value="emit('toggleSelect', issue.id)"
|
@update:model-value="emit('toggleSelect', issue.id)"
|
||||||
/>
|
/>
|
||||||
<v-icon icon="mdi-square" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -31,10 +31,17 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="projects.length" class="d-flex align-center ga-2">
|
<div v-if="projects.length" class="d-flex align-center ga-2">
|
||||||
Project:
|
<div>Project:</div>
|
||||||
<v-menu>
|
<v-menu>
|
||||||
<template v-slot:activator="{ props }">
|
<template v-slot:activator="{ props }">
|
||||||
<v-btn border density="comfortable" variant="flat" :text="currentProjectName" v-bind="props" />
|
<v-btn density="comfortable" v-bind="props">
|
||||||
|
<div class="d-flex ga-1 align-center">
|
||||||
|
<v-chip v-if="current" size="small" variant="flat" color="primary" density="compact">
|
||||||
|
{{ current.key }}
|
||||||
|
</v-chip>
|
||||||
|
<div>{{ currentProjectName }}</div>
|
||||||
|
</div>
|
||||||
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<v-list
|
<v-list
|
||||||
@@ -48,9 +55,9 @@ onMounted(async () => {
|
|||||||
@update:selected="select"
|
@update:selected="select"
|
||||||
>
|
>
|
||||||
<v-list-item slim v-for="project in projects" :key="project.id" :value="project.id">
|
<v-list-item slim v-for="project in projects" :key="project.id" :value="project.id">
|
||||||
<v-list-item-title>
|
<v-list-item-title class="d-flex ga-1 align-center">
|
||||||
<v-chip size="small" color="primary" variant="flat" density="compact">{{ project.key }}</v-chip>
|
<v-chip size="small" color="primary" variant="flat" density="compact">{{ project.key }}</v-chip>
|
||||||
{{ project.name }}
|
<div>{{ project.name }}</div>
|
||||||
</v-list-item-title>
|
</v-list-item-title>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useStatusStore } from '@/stores/status.ts'
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
status?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const { status } = defineProps<Props>()
|
||||||
|
const emit = defineEmits<{ changed: [value: string] }>()
|
||||||
|
const statusStore = useStatusStore()
|
||||||
|
const selected = ref(status)
|
||||||
|
const onUpdate = (val: unknown) => emit('changed', val as string)
|
||||||
|
watch(
|
||||||
|
() => status,
|
||||||
|
(val) => {
|
||||||
|
selected.value = val
|
||||||
|
},
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-btn-toggle
|
||||||
|
v-model="selected"
|
||||||
|
divided
|
||||||
|
size="default"
|
||||||
|
density="compact"
|
||||||
|
variant="text"
|
||||||
|
@update:model-value="onUpdate"
|
||||||
|
>
|
||||||
|
<v-btn slim v-for="el in statusStore.statuses" :color="el.color" :value="el.alias" :key="el.alias" rounded="0">
|
||||||
|
<div class="text-default">{{ el.name }}</div>
|
||||||
|
</v-btn>
|
||||||
|
</v-btn-toggle>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
||||||
@@ -25,14 +25,9 @@ const menu = ref([
|
|||||||
value: 2,
|
value: 2,
|
||||||
props: { prependIcon: 'mdi-list-status', to: '/issues', requiresAuth: true },
|
props: { prependIcon: 'mdi-list-status', to: '/issues', requiresAuth: true },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: 'summary',
|
|
||||||
value: 3,
|
|
||||||
props: { prependIcon: 'mdi-view-dashboard', to: '/summary', requiresAuth: true },
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: 'games',
|
title: 'games',
|
||||||
value: 4,
|
value: 3,
|
||||||
props: { prependIcon: 'mdi-view-dashboard', to: '/games', requiresAuth: false },
|
props: { prependIcon: 'mdi-view-dashboard', to: '/games', requiresAuth: false },
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import ProfileView from '@/views/users/ProfileView.vue'
|
import ProfileView from '@/views/users/ProfileView.vue'
|
||||||
import { issuesRoutes } from '@/router/issues.ts'
|
import { issuesRoutes } from '@/router/issues.ts'
|
||||||
import { reportsRoutes } from '@/router/reports.ts'
|
|
||||||
import { summaryRoutes } from '@/router/summary.ts'
|
|
||||||
import HomeView from '@/views/HomeView.vue'
|
import HomeView from '@/views/HomeView.vue'
|
||||||
import VerifyEmailView from '@/views/users/VerifyEmailView.vue'
|
import VerifyEmailView from '@/views/users/VerifyEmailView.vue'
|
||||||
import SignUpView from '@/views/users/SignUpView.vue'
|
import SignUpView from '@/views/users/SignUpView.vue'
|
||||||
@@ -44,8 +42,6 @@ const router = createRouter({
|
|||||||
meta: { requiresAuth: true },
|
meta: { requiresAuth: true },
|
||||||
},
|
},
|
||||||
issuesRoutes,
|
issuesRoutes,
|
||||||
summaryRoutes,
|
|
||||||
reportsRoutes,
|
|
||||||
gamesRoutes,
|
gamesRoutes,
|
||||||
settingsRoutes,
|
settingsRoutes,
|
||||||
],
|
],
|
||||||
|
|||||||
+2
-2
@@ -75,9 +75,9 @@ export const useIssueStore = defineStore('item', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getStat(): Promise<IssueStat | undefined> {
|
async function getStat(projectId?: string | null): Promise<IssueStat | undefined> {
|
||||||
try {
|
try {
|
||||||
stats.value = await itemStat()
|
stats.value = await itemStat(projectId ?? undefined)
|
||||||
|
|
||||||
return stats.value
|
return stats.value
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
+207
-3
@@ -1,7 +1,211 @@
|
|||||||
<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 DashboardCountCard from '@/components/DashboardCountCard.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>
|
<template>
|
||||||
<div>Home</div>
|
<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"
|
||||||
|
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>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
<style scoped lang="scss">
|
||||||
|
.cursor-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.dashboard_grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.5rem;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -27,6 +27,7 @@ const serverItems = ref<Issue[]>([])
|
|||||||
|
|
||||||
const text = ref<string | null>(null)
|
const text = ref<string | null>(null)
|
||||||
const assigneeId = ref<string | null>(null)
|
const assigneeId = ref<string | null>(null)
|
||||||
|
const status = ref<string | null>(null)
|
||||||
const selected = ref<[string] | []>([])
|
const selected = ref<[string] | []>([])
|
||||||
const batch = ref<number[]>([])
|
const batch = ref<number[]>([])
|
||||||
const deleting = ref(false)
|
const deleting = ref(false)
|
||||||
@@ -36,6 +37,7 @@ async function fetch() {
|
|||||||
page: page.value,
|
page: page.value,
|
||||||
limit: itemsPerPage.value,
|
limit: itemsPerPage.value,
|
||||||
text: text.value,
|
text: text.value,
|
||||||
|
status: status.value,
|
||||||
assigneeId: assigneeId.value,
|
assigneeId: assigneeId.value,
|
||||||
projectId: currentProjectId.value,
|
projectId: currentProjectId.value,
|
||||||
})
|
})
|
||||||
@@ -55,6 +57,7 @@ function syncQuery() {
|
|||||||
|
|
||||||
if (text.value) query.text = text.value
|
if (text.value) query.text = text.value
|
||||||
if (assigneeId.value) query.assignee = assigneeId.value
|
if (assigneeId.value) query.assignee = assigneeId.value
|
||||||
|
if (status.value) query.status = status.value
|
||||||
if (selected.value.length) query.selected_issue = selected.value[0]
|
if (selected.value.length) query.selected_issue = selected.value[0]
|
||||||
|
|
||||||
router.replace({ query })
|
router.replace({ query })
|
||||||
@@ -66,6 +69,7 @@ watch(
|
|||||||
page.value = query.page ? Number(query.page) : 1
|
page.value = query.page ? Number(query.page) : 1
|
||||||
text.value = query.text ? String(query.text) : null
|
text.value = query.text ? String(query.text) : null
|
||||||
assigneeId.value = query.assignee ? String(query.assignee) : null
|
assigneeId.value = query.assignee ? String(query.assignee) : null
|
||||||
|
status.value = query.status ? String(query.status) : null
|
||||||
selected.value = query.selected_issue ? [String(query.selected_issue)] : []
|
selected.value = query.selected_issue ? [String(query.selected_issue)] : []
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
@@ -79,9 +83,13 @@ watch(currentProjectId, async () => {
|
|||||||
clearBatch()
|
clearBatch()
|
||||||
close()
|
close()
|
||||||
await load()
|
await load()
|
||||||
|
await issueStore.getStat(currentProjectId.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => await load())
|
onMounted(async () => {
|
||||||
|
await load()
|
||||||
|
await issueStore.getStat(currentProjectId.value)
|
||||||
|
})
|
||||||
const gridTemplate = computed(() => (selected.value.length ? '1fr 1fr' : '1fr'))
|
const gridTemplate = computed(() => (selected.value.length ? '1fr 1fr' : '1fr'))
|
||||||
const goToIssue = (key: string[]) => router.push(`/issues/${key[0]}`)
|
const goToIssue = (key: string[]) => router.push(`/issues/${key[0]}`)
|
||||||
|
|
||||||
@@ -102,9 +110,16 @@ async function onAssigneeChanged(value: string | null) {
|
|||||||
await load()
|
await load()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onStatusChanged(value: string | null) {
|
||||||
|
status.value = value
|
||||||
|
page.value = 1
|
||||||
|
await load()
|
||||||
|
}
|
||||||
|
|
||||||
async function onClear() {
|
async function onClear() {
|
||||||
text.value = null
|
text.value = null
|
||||||
assigneeId.value = null
|
assigneeId.value = null
|
||||||
|
status.value = null
|
||||||
page.value = 1
|
page.value = 1
|
||||||
|
|
||||||
await load()
|
await load()
|
||||||
@@ -175,8 +190,10 @@ async function onDeleteBatch() {
|
|||||||
class="mb-2"
|
class="mb-2"
|
||||||
:text="text"
|
:text="text"
|
||||||
:assignee-id="assigneeId"
|
:assignee-id="assigneeId"
|
||||||
|
:status="status"
|
||||||
@update:text="onTextChanged"
|
@update:text="onTextChanged"
|
||||||
@update:assignee-id="onAssigneeChanged"
|
@update:assignee-id="onAssigneeChanged"
|
||||||
|
@update:status="onStatusChanged"
|
||||||
@update:clear="onClear"
|
@update:clear="onClear"
|
||||||
/>
|
/>
|
||||||
<v-list class="pa-0" density="compact" v-model:selected="selected" mandatory>
|
<v-list class="pa-0" density="compact" v-model:selected="selected" mandatory>
|
||||||
@@ -198,7 +215,10 @@ async function onDeleteBatch() {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #append>
|
<template #append>
|
||||||
<issue-stats />
|
<div class="d-flex ga-4 align-center">
|
||||||
|
<issue-stats />
|
||||||
|
<v-btn density="compact" icon="mdi-dots-horizontal"></v-btn>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user