Files
Lesstime/frontend/components/mail/MailMessageViewer.vue
matthieu a40d11503a feat(mail) : allège la barre d'actions du lecteur de mail
Boutons "Créer une tâche" / "Lier à une tâche" réduits (text-xs, padding compact),
suppression des actions "Marquer comme non lu" et "Marquer important".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 08:24:00 +02:00

184 lines
7.5 KiB
Vue

<script setup lang="ts">
import type { MailMessageDetailDto, MailAddressDto } from '~/services/dto/mail'
import { sanitizeMailHtml } from '~/utils/sanitizeMailHtml'
import { useMailService } from '~/services/mail'
const props = defineProps<{
/** Détail complet du message. null = aucun message sélectionné. */
detail: MailMessageDetailDto | null
loading: boolean
}>()
const emit = defineEmits<{
createTask: [mailId: number]
linkTask: [mailId: number]
}>()
const { t } = useI18n()
const mailService = useMailService()
const showImages = ref(false)
const sanitizedBody = computed((): string => {
if (!props.detail?.bodyHtml) return ''
return sanitizeMailHtml(props.detail.bodyHtml, { allowImages: showImages.value })
})
watch(
() => props.detail?.header.id,
() => {
showImages.value = false
},
)
async function handleDownload(downloadId: string, filename: string): Promise<void> {
try {
const { data } = await mailService.downloadAttachment(downloadId)
const url = URL.createObjectURL(data)
const a = document.createElement('a')
a.href = url
a.download = filename
a.click()
URL.revokeObjectURL(url)
} catch {
// L'erreur est gérée par useApi (toast automatique)
}
}
function formatDate(iso: string | null): string {
if (!iso) return ''
return new Date(iso).toLocaleString('fr', {
dateStyle: 'long',
timeStyle: 'short',
})
}
function joinAddresses(addresses: MailAddressDto[]): string {
return addresses
.map((a) => (a.name ? `${a.name} <${a.email}>` : a.email))
.join(', ')
}
</script>
<template>
<div class="flex h-full flex-col overflow-hidden">
<div
v-if="!detail && !loading"
class="flex flex-1 items-center justify-center text-sm text-neutral-400 italic px-8 text-center"
>
{{ t('mail.empty.viewer') }}
</div>
<div v-else-if="loading" class="flex flex-1 items-center justify-center">
<Icon name="material-symbols:progress-activity" size="28" class="animate-spin text-neutral-400" />
</div>
<template v-else-if="detail">
<div class="flex-shrink-0 border-b border-neutral-200 px-4 py-3 space-y-1.5">
<h2 class="text-base font-semibold text-neutral-900 break-words">
{{ detail.header.subject ?? t('mail.noSubject') }}
</h2>
<dl class="text-xs text-neutral-500 space-y-0.5">
<div class="flex gap-1.5">
<dt class="font-medium text-neutral-600 w-5 flex-shrink-0">{{ t('mail.from') }}</dt>
<dd class="break-all">
{{
detail.header.fromName
? `${detail.header.fromName} <${detail.header.fromEmail}>`
: (detail.header.fromEmail ?? '')
}}
</dd>
</div>
<div v-if="detail.header.toRecipients.length > 0" class="flex gap-1.5">
<dt class="font-medium text-neutral-600 w-5 flex-shrink-0">{{ t('mail.to') }}</dt>
<dd class="break-all">{{ joinAddresses(detail.header.toRecipients) }}</dd>
</div>
<div v-if="detail.header.ccRecipients.length > 0" class="flex gap-1.5">
<dt class="font-medium text-neutral-600 w-5 flex-shrink-0">{{ t('mail.cc') }}</dt>
<dd class="break-all">{{ joinAddresses(detail.header.ccRecipients) }}</dd>
</div>
<div class="flex gap-1.5">
<dt class="font-medium text-neutral-600 w-5 flex-shrink-0">{{ t('mail.date') }}</dt>
<dd>{{ formatDate(detail.header.sentAt ?? detail.header.receivedAt) }}</dd>
</div>
</dl>
<div class="flex flex-wrap items-center gap-2 pt-1">
<MalioButton
:label="t('mail.actions.createTask')"
variant="primary"
icon-name="material-symbols:add-task-outline"
icon-position="left"
:icon-size="13"
button-class="text-xs px-2.5 py-1"
@click="emit('createTask', detail.header.id)"
/>
<MalioButton
:label="t('mail.actions.linkTask')"
variant="secondary"
icon-name="material-symbols:link"
icon-position="left"
:icon-size="13"
button-class="text-xs px-2.5 py-1"
@click="emit('linkTask', detail.header.id)"
/>
</div>
</div>
<div class="flex-1 overflow-y-auto px-4 py-3">
<div
v-if="!showImages && detail.bodyHtml"
class="mb-3 flex items-center gap-3 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm"
>
<Icon name="material-symbols:image-outline" size="16" class="text-amber-500 flex-shrink-0" />
<span class="flex-1 text-amber-700">
{{ t('mail.remoteImagesBlocked') }}
</span>
<button
type="button"
class="text-xs font-medium text-amber-700 underline hover:text-amber-900 transition-colors"
@click="showImages = true"
>
{{ t('mail.actions.showImages') }}
</button>
</div>
<div
v-if="detail.bodyHtml"
class="prose prose-sm max-w-none text-neutral-800"
v-html="sanitizedBody"
/>
<pre
v-else-if="detail.bodyText"
class="whitespace-pre-wrap font-sans text-sm text-neutral-700"
>{{ detail.bodyText }}</pre>
</div>
<div
v-if="detail.attachments.length > 0"
class="flex-shrink-0 border-t border-neutral-200 px-4 py-3"
>
<p class="mb-2 text-xs font-semibold uppercase tracking-wide text-neutral-500">
{{ t('mail.attachments') }} ({{ detail.attachments.length }})
</p>
<div class="flex flex-wrap gap-2">
<button
v-for="att in detail.attachments"
:key="att.downloadId"
type="button"
class="flex items-center gap-1.5 rounded border border-neutral-200 bg-neutral-50 px-2.5 py-1.5 text-xs text-neutral-700 transition-colors hover:bg-neutral-100 hover:border-neutral-300"
:title="att.filename"
@click="handleDownload(att.downloadId, att.filename)"
>
<Icon name="material-symbols:attach-file" size="14" class="flex-shrink-0 text-neutral-400" />
<span class="max-w-[180px] truncate">{{ att.filename }}</span>
<span class="text-neutral-400">({{ Math.round(att.size / 1024) }} Ko)</span>
</button>
</div>
</div>
</template>
</div>
</template>