feat(portal) : replace ticket list with kanban board

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-15 20:41:23 +01:00
parent 2a874046d3
commit d2f6d84d03

View File

@@ -30,34 +30,46 @@
{{ $t('clientTicket.noTickets') }}
</div>
<div v-else class="mt-4 space-y-3">
<!-- Kanban board -->
<div v-else class="mt-4 flex flex-col gap-4 sm:flex-row sm:overflow-x-auto sm:pb-4">
<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)"
v-for="col in columns"
:key="col.status"
class="min-w-0 flex-1 sm:min-w-[280px]"
>
<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 class="mb-3 flex items-center gap-2">
<div class="h-2 w-2 rounded-full" :class="col.dotClass" />
<h3 class="text-sm font-bold text-neutral-700">{{ col.label }}</h3>
<span class="ml-auto rounded-full bg-neutral-100 px-2 py-0.5 text-xs font-semibold text-neutral-500">
{{ col.tickets.length }}
</span>
</div>
<div class="space-y-2">
<div
v-for="ticket in col.tickets"
:key="ticket.id"
class="cursor-pointer rounded-lg border border-neutral-200 bg-white p-3 shadow-sm transition hover:shadow-md"
@click="openDetail(ticket)"
>
<div class="flex items-center gap-2">
<span class="text-xs 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.5 text-sm font-semibold text-neutral-900 leading-snug">{{ ticket.title }}</h4>
<p class="mt-1.5 text-xs text-neutral-400">{{ formatDate(ticket.createdAt) }}</p>
</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
v-if="col.tickets.length === 0"
class="py-4 text-center text-xs text-neutral-400"
>
{{ $t('clientTicket.noTickets') }}
</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>
@@ -65,6 +77,7 @@
<ClientTicketDetailModal
v-model="detailOpen"
:ticket="selectedTicket"
@refresh="loadTickets"
/>
</div>
</template>
@@ -101,7 +114,26 @@ const projectName = computed(() => {
})
const isClient = computed(() => auth.user?.roles?.includes('ROLE_CLIENT') ?? false)
const { typeBadgeClass, statusBadgeClass, formatDate } = useClientTicketHelpers()
const { typeBadgeClass, formatDate } = useClientTicketHelpers()
const statuses = ['new', 'in_progress', 'done', 'rejected'] as const
function statusDotClass(status: string): string {
switch (status) {
case 'new': return 'bg-blue-500'
case 'in_progress': return 'bg-yellow-500'
case 'done': return 'bg-green-500'
case 'rejected': return 'bg-red-500'
default: return 'bg-neutral-400'
}
}
const columns = computed(() => statuses.map(status => ({
status,
label: t(`clientTicket.status.${status}`),
dotClass: statusDotClass(status),
tickets: tickets.value.filter(tk => tk.status === status),
})))
function openDetail(ticket: ClientTicket) {
selectedTicket.value = ticket