gigafibre-fsm/apps/ops/src/components/shared/UserAvatar.vue
louispaulb 14530787bb consolidate uncommitted multi-session work (NL staff-agent, user avatars/profil, HelpHint, ticket detail modules, skill icons, hub agent/payments/sync)
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>
2026-07-09 20:43:50 -04:00

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>