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') }} {{ $t('clientTicket.noTickets') }}
</div> </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 <div
v-for="ticket in tickets" v-for="col in columns"
:key="ticket.id" :key="col.status"
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" class="min-w-0 flex-1 sm:min-w-[280px]"
@click="openDetail(ticket)"
> >
<div class="min-w-0 flex-1"> <div class="mb-3 flex items-center gap-2">
<div class="flex items-center gap-2"> <div class="h-2 w-2 rounded-full" :class="col.dotClass" />
<span class="text-sm font-bold text-primary-500">CT-{{ String(ticket.number).padStart(3, '0') }}</span> <h3 class="text-sm font-bold text-neutral-700">{{ col.label }}</h3>
<span <span class="ml-auto rounded-full bg-neutral-100 px-2 py-0.5 text-xs font-semibold text-neutral-500">
class="rounded-full px-2 py-0.5 text-xs font-semibold text-white" {{ col.tickets.length }}
:class="typeBadgeClass(ticket.type)" </span>
> </div>
{{ $t(`clientTicket.type.${ticket.type}`) }} <div class="space-y-2">
</span> <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> </div>
<h4 class="mt-1 text-sm font-semibold text-neutral-900">{{ ticket.title }}</h4> <p
<p class="mt-1 text-xs text-neutral-400"> v-if="col.tickets.length === 0"
{{ formatDate(ticket.createdAt) }} class="py-4 text-center text-xs text-neutral-400"
>
{{ $t('clientTicket.noTickets') }}
</p> </p>
</div> </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>
</div> </div>
@@ -65,6 +77,7 @@
<ClientTicketDetailModal <ClientTicketDetailModal
v-model="detailOpen" v-model="detailOpen"
:ticket="selectedTicket" :ticket="selectedTicket"
@refresh="loadTickets"
/> />
</div> </div>
</template> </template>
@@ -101,7 +114,26 @@ const projectName = computed(() => {
}) })
const isClient = computed(() => auth.user?.roles?.includes('ROLE_CLIENT') ?? false) 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) { function openDetail(ticket: ClientTicket) {
selectedTicket.value = ticket selectedTicket.value = ticket