feat(auth) : redirect client users to /portal after login and extract ticket helpers composable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-15 19:42:53 +01:00
parent 0d21e59023
commit 68dd9599a9
3 changed files with 152 additions and 1 deletions

View File

@@ -0,0 +1,29 @@
export function useClientTicketHelpers() {
function typeBadgeClass(type: string): string {
switch (type) {
case 'bug': return 'bg-red-500'
case 'improvement': return 'bg-blue-500'
default: return 'bg-neutral-500'
}
}
function statusBadgeClass(status: string): string {
switch (status) {
case 'new': return 'bg-blue-100 text-blue-700'
case 'in_progress': return 'bg-yellow-100 text-yellow-700'
case 'done': return 'bg-green-100 text-green-700'
case 'rejected': return 'bg-red-100 text-red-700'
default: return 'bg-neutral-100 text-neutral-700'
}
}
function formatDate(iso: string): string {
return new Date(iso).toLocaleDateString('fr-FR', {
day: 'numeric',
month: 'short',
year: 'numeric',
})
}
return { typeBadgeClass, statusBadgeClass, formatDate }
}

View File

@@ -63,7 +63,8 @@ const handleSubmit = async () => {
try {
await auth.login(username.value, password.value)
await router.push('/')
const isClient = auth.user?.roles?.includes('ROLE_CLIENT') ?? false
await router.push(isClient ? '/portal' : '/')
} finally {
isSubmitting.value = false
}

View File

@@ -0,0 +1,121 @@
<template>
<div>
<div class="sticky top-8 z-20 bg-white pb-4 sm:top-12">
<div class="flex items-center justify-between gap-3">
<div class="min-w-0">
<NuxtLink
to="/portal"
class="text-sm text-neutral-400 hover:text-primary-500"
>
{{ $t('portal.backToProject') }}
</NuxtLink>
<h1 class="mt-1 text-xl font-bold text-primary-500 sm:text-2xl">{{ projectName }}</h1>
</div>
<NuxtLink
:to="`/portal/projects/${projectId}/new-ticket`"
class="shrink-0 rounded-md bg-primary-500 px-3 py-2 text-xs font-semibold text-white hover:bg-secondary-500 sm:px-4 sm:text-sm"
>
<span class="hidden sm:inline">+ {{ $t('portal.newTicket') }}</span>
<span class="sm:hidden">+ Ticket</span>
</NuxtLink>
</div>
</div>
<div v-if="isLoading" class="py-8 text-center text-sm text-neutral-400">
{{ $t('common.loading') }}
</div>
<div v-else-if="tickets.length === 0" class="py-8 text-center text-sm text-neutral-400">
{{ $t('clientTicket.noTickets') }}
</div>
<div v-else class="mt-4 space-y-3">
<div
v-for="ticket in tickets"
:key="ticket.id"
class="flex cursor-pointer items-center justify-between gap-3 rounded-lg border border-neutral-200 bg-white p-4 shadow-sm transition hover:shadow-md"
@click="openDetail(ticket)"
>
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2">
<span class="text-sm font-bold text-primary-500">CT-{{ String(ticket.number).padStart(3, '0') }}</span>
<span
class="rounded-full px-2 py-0.5 text-xs font-semibold text-white"
:class="typeBadgeClass(ticket.type)"
>
{{ $t(`clientTicket.type.${ticket.type}`) }}
</span>
</div>
<h4 class="mt-1 text-sm font-semibold text-neutral-900">{{ ticket.title }}</h4>
<p class="mt-1 text-xs text-neutral-400">
{{ formatDate(ticket.createdAt) }}
</p>
</div>
<span
class="shrink-0 rounded-full px-3 py-1 text-xs font-semibold"
:class="statusBadgeClass(ticket.status)"
>
{{ $t(`clientTicket.status.${ticket.status}`) }}
</span>
</div>
</div>
<!-- Ticket detail modal -->
<ClientTicketDetailModal
v-model="detailOpen"
:ticket="selectedTicket"
/>
</div>
</template>
<script setup lang="ts">
import type { ClientTicket } from '~/services/dto/client-ticket'
import { useClientTicketService } from '~/services/client-tickets'
definePageMeta({
layout: 'portal',
})
const route = useRoute()
const { t } = useI18n()
const projectId = computed(() => Number(route.params.id))
useHead({ title: t('portal.title') })
const clientTicketService = useClientTicketService()
const auth = useAuthStore()
const tickets = ref<ClientTicket[]>([])
const isLoading = ref(true)
const detailOpen = ref(false)
const selectedTicket = ref<ClientTicket | null>(null)
const projectName = computed(() => {
const me = auth.user as any
if (me?.allowedProjects) {
const project = me.allowedProjects.find((p: any) => p.id === projectId.value)
return project?.name ?? ''
}
return ''
})
const { typeBadgeClass, statusBadgeClass, formatDate } = useClientTicketHelpers()
function openDetail(ticket: ClientTicket) {
selectedTicket.value = ticket
detailOpen.value = true
}
async function loadTickets() {
isLoading.value = true
try {
tickets.value = await clientTicketService.getAll({ project: projectId.value })
} finally {
isLoading.value = false
}
}
onMounted(() => {
loadTickets()
})
</script>