Lot de travaux accumulés non commités (plusieurs sessions), vérifiés : hub `node --check` tout OK + build SPA propre. - ops : AssignmentField, CommandPalette, IssueDetail/DetailModal + detail-sections/modules, UserAvatar/useAvatar, UserProfileDialog/EmployeeEditDialog, HelpHint, useSkillIcons, pages (Dashboard/Equipe/Clients/Tickets/Reports/Settings/Evaluations/LegacySync…), usePermissions/useUserGroups/useDetailModal, MainLayout, TicketStatusControl - hub : staff-agent.js (couche commandes NL), agent.js/agent-tools.json/voice-agent.js, avatars.js, conversation.js, payments.js, sync-orchestrator.js, auth.js, campaigns.js, legacy-dispatch-sync.js, server.js Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
54 lines
2.4 KiB
Vue
54 lines
2.4 KiB
Vue
<template>
|
|
<!-- Avatar CANONIQUE d'un membre : photo si présente, sinon repli initiales +
|
|
couleur déterministe (useFormatters). Réutiliser PARTOUT plutôt que de
|
|
recoder q-avatar + initial() dans chaque page. -->
|
|
<q-avatar :size="size + 'px'" :class="['user-avatar', { 'user-avatar--img': showImg }]"
|
|
:style="showImg ? null : { background: bg, color: '#fff', fontSize: Math.round(size * 0.42) + 'px' }">
|
|
<img v-if="showImg" :src="src" :alt="name || email" referrerpolicy="no-referrer" @error="failed = true">
|
|
<template v-else>{{ ini }}</template>
|
|
<q-tooltip v-if="tooltip && (name || email)">{{ name || email }}</q-tooltip>
|
|
<slot />
|
|
</q-avatar>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, watch, onMounted } from 'vue'
|
|
import { staffColor, staffInitials } from 'src/composables/useFormatters'
|
|
import { avatarVersion, hasAvatar, ensureAvatarIndex } from 'src/composables/useAvatar'
|
|
import { HUB_URL } from 'src/config/hub'
|
|
|
|
const props = defineProps({
|
|
email: { type: String, default: '' },
|
|
name: { type: String, default: '' },
|
|
size: { type: Number, default: 28 },
|
|
tooltip: { type: Boolean, default: false },
|
|
// Change de valeur après un upload → force le rechargement de l'image (cache-bust).
|
|
version: { type: [Number, String], default: '' },
|
|
})
|
|
|
|
const failed = ref(false)
|
|
const src = computed(() => {
|
|
if (!props.email) return ''
|
|
// Version explicite (prop) sinon version partagée (bump après upload) → cache-bust.
|
|
const ver = props.version || avatarVersion(props.email)
|
|
const v = ver ? ('?v=' + ver) : ''
|
|
return `${HUB_URL}/avatars/${encodeURIComponent(props.email.toLowerCase())}${v}`
|
|
})
|
|
// On ne tente le <img> QUE si l'index indique une photo pour ce courriel (évite un
|
|
// 404 par membre sans photo) ; sinon initiales directement. @error → repli aussi.
|
|
const showImg = computed(() => !!props.email && hasAvatar(props.email) && !failed.value)
|
|
const bg = computed(() => staffColor(props.email || props.name))
|
|
const ini = computed(() => staffInitials(props.name) || (props.email ? props.email[0].toUpperCase() : '?'))
|
|
|
|
// Réinitialise l'état d'échec quand la cible change (email/version).
|
|
watch(src, () => { failed.value = false })
|
|
|
|
// Charge l'index d'avatars une fois (partagé/idempotent).
|
|
onMounted(() => { ensureAvatarIndex() })
|
|
</script>
|
|
|
|
<style scoped>
|
|
.user-avatar { font-weight: 600; }
|
|
.user-avatar--img :deep(img) { object-fit: cover; }
|
|
</style>
|