feat(avatar) : replace initials with UserAvatar component everywhere

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-15 21:58:46 +01:00
parent e8f0202b15
commit afd4baed92
4 changed files with 34 additions and 30 deletions

View File

@@ -79,7 +79,16 @@
</span> </span>
</td> </td>
<td class="px-3 py-3 text-neutral-600">{{ getProjectName(ticket.project) }}</td> <td class="px-3 py-3 text-neutral-600">{{ getProjectName(ticket.project) }}</td>
<td class="px-3 py-3 text-neutral-600">{{ getSubmitterName(ticket.submittedBy) }}</td> <td class="px-3 py-3 text-neutral-600">
<div class="flex items-center gap-2">
<UserAvatar
v-if="getSubmitterUser(ticket.submittedBy)"
:user="getSubmitterUser(ticket.submittedBy)!"
size="sm"
/>
{{ getSubmitterName(ticket.submittedBy) }}
</div>
</td>
<td class="px-3 py-3 text-neutral-400">{{ formatDate(ticket.createdAt) }}</td> <td class="px-3 py-3 text-neutral-400">{{ formatDate(ticket.createdAt) }}</td>
<td class="px-3 py-3"> <td class="px-3 py-3">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@@ -216,7 +225,7 @@ const { t } = useI18n()
const clientTicketService = useClientTicketService() const clientTicketService = useClientTicketService()
const projectService = useProjectService() const projectService = useProjectService()
const userService = useUserService() const userService = useUserService()
const { typeBadgeClass, statusBadgeClass, formatDate } = useClientTicketHelpers() const { typeBadgeClass, statusBadgeClass, formatDate, getAvailableStatusTransitions } = useClientTicketHelpers()
const tickets = ref<ClientTicket[]>([]) const tickets = ref<ClientTicket[]>([])
const projects = ref<Project[]>([]) const projects = ref<Project[]>([])
@@ -261,19 +270,7 @@ const detailTicket = ref<ClientTicket | null>(null)
const availableStatusTransitions = computed(() => { const availableStatusTransitions = computed(() => {
if (!statusTarget.value) return [] if (!statusTarget.value) return []
const current = statusTarget.value.status return getAvailableStatusTransitions(statusTarget.value.status, t)
const allStatuses: { label: string; value: ClientTicketStatus }[] = [
{ label: t('clientTicket.status.new'), value: 'new' },
{ label: t('clientTicket.status.in_progress'), value: 'in_progress' },
{ label: t('clientTicket.status.done'), value: 'done' },
{ label: t('clientTicket.status.rejected'), value: 'rejected' },
]
// Filter out forbidden transitions
return allStatuses.filter(s => {
if (s.value === current) return false
if ((current === 'done' || current === 'rejected') && s.value === 'new') return false
return true
})
}) })
function getProjectName(iri: string): string { function getProjectName(iri: string): string {
@@ -291,6 +288,14 @@ function getSubmitterName(iri: string | null): string {
return users.value.find(u => u.id === id)?.username ?? '' return users.value.find(u => u.id === id)?.username ?? ''
} }
function getSubmitterUser(iri: string | null): UserData | undefined {
if (!iri) return undefined
const match = iri.match(/\/api\/users\/(\d+)/)
if (!match) return undefined
const id = Number(match[1])
return users.value.find(u => u.id === id)
}
function openDetail(ticket: ClientTicket) { function openDetail(ticket: ClientTicket) {
detailTicket.value = ticket detailTicket.value = ticket
detailOpen.value = true detailOpen.value = true

View File

@@ -44,13 +44,12 @@
> >
{{ tag.label }} {{ tag.label }}
</span> </span>
<span <UserAvatar
v-if="task.assignee" v-if="task.assignee"
class="ml-auto flex h-5 w-5 items-center justify-center rounded-full bg-primary-500 text-[10px] font-bold text-white" :user="task.assignee"
:title="task.assignee.username" size="xs"
> class="ml-auto"
{{ task.assignee.username.substring(0, 2).toUpperCase() }} />
</span>
<span <span
v-else v-else
class="ml-auto flex h-5 w-5 items-center justify-center rounded-full bg-neutral-200 text-neutral-400" class="ml-auto flex h-5 w-5 items-center justify-center rounded-full bg-neutral-200 text-neutral-400"

View File

@@ -10,12 +10,14 @@
<div class="ml-auto flex items-center gap-4 text-xl text-white sm:gap-8"> <div class="ml-auto flex items-center gap-4 text-xl text-white sm:gap-8">
<NotificationBell /> <NotificationBell />
<div class="group relative flex gap-2 sm:gap-4"> <div class="group relative flex gap-2 sm:gap-4">
<Icon name="mdi:account-circle-outline" class="self-center cursor-pointer" size="36" /> <UserAvatar v-if="user" :user="user" size="md" class="cursor-pointer" />
<Icon v-else name="mdi:account-circle-outline" class="self-center cursor-pointer" size="36" />
<p class="hidden self-center cursor-pointer sm:block">{{ user?.username }}</p> <p class="hidden self-center cursor-pointer sm:block">{{ user?.username }}</p>
<div class="invisible absolute right-0 top-full z-50 mt-2 w-44 rounded-md border border-neutral-200 bg-white py-1 text-sm text-neutral-800 opacity-0 shadow-lg transition-all group-hover:visible group-hover:opacity-100"> <div class="invisible absolute right-0 top-full z-50 mt-2 w-44 rounded-md border border-neutral-200 bg-white py-1 text-sm text-neutral-800 opacity-0 shadow-lg transition-all group-hover:visible group-hover:opacity-100">
<button <button
type="button" type="button"
class="block w-full px-3 py-2 text-left hover:bg-neutral-100" class="block w-full px-3 py-2 text-left hover:bg-neutral-100"
@click="navigateTo('/profile')"
> >
Mon profil Mon profil
</button> </button>
@@ -43,7 +45,7 @@ defineProps<{
const auth = useAuthStore() const auth = useAuthStore()
const ui = useUiStore() const ui = useUiStore()
const handleLogout = async () => { async function handleLogout() {
await auth.logout() await auth.logout()
await navigateTo('/login') await navigateTo('/login')
} }

View File

@@ -46,13 +46,11 @@
> >
{{ task.group.title }} {{ task.group.title }}
</span> </span>
<span <UserAvatar
v-if="task.assignee" v-if="task.assignee"
class="flex h-5 w-5 items-center justify-center rounded-full bg-primary-500 text-[10px] font-bold text-white" :user="task.assignee"
:title="task.assignee.username" size="xs"
> />
{{ task.assignee.username.substring(0, 2).toUpperCase() }}
</span>
</div> </div>
</div> </div>
</div> </div>
@@ -130,7 +128,7 @@ const filteredTasks = computed(() => {
async function loadData() { async function loadData() {
const [p, t, s, e, pr, ty, g, u] = await Promise.all([ const [p, t, s, e, pr, ty, g, u] = await Promise.all([
projectService.getById(projectId.value), projectService.getById(projectId.value),
taskService.getByProjectArchived(projectId.value), taskService.getByProject(projectId.value, true),
statusService.getAll(), statusService.getAll(),
effortService.getAll(), effortService.getAll(),
priorityService.getAll(), priorityService.getAll(),