- Add admin middleware protecting /admin page (ROLE_ADMIN check) - Fix useAvatarService to use useApi() with FormData detection - Create extractIdFromIri() utility, replace manual IRI parsing - Remove redundant Nitro devProxy (Vite proxy handles dev) Tickets: T-014, T-015, T-017, T-021 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
85 lines
2.7 KiB
Vue
85 lines
2.7 KiB
Vue
<template>
|
|
<div>
|
|
<div class="sticky top-8 z-20 bg-white pb-4 sm:top-12">
|
|
<h1 class="text-xl font-bold text-primary-500 sm:text-2xl">{{ $t('portal.projects') }}</h1>
|
|
</div>
|
|
|
|
<div v-if="isLoading" class="py-8 text-center text-sm text-neutral-400">
|
|
{{ $t('common.loading') }}
|
|
</div>
|
|
|
|
<div v-else-if="projects.length === 0" class="py-8 text-center text-sm text-neutral-400">
|
|
{{ $t('portal.noProjects') }}
|
|
</div>
|
|
|
|
<div v-else class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
<NuxtLink
|
|
v-for="project in projects"
|
|
:key="project.id"
|
|
:to="`/portal/projects/${project.id}`"
|
|
class="rounded-lg border border-neutral-200 bg-white p-5 shadow-sm transition hover:shadow-md"
|
|
>
|
|
<h3 class="text-lg font-bold text-neutral-900">{{ project.name }}</h3>
|
|
<p class="mt-2 text-sm text-neutral-500">
|
|
{{ ticketCountByProject[project.id] ?? 0 }} {{ $t('portal.openTickets') }}
|
|
</p>
|
|
</NuxtLink>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { Project } from '~/services/dto/project'
|
|
import type { ClientTicket } from '~/services/dto/client-ticket'
|
|
import { useClientTicketService } from '~/services/client-tickets'
|
|
import { useProjectService } from '~/services/projects'
|
|
|
|
definePageMeta({
|
|
layout: 'portal',
|
|
})
|
|
|
|
const { t } = useI18n()
|
|
useHead({ title: t('portal.title') })
|
|
|
|
const auth = useAuthStore()
|
|
const clientTicketService = useClientTicketService()
|
|
const projectService = useProjectService()
|
|
|
|
const projects = ref<Project[]>([])
|
|
const tickets = ref<ClientTicket[]>([])
|
|
const isLoading = ref(true)
|
|
|
|
const ticketCountByProject = computed(() => {
|
|
const counts: Record<number, number> = {}
|
|
for (const ticket of tickets.value) {
|
|
if (ticket.status === 'new' || ticket.status === 'in_progress') {
|
|
const projectId = extractIdFromIri(ticket.project)
|
|
if (projectId) {
|
|
counts[projectId] = (counts[projectId] ?? 0) + 1
|
|
}
|
|
}
|
|
}
|
|
return counts
|
|
})
|
|
|
|
async function loadData() {
|
|
isLoading.value = true
|
|
try {
|
|
if (auth.user?.roles?.includes('ROLE_ADMIN')) {
|
|
projects.value = await projectService.getAll({ archived: false })
|
|
} else {
|
|
// allowedProjects are embedded objects from /api/me (with me:read group)
|
|
projects.value = (auth.user?.allowedProjects ?? []) as Project[]
|
|
}
|
|
|
|
tickets.value = await clientTicketService.getAll()
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadData()
|
|
})
|
|
</script>
|