<script setup lang="ts">
import { computed, onMounted, ref, watch } from "vue";
import { useRoute } from "vue-router";
import { useI18n } from "vue-i18n";
import {
GnBadge,
GnButton,
GnCopyButton,
GnEmptyState,
GnPageHeader,
GnPagination,
GnSelect,
GnTable,
GnToolbar,
useToast,
} from "gnexus-ui-kit/vue";
import type { ProjectDetail, ReportListItem } from "@ltt/shared";
import * as api from "../api";
const { t } = useI18n();
const toast = useToast();
const route = useRoute();
const token = computed(() => String(route.params.token));
const project = ref<ProjectDetail | null>(null);
const reports = ref<ReportListItem[]>([]);
const total = ref(0);
const loading = ref(true);
const notFound = ref(false);
const page = ref(1);
const perPage = 20;
const filterType = ref<string | null>(null);
const filterStatus = ref<string | null>(null);
const typeOptions = [
{ value: null, label: t("reports.allTypes") },
{ value: "element_note", label: t("reports.type.element_note") },
{ value: "recording", label: t("reports.type.recording") },
];
const statusOptions = [
{ value: null, label: t("reports.allStatuses") },
{ value: "open", label: t("reports.status.open") },
{ value: "fixed", label: t("reports.status.fixed") },
{ value: "wont_fix", label: t("reports.status.wont_fix") },
];
const columns = [
{ key: "title", label: t("reports.columns.title") },
{ key: "type", label: t("reports.columns.type") },
{ key: "status", label: t("reports.columns.status") },
{ key: "author", label: t("reports.columns.author") },
{ key: "created", label: t("reports.columns.created") },
];
function fmtDate(iso: string) {
return new Date(iso).toLocaleString();
}
async function load() {
loading.value = true;
notFound.value = false;
try {
project.value = await api.getProjectByToken(token.value);
const list = await api.listProjectReports(project.value.id, {
limit: perPage,
offset: (page.value - 1) * perPage,
report_type: filterType.value,
report_status: filterStatus.value,
});
reports.value = list.items;
total.value = list.total;
} catch (e) {
if (e instanceof Error && e.message === "not-found") {
notFound.value = true;
} else {
toast.error({ title: t("common.error") });
}
} finally {
loading.value = false;
}
}
watch([page, filterType, filterStatus], load);
watch(token, () => {
page.value = 1;
load();
});
onMounted(load);
function statusVariant(status: string) {
return status === "open" ? "warning" : status === "fixed" ? "success" : "secondary";
}
</script>
<template>
<div class="project-page">
<div v-if="notFound" class="project-page-notfound">
<GnEmptyState icon="ph-link-break" :title="t('reports.notFound')" :text="t('reports.notFoundHint')" />
</div>
<template v-else-if="project">
<GnPageHeader :title="project.name" :subtitle="project.description ?? undefined">
<template #actions>
<GnCopyButton :text="api.projectShareUrl(project)" :label="t('projects.share')" />
</template>
</GnPageHeader>
<GnToolbar
:title="t('reports.title')"
:meta="`${project.report_counts.total} total / ${project.report_counts.open} open`"
>
<template #actions>
<GnSelect v-model="filterType" :options="typeOptions" class="filter-select" />
<GnSelect v-model="filterStatus" :options="statusOptions" class="filter-select" />
</template>
</GnToolbar>
<GnEmptyState
v-if="!loading && reports.length === 0"
icon="ph-bug"
:title="t('reports.empty')"
:text="t('reports.emptyHint')"
/>
<GnTable
v-else-if="reports.length"
:columns="columns"
:rows="reports"
:caption="t('reports.title')"
>
<template #cell-title="{ row }">
<RouterLink :to="`/r/${row.share_token}`" class="report-link">{{ row.title }}</RouterLink>
</template>
<template #cell-type="{ row }">
<GnBadge variant="accent" outline>{{ t(`reports.type.${row.type}`) }}</GnBadge>
</template>
<template #cell-status="{ row }">
<GnBadge :variant="statusVariant(row.status)">{{ t(`reports.status.${row.status}`) }}</GnBadge>
</template>
<template #cell-author="{ row }">{{ row.author.nickname }}</template>
<template #cell-created="{ row }">{{ fmtDate(row.created_at) }}</template>
</GnTable>
<GnPagination
v-if="total > perPage"
:page="page"
:total-pages="Math.ceil(total / perPage)"
@update:page="page = $event"
/>
</template>
<p v-else class="project-page-loading">{{ t("common.loading") }}</p>
</div>
</template>
<style scoped>
.project-page {
max-width: 1100px;
margin: 0 auto;
padding: 20px 24px 60px;
display: flex;
flex-direction: column;
gap: 16px;
}
.filter-select {
min-width: 160px;
}
.report-link {
color: inherit;
text-decoration: none;
font-weight: 600;
}
.report-link:hover {
color: var(--color-accent, #7aa2f7);
}
.project-page-loading {
opacity: 0.6;
}
.project-page-notfound {
padding-top: 15vh;
}
</style>