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:
29
frontend/composables/useClientTicketHelpers.ts
Normal file
29
frontend/composables/useClientTicketHelpers.ts
Normal 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 }
|
||||||
|
}
|
||||||
@@ -63,7 +63,8 @@ const handleSubmit = async () => {
|
|||||||
try {
|
try {
|
||||||
await auth.login(username.value, password.value)
|
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 {
|
} finally {
|
||||||
isSubmitting.value = false
|
isSubmitting.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
121
frontend/pages/portal/projects/[id]/index.vue
Normal file
121
frontend/pages/portal/projects/[id]/index.vue
Normal 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>
|
||||||
Reference in New Issue
Block a user