feat(portal) : replace ticket list with kanban board
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user