feat : add archive/unarchive to TaskGroupDrawer and fix isFinal serialization
Fix TaskStatus getter naming (isFinal -> getIsFinal) so Symfony serializer properly exposes the isFinal field. Add archive/unarchive buttons and non-final tasks info message to TaskGroupDrawer. Remove obsolete TaskType entity and repository. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -62,6 +62,7 @@
|
|||||||
v-model="drawerOpen"
|
v-model="drawerOpen"
|
||||||
:group="selectedItem"
|
:group="selectedItem"
|
||||||
:project-id="projectId"
|
:project-id="projectId"
|
||||||
|
:tasks="[...activeTasks, ...archivedTasks]"
|
||||||
@saved="onSaved"
|
@saved="onSaved"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,7 +17,32 @@
|
|||||||
<ColorPicker v-model="form.color" />
|
<ColorPicker v-model="form.color" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 flex justify-end">
|
<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'">
|
||||||
|
<button
|
||||||
|
v-if="canArchive"
|
||||||
|
type="button"
|
||||||
|
class="rounded-md bg-neutral-500 px-4 py-2 text-sm font-semibold text-white hover:bg-neutral-600 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
@click="handleArchive"
|
||||||
|
>
|
||||||
|
{{ $t('archive.archiveButton') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="canUnarchive"
|
||||||
|
type="button"
|
||||||
|
class="rounded-md bg-neutral-500 px-4 py-2 text-sm font-semibold text-white hover:bg-neutral-600 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
@click="handleUnarchive"
|
||||||
|
>
|
||||||
|
{{ $t('archive.unarchiveButton') }}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="rounded-md bg-primary-500 px-6 py-2 text-sm font-semibold text-white hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
|
class="rounded-md bg-primary-500 px-6 py-2 text-sm font-semibold text-white hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
@@ -32,12 +57,15 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { TaskGroup, TaskGroupWrite } from '~/services/dto/task-group'
|
import type { TaskGroup, TaskGroupWrite } from '~/services/dto/task-group'
|
||||||
|
import type { Task } from '~/services/dto/task'
|
||||||
import { useTaskGroupService } from '~/services/task-groups'
|
import { useTaskGroupService } from '~/services/task-groups'
|
||||||
|
import { useTaskService } from '~/services/tasks'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
group: TaskGroup | null
|
group: TaskGroup | null
|
||||||
projectId: number
|
projectId: number
|
||||||
|
tasks?: Task[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -79,6 +107,51 @@ watch(() => props.modelValue, (open) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const { create, update } = useTaskGroupService()
|
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() {
|
async function handleSubmit() {
|
||||||
touched.title = true
|
touched.title = true
|
||||||
|
|||||||
@@ -83,6 +83,7 @@
|
|||||||
"showArchived": "Voir les groupes archivés",
|
"showArchived": "Voir les groupes archivés",
|
||||||
"hideArchived": "Masquer les groupes archivés",
|
"hideArchived": "Masquer les groupes archivés",
|
||||||
"statusFinal": "Statut final",
|
"statusFinal": "Statut final",
|
||||||
"groupArchiveDisabled": "Tous les tickets doivent être en statut final pour archiver le groupe."
|
"groupArchiveDisabled": "Tous les tickets doivent être en statut final pour archiver le groupe.",
|
||||||
|
"groupNonFinalTasks": "Il reste {count} ticket(s) sans statut final dans ce groupe."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class TaskStatus
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isFinal(): bool
|
public function getIsFinal(): bool
|
||||||
{
|
{
|
||||||
return $this->isFinal;
|
return $this->isFinal;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Entity;
|
|
||||||
|
|
||||||
use ApiPlatform\Metadata\ApiResource;
|
|
||||||
use ApiPlatform\Metadata\Delete;
|
|
||||||
use ApiPlatform\Metadata\Get;
|
|
||||||
use ApiPlatform\Metadata\GetCollection;
|
|
||||||
use ApiPlatform\Metadata\Patch;
|
|
||||||
use ApiPlatform\Metadata\Post;
|
|
||||||
use App\Repository\TaskTypeRepository;
|
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
|
||||||
use Symfony\Component\Serializer\Attribute\Groups;
|
|
||||||
|
|
||||||
#[ApiResource(
|
|
||||||
operations: [
|
|
||||||
new GetCollection(),
|
|
||||||
new Get(),
|
|
||||||
new Post(security: "is_granted('ROLE_ADMIN')"),
|
|
||||||
new Patch(security: "is_granted('ROLE_ADMIN')"),
|
|
||||||
new Delete(security: "is_granted('ROLE_ADMIN')"),
|
|
||||||
],
|
|
||||||
normalizationContext: ['groups' => ['task_type:read']],
|
|
||||||
denormalizationContext: ['groups' => ['task_type:write']],
|
|
||||||
order: ['label' => 'ASC'],
|
|
||||||
)]
|
|
||||||
#[ORM\Entity(repositoryClass: TaskTypeRepository::class)]
|
|
||||||
class TaskType
|
|
||||||
{
|
|
||||||
#[ORM\Id]
|
|
||||||
#[ORM\GeneratedValue]
|
|
||||||
#[ORM\Column]
|
|
||||||
#[Groups(['task_type:read', 'task:read', 'time_entry:read'])]
|
|
||||||
private ?int $id = null;
|
|
||||||
|
|
||||||
#[ORM\Column(length: 255)]
|
|
||||||
#[Groups(['task_type:read', 'task_type:write', 'task:read', 'time_entry:read'])]
|
|
||||||
private ?string $label = null;
|
|
||||||
|
|
||||||
#[ORM\Column(length: 7)]
|
|
||||||
#[Groups(['task_type:read', 'task_type:write', 'task:read', 'time_entry:read'])]
|
|
||||||
private ?string $color = '#222783';
|
|
||||||
|
|
||||||
public function getId(): ?int
|
|
||||||
{
|
|
||||||
return $this->id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getLabel(): ?string
|
|
||||||
{
|
|
||||||
return $this->label;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setLabel(string $label): static
|
|
||||||
{
|
|
||||||
$this->label = $label;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getColor(): ?string
|
|
||||||
{
|
|
||||||
return $this->color;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setColor(string $color): static
|
|
||||||
{
|
|
||||||
$this->color = $color;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Repository;
|
|
||||||
|
|
||||||
use App\Entity\TaskType;
|
|
||||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
|
||||||
use Doctrine\Persistence\ManagerRegistry;
|
|
||||||
|
|
||||||
class TaskTypeRepository extends ServiceEntityRepository
|
|
||||||
{
|
|
||||||
public function __construct(ManagerRegistry $registry)
|
|
||||||
{
|
|
||||||
parent::__construct($registry, TaskType::class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user