feat(mail) : onglet Mails dans TaskModal — liste mails liés, bouton lier, MailPickerModal
This commit is contained in:
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user