Avec MalioInputRichText qui émet désormais du HTML par défaut, plusieurs points d'affichage rendaient les balises brutes au lieu du texte. Ajoute un helper stripRichText() (frontend) et descriptionToPlainText() (backend) pour neutraliser ces cas. - TimeEntryList : strip avant truncate dans la liste des time entries. - ProjectGroupTab : strip dans la cellule description du tableau des groupes. - CalDavService : strip_tags + html_entity_decode avant injection dans le DESCRIPTION VEVENT/VTODO iCal (sinon Outlook/Apple Calendar affichaient les <p>...</p> à l'utilisateur). Co-Authored-By: RuFlo <ruv@ruv.net>
170 lines
5.2 KiB
Vue
170 lines
5.2 KiB
Vue
<template>
|
|
<div>
|
|
<div class="flex items-center justify-between">
|
|
<h2 class="text-lg font-bold text-neutral-900">Groupes</h2>
|
|
<div class="flex items-center gap-3">
|
|
<MalioButton
|
|
variant="tertiary"
|
|
button-class="w-auto px-3"
|
|
:label="showArchived ? $t('archive.hideArchived') : $t('archive.showArchived')"
|
|
@click="showArchived = !showArchived"
|
|
/>
|
|
<MalioButton
|
|
v-if="!showArchived"
|
|
icon-name="mdi:plus"
|
|
icon-position="left"
|
|
button-class="w-auto px-4"
|
|
label="Ajouter un groupe"
|
|
@click="openCreate"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<DataTable
|
|
:columns="columns"
|
|
:items="items"
|
|
:loading="isLoading"
|
|
empty-message="Aucun groupe trouvé."
|
|
:deletable="!showArchived"
|
|
@row-click="openEdit"
|
|
@delete="(item) => handleDelete(item.id)"
|
|
>
|
|
<template #cell-color="{ item }">
|
|
<span
|
|
class="inline-block h-6 w-6 rounded-full"
|
|
:style="{ backgroundColor: item.color }"
|
|
/>
|
|
</template>
|
|
<template #cell-description="{ item }">
|
|
{{ stripRichText(item.description) || '—' }}
|
|
</template>
|
|
<template #actions="{ item }">
|
|
<MalioButton
|
|
v-if="!showArchived && canArchiveGroup(item)"
|
|
variant="secondary"
|
|
:label="$t('archive.archiveButton')"
|
|
button-class="w-auto px-3"
|
|
@click.stop="handleArchive(item)"
|
|
/>
|
|
<MalioButton
|
|
v-if="showArchived"
|
|
variant="secondary"
|
|
:label="$t('archive.unarchiveButton')"
|
|
button-class="w-auto px-3"
|
|
@click.stop="handleUnarchive(item)"
|
|
/>
|
|
</template>
|
|
</DataTable>
|
|
|
|
<TaskGroupDrawer
|
|
v-model="drawerOpen"
|
|
:group="selectedItem"
|
|
:project-id="projectId"
|
|
:tasks="[...activeTasks, ...archivedTasks]"
|
|
@saved="onSaved"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { TaskGroup } from '~/services/dto/task-group'
|
|
import type { Task } from '~/services/dto/task'
|
|
import { useTaskGroupService } from '~/services/task-groups'
|
|
import { useTaskService } from '~/services/tasks'
|
|
import { stripRichText } from '~/utils/format'
|
|
|
|
const props = defineProps<{
|
|
projectId: number
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'updated'): void
|
|
}>()
|
|
|
|
import type { DataTableColumn } from '~/components/ui/DataTable.vue'
|
|
|
|
const columns: DataTableColumn[] = [
|
|
{ key: 'title', label: 'Titre', primary: true },
|
|
{ key: 'color', label: 'Couleur' },
|
|
{ key: 'description', label: 'Description', class: 'max-w-xs truncate text-neutral-700' },
|
|
]
|
|
|
|
const groupService = useTaskGroupService()
|
|
const taskService = useTaskService()
|
|
|
|
const allGroups = ref<TaskGroup[]>([])
|
|
const activeTasks = ref<Task[]>([])
|
|
const archivedTasks = ref<Task[]>([])
|
|
const isLoading = ref(true)
|
|
const drawerOpen = ref(false)
|
|
const selectedItem = ref<TaskGroup | null>(null)
|
|
const showArchived = ref(false)
|
|
|
|
const items = computed(() =>
|
|
allGroups.value.filter(g => showArchived.value ? g.archived : !g.archived)
|
|
)
|
|
|
|
function canArchiveGroup(group: TaskGroup): boolean {
|
|
const groupTasks = activeTasks.value.filter(t => t.group?.id === group.id)
|
|
if (groupTasks.length === 0) return false
|
|
return groupTasks.every(t => t.status?.isFinal === true)
|
|
}
|
|
|
|
async function loadItems() {
|
|
isLoading.value = true
|
|
try {
|
|
const [g, t, at] = await Promise.all([
|
|
groupService.getByProject(props.projectId),
|
|
taskService.getByProject(props.projectId),
|
|
taskService.getByProject(props.projectId, true),
|
|
])
|
|
allGroups.value = g
|
|
activeTasks.value = t
|
|
archivedTasks.value = at
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
function openCreate() {
|
|
selectedItem.value = null
|
|
drawerOpen.value = true
|
|
}
|
|
|
|
function openEdit(item: TaskGroup) {
|
|
selectedItem.value = item
|
|
drawerOpen.value = true
|
|
}
|
|
|
|
async function handleDelete(id: number) {
|
|
await groupService.remove(id)
|
|
await loadItems()
|
|
emit('updated')
|
|
}
|
|
|
|
async function handleArchive(group: TaskGroup) {
|
|
const groupTasks = activeTasks.value.filter(t => t.group?.id === group.id)
|
|
await Promise.all(groupTasks.map(t => taskService.update(t.id, { archived: true })))
|
|
await groupService.update(group.id, { archived: true })
|
|
await loadItems()
|
|
emit('updated')
|
|
}
|
|
|
|
async function handleUnarchive(group: TaskGroup) {
|
|
const groupTasks = archivedTasks.value.filter(t => t.group?.id === group.id)
|
|
await Promise.all(groupTasks.map(t => taskService.update(t.id, { archived: false })))
|
|
await groupService.update(group.id, { archived: false })
|
|
await loadItems()
|
|
emit('updated')
|
|
}
|
|
|
|
async function onSaved() {
|
|
await loadItems()
|
|
emit('updated')
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadItems()
|
|
})
|
|
</script>
|