Files
Lesstime/frontend/components/task/TaskGroupDrawer.vue
Matthieu 2a0b202d32 feat(absences) : avancement module absences + suppression du portail client
Deux lots regroupés sur la branche feat/absence-management.

Suppression complète du portail client :
- retire ROLE_CLIENT (security.yaml) ; User::getRoles() ajoute toujours ROLE_USER
- supprime l'entité ClientTicket (+ repo, states, relations), User.client et
  User.allowedProjects, NotificationService, ProjectAllowedExtension, le bloc
  ROLE_CLIENT de MailAccessChecker
- front : pages /portal, layout portal, composants client-ticket/,
  AdminClientTicketTab, services/dto/i18n/docs associés
- fixtures : retire les users client-liot / client-acme
- migration Version20260522110000 (drop client_ticket, user_allowed_projects,
  colonnes liées ; task_document.task_id -> NOT NULL)
- tests : retire les cas obsolètes testant le blocage des clients sur le mail

Module gestion des absences (WIP) :
- entités / migrations (Version20260521160000, Version20260522090000)
- pages absences.vue / team-absences.vue, composants frontend/components/absence/
- services front, AccrueLeaveCommand, PublicHolidayController

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

182 lines
5.3 KiB
Vue

<template>
<MalioDrawer v-model="isOpen">
<template #header>
<h2 class="text-xl font-bold">{{ isEditing ? $t('taskGroups.editGroup') : $t('taskGroups.addGroup') }}</h2>
</template>
<form @submit.prevent="handleSubmit" class="flex flex-col gap-2">
<MalioInputText
v-model="form.title"
label="Titre"
input-class="w-full"
:error="touched.title && !form.title.trim() ? 'Le titre est requis' : ''"
@blur="touched.title = true"
/>
<MalioInputRichText
v-model="form.description"
label="Description"
min-height="120px"
/>
<div class="mt-4">
<ColorPicker v-model="form.color" />
</div>
<div
v-if="isEditing && !canArchive && !canUnarchive && nonFinalTasksCount > 0"
class="mt-4 rounded-md bg-amber-50 px-4 py-3 text-sm text-amber-700"
>
{{ $t('archive.groupNonFinalTasks', { count: nonFinalTasksCount }) }}
</div>
<div class="mt-6 flex items-center" :class="isEditing ? 'justify-between' : 'justify-end'">
<MalioButton
v-if="canArchive"
variant="secondary"
:label="$t('archive.archiveButton')"
button-class="w-auto px-4"
:disabled="isSubmitting"
@click="handleArchive"
/>
<MalioButton
v-if="canUnarchive"
variant="secondary"
:label="$t('archive.unarchiveButton')"
button-class="w-auto px-4"
:disabled="isSubmitting"
@click="handleUnarchive"
/>
<MalioButton
label="Enregistrer"
button-class="w-auto px-6"
:disabled="isSubmitting"
@click="handleSubmit"
/>
</div>
</form>
</MalioDrawer>
</template>
<script setup lang="ts">
import type { TaskGroup, TaskGroupWrite } from '~/services/dto/task-group'
import type { Task } from '~/services/dto/task'
import { useTaskGroupService } from '~/services/task-groups'
import { useTaskService } from '~/services/tasks'
const props = defineProps<{
modelValue: boolean
group: TaskGroup | null
projectId: number
tasks?: Task[]
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
(e: 'saved'): void
}>()
const isOpen = computed({
get: () => props.modelValue,
set: (v) => emit('update:modelValue', v),
})
const isEditing = computed(() => !!props.group)
const isSubmitting = ref(false)
const form = reactive({
title: '',
description: '',
color: '#222783',
})
const touched = reactive({
title: false,
})
watch(() => props.modelValue, (open) => {
if (open) {
if (props.group) {
form.title = props.group.title ?? ''
form.description = props.group.description ?? ''
form.color = props.group.color ?? '#222783'
} else {
form.title = ''
form.description = ''
form.color = '#222783'
}
touched.title = false
}
})
const { create, update } = useTaskGroupService()
const taskService = useTaskService()
const groupTasks = computed(() =>
(props.tasks ?? []).filter(t => t.group?.id === props.group?.id)
)
const nonFinalTasksCount = computed(() =>
groupTasks.value.filter(t => t.status?.isFinal !== true).length
)
const canArchive = computed(() => {
if (!isEditing.value || !props.group || props.group.archived) return false
if (groupTasks.value.length === 0) return false
return nonFinalTasksCount.value === 0
})
const canUnarchive = computed(() => {
return isEditing.value && !!props.group?.archived
})
async function handleArchive() {
if (!props.group) return
isSubmitting.value = true
try {
await Promise.all(groupTasks.value.map(t => taskService.update(t.id, { archived: true })))
await update(props.group.id, { archived: true })
emit('saved')
isOpen.value = false
} finally {
isSubmitting.value = false
}
}
async function handleUnarchive() {
if (!props.group) return
isSubmitting.value = true
try {
await Promise.all(groupTasks.value.map(t => taskService.update(t.id, { archived: false })))
await update(props.group.id, { archived: false })
emit('saved')
isOpen.value = false
} finally {
isSubmitting.value = false
}
}
async function handleSubmit() {
touched.title = true
if (!form.title.trim()) return
isSubmitting.value = true
try {
const payload: TaskGroupWrite = {
title: form.title.trim(),
description: form.description.trim() || null,
color: form.color,
project: `/api/projects/${props.projectId}`,
}
if (isEditing.value && props.group) {
await update(props.group.id, payload)
} else {
await create(payload)
}
emit('saved')
isOpen.value = false
} finally {
isSubmitting.value = false
}
}
</script>