212 lines
8.8 KiB
Vue
212 lines
8.8 KiB
Vue
<script setup lang="ts">
|
|
import { Head, Link } from '@inertiajs/vue3';
|
|
import {
|
|
ArrowUpRight,
|
|
FolderGit2,
|
|
ListTodo,
|
|
OctagonAlert,
|
|
} from 'lucide-vue-next';
|
|
import { computed } from 'vue';
|
|
import FeedbackState from '@/components/FeedbackState.vue';
|
|
import { TicketCard } from '@/components/tickets';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { useLocalization } from '@/composables/useLocalization';
|
|
import AppLayout from '@/layouts/AppLayout.vue';
|
|
import { dashboard } from '@/routes/index';
|
|
import { index as projectsIndex, show as showProject } from '@/routes/projects';
|
|
import type { BreadcrumbItem, DashboardPageProps } from '@/types';
|
|
|
|
const props = defineProps<DashboardPageProps>();
|
|
const { t } = useLocalization();
|
|
const breadcrumbs = computed<BreadcrumbItem[]>(() => [
|
|
{ title: t('navigation.dashboard'), href: dashboard() },
|
|
]);
|
|
const assignedTickets = computed(() =>
|
|
props.assignedTicketsByProject.flatMap((group) => group.tickets),
|
|
);
|
|
const summaryItems = computed(() => [
|
|
{
|
|
label: t('dashboard.summary.assignedTickets'),
|
|
value: assignedTickets.value.length,
|
|
icon: ListTodo,
|
|
},
|
|
{
|
|
label: t('dashboard.summary.projects'),
|
|
value: props.assignedTicketsByProject.length,
|
|
icon: FolderGit2,
|
|
},
|
|
{
|
|
label: t('dashboard.summary.blockedTickets'),
|
|
value: assignedTickets.value.filter((ticket) => ticket.is_blocked)
|
|
.length,
|
|
icon: OctagonAlert,
|
|
},
|
|
]);
|
|
</script>
|
|
|
|
<template>
|
|
<AppLayout :breadcrumbs="breadcrumbs">
|
|
<Head :title="t('navigation.dashboard')" />
|
|
|
|
<div class="gap-page md:p-page flex min-w-0 flex-1 flex-col p-4">
|
|
<header class="gap-stack grid min-w-0">
|
|
<p class="text-label text-muted-foreground uppercase">
|
|
{{ t('dashboard.eyebrow') }}
|
|
</p>
|
|
<h1 class="text-headline-mobile md:text-headline break-words">
|
|
{{
|
|
t('dashboard.greeting', {
|
|
name: $page.props.auth.user.name,
|
|
})
|
|
}}
|
|
</h1>
|
|
<p class="text-muted-foreground max-w-2xl">
|
|
{{ t('dashboard.description') }}
|
|
</p>
|
|
</header>
|
|
|
|
<section :aria-label="t('dashboard.summary')">
|
|
<ul class="grid gap-3 sm:grid-cols-3">
|
|
<li
|
|
v-for="item in summaryItems"
|
|
:key="item.label"
|
|
class="min-w-0"
|
|
>
|
|
<Card class="h-full gap-0 rounded-md py-0">
|
|
<CardContent
|
|
class="flex min-w-0 items-center justify-between gap-4 p-4"
|
|
>
|
|
<div class="min-w-0">
|
|
<p
|
|
class="text-caption text-muted-foreground break-words"
|
|
>
|
|
{{ item.label }}
|
|
</p>
|
|
<p class="text-title mt-1 tabular-nums">
|
|
{{ item.value }}
|
|
</p>
|
|
</div>
|
|
<span
|
|
class="bg-accent text-primary flex size-10 shrink-0 items-center justify-center rounded-md"
|
|
>
|
|
<component
|
|
:is="item.icon"
|
|
class="size-5"
|
|
aria-hidden="true"
|
|
/>
|
|
</span>
|
|
</CardContent>
|
|
</Card>
|
|
</li>
|
|
</ul>
|
|
</section>
|
|
|
|
<section
|
|
aria-labelledby="assigned-work-title"
|
|
class="gap-column grid min-w-0"
|
|
>
|
|
<header class="gap-stack grid min-w-0">
|
|
<h2 id="assigned-work-title" class="text-title">
|
|
{{ t('dashboard.assignedWork') }}
|
|
</h2>
|
|
<p class="text-muted-foreground max-w-2xl">
|
|
{{ t('dashboard.assignedWorkDescription') }}
|
|
</p>
|
|
</header>
|
|
|
|
<FeedbackState
|
|
v-if="assignedTicketsByProject.length === 0"
|
|
state="empty"
|
|
:title="t('dashboard.empty.title')"
|
|
:description="t('dashboard.empty.description')"
|
|
>
|
|
<Button as-child variant="outline">
|
|
<Link :href="projectsIndex()">
|
|
<FolderGit2 aria-hidden="true" />
|
|
{{ t('dashboard.empty.action') }}
|
|
</Link>
|
|
</Button>
|
|
</FeedbackState>
|
|
|
|
<div v-else class="gap-page grid min-w-0">
|
|
<section
|
|
v-for="group in assignedTicketsByProject"
|
|
:key="group.project.id"
|
|
:aria-labelledby="`dashboard-project-${group.project.id}`"
|
|
class="gap-column grid min-w-0"
|
|
>
|
|
<header
|
|
class="flex min-w-0 flex-col gap-3 border-b pb-3 sm:flex-row sm:items-center sm:justify-between"
|
|
>
|
|
<div class="flex min-w-0 items-center gap-3">
|
|
<Badge
|
|
variant="outline"
|
|
class="max-w-full shrink-0 whitespace-normal break-all"
|
|
>
|
|
{{ group.project.key }}
|
|
</Badge>
|
|
<div class="min-w-0">
|
|
<h3
|
|
:id="`dashboard-project-${group.project.id}`"
|
|
class="text-body break-words font-semibold"
|
|
>
|
|
{{ group.project.name }}
|
|
</h3>
|
|
<p
|
|
class="text-caption text-muted-foreground"
|
|
>
|
|
{{
|
|
t('tickets.count', {
|
|
count: group.tickets.length,
|
|
})
|
|
}}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
as-child
|
|
variant="link"
|
|
class="self-start sm:self-auto"
|
|
>
|
|
<Link
|
|
:href="showProject(group.project.id)"
|
|
:aria-label="
|
|
t('dashboard.openProject', {
|
|
name: group.project.name,
|
|
})
|
|
"
|
|
>
|
|
{{ t('dashboard.openProjectAction') }}
|
|
<ArrowUpRight aria-hidden="true" />
|
|
</Link>
|
|
</Button>
|
|
</header>
|
|
|
|
<ul
|
|
:aria-label="
|
|
t('dashboard.projectTickets', {
|
|
name: group.project.name,
|
|
})
|
|
"
|
|
class="gap-column grid min-w-0 md:grid-cols-2 2xl:grid-cols-3"
|
|
>
|
|
<li
|
|
v-for="ticket in group.tickets"
|
|
:key="ticket.id"
|
|
class="min-w-0"
|
|
>
|
|
<TicketCard
|
|
:project-id="group.project.id"
|
|
:ticket="ticket"
|
|
/>
|
|
</li>
|
|
</ul>
|
|
</section>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</AppLayout>
|
|
</template>
|