feat(mail) : onglet Mails dans TaskModal — liste mails liés, bouton lier, MailPickerModal

This commit is contained in:
2026-05-20 00:49:57 +02:00
parent 96c7d902e7
commit 273234626f

View File

@@ -60,16 +60,16 @@
<div class="border-b border-neutral-100 -mx-4 px-4 sm:-mx-8 sm:px-8 mb-4">
<nav class="flex gap-6">
<button
v-for="tab in ['details', 'planning']"
v-for="tab in availableTabs"
:key="tab"
type="button"
class="px-1 pb-3 text-sm font-semibold transition"
:class="activeTab === tab
? 'border-b-2 border-primary-500 text-primary-500'
: 'text-neutral-500 hover:text-neutral-700'"
@click="activeTab = tab as 'details' | 'planning'"
@click="activeTab = tab as 'details' | 'planning' | 'mails'"
>
{{ $t(`tasks.${tab}Tab`) }}
{{ tab === 'mails' ? $t('mail.taskTab.title') : $t(`tasks.${tab}Tab`) }}
</button>
</nav>
</div>
@@ -433,6 +433,76 @@
</div>
</div>
<!-- Onglet Mails -->
<div v-show="activeTab === 'mails'" class="space-y-4">
<!-- Chargement -->
<div v-if="mailsLoading" class="flex items-center justify-center py-8">
<Icon name="material-symbols:progress-activity" size="24" class="animate-spin text-neutral-400" />
</div>
<!-- Vide -->
<div
v-else-if="linkedMails.length === 0"
class="flex flex-col items-center justify-center gap-3 py-8 text-center"
>
<Icon name="material-symbols:mail-outline" size="32" class="text-neutral-300" />
<p class="text-sm text-neutral-400 italic">{{ $t('mail.taskTab.empty') }}</p>
</div>
<!-- Liste mails liés -->
<div v-else class="divide-y divide-neutral-100 rounded-lg border border-neutral-200">
<NuxtLink
v-for="mail in linkedMails"
:key="mail.id"
:to="`/mail?messageId=${mail.id}`"
class="flex items-start gap-3 px-4 py-3 text-sm transition-colors hover:bg-neutral-50"
:title="$t('mail.taskTab.openInMailer')"
>
<Icon
name="material-symbols:mail-outline"
size="16"
class="mt-0.5 flex-shrink-0 text-neutral-400"
/>
<div class="min-w-0 flex-1">
<p class="truncate font-medium text-neutral-800">
{{ mail.subject ?? $t('mail.noSubject') }}
</p>
<p class="flex items-center gap-2 text-xs text-neutral-500">
<span class="truncate">{{ mail.fromName ?? mail.fromEmail }}</span>
<span>·</span>
<span class="flex-shrink-0">{{ formatMailDate(mail.sentAt ?? mail.receivedAt) }}</span>
</p>
</div>
<Icon
name="material-symbols:open-in-new"
size="14"
class="flex-shrink-0 text-neutral-300"
/>
</NuxtLink>
</div>
<!-- Bouton lier un mail -->
<div class="pt-2">
<MalioButton
:label="$t('mail.taskTab.linkButton')"
variant="secondary"
icon-name="material-symbols:link"
icon-position="left"
:icon-size="14"
button-class="w-auto"
@click="showMailPickerModal = true"
/>
</div>
<!-- Modal picker mail -->
<MailPickerModal
v-if="task"
v-model="showMailPickerModal"
:task-id="task.id"
@linked="handleMailLinked"
/>
</div>
<!-- Footer -->
<div
class="mt-6 flex items-center border-t border-neutral-100 pt-5"
@@ -513,6 +583,8 @@ import { useTaskService } from '~/services/tasks'
import { useTaskRecurrenceService } from '~/services/task-recurrences'
import type { Project } from '~/services/dto/project'
import { useMailService } from '~/services/mail'
import type { MailMessageHeaderDto } from '~/services/dto/mail'
const props = defineProps<{
modelValue: boolean
@@ -545,7 +617,14 @@ function close() {
const isEditing = computed(() => !!props.task)
const isSubmitting = ref(false)
const confirmDeleteOpen = ref(false)
const activeTab = ref<'details' | 'planning'>('details')
const activeTab = ref<'details' | 'planning' | 'mails'>('details')
// ─── Onglet Mails ─────────────────────────────────────────────────────────
const mailService = useMailService()
const linkedMails = ref<MailMessageHeaderDto[]>([])
const mailsLoading = ref(false)
const showMailPickerModal = ref(false)
const giteaUrl = ref('')
const { getSettings: getGiteaSettings } = useGiteaService()
@@ -765,6 +844,7 @@ watch(() => props.modelValue, async (open) => {
activeTab.value = 'details'
confirmDeleteDocOpen.value = false
documentToDelete.value = null
linkedMails.value = []
populateForm(props.task)
const pid = resolvedProjectId.value
if (pid) {
@@ -823,6 +903,49 @@ watch(() => form.projectId, async (pid) => {
const authStore = useAuthStore()
const isAdmin = computed(() => authStore.user?.roles?.includes('ROLE_ADMIN') ?? false)
const isClientOnly = computed(() =>
authStore.user?.roles?.includes('ROLE_CLIENT') === true
&& authStore.user?.roles?.includes('ROLE_ADMIN') !== true,
)
const isMailUser = computed(() => !isClientOnly.value)
const availableTabs = computed(() => {
const base: Array<'details' | 'planning' | 'mails'> = ['details', 'planning']
if (isEditing.value && isMailUser.value) base.push('mails')
return base
})
async function loadLinkedMails(): Promise<void> {
if (!props.task || !isMailUser.value) return
mailsLoading.value = true
try {
linkedMails.value = await mailService.listMailsForTask(props.task.id)
} catch {
linkedMails.value = []
} finally {
mailsLoading.value = false
}
}
watch(activeTab, async (tab) => {
if (tab === 'mails' && props.task) {
await loadLinkedMails()
}
})
async function handleMailLinked(): Promise<void> {
showMailPickerModal.value = false
await loadLinkedMails()
}
function formatMailDate(iso: string | null): string {
if (!iso) return ''
return new Date(iso).toLocaleDateString('fr', {
day: '2-digit',
month: 'short',
})
}
function ticketStatusClass(status: string): string {
switch (status) {
case 'new': return 'bg-blue-100 text-blue-700'