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">
|
<div class="border-b border-neutral-100 -mx-4 px-4 sm:-mx-8 sm:px-8 mb-4">
|
||||||
<nav class="flex gap-6">
|
<nav class="flex gap-6">
|
||||||
<button
|
<button
|
||||||
v-for="tab in ['details', 'planning']"
|
v-for="tab in availableTabs"
|
||||||
:key="tab"
|
:key="tab"
|
||||||
type="button"
|
type="button"
|
||||||
class="px-1 pb-3 text-sm font-semibold transition"
|
class="px-1 pb-3 text-sm font-semibold transition"
|
||||||
:class="activeTab === tab
|
:class="activeTab === tab
|
||||||
? 'border-b-2 border-primary-500 text-primary-500'
|
? 'border-b-2 border-primary-500 text-primary-500'
|
||||||
: 'text-neutral-500 hover:text-neutral-700'"
|
: '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>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
@@ -433,6 +433,76 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Footer -->
|
||||||
<div
|
<div
|
||||||
class="mt-6 flex items-center border-t border-neutral-100 pt-5"
|
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 { useTaskRecurrenceService } from '~/services/task-recurrences'
|
||||||
|
|
||||||
import type { Project } from '~/services/dto/project'
|
import type { Project } from '~/services/dto/project'
|
||||||
|
import { useMailService } from '~/services/mail'
|
||||||
|
import type { MailMessageHeaderDto } from '~/services/dto/mail'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
@@ -545,7 +617,14 @@ function close() {
|
|||||||
const isEditing = computed(() => !!props.task)
|
const isEditing = computed(() => !!props.task)
|
||||||
const isSubmitting = ref(false)
|
const isSubmitting = ref(false)
|
||||||
const confirmDeleteOpen = 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 giteaUrl = ref('')
|
||||||
const { getSettings: getGiteaSettings } = useGiteaService()
|
const { getSettings: getGiteaSettings } = useGiteaService()
|
||||||
@@ -765,6 +844,7 @@ watch(() => props.modelValue, async (open) => {
|
|||||||
activeTab.value = 'details'
|
activeTab.value = 'details'
|
||||||
confirmDeleteDocOpen.value = false
|
confirmDeleteDocOpen.value = false
|
||||||
documentToDelete.value = null
|
documentToDelete.value = null
|
||||||
|
linkedMails.value = []
|
||||||
populateForm(props.task)
|
populateForm(props.task)
|
||||||
const pid = resolvedProjectId.value
|
const pid = resolvedProjectId.value
|
||||||
if (pid) {
|
if (pid) {
|
||||||
@@ -823,6 +903,49 @@ watch(() => form.projectId, async (pid) => {
|
|||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const isAdmin = computed(() => authStore.user?.roles?.includes('ROLE_ADMIN') ?? false)
|
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 {
|
function ticketStatusClass(status: string): string {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'new': return 'bg-blue-100 text-blue-700'
|
case 'new': return 'bg-blue-100 text-blue-700'
|
||||||
|
|||||||
Reference in New Issue
Block a user